ARouter系列一:Activity跳转原理详解

本篇文章默认读者已经会使用 ARouter,我们通过使用代码及源码详解其原理。

ARouter是一个路由框架。使用AndroidStudio 开发环境,我们知道在Coding阶段,没有依赖关系的Module,它们的类是无法被对方直接引用。这是AS 的项目组织方式。但是不管所属哪一个Moudle的代码,最终都会打包进apk,在软件运行时,我们可以拿得所有的类。

现在有一个问题:两个没有依赖关系的Moudle,它们之间的页面该如何跳转呢?
既然Coding阶段无法拿到对象,那我们就运行时获取到进行页面跳转所需的信息。
假如有 module1 和 module2,之间没有依赖关系。AActivity(包名:com.test) 属于 moudle1;BActivity(包名:con.test) 属于 module2。从 A 页面跳转到 B 页面,我们可以使用如下方式:

startActivity(new Intent(this, Class.forName("com.test.BAcctivity")))

或者

Intent i = new Intent();
i.setComponent(new ComponentName("com.test", "com.test.BActivity"));
startActivity(i);

ARouter的解决方案是,全局维护一个Map集合,key是一段用户定义的字符串(本例中的a,b),value是对应的类对象。类似这种(概念上):

Map<String, Class> map = new HashMap<String, Class>();
map.put("a", AActivity.class);
map.put("b", BActivity.class);

我们在开发阶段只用使用a,b就可以代表 AActivity,BActivity,然后在运行阶段通过映射关系进行转换后创建 Intent 对象进行跳转。(Intent(Context packageContext, Class<?> cls) )

使用的时候类似(概念上):

ARouter.getInstance().build("b").navigation();

接下来探寻一下,ARouter的具体实现。看ARouter的源码能学到:框架的设计、APT、Gradle-Plugin。


一、项目目录结构概述

项目结构

arouter-annotation:定义注解类
arouter-api核心库,封装了开发人员使用的API
arouter-compiler:定义编译时注解处理器,通过扫描项目,生成辅助Java类
arouter-gradle-plugin:gradle插件(可选),用于在编译时通过Transform处理字节码

二、注解

/**
 * Mark a page can be route by router.
 */
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.CLASS)
public @interface Route {
    /**
     * Path of route
     */
    String path();

    /**
     * Used to merger routes, the group name MUST BE USE THE COMMON WORDS !!!
     */
    String group() default "";

    ...
}

Route 注解类,是一个标记,被标记的实体能够被路由进行分发,例如:Activity。 path就是将来路由表中的 "key",group 代表由Route标记的实体 所属的路由组。

路由组的概念:把所有的路由表信息,分成N组,这样在加载路由表的时候,可以分组加载,提高效率。即按需加载,有延迟加载的特性,同时又可以减少路由表加载时间。

三、注解处理器

项目编译时通过扫描注解,根据这些有用的信息,动态生成辅助的Java类文件.javaRouteProcessor.java 注解处理器就是为了处理那些被 Route标注的类。因为需要动态生成 .java文件,项目中使用了 javapoet 作为辅助工具,以提高生产效率。

注解处理器生成的 “辅助的文件”,也可以我们自己手动编写。但通常情况是,这些java文件中的逻辑单一简单,且有一定的共同特性(这需要大家在开发过程中去观察),所以通过注解信息去生成相关的 .java 可以大大提高生产力。 ARouter、ButterKnife 中大量使用了此技术。

这里不打算对 RouteProcessor.java 的细节进行介绍,有兴趣的可以去看代码。过度讲解细节,会让主线模糊不清。(当然这些细节也很重要,可以考虑再开一节来深入细节)

我们结合具体的代码,看一下帮助我们生成了哪些 “辅助类”

新建一个 空的Android项目,只有一个默认的module:app,创建两个页面:FirstActivitySecondActivity(这里省略 ARouter 依赖的引用以及库的初始化)

package com.daddyno1.projectmoduledemo;
...
@Route(path = "/group1/first")  
public class FirstActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main2);
    }
}

package com.daddyno1.projectmoduledemo;
...
@Route(path = "/group2/second")
public class SecondActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main3);
    }
}

这两个 Activvity 的path设定,特意分属两个路由组,group1 和 group2 (会截取path 前两个/ 的字符串作为组)

编译一下,我们可以看看生成了哪些辅助文件。(辅助文件的位置 在 app/build/generated/ap_generated_sources下),生成的路由辅助文件包名:com.alibaba.android.arouter.routes

