ARouter使用解析

ARouter

ARouter 是阿里云出品的Android中间件,负责处理盅跳转页面时的逻辑,简化和优化之前跳转页面的方法。同时他也是组件化的基础之一,实现了模块间的解耦。

ARouter使用

项目的主页有提供ARouter的使用方法,主要就是

  • 注解可以跳转的类(Activity,Service,ContentProvider,Fragment等)
  • 要跳转页面的时候使用Arouter,类似于ARouter.getInstance().build("/kotlin/test").withString("name", "老王").withInt("age", 23).navigation();

用法是不能再简单了,猜想是把一个String与一个Activity对应起来就可以了,然而实际代码应该比猜想复杂N多倍。下面一起分析一下这个中间,挖掘他的所有信息。

ARouter源码解析

下面分析源码分为几部分

  • 跳转页面流程分析
  • 在类上和成员上的注解都做了什么
  • 解释在ARouter文档上的所有功能特点和典型应用是如何实现的,把这些特点抄到下面来,方便查看,我们会挨个把所有的特点分析一遍

一、功能介绍

  1. 支持直接解析标准URL进行跳转,并自动注入参数到目标页面中
  2. 支持多模块工程使用
  3. 支持添加多个拦截器,自定义拦截顺序
  4. 支持依赖注入,可单独作为依赖注入框架使用
  5. 支持InstantRun
  6. 支持MultiDex(Google方案)
  7. 映射关系按组分类、多级管理,按需初始化
  8. 支持用户指定全局降级与局部降级策略
  9. 页面、拦截器、服务等组件均自动注册到框架
  10. 支持多种方式配置转场动画
  11. 支持获取Fragment
  12. 完全支持Kotlin以及混编(配置见文末 其他#5)
  13. 支持第三方 App 加固(使用 arouter-register 实现自动注册)

二、典型应用

  1. 从外部URL映射到内部页面,以及参数传递与解析
  2. 跨模块页面跳转,模块间解耦
  3. 拦截跳转过程,处理登陆、埋点等逻辑
  4. 跨模块API调用,通过控制反转来做组件解耦

跳转页面流程分析

使用的方法是

ARouter.getInstance()
        .build("/kotlin/test")
        .withString("name", "老王")
        .withInt("age", 23)
        .navigation();

ARouter使用了单例,内部存储了页面的映射表,初始化状态,debug信息等,等一下都会用到的信息(其实在保存在_ARouter单例中),同时也方便在使用的时候直接使用ARouter的静态方法。
实际上除了ARouter之外还有一个_ARouter类,ARouter中几乎是把所有的方法都直接给了_ARouter处理,这一层的转换把_ARouter中复杂的方法转化为ARouter中简单的方法向外暴露,算是一种门面吧,值得学习一下。
build方法不例外地转给了_ARouter

    public Postcard build(String path) {
        return _ARouter.getInstance().build(path);
    }

在_ARouter中构造了PostCard

/**
* Build postcard by path and default group
*/
protected Postcard build(String path) {
    if (TextUtils.isEmpty(path)) {
        throw new HandlerException(Consts.TAG + "Parameter is invalid!");
    } else {
        //这个Service里可以将传入的path处理,换在另外一个,也相当于一个拦截器
        PathReplaceService pService = ARouter.getInstance().navigation(PathReplaceService.class);
        if (null != pService) {
            path = pService.forString(path);
        }
        return build(path, extractGroup(path));
    }
}

其中PathReplaceService相关处理逻辑是根据当前的path换成另外的path继续走下面的流程,所以主流程还是build方法,其中有有个extractGroup(path)方法调用,将path中第一部分抽取出来作为group,继续看主线

/**
 * Build postcard by path and 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);
    }
}

这里又找了一次PathReplaceService,出于什么目的,很简单,因为这个方法有可能不是上面那个路径调过来的,_ARouter还有两个参数的builde方法,直接调用了这个方法,但是不做任何区分直接再找一次也有点可以优化的空间🙃
返回的结果就是最后构造出来的一个PostCard,这个类上的解释是A container that contains the roadmap.,包含这一次路由过程中所要的所有信息。PostCard的构造方法没有什么逻辑,就是另外构造了一个bundle放在了PostCard里面备下面使用。

到这里就返回到了最初调用build的地方,再往下是两个withXXX方法,就是向PostCard中放入几个跳转页面要带过去的信息,都是直接放到了bundle里面,当然这不是主线。
继续看下面的navigation方法。

/**
 * Navigation to the route with path in postcard.
 * No param, will be use application context.
 */
public Object navigation() {
    //后面会用application的Context
    return navigation(null);
}

给出的例子都是使用没有参数所navigation方法,后面拿applicationContext,要设置intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);感觉有点误导的感觉,这里正正经经地传个activity过来才应该是最广泛的使用方法。

