在app的开发中,页面之间的相互跳转是*基本常用的功能。在Android中的跳转一般通过显式intent和隐式intent两种方式实现的,而Android的原生跳转方式会存在一些缺点:
- 显式intent的实现方式,因为会存在直接的类依赖的问题,导致耦合严重;
- 隐式intent的实现方式,则会出现规则集中式管理,导致协作变得困难;
- 可配置性较差,一般而言配置规则都是在Manifest中的,这就导致了扩展性较差;
- 跳转过程无法控制,一旦使用了StartActivity()就无法插手其中任何环节了,只能交给系统管理;
- 当多组件化开发,使用原生的路由方式很难实现完全解耦;
而阿里的ARouter路由框架具有解耦、简单易用、支持多模块项目、定制性较强、支持拦截逻辑等诸多优点,很好的解决了上述的问题。关于ARouter具体实现功能,典型应用以及相应技术方案实现的介绍不在这详细介绍,具体可参见开源*佳实践:Android平台页面路由框架ARouter。
阿里ARouter的分析计划
- 阿里ARouter使用及源码解析(一)
- 阿里ARouter拦截器使用及源码解析(二)
- 阿里ARouter参数自动装载使用及源码解析(三)
基本功能使用
1.添加依赖和配置
android {
defaultConfig {
...
javaCompileOptions {
annotationProcessorOptions {
arguments = [ moduleName : project.getName() ]
}
}
}
}
dependencies {
compile 'com.alibaba:arouter-api:1.2.1.1'
annotationProcessor 'com.alibaba:arouter-compiler:1.1.2.1'
...
}
2.添加注解
3.初始化SDK
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private Button btn1,btn2;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
btn1 = (Button) findViewById(R.id.btn1);
btn2 = (Button) findViewById(R.id.btn2);
btn1.setOnClickListener(this);
btn2.setOnClickListener(this);
}
@Override
public void onClick(View v) {
if (v.getId() == R.id.btn1) {
4.发起跳转操作
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private Button btn1,btn2;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
btn1 = (Button) findViewById(R.id.btn1);
btn2 = (Button) findViewById(R.id.btn2);
btn1.setOnClickListener(this);
btn2.setOnClickListener(this);
}
@Override
public void onClick(View v) {
if (v.getId() == R.id.btn1) {
....
} else if (v.getId() == R.id.btn2){
ARouter.getInstance().build("/test/test1").navigation();
}
}
}
以上相关代码就是ARouter的*基本功能使用的步骤,下面来分析跳转功能是如何实现的。
原理分析
1.ARouter编译的过程
ARouter在编译期的时候,利用自定义注解完成了页面的自动注册。相关注解源码参见arouter-annotation,编译处理器源码参见arouter-compiler
下面是注解@Route
的源码介绍:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.CLASS)
public @interface Route {
Route中的extra
值是个int值,由32位表示,即转换成二进制后,一个int中可以配置31个1或者0,而每一个0或者1都可以表示一项配置(排除符号位),如果从这31个位置中随便挑选出一个表示是否需要登录就可以了,只要将标志位置为1,就可以在声明的拦截器中获取到这个标志位,通过位运算的方式判断目标页面是否需要登录。所以可以通过extra
给页面配置30多个属性,然后在拦截器中去进行处理。
ARouter在拦截器中会把目标页面的信息封装一个类Postcard
,这个类就包含了目标页面注解上@Route
标识的各种信息。关于拦截器的使用以及源码分析,后续会有介绍。
将代码编译一遍,可以看到ARouter生成下面几个源文件:
上面三个文件均是通过注解处理器RouteProcessor
生成的,关于如何自定义注解处理器,可以阅读Android编译时注解APT实战(AbstractProcessor),同时也需要学习JavaPoet的基本使用。下面我们看RouteProcessor
是如何生成相关文件的。
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
process()
方法相当于处理器的主函数main()
,可以在这个方法中扫描、评估和处理注解的代码,以及生成Java文件。RouteProcessor
中调用了parseRoutes()
,用来处理所有被@Route
注解的元素。在分析上述三个java文件如何生成之前,先看看生成文件的具体代码。
public class ARouter$$Root$$app implements IRouteRoot {
@Override
public void loadInto(Map<String, Class<? extends IRouteGroup>> routes) {
routes.put("test", ARouter$$Group$$test.class);
}
}
public class ARouter$$Group$$test implements IRouteGroup {
@Override
public void loadInto(Map<String, RouteMeta> atlas) {
atlas.put("/test/test1", RouteMeta.build(RouteType.ACTIVITY, Test1Activity.class, "/test/test1", "test", null, -1, -2147483648));
}
}
public class ARouter$$Providers$$app implements IProviderGroup {
@Override
public void loadInto(Map<String, RouteMeta> providers) {
}
}
我们接着分析上述三个文件是如何生成的
1.首先获取生成方法的参数的类型和参数名称
private void parseRoutes(Set<? extends Element> routeElements) throws IOException {
if (CollectionUtils.isNotEmpty(routeElements)) {
logger.info(">>> Found routes, size is " + routeElements.size() + " <<<");
rootMap.clear();
2.获取了方法的参数的类型和参数名称后,下面便是生成相应的方法
private void parseRoutes(Set<? extends Element> routeElements) throws IOException {
if (CollectionUtils.isNotEmpty(routeElements)) {
........
以上代码主要功能就是遍历所有被@Route注解的元素,然后将每个路由节点的信息按照类型(ACTIVITY类型,实现了IProvider 接口类型以及SERVICE类型)封装到RouteMeta
中,*后调用categories(routeMete)
方法将节点分组,保存在groupMap
集合。
继续往下分析
private void parseRoutes(Set<? extends Element> routeElements) throws IOException {
if (CollectionUtils.isNotEmpty(routeElements)) {
........
以上代码主要功能由几点:
- 遍历
groupmap
集合给ARouter$$Group$$xxx类中的loadInto()
添加方法体,并且生成ARouter$$Group$$xxx JAVA文件,而文件命名为ARouter$$Group$$+groupname,其中有多个分组就会创建多个组文件。比如AROUTER
源码中的样例就生成了多个分组文件
关于生成的loadInto()
中的方法体的例子,来自 AROUTER
源码中的样例:
public class ARouter$$Group$$test implements IRouteGroup {
@Override
public void loadInto(Map<String, RouteMeta> atlas) {
- 遍历每个组里面的路由节点,查找节点类型是否为PROVIDER类型,如果是就向给ARouter$$Providers$$xxx类中的
loadInto()
添加方法,其文件命名ARouter$$Providers$$+modulename。关于生成的loadInto()
中的方法体的例子,来自 AROUTER源码中的样例:
public class ARouter$$Providers$$app implements IProviderGroup {
@Override
public void loadInto(Map<String, RouteMeta> providers) {
providers.put("com.alibaba.android.arouter.demo.testservice.HelloService", RouteMeta.build(RouteType.PROVIDER, HelloServiceImpl.class, "/service/hello", "service", null, -1, -2147483648));
- 将生成的组文件放在rootmap集合中去,为下面生成ARouter$$Root$$xxx文件做准备,其文件命名ARouter$$Root$$+modulename。
我们接着分析parseRoutes()
方法*后一段代码,这段代码其实很简单,主要目的就是给ARouter$$Root$$xxx的loadInto()
添加方法体,*后生成Router$$Providers$$xxx,ARouter$$Root$$xxx文件
private void parseRoutes(Set<? extends Element> routeElements) throws IOException {
if (CollectionUtils.isNotEmpty(routeElements)) {
........
关于生成的loadInto()
中的方法体的例子,来自 AROUTER源码中的样例:
public class ARouter$$Root$$app implements IRouteRoot {
@Override
public void loadInto(Map<String, Class<? extends IRouteGroup>> routes) {
routes.put("service", ARouter$$Group$$service.class);
routes.put("test", ARouter$$Group$$test.class);
}
}
上面分析的便是parseRoutes()
方法所有代码的解析
3.*后我们看下categories()
方法是如何分组的
private void categories(RouteMeta routeMete) {
通过分析,如果@Route注解中有设置group标识,作为groupname,如果没有就取/xxx1/xxx2,xxx1作为groupname,并将同一组的路由节点放到同一个集合中去。
至此关于@Route
注解在编译期时生成ARouter$$Root$$xxx,Router$$Providers$$xxx,ARouter$$Group$$xxx三种映射文件的源码分析完毕。
2.ARouter初始化过程
ARouter经过代码编译后,生成了相应的映射文件,我们可以断定,ARouter 的初始化会将这些文件加载到内存中去,形成一个路由表,以供后面路由查找跳转之用。其相关源码可参见 arouter-api
public static void init(Application application) {
if (!hasInit) {
logger = _ARouter.logger;
_ARouter.logger.info(Consts.TAG, "ARouter init start.");
hasInit = _ARouter.init(application);
if (hasInit) {
_ARouter.afterInit();
}
_ARouter.logger.info(Consts.TAG, "ARouter init over.");
}
}
由上面代码可以看出,其初始化实际上是调用了_ARouter
的 init ()
方法,而且其他的跳转方法*终调用的也是_ARouter
种的方法。
protected static synchronized boolean init(Application application) {
mContext = application;
LogisticsCenter.init(mContext, executor);
logger.info(Consts.TAG, "ARouter init success!");
hasInit = true;
return true;
}
_ARouter
中又调用了LogisticsCenter.init()
,继续追踪下去,其中传入了一个线程池executor
,这个线程池在拦截器的时候会使用到。
public synchronized static void init(Context context, ThreadPoolExecutor tpe) throws HandlerException {
mContext = context;
executor = tpe;
try {
static void afterInit() {
以上就是ARouter初始化的所有代码,关于如何查找到com.alibaba.android.arouter.routes
包内所有文件这里便不做过多分析,大家可以去阅读 arouter-api中ClassUtils
这个类的源码。
总结下来,其实ARouter 的初始化只做了一件事,找到自己编译期产生的清单文件,把 Group 、Interceptor 、Provider 三种清单加载到 Warehouse 内存仓库中。即下面这些文件,来源自AROUTER源码中的样例
值得注意的是,在初始化阶段,ARouter 仅载入了 Group 清单,并没有具体载入每个 Group 中包含的具体的路由节点清单,只有当使用到具体的 Group 时,才会加载对应的 Group 列表。这种分组管理,按需加载,大大的降低了初始化时的内存压力。并且Warehouse
类中保存了路由清单,并且将使用过的路由对象缓存起来,之后查找都是直接使用缓存的对象 。
3.ARouter调用过程分析
页面跳转*基本方法
ARouter.getInstance().build(“/test/activity2”).navigation();
获取Provider服务(实现了IProvider接口以及IProvider子类接口的服务类)的方法有两种:
1.byName方式
ARouter.getInstance().build(“/service/hello”).navigation()
2.byType方式
ARouter.getInstance().navigation(HelloService.class)
ARouter路由跳转采用链式调用,ARouter.getInstance()
其中采用的单例模式,获取ARouter的实例,这个就不作过多分析,主要分析build()
和navigation()
。
build()方法
ARouter的build(String path)
和init()
方法一样,调用的是_ARouter
的build(String path)
方法。
protected Postcard build(String path) {
if (TextUtils.isEmpty(path)) {
throw new HandlerException(Consts.TAG + "Parameter is invalid!");
} else {
PathReplaceService pService = ARouter.getInstance().navigation(PathReplaceService.class);
if (null != pService) {
path = pService.forString(path);
}
return build(path, extractGroup(path));
}
}
其中extractGroup(String path)
就是根据path获取分组名,即path*段“/”符号之间的值
private String extractGroup(String path) {
if (TextUtils.isEmpty(path) || !path.startsWith("/")) {
throw new HandlerException(Consts.TAG + "Extract the default group failed, the path must be start with '/' and contain more than 2 '/'!");
}
try {
build(String path)
方法*终调用的是build(String path, String group)
protected Postcard build(String path, String group) {
if (TextUtils.isEmpty(path) || TextUtils.isEmpty(group)) {
throw new HandlerException(Consts.TAG + "Parameter is invalid!");
} else {
PathReplaceService pService = ARouter.getInstance().navigation(PathReplaceService.class);
if (null != pService) {
path = pService.forString(path);
}
return new Postcard(path, group);
}
}
值得注意的是其中ARouter.getInstance().navigation(PathReplaceService.class)
就是得到实现PathReplaceService
接口的一个服务对象,对原始path进行处理后,生成新的path路径。而这个类需要我们自己自定义去实现,如果没有实现,pService=null,原始path不做任何处理。
下面是PathReplaceService
接口,我们可以通过实现forString()
和forUri()
方法,对某些url进行替换处理,跳转到其他的目标页面。
public interface PathReplaceService extends IProvider {
*后返回一个Postcard
实例对象,里面封装了路由节点的路径,分组等节点信息。其实build()
方法的目的只有一个就是根据路由,封装成Postcard
对象,其对象贯穿之后整个路由过程。Postcard 包含了众多的属性值,提供了路由过程中所有的控制变量。
public final class Postcard extends RouteMeta {
private Uri uri;
private Object tag;
navigation()方法
关于页面跳转的navigation()
方法有多个重载的方法,但*终都会调用_ARouter
下面这个方法
protected Object navigation(final Context context, final Postcard postcard, final int requestCode, final NavigationCallback callback) {
try {
值得注意的是,当跳转路由处理失败的时候,会获取一个降级服务,我们可以实现DegradeService
接口,实现onLost()
方法,对路由处理失败的情况进行处理,比如跳转到一个信息提示页面,让用户去更新版本等操作等。下面是DegradeService
接口:
public interface DegradeService extends IProvider {
通过上面代码的分析,不管是否通过拦截器进行处理,*后都会调用_navigation()
达到路由的目的:
private Object _navigation(final Context context, final Postcard postcard, final int requestCode, final NavigationCallback callback) {
final Context currentContext = null == context ? mContext : context;
switch (postcard.getType()) {
case ACTIVITY:
目前仅ARouter实现了 ACTIVITY , PROVIDER ,FRAGMENT三种种类型。上面关于postcard的provider,destination的值都是在completion()
中设置的。我们接着看LogisticsCenter
的completion(Postcard postcard)
。
public synchronized static void completion(Postcard postcard) {
if (null == postcard) {
throw new NoRouteFoundException(TAG + "No postcard!");
}
分析到这里,关于页面基本跳转的原理分析就已经结束了。*后就是关于获取Provider服务两种方法的源码分析。其中byName方式,和页面跳转是一模一样的。我们只需要看看byType方式即可。byType方式*后调用的是_ARouter
的navigation(Class<? extends T> service)
protected <T> T navigation(Class<? extends T> service) {
try {
上面代码中的completion()
方法之前已经分析过了,只需要看下LogisticsCenter.buildProvider(service.getName())
即可。
public static Postcard buildProvider(String serviceName) {
RouteMeta meta = Warehouse.providersIndex.get(serviceName);
if (null == meta) {
return null;
} else {
return new Postcard(meta.getPath(), meta.getGroup());
}
}
这个方法非常的简单,就是根据服务类名去仓库Warehouse.providersIndex中获去路由节点元素,然后封装在Postcard对象中。服务类清单列表Warehouse.providersIndex中的值是在初始化时缓存的。值得注意的是,PROVIDER 类型的路由节点既存在于对应的分组中,也存在于服务类清单列表中。所以,ARouter 可通过byType,byName两种方式来获取。
补充
关于ARouter的基本用法上面只有*基本跳转的介绍,下面对其他一些基本使用进行下补充
这些传参都是保存在生成的postcard
对象中的mBundle
属性里,然后在跳转的时候通过intent.putExtras(postcard.getExtras())
达到传送参数的目的。
值得注意的是,关于对象的传递有两种,一种是withParcelable()
方法,不过此方法需要传递的对象实现Parcelable
接口,达到序列化的目的;另外一种是withObject()
方法,此方法的原理是将实体类转换成json字符串,通过String的方式进行传递,而且使用这种方式需要实现 SerializationService,并使用@Route注解标注,下面是ARouter样例:
@Route(path = "/service/json")
public class JsonServiceImpl implements SerializationService {
@Override
public void init(Context context) {
}
@Override
public <T> T json2Object(String text, Class<T> clazz) {
return JSON.parseObject(text, clazz);
}
@Override
public String object2Json(Object instance) {
return JSON.toJSONString(instance);
}
}
而且,需要在跳转到的页面获取JsonServiceImpl
服务,将json字符串转换成对象。
SerializationService serializationService = ARouter.getInstance().navigation(SerializationService.class);
TestObj obj = serializationService.json2Object(getIntent().getString("obj"), TestObj.class);
ARouter.getInstance().build("/test/activity2").navigation(this, 666);
值得注意的是,这时候的 navigation
需要传递activit和requestCode。
定义一个fragment
@Route(path = "/test/fragment")
public class BlankFragment extends Fragment {
public BlankFragment() {
获取frament
Fragment fragment = (Fragment) ARouter.getInstance().build("/test/fragment").navigation();
服务是全局单例的,只有在*次使用到的时候才会被初始化。
暴露服务,必须实现IProvider 接口 或者其子类型
获取服务
app中可能存在多个模块,每个模块下面都有一个root结点,每个root结点都会管理整个模块中的group节点,每个group结点则包含了该分组下的所有页面,而每个模块允许存在多个分组,每个模块中都会有一个拦截器节点就是Interceptor结点,除此之外每个模块还会有控制拦截反转的provider结点
*后
到此,关于ARouter的基本用法以及原理分析的就全部结束了,如果有不清楚或者错误的地方,希望各位同学指出。关于ARouter拦截器,各种服务,依赖注入等更多进阶用法及源码分析会更新在后续的文章。
如果各位同学认为本文对你有一些帮助,希望能点个喜欢,谢谢!