APT 生成的辅助类

ARouter$$Root$$xxx:xxx 为本module的名称。本例中module名称是app,此类记录了 app 中所有路由组信息。

ARouter$$Group$$xxx:xxx为本module中路由组的名称。本例中共有2个分组:group1 和 group2。此类记录了 某个路由组中 所有的路由表信息。

package com.alibaba.android.arouter.routes;
...
/**
 * DO NOT EDIT THIS FILE!!! IT WAS GENERATED BY AROUTER. */
public class ARouter$$Root$$app implements IRouteRoot {
  @Override
  public void loadInto(Map<String, Class<? extends IRouteGroup>> routes) {
    routes.put("group1", ARouter$$Group$$group1.class);
    routes.put("group2", ARouter$$Group$$group2.class);
  }
}
package com.alibaba.android.arouter.routes;
...
/**
 * DO NOT EDIT THIS FILE!!! IT WAS GENERATED BY AROUTER. */
public class ARouter$$Group$$group1 implements IRouteGroup {
  @Override
  public void loadInto(Map<String, RouteMeta> atlas) {
    atlas.put("/group1/first", RouteMeta.build(RouteType.ACTIVITY, FirstActivity.class, "/group1/first", "group1", new java.util.HashMap<String, Integer>(){{put("paramInt", 3); }}, -1, -2147483648));
  }
}
package com.alibaba.android.arouter.routes;
...
/**
 * DO NOT EDIT THIS FILE!!! IT WAS GENERATED BY AROUTER. */
public class ARouter$$Group$$group2 implements IRouteGroup {
  @Override
  public void loadInto(Map<String, RouteMeta> atlas) {
    atlas.put("/group2/second", RouteMeta.build(RouteType.ACTIVITY, SecondActivity.class, "/group2/second", "group2", null, -1, -2147483648));
  }
}

根据代码我们可以看到 ARouter$$Group$$xxx 中路由表信息集合的 key 就是 之前用@Route 注解的 path 参数,如 /group1/first/group/secondvalue 就是一个 RouteMeta 对象,即路由的元数据,此对象记录了本次路由映射需要的最基本必要的信息。

public class RouteMeta {
    ...
    private RouteType type;         // Type of route
    private Class<?> destination;   // Destination
    private String path;            // Path of route
    private String group;           // Group of route
    ...

至此,至此我们了解到这些辅助文件的生成规则。

四、核心库的设计

接下来就是核心库的部分,之前的动态生成了那么多 “辅助类”,现在看一下该怎么使用。

4.1 初始化

ARouter.init(this); 在Application中调用 init 方法进行初始化路由。

public final class ARouter {
    /**
     * 初始化。 在使用路由之前必须先调用
     */
    public static void init(Application application) {
        if (!hasInit) {
            ...
            hasInit = _ARouter.init(application);
            ...
        }
    }
}
final class _ARouter {
    protected static synchronized boolean init(Application application) {
        ...
        LogisticsCenter.init(mContext, executor);
        ...
    }
}

以上就是最基本的 门面设计模式 的应用。核心的初始化工作在LogisticsCenter

public class LogisticsCenter {
   public synchronized static void init(Context context, ThreadPoolExecutor tpe) throws HandlerException {
        Set<String> routerMap;
         // 遍历dex文件,找到 com.alibaba.android.arouter.routes 下被 arouter-compiler 创建的辅助类
         routerMap = ClassUtils.getFileNameByPackageName(mContext, ROUTE_ROOT_PAKCAGE);
          ...
          for (String className : routerMap) {
               //com.alibaba.android.arouter.routes.ARouter$$Root
               if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_ROOT)) {
                    // This one of root elements, load root.
                    ((IRouteRoot) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.groupsIndex);
               } else if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_INTERCEPTORS)) {
                    // Load interceptorMeta
                    ((IInterceptorGroup) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.interceptorsIndex);
               } else if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_PROVIDERS)) {
                    // Load providerIndex
                    ((IProviderGroup) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.providersIndex);
                }
          }
    }
}

以上代码是简化版本,核心逻辑是扫描dex文件中 APT 生成的辅助类,因为本篇讲的是 Activity 跳转,所以会忽略其他部分的内容。类的全类名 以com.alibaba.android.arouter.routes.ARouter$$Root 开头的,将会被通过反射构建对象实体,然后调用其 loadInto 方法把路由分组信息注入到Warehouse.groupsIndex。 Warehouse 是缓存了所有路由信息的工具类。