/**
 * Navigation to the route with path in postcard.
 *
 * @param context Activity and so on.
 */
public Object navigation(Context context) {
    //这里没有caback,这个callback有onFound,onLost,onArrival,和onInterrupt方法,都是跳转时的各种回调,单独的跳转降级就是通过这个callback完成的
    return navigation(context, null);
}

/**
 * Navigation to the route with path in postcard.
 *
 * @param context Activity and so on.
 */
public Object navigation(Context context, NavigationCallback callback) {
    //这里又增加了一个-1的参数,不求不需求forResult
    return ARouter.getInstance().navigation(context, this, -1, callback);
}

到这里方法调用就出了PostCard,到了ARouter中,当然又会委托给_ARouter进行真正的业务。
看_ARouter的方法。

/**
 * Use router navigation.
 */
protected Object navigation(final Context context, final Postcard postcard, final int requestCode, final NavigationCallback callback) {
    try {
        //主流程,补全路由所要的信息
        LogisticsCenter.completion(postcard);
    } catch (NoRouteFoundException ex) {
        logger.warning(Consts.TAG, ex.getMessage());
        if (debuggable()) { // Show friendly tips for user.
            Toast...
        }
        if (null != callback) {
            callback.onLost(postcard);
        } else {    // No callback for this invoke, then we use the global degrade service.
            //统一降级逻辑
            DegradeService degradeService = ARouter.getInstance().navigation(DegradeService.class);
            if (null != degradeService) {
                degradeService.onLost(context, postcard);
            }
        }
        return null;
    }
    if (null != callback) {
        callback.onFound(postcard);
    }

    //主流程,根据是否绿色通道走拦截的逻辑
    //这里还有个友情提示: It must be run in async thread, maybe interceptor cost too mush time made ANR.
    if (!postcard.isGreenChannel()) {  
        interceptorService.doInterceptions(postcard, new InterceptorCallback() {
            @Override
            public void onContinue(Postcard postcard) {
                //走完拦截器并通过的,继续走跳转的逻辑
                _navigation(context, postcard, requestCode, callback);
            }
            @Override
            public void onInterrupt(Throwable exception) {
                if (null != callback) {
                    callback.onInterrupt(postcard);
                }
                logger.info(Consts.TAG, "Navigation failed, termination by interceptor : " + exception.getMessage());
            }
        });
    } else {
        //主流程,绿色通道的主要逻辑,此时所有的信息已经准备完成,下面就是与系统交互进行跳转了
        return _navigation(context, postcard, requestCode, callback);
    }
    return null;
}

主流程没有几名话,先补全路由的信息,走拦截逻辑,然后真正的跳转,其中补全PostCard是重要过程,我们看一下方法详情

/**
 * Completion the postcard by route metas
 * @param postcard Incomplete postcard, should complete by this method.
 */
