美文网首页androidARouter
ARouter使用与原理分析

ARouter使用与原理分析

作者: 邱穆 | 来源:发表于2020-11-11 21:21 被阅读0次

    ARouter使用与原理分析

    一、使用入门

    见官方文档:ARouter

    二、技术原理分析

    ARouter采用注解与APT技术,自动生成代码,在运行时调用,那么这里也就分为两部分,生成的代码和ARouter源代码,我们也分为两部分学习。

    2.1 调用过程和代码探析

    2.1.1 调用过程

    根据官方文档,基本的使用方法为下面这行代码:

    ARouter.getInstance().build("/test/second").navigation();
    

    根据这行代码,从Activity出发,调用时许过程如下图:

    img

    第一步:构建Postcard

    Postcard顾名思义,为“跳转卡片”,ARouter.getInstance().build("/xxx/xxxx")返回的就是Postcard对象,主要用来携带bundle跳转参数。而ARouter只是对外统一的api接口,实现基本由_ARouter完成,所以构建Postcard也是由_ARouter。

    第二步:Postcard.navigation()实现跳转

    Postcard.navigation()其实也是调用_ARouter.getInstance().navigation()完成的,即最后也是由_ARouter完成,在_ARouter.navigation时,首先调用LogisticsCenter.completion(postcard)完善postcard的信息,而completion方法,则完成了path到activity的转换关系。完善后,再调用_navigation完成最终跳转。

    第三步:LogisticsCenter.completion(postcard)将path映射到activity

    看到这一步时,会发现大致看不懂,没关系,大致明白这里的作用,待会换个思路看源码就可以了。

    第四步:_ARouter._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:
                    // Build intent
                    final Intent intent = new Intent(currentContext, postcard.getDestination());
                    intent.putExtras(postcard.getExtras());
    
                    // Set 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);
                    }
    
                    // Set Actions
                    String action = postcard.getAction();
                    if (!TextUtils.isEmpty(action)) {
                        intent.setAction(action);
                    }
    
                    // Navigation in main looper.
                    runInMainThread(new Runnable() {
                        @Override
                        public void run() {
                            startActivity(requestCode, currentContext, intent, postcard, callback);
                        }
                    });
    
                    break;
               ···············
                    return null;
            }
    
            return null;
        }
    
    

    可以很明显的看到,这一步是判断postcard的信息,然后建立intent进行跳转,也就是仍然还是通过startActivity()这个函数进行跳转,并没有太多的奥秘。重点是ARouter内部映射的实现原理和方法。

    2.1.2 主体源码分析

    刚才在调用过程中接触到了几个类,趁热打铁,先介绍下这几个类:

    • ARouter.java: 对外提供调用方法的类。

    • _ARouter.java: ARouter.java的代理类,ARouter.java中的关键方法都在_ARouter.java里实现。

    • RouteMeta.java: 包含了路由跳转所需要的信息,比如跳转类型,目标等。Postcard 继承 RouteMeta。

    • Postcard.java: 直译“跳转卡片”,ARouter.getInstance().build("/xxx/xxxx")返回的就是Postcard对象,主要用来携带bundle跳转参数。

    • Warehouse.java: 仅包含了几个集合类型(主要是Map类型)的静态变量,这几个静态集合用来保存activity和其path对应关系,以及其分组关系等。

      如:

      // Cache route and metas
      static Map<String, Class<? extends IRouteGroup>> groupsIndex = new HashMap<>();
      static Map<String, RouteMeta> routes = new HashMap<>();
      
    • LogisticsCenter.java: 实现Arouter的初始化,以及对Warehouse类里的几个集合进行填充等。

    ARouter同样也定义了好几个注解类,其中最关键的就是@Route注解,而@Route注解是使用了APT技术来发挥作用的。APT的核心类-AbstractProcessor类就是编译时用来生成代码的。ARouter源码中有个RouteProcessor.java类,继承自AbstractProcessor类,就是用来处理@Route注解的,重写的process()方法,实现了对@Route注解的处理和自动生成文件代码的逻辑。

    项目编译后,会生成代码,结构如图:

    生成的代码结构

    这里先说明下,目前我们的项目一共有4个Activity,分别是登录用的LoginActivity和LoginWeb,以及项目主体的MainActivity和ProfileActivity,所以目前的路由分组,前两个Activity分为一组,后两个Activity分为一组,如下图:

    路由分组信息

    ARouter默认用第一个斜杠后面的字符串表示分组信息。

    于是ARouter生成的代码文件,Group有两个,后缀分别是我们的两个分组。我们分别学习这几个文件:

    • 以"Arouter$$Root$$组件名"命名的文件,即我们的ARouter$$Root$$app.java,是用来保存页面路径的分组和路径文件间的对应关系的,甚至代码只有4行:
    public class ARouter$$Root$$app implements IRouteRoot {
      @Override
      public void loadInto(Map<String, Class<? extends IRouteGroup>> routes) {
        routes.put("app", ARouter$$Group$$app.class);
        routes.put("login", ARouter$$Group$$login.class);
      }
    }
    

    这个类中的loadInto()方法,是在ARouter初始化的时候,即ARouter.init(mApplication) -> LogisticsCenter.init()中被调用的。传入的routes即为Warehouse类的静态变量Warehouse.groupsIndex。也就是为路由分组这个Map设置了两个键值关系。

    • 以“Arouter$$Group$$分组”命名的文件,即我们的Arouter$$Group$$login.java和Arouter$$Group$$app.java,是用来保存该分组下,页面path和对应的页面关系的。有几个分组,就会有几个“Arouter$$Root$$分组”文件。
    public class ARouter$$Group$$app implements IRouteGroup {
      @Override
      public void loadInto(Map<String, RouteMeta> atlas) {
        atlas.put("/app/main", RouteMeta.build(RouteType.ACTIVITY, MainActivity.class, "/app/main", "app", null, -1, -2147483648));
        atlas.put("/app/profile", RouteMeta.build(RouteType.ACTIVITY, ProfileActivity.class, "/app/profile", "app", new java.util.HashMap<String, Integer>(){{put("login", 8); }}, -1, -2147483648));
      }
    }
    
    public class ARouter$$Group$$login implements IRouteGroup {
      @Override
      public void loadInto(Map<String, RouteMeta> atlas) {
        atlas.put("/login/index", RouteMeta.build(RouteType.ACTIVITY, LoginActivity.class, "/login/index", "login", null, -1, -2147483648));
        atlas.put("/login/oauth", RouteMeta.build(RouteType.ACTIVITY, LoginWeb.class, "/login/oauth", "login", new java.util.HashMap<String, Integer>(){{put("url", 8); }}, -1, -2147483648));
      }
    }
    

    这个类的loadInto()方法,传入的是Warehouse类的另一个静态变量Warehouse.routes,但是此方法不是在ARouter初始化的时候调用,而是在页面跳转的时候,如果满足条件“要跳转的页面是其所在的分组首个要跳转的”才调用,加载完Warehouse.routes后继续跳转页面。这样也就实现了分组加载,避免加载过多,只加载该页面所属分组的映射关系,减少了不必要的内存耗费。

    代码调用链:ARouter.getInstance().navigation() -> _ARouter.getInstance().navigation() -> LogisticsCenter.completion() -> IRouteGroup.loadInto(Warehouse.routes),这里也就接上了之前讲到的调用过程的第三步。

    下面我们再来详细看一下_Arouter类的build()方法:

        /**
         * 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 {
            PathReplaceService pService = ARouter.getInstance().navigation(PathReplaceService.class);
            if (null != pService) {
                path = pService.forString(path);
            }
            return build(path, extractGroup(path), true);
        }
    }
    

    一般情况下我们都不会自定义路径替换逻辑,即不会实现PathReplaceService,因此代码会走到最后一行:return build(path, extractGroup(path), true),extractGroup(path)这个方法的作用是提取出路径的分组部分,比如页面path为“/login/index”,则extractGroup(path)得到的结果是“login”,再继续看build(path, extractGroup(path), true)的代码:

        /**
         * Build postcard by path and group
         */
        protected Postcard build(String path, String group, Boolean afterReplace) {
            if (TextUtils.isEmpty(path) || TextUtils.isEmpty(group)) {
                throw new HandlerException(Consts.TAG + "Parameter is invalid!");
            } else {
                if (!afterReplace) {
                    PathReplaceService pService = ARouter.getInstance().navigation(PathReplaceService.class);
                    if (null != pService) {
                        path = pService.forString(path);
                    }
                }
                return new Postcard(path, group);
            }
        }
    

    afterReplace为true,因此ARouter.getInstance().build("/login/index")的运行结果就是new Postcard("/login/index", “/login”)。

    那么ARouter.getInstance().build("/login/index")..navigation()就是:new Postcard("/login/index", “/login”).navigation()。

    再来看Postcard做了什么处理:

    public Postcard() {
            this(null, null);
        }
    
        public Postcard(String path, String group) {
            this(path, group, null, null);
        }
    
        public Postcard(String path, String group, Uri uri, Bundle bundle) {
            setPath(path);
            setGroup(group);
            setUri(uri);
            this.mBundle = (null == bundle ? new Bundle() : bundle);
        }
    

    可以看到new Postcard("/login/index", “/login”)是得到一个Postcard对象,其成员变量path,group和mBundle都已经赋值。

    再看Postcard的navigation()方法:

    public Object navigation() {
            return navigation(null);
        }
    
        public Object navigation(Context context) {
            return navigation(context, null);
        }
    
        public Object navigation(Context context, NavigationCallback callback) {
            return ARouter.getInstance().navigation(context, this, -1, callback);
        }
    

    ARouter的navigation()方法是由_ARouter实现的,方法较长,这里贴出简化后的代码:

    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());
            xxx处理
            return null;
        }
        xxxx处理
        return _navigation(context, postcard, requestCode, callback);
    }
    

    可见,通过LogisticsCenter.completion(postcard)补充了postcard后,传入了_navigation()方法。而_navigation()在之前的2.1.1中就有说明,就是根据postcard携带的信息构造出Intent对象等,然后调用ActivityCompat.startActivity()方法启动页面。最后再来看看按需加载的实现方法:

        public synchronized static void completion(Postcard postcard) {
            RouteMeta routeMeta = Warehouse.routes.get(postcard.getPath());
            if (null == routeMeta) {    
                // 首次跳转此分组的页面时,Warehouse.routes未加载此分组,因此会走到这里
                // 此路由信息不存在,或者未加载,所以找不到
                Class<? extends IRouteGroup> groupMeta = Warehouse.groupsIndex.get(postcard.getGroup());  // Load route meta.
                
                // 加载此分组的path和页面映射关系到Warehouse.routes中
                try {
                    IRouteGroup iGroupInstance = groupMeta.getConstructor().newInstance();
                    iGroupInstance.loadInto(Warehouse.routes);
                } catch (Exception e) {
                    throw new HandlerException(TAG + "Fatal exception when loading group meta. [" + e.getMessage() + "]");
                }
    
                completion(postcard);   // 再次调用本方法,补充postcard
                
            } else {
                // 非首次跳转此分组的页面,则会走到这里
                // 已找到路由信息,直接填充给postcard
                postcard.setDestination(routeMeta.getDestination());
                postcard.setType(routeMeta.getType());
                postcard.setPriority(routeMeta.getPriority());
                postcard.setExtra(routeMeta.getExtra());
                
                xxx其它信息的添加
            }
        }
    

    2.1.3 映射和分组源码分析

    刚才提到,Warehouse.java类包含了几个集合类型(主要是Map类型)的静态变量,这几个静态集合用来保存activity和其path对应关系,以及其分组关系等。

    首先看如何分组的,Arouter在一层map之外,增加了一层map,我们看WareHouse这个类,里面有两个静态Map:

        // Cache route and metas
        static Map<String, Class<? extends IRouteGroup>> groupsIndex = new HashMap<>();
        static Map<String, RouteMeta> routes = new HashMap<>();
    
    • groupsIndex 保存了group名字到IRouteGroup类的映射,这一层映射就是Arouter新增的一层映射关系。
    • routes 保存了路径path到RouteMeta的映射,其中,RouteMeta是目标地址的包装。这一层映射关系跟我门自己方案里的map是一致的,我们路径跳转也是要用这一层来映射。

    RouteMeta很简单,就是对跳转目标的封装,其内包含了目标组件的信息,比如Activity的Class。定义如下:

    public class RouteMeta {
        private RouteType type;         // Type of route
        private Element rawType;        // Raw type of route
        private Class<?> destination;   // Destination
        private String path;            // Path of route
        private String group;           // Group of route
        private int priority = -1;      // The smaller the number, the higher the priority
        private int extra;              // Extra data
        private Map<String, Integer> paramsType;  // Param type
        private String name;
    
        private Map<String, Autowired> injectConfig;  // Cache inject config.
    
        public RouteMeta() {
        }
    

    IRouteGroup是一个接口,只有一个方法loadInto。

    public interface IRouteGroup {
        /**
         * Fill the atlas with routes in group.
         */
        void loadInto(Map<String, RouteMeta> atlas);
    }
    

    至于实现了这个方法的类,就是之前提到的apt生成的以“Arouter$$Group$$分组”命名的类,他们创建了RouteMeta并填充到atlas中,形成Map映射文件,映射路径到具体的类。我们从上面拿一个下来看看:

    public class ARouter$$Group$$app implements IRouteGroup {
      @Override
      public void loadInto(Map<String, RouteMeta> atlas) {
        atlas.put("/app/main", RouteMeta.build(RouteType.ACTIVITY, MainActivity.class, "/app/main", "app", null, -1, -2147483648));
        atlas.put("/app/profile", RouteMeta.build(RouteType.ACTIVITY, ProfileActivity.class, "/app/profile", "app", new java.util.HashMap<String, Integer>(){{put("login", 8); }}, -1, -2147483648));
      }
    }
    

    一目了然,这个类加载的映射关系,都是在一个组内。可以总结出:ARouter通过apt技术,为每个组生成了一个以Arouter$$Group开头的类,这个类负责向atlas这个Hashmap中填充组内路由数据。

    那IRouteGroup外面的这一层HashMap呢?也就是groupsIndex呢?经过查阅,这个Map对应的正是以"Arouter$$Root$$组件名"命名的类,也就是我们的ARouter$$Root$$app.java:

    public class ARouter$$Root$$app implements IRouteRoot {
      @Override
      public void loadInto(Map<String, Class<? extends IRouteGroup>> routes) {
        routes.put("app", ARouter$$Group$$app.class);
        routes.put("login", ARouter$$Group$$login.class);
      }
    }
    

    这个类实现了IRouteRoot,在loadInto方法中,他将组名和组对应的“组加载器”保存到了routes这个map中。也就是说,这个类将所有的“组加载器”给索引了下来,通过任意一个组名,可以找到对应的“组加载器”。

    到此为止,Arouter只是完成了分组工作,而这个分组,也就是之前2.1.2讲到的分组加载的依仗。

    下面用一张图来理清他们的关系:

    Arouter分组按需加载演示图

    左侧groupsIndex是“组映射”,右侧routes是“路由映射”。Arouter在初始化的时候,通过反射技术,将所有的“组加载器”索引到groupsIndex这个map中,而此时,右侧的routes还是空的。在用户调用navigation()进行跳转的时候,会根据路径提取组名,由组名根据groupsIndex获取到相应组的“组加载器”,由组加载器加载对应组内的路由信息,此时保存全局“路由目标映射的”routes这个map中就保存了刚才组的所有路由映射关系了。同样,当其他组请求时,其他组也会加载组对应的路由映射,这样就实现了整个App运行时,只有用到的组才会加到内存中,没有去过的组就不会加载到内存中,达到了节省内存的目的。

    相关文章

      网友评论

        本文标题:ARouter使用与原理分析

        本文链接:https://www.haomeiwen.com/subject/cbpybktx.html