class Warehouse {
    //路由组信息缓存
    static Map<String, Class<? extends IRouteGroup>> groupsIndex = new HashMap<>();
    //路由表信息缓存
    static Map<String, RouteMeta> routes = new HashMap<>();
    ...
}

经过 init 初始化以后, Warehouse.groupsIndex 缓存了项目中所有路由组信息:

1、这里让大家思考一个问题,既然路由初始化时,会扫描 dex 文件,获取指定包下的类信息,假如APK被加固了, 正确的dex文件被隐藏,就无法找到相关的辅助类,该如何处理?
2、LogisticsCenter#init 初始化中,也使用到了缓存,从dex文件扫描获取到的类信息会缓存在SP里,下次就直接从SP里获取了,不必扫描 dex文件。而且这里还有版本控制,如果是新版本的话,就会重新扫描dex文件,之后更新SP里的信息。

4.2 使用路由进行页面跳转
ARouter.getInstance().build("/group1/first").navigation();

这行代码就是 跳转到 /group1/first 在路由表中所标识的真实地址: FirstActivity(参考之前APT 生成的辅助类)。其实很好想明白怎么实现,执行 navigation 方法时,根据路由表的信息,获取跳转的目标地址:FirstActivity.class,然后构造 Intent,执行startActivity方法,即可完成跳转。

在分析代码之前,我们先来了解一个类 Postcard,中文名字叫明信片,继承自 RouteMeta,除了包含最基本的路由信息外,还有一些其他信息,比如参数传递的 Bundle 对象、动画、IntentFlag等等,这个类是进行路由分发的基本数据组成单元。(注意区分 Postcard 和 RouteMeta)

/**
 * A container that contains the roadmap.
 */
public final class Postcard extends RouteMeta {
    // Base
    private Uri uri;
    private Object tag;             // A tag prepare for some thing wrong.
    private Bundle mBundle;         // Data to transform
    private int flags = -1;         // Flags of route
    private int timeout = 300;      // Navigation timeout, TimeUnit.Second
    private IProvider provider;     // It will be set value, if this postcard was provider.
    private boolean greenChannel;
    private SerializationService serializationService;

    // Animation
    private Bundle optionsCompat;    // The transition animation of activity
    private int enterAnim;
    private int exitAnim;
}

ARouter.getInstance().build("/group1/first") 此时就会构造一个 Postcard对象,此时对象属性只有 pathgroup是确定的,之后初始化Postcard对象(LogisticsCenter#completion),然后使用 Postcard 进行导航,如果 RouteTypeACTIVITY,则会构建Intent,调用ActivityCompat#startActivity 启动页面:

final class _ARouter {
    ...
    protected Object navigation(final Context context, final Postcard postcard, final int requestCode, final NavigationCallback callback) {
        try {
            // 完善 Postcard 信息
            LogisticsCenter.completion(postcard);
        } catch (NoRouteFoundException ex) {
            ...  //未找到路由信息的处理逻辑
            return null;
        }

        if (null != callback) {
            callback.onFound(postcard);
        }

        if (!postcard.isGreenChannel()) {   // It must be run in async thread, maybe interceptor cost too mush time made ANR.
            ... //拦截器相关处理
        } else {
            //导航
            return _navigation(context, postcard, requestCode, callback);
        }

        return null;
    }

      // 进行导航
     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:  //如果是 Activity
                // 构造Intent
                final Intent intent = new Intent(currentContext, postcard.getDestination());
                intent.putExtras(postcard.getExtras()); //设置bundle

                // 设置 flags
                int flags = postcard.getFlags();
                if (-1 != flags) {
                    intent.setFlags(flags);
                } else if (!(currentContext instanceof Activity)) {    // Non activity, need less one flag.
                    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                }

                // Navigation in main looper.
                new Handler(Looper.getMainLooper()).post(new Runnable() {
                    @Override
                    public void run() {
                        if (requestCode > 0) {  // Need start for result
                            ActivityCompat.startActivityForResult((Activity) currentContext, intent, requestCode, postcard.getOptionsBundle());
                        } else {
                            ActivityCompat.startActivity(currentContext, intent, postcard.getOptionsBundle());
                        }
                        //设置出入动画
                        if ((0 != postcard.getEnterAnim() || 0 != postcard.getExitAnim()) && currentContext instanceof Activity) {    // Old version.
                            ((Activity) currentContext).overridePendingTransition(postcard.getEnterAnim(), postcard.getExitAnim());
                        }
                        //导航回调
                        if (null != callback) { // Navigation over.
                            callback.onArrival(postcard);
                        }
                    }
                });

                break;
                ...
        }

        return null;
    }
}