public synchronized static void completion(Postcard postcard) {
    RouteMeta routeMeta = Warehouse.routes.get(postcard.getPath());
    if (null == routeMeta) {    // Maybe its does't exist, or didn't load.
        //没有找到RouteMeta,检查他所有的组是否还没有加载,如果已经加载,则异常,没有加载去加载
        Class<? extends IRouteGroup> groupMeta = Warehouse.groupsIndex.get(postcard.getGroup());  // Load route meta.
        if (null == groupMeta) {
            throw new NoRouteFoundException(...);
        } else {
            // Load route and cache it into memory, then delete from metas.
            try {
                IRouteGroup iGroupInstance = groupMeta.getConstructor().newInstance();
                //加载所在的组,如果加载完成就把这个group从未加载列表中删除
                iGroupInstance.loadInto(Warehouse.routes);
                //将些组从未加载中移除,防止重复加载,
                Warehouse.groupsIndex.remove(postcard.getGroup());
            } catch (Exception e) {
                throw new HandlerException(...);
            }
            // 加载了组信息,重新去重新去走补全的逻辑
            completion(postcard);   // Reload
    } else {
        将RouteMeta的信息放到PostCard中,
        如果是通过uri跳转的话再将路径中的信息解析出来放到postCard的bound中

        下面又对两种类型的跳转做的特殊处理
        1. PROVIDER
           从仓库中找到provider的实例,疳赋值给postCard
           设置绿色通道,防止拦截
        2. FRAGMENT
           设置绿色通道,防止拦截
    }
}

此时post的信息已经完全了,我们路过拦截的逻辑,直接看下面真正的跳转方法,感觉是没有必要把代码再拿出来,就是根据类型区分了一下,activity就直接new Intent进行跳转,如果是Privider,Fragment就返回实例。
到这里基本完成了一次跳转页面所走的全部路径。并没有高深难懂的逻辑,一个比较好玩的就是PathReplaceService,DegradeService,SerializationService等都是通过注册一个Service完成的,这就大大增加了这个框架的灵活性,而且框架向外提供的这个功能,自己内部已经先用起来了,这个也是挺有意思的。

ARouter中的注解有什么用,是怎么起作用的

@Route

作用:注解一个类,这个类就可以通过ARouter找到使用
Route主要有两个属性,path和group,在RouteProcessor中处理这个注解,在注解处理的方法中会根据注解的类型创建上面使用过的RouteMeta

for (Element element : routeElements) {
    TypeMirror tm = element.asType();
    Route route = element.getAnnotation(Route.class);
    RouteMeta routeMeta = null;

    if (types.isSubtype(tm, type_Activity)) {                 // Activity
        logger.info(">>> Found activity route: " + tm.toString() + " <<<");

        // Get all fields annotation by @Autowired
        Map<String, Integer> paramsType = new HashMap<>();
        for (Element field : element.getEnclosedElements()) {
            if (field.getKind().isField() && field.getAnnotation(Autowired.class) != null && !types.isSubtype(field.asType(), iProvider)) {
                // It must be field, then it has annotation, but it not be provider.
                Autowired paramConfig = field.getAnnotation(Autowired.class);
                paramsType.put(StringUtils.isEmpty(paramConfig.name()) ? field.getSimpleName().toString() : paramConfig.name(), typeUtils.typeExchange(field));
            }
        }
        routeMeta = new RouteMeta(route, element, RouteType.ACTIVITY, paramsType);
    } else if (types.isSubtype(tm, iProvider)) {         // IProvider
        logger.info(">>> Found provider route: " + tm.toString() + " <<<");
        routeMeta = new RouteMeta(route, element, RouteType.PROVIDER, null);
    } else if (types.isSubtype(tm, type_Service)) {           // Service
        logger.info(">>> Found service route: " + tm.toString() + " <<<");
        routeMeta = new RouteMeta(route, element, RouteType.parse(SERVICE), null);
    } else if (types.isSubtype(tm, fragmentTm) || types.isSubtype(tm, fragmentTmV4)) {
        logger.info(">>> Found fragment route: " + tm.toString() + " <<<");
        routeMeta = new RouteMeta(route, element, RouteType.parse(FRAGMENT), null);
    } else {
        throw new RuntimeException("ARouter::Compiler >>> Found unsupported class type, type = [" + types.toString() + "].");
    }

    categories(routeMeta);
    // if (StringUtils.isEmpty(moduleName)) {   // Hasn't generate the module name.
    //     moduleName = ModuleUtils.generateModuleName(element, logger);
    // }
}

分别构建出来RouteMeta,还构建出来一个分组的信息,下面将这些信息构建两个java文件。类似于这样

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);
  }
}

rootInfo,存放所有的组的信息,就是上面找不到RouteMeta的时候会从这里找到对应的组,再找到组信息对应的类,然后加载
还有一个组详细信息的类,类似开这样

public class ARouter$$Group$$test implements IRouteGroup {
    @Override
    public void loadInto(Map<String, RouteMeta> atlas) {
      atlas.put("/test/activity2", RouteMeta.build(RouteType.ACTIVITY, Test2Activity.class, "/test/activity2", "test", new java.util.HashMap<String, Integer>(){{put("key1", 8); }}, -1, -2147483648));
      atlas.put("/test/activity4", RouteMeta.build(RouteType.ACTIVITY, Test4Activity.class, "/test/activity4", "test", null, -1, -2147483648));
      atlas.put("/test/fragment", RouteMeta.build(RouteType.FRAGMENT, BlankFragment.class, "/test/fragment", "test", null, -1, -2147483648));
    }
}  

加载的时候把这些信息加载到map中,以后跳转使用

@Interceptor

作用:设置全局跳转的拦截器,可以设置优先级
处理注解基本和和@Route一样,得到类,得到属性,javapoet写出一个类似于这样的类:

public class ARouter$$Interceptors$$app implements IInterceptorGroup {
  @Override
  public void loadInto(Map<Integer, Class<? extends IInterceptor>> interceptors) {
    interceptors.put(7, Test1Interceptor.class);
  }
}

这个map是个特别的map,根据key的值自动排序,如果key重复会异常,也也是这个拦截器可以按优先级排序的原因

@Autowired

作用:自动装配,注解成员后,可以自动从Intent中解出数据并赋值给变量
实现也很相似,找到被注解的成员,生成一个helper,在需要将intent的数据解出来的时候使用helper的inject方法,ARouter又使用了一个AutowiredService专门做这个事,只要将要注入的类传过来就可以了

@Override
public void autowire(Object instance) {
    String className = instance.getClass().getName();
    try {
        if (!blackList.contains(className)) {
            // 只有一个inject方法
            ISyringe autowiredHelper = classCache.get(className);
            if (null == autowiredHelper) {  // No cache.
                autowiredHelper = (ISyringe) Class.forName(instance.getClass().getName() + SUFFIX_AUTOWIRED).getConstructor().newInstance();
            }
            // autowiredHelper就是根据注解生成的特定helper
            autowiredHelper.inject(instance);
            classCache.put(className, autowiredHelper);
        }
    } catch (Exception ex) {
        blackList.add(className);    // This instance need not autowired.
    }
}

javaPoet实在是有点烦琐,真的不愿把他的代码拿来。有意的同学可以直接去arouter查看

解释所有官方列举的特点

  1. 支持直接解析标准URL进行跳转,并自动注入参数到目标页面中

在Manifast页面中注册了两个filter

<intent-filter>
    <data
        android:host="m.aliyun.com"
        android:scheme="arouter"/>
    <action android:name="android.intent.action.VIEW"/>
    <category android:name="android.intent.category.DEFAULT"/>
    <category android:name="android.intent.category.BROWSABLE"/>
</intent-filter>

<!-- App Links -->
<intent-filter android:autoVerify="true">
    <action android:name="android.intent.action.VIEW"/>
    <category android:name="android.intent.category.DEFAULT"/>
    <category android:name="android.intent.category.BROWSABLE"/>
    <data
        android:host="m.aliyun.com"
        android:scheme="http"/>
    <data
        android:host="m.aliyun.com"
        android:scheme="https"/>
</intent-filter>

第一个是处理特定的协议,如果协议中有arouter,则会用这个Acitity处理
还有一个是处理applink的,在网页上点击特定连接,也是在这个Activity中处理
在这个Activity主要代码就两行:

Uri uri = getIntent().getData();
ARouter.getInstance().build(uri).navigation(this, new NavCallback() {
            @Override
            public void onArrival(Postcard postcard) {
                finish();
            }
        });

这里使用的是一个URI,在构造PostCard的时候会将URI后面挂的参数直接转化到bound中去,也就解释了第一个特征。

  1. 支持多模块工程使用