从上边的分析可知,每次进行 navigation的时候都要创建一个 Postcard,然后根据路由表的信息,对Postcard对象进行初始化,之后进行真正的 navigation() 。我们接下来看一下,如何对 Postcard 对象进行初始化:LogisticsCenter#completion

public synchronized static void completion(Postcard postcard) {
        ...
        //根据path获取路由信息
        RouteMeta routeMeta = Warehouse.routes.get(postcard.getPath());
        //如果路由信息为空,则可能此分组的路由表还从未加载过;当然也可能是压根就找不到路由信息。
        if (null == routeMeta) {    
            // 获取分组信息
            Class<? extends IRouteGroup> groupMeta = Warehouse.groupsIndex.get(postcard.getGroup()); 
            //如果分组信息不存在,则抛出异常。有可能是开始就没有分组信息,也有可能是 路由分组信息加载过了之后会被清理。
            if (null == groupMeta) {
                throw new NoRouteFoundException(TAG + "There is no route match the path [" + postcard.getPath() + "], in group [" + postcard.getGroup() + "]");
            } else {
                try {
                    //加载此分组的路由信息到路由表,然后把此路由分组删除。
                    IRouteGroup iGroupInstance = groupMeta.getConstructor().newInstance();
                    iGroupInstance.loadInto(Warehouse.routes);
                    Warehouse.groupsIndex.remove(postcard.getGroup());
                    //如果不删除,假设navigatoin一个分组中没有的路由表信息,就会无限循环找下去了。其实只要路由分组表被加载过一次,路由表(Warehouse.routes)就会包含此分组的路由表。
                    }
                } catch (Exception e) {
                    throw new HandlerException(TAG + "Fatal exception when loading group meta. [" + e.getMessage() + "]");
                }

                completion(postcard);   // 路由分组的路由表被加载以后,调用初始化方法
            }
        } else {
            //找到路由信息,初始化Postcard
            postcard.setDestination(routeMeta.getDestination());
            postcard.setType(routeMeta.getType());
            postcard.setPriority(routeMeta.getPriority());
            postcard.setExtra(routeMeta.getExtra());

            Uri rawUri = postcard.getUri();
            ...       
        }
    }

至此, 一次完整的页面跳转的处理过程分析完成。

五、Gradle-Plugin(可选)

之前有一个问题是,加固以后APK找不到,路由信息的这些类了,怎么办?通过以上流程分析,我们可知 ARouter.init 初始化路由的时候,会去扫描 dex 文件,找到路由信息的类,然后装载 路由分组类 到内存。gradle-plugin 的作用 在编译期间动态修改字节码文件,动态插入到 LogisticsCenter 类中,改变init的逻辑。当然这是一个可选的模块。

使用gradle-plugin之前,init 的代码逻辑一定会执行 dex 扫描的逻辑,因为 loadRouterMap 方法为空, registerByPlugin 永远为false。代码详情:

     /**
     * arouter-auto-register plugin will generate code inside this method
     * call this method to register all Routers, Interceptors and Providers
     * @author billy.qi <a href="mailto:qiyilike@163.com">Contact me.</a>
     * @since 2017-12-06
     */
    private static void loadRouterMap() {
        registerByPlugin = false;
        //auto generate register code by gradle plugin: arouter-auto-register
        // looks like below:
        // registerRouteRoot(new ARouter..Root..modulejava());
        // registerRouteRoot(new ARouter..Root..modulekotlin());
    }

    public synchronized static void init(Context context, ThreadPoolExecutor tpe) throws HandlerException {
        try {
            long startInit = System.currentTimeMillis();
            //billy.qi modified at 2017-12-06
            //load by plugin first
            loadRouterMap();
            if (registerByPlugin) {
                logger.info(TAG, "Load router map by arouter-auto-register plugin.");
            } else {
                //扫描 dex,初始化路由分组信息
           }
}

使用了 gradle-plugin 之后,我们把生成的apk反编译后,看一下真实的代码是什么样的:

会在执行 loadRouterMap 方法的时候,去装载所有路由分组信息,之后 registerByPlugin 被设置成true,接下来逻辑判断的时候就不会继续使用 dex 扫描的方式。

至此,ARouter 的全貌已浮出水面,对它也有了一个更清晰的认识。

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