各模块只是依赖一个String,编译时会扫描整个所有的工程,所以直接就支持多模块的工程。但是有个问题就是别的页面要跳转的时候都要将字符串写死进去,如果定义常量的话会出现多个模块依赖一个常量类的情况。

  1. 支持添加多个拦截器,自定义拦截顺序
    拦截器注解定义

如果设置了这个优先级别,生成的java代码中会将这个优先级做为key,放到传过来的一个容器中,而这个窗口的定义在com.alibaba.android.arouter.core.Warehouse中,是一个UniqueKeyTreeMap,保证key是唯一的,并且按key进行排序

UniqueKeyTreeMap

这里也就解释了自定义拦截顺序的特点

  1. 支持依赖注入,可单独作为依赖注入框架使用

不知道讲的是什么。。
navigation方法返回的是一个Object

  1. 支持InstantRun
  2. 支持MultiDex(Google方案)

这里看到源码中找了一个所有的的dex文件,再从这些所有的dex中查找要找的router的类,应该就是处理这个问题。等于没有说。。就简单看一下调用链吧

Arouter.init() -> LogisticsCenter.init(mContext, executor) 
-> ClassUtils.getFileNameByPackageName(mContext, ROUTE_ROOT_PAKCAGE);//这里就是是指定的ali的包名,就是生成的那些类包名
-> ClassUtils.getSourcePaths(context);//从多dex下找类 
-> ClassUtils.tryLoadInstantRunDexFile(applicationInfo)//instantRun
-> 找到所有生成的注册router的类
  1. 映射关系按组分类、多级管理,按需初始化

路由的注解中可以注册一个group信息,如果不定义这个group信息,arouter会拿路径中的第一段做为group。处理注解的时候会生成两组信息,第一是组信息,其中有所有group的信息,每一组都会指向一个描述这个组中所有路径的类。
初始化时仅仅加载了组的信息,并没有加载每一组内的所有路由,使用路由时会先查找有没有这个路由信息,如果没有的话就去加载这一组所有的路由。做到了按需初始化。
这个过程上面路由过程已经用代码分析过了。

  1. 支持用户指定全局降级与局部降级策略

每一次使用路由时可以传入一个callback,作为单次路由失败的降级策略,其实也不仅仅是降级策略,callback提供了多个回调方法使用:

public interface NavigationCallback {
    //找到路由
    void onFound(Postcard postcard);
    //没有找到,降级吧
    void onLost(Postcard postcard);
    //向android发出了startActivity的请求
    void onArrival(Postcard postcard);
    //使用拦截器时
    void onInterrupt(Postcard postcard);
}

也可以注册一个IProvider,用来处理所有的降级策略。

  1. 页面、拦截器、服务等组件均自动注册到框架

使用注解,编译期处理,运行时直接无反射运行(多dex什么的还是要反射)

  1. 支持多种方式配置转场动画

支持,无特殊

  1. 支持获取Fragment

navigate的时候支持返回一个fragment,只要注册了路由的fragment,都可以通过路由来得到实例。

  1. 完全支持Kotlin以及混编(配置见文末 其他#5)
  2. 支持第三方 App 加固(使用 arouter-register 实现自动注册)
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容

  • 组件化 模块化、组件化与插件化 在项目发展到一定程度,随着人员的增多,代码越来越臃肿,这时候就必须进行模块化的拆分...
    silentleaf阅读 4,945评论 2 12
  • ARouter源码解读 以前看优秀的开源项目,看到了页面路由框架ARouter,心想页面路由是个啥东东,于是乎网上...
    陆元伟阅读 513评论 0 1
  • 盛夏将尽,时光染暖 答应会在 牵起的手小心翼翼 仿佛那是稀世珍宝,稍一用力就会破碎 盛夏将尽,温度刚好 约定了一世...
    戀物念一樣阅读 283评论 1 2
  • 轩兒阅读 91评论 0 0
  • 无形的网 我们都是些 负重的 带壳儿的虫 动弹不了 被它粘着 全都低着头 专注着 蚕食每本起皱的书 多刻苦 每当翻...
    灿7阅读 239评论 1 5