美文网首页
ARouter 简单使用及原理

ARouter 简单使用及原理

作者: jkwen | 来源:发表于2021-04-14 08:05 被阅读0次

    ARouter

    帮助 Android App 进行组件化改造的路由框架。
    一个最显著的作用就是实现两个互相不依赖的 module 进行页面跳转和数据传递,是项目组件化中衔接各 module 页面跳转通信很好的实现方式。

    如何使用

    官方 github 上的说明文档还是蛮清晰明了易上手的 在这我更想记录我对它的源码分析和理解。

    源码分析

    先从初始化说起,

    //首先会在 Application 里调用 ARouter 的 init() 方法
    public static void init(Application application) {
        //hasInit 是用 volatile 修饰的,
        //volatile 是 Java 虚拟机提供的最轻量级的同步机制,volatile 修饰的变量对所有线程具有可见性
        //也就是说可以认为任一线程获取到的值就是实际值,并非缓存值
        if (!hasInit) {
            //调用 _ARouter 的 init 方法进行实际初始化
            hasInit = _ARouter.init(application);
            if (hasInit) {
                _ARouter.afterInit();
            }
        }
    }
    //_ARouter
    protected static synchronized boolean init(Application application) {
        //入参赋值给了 Context 类型对象,看来是要用 Application 的 Context 类型功能
        mContext = application;
        //executor 是个线程池处理器,默认线程数是设备 cpu数 + 1
        LogisticsCenter.init(mContext, executor);
        //这里也有一个标记位
        hasInit = true;
        //创建一个主线程的 Handler 对象,估计用来做主线程相关的操作,例如 UI 更新等
        mHandler = new Handler(Looper.getMainLooper());
        //返回结果给前面的 hasInit,这样一来就会执行 _ARouter 的 afterInit() 方法
        return true;
    }
    //再来看看 LogisticsCenter 的 init() 方法
    public synchronized static void init(Context context, ThreadPoolExecutor tpe) {
        mContext = context;
        executor = tpe;
        //这里有个通过插件方式的加载,不过看似好像废弃了,我们直接看 else 部分
        Set<String> routerMap;
        if (ARouter.debuggable() || PackageUtils.isNewVersion(context)) {
            //ROUTE_ROOT_PAKCAGE 指的是 ARoute 包名,~这个package单词是拼错了吗,还是故意这么写~
            //这个工具方法作用就是找出该包下面的所有类名
            //这里有个疑问,如果是 ARoute 自己的类,那每次应该都一样,感觉没用。结合这个变量名 routerMap,
            //我想应该存的是要路由的类名,有可能是通过 @Route,这里先记着。
            routerMap = ClassUtils.getFileNameByPackageName(mContext, ROUTE_ROOT_PAKCAGE);
            if (!routerMap.isEmpty()) {
                context.getSharedPreferences(AROUTER_SP_CACHE_KEY, Context.MODE_PRIVATE).edit().putStringSet(AROUTER_SP_KEY_MAP, routerMap).apply();
            }
            PackageUtils.updateVersion(context);
        } else {
            //如果 debug 没开或者不是新包或者新版本的话就从缓存里取
            //所以应该说,只要是第一次跑,都会执行上面 if 语句
            routerMap = new HashSet<>(context.getSharedPreferences(AROUTER_SP_CACHE_KEY, Context.MODE_PRIVATE).getStringSet(AROUTER_SP_KEY_MAP, new HashSet<String>()));
        }
        //遍历 roterMap,缓存一些对象,看循环内的逻辑,可见 routerMap 应该是 ARoute 包自身的类名集合
        for (String className : routerMap) {
            //根据类名判断,结合源码其实可以知道,要找的类就是 routes 包下面的
            //ARouter$$Root$$arouterapi
            //ARouter$$Providers$$arouterapi
            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);
            }
        }
    }
    

    路径注解,就是在具体 Activity 页面上添加的 @Route 注解标识路径,

    @Route(path = "/xxx/xxx/xxx")
    //像这样在指定 Activity 上加上路径说明,
    //不过 Route 这个注解其实还可以配置其他值,例如 name,group
    

    接下来就是进行页面跳转时的路由操作,

    //像这样可实现路由跳转
    ARouter.getInstance().build("/xxx/xxx/xxx").navigation();
    //先看 ARouter 的 build() 方法
    public Postcard build(String path) {
        //调用了 _ARouter 的 build() 方法,返回 Postcard 类型对象
        return _ARouter.getInstance().build(path);
    }
    

    Postcard

    这个概念说是代表着包含着路由表信息的实体,继承自 RouteMeta,这个父类倒是包含了路由的一些基础信息。总之路由过程中,它应该就是那个载体了。另外还可配置切换动画,用于页面转场效果。

    //继续 _ARouter 的 build() 方法
    protected Postcard build(String path) {
        //这个 Service 会从刚才初始化时遍历 routeMap 过程的 Warehouse.providersIndex 里去找
        //但根据分析肯定找不到,然后会去重建,但我经过最新版本 1.5.1 的断点也没看到创建成功,
        //我们就按代码实际运行的逻辑来
        PathReplaceService pService = ARouter.getInstance().navigation(PathReplaceService.class);
        if (null != pService) {
            path = pService.forString(path);
        }
        //这里会先提取 group 字符串,假设路径为 /aaa/bbb/ccc,那么 aaa 就是这里的 group
        return build(path, extractGroup(path), true);
    }
    protected Postcard build(String path, String group, Boolean afterReplace) {
        if (!afterReplace) {
            PathReplaceService pService = ARouter.getInstance().navigation(PathReplaceService.class);
            if (null != pService) {
                path = pService.forString(path);
            }
        }
        //最终创建 Postcard 对象,赋值 Postcard 对象的 path 和 group 字段
        return new Postcard(path, group);
    }
    

    完了之后是 Postcard 对象调用 navigation() 方法,

    //Postcard 的三个重载方法
    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
    public Object navigation(Context mContext, Postcard postcard, int requestCode, NavigationCallback callback) {
        return _ARouter.getInstance().navigation(mContext, postcard, requestCode, callback);
    }
    //_ARouter
    protected Object navigation(final Context context, final Postcard postcard, final int requestCode, final NavigationCallback callback) {
        try {
            //核心部分
            LogisticsCenter.completion(postcard);
        }
        //这里如果需要回调就要在入参里传入 NavigationCallback对象
        if (null != callback) {
            callback.onFound(postcard);
        }
        // greenChannel 说是配置是否忽略拦截
        //因为默认没有配置拦截器,也没有配置不忽略,所以会进入拦截处理
        if (!postcard.isGreenChannel()) {   // It must be run in async thread, maybe interceptor cost too mush time made ANR.
            //这个 interceptorService 就是初始化的时候 afterInit() 里面做的创建对象
            //它的创建过程和页面跳转很像,区别在于 path 不同
            //这里先假设它已经创建好了,如果需要拦截处理,应该就是通过 onInterrupt 回调方法处理
            //下一步就会调用 _navigation() 方法
            interceptorService.doInterceptions(postcard, new InterceptorCallback() {
                public void onContinue(Postcard postcard) {
                    _navigation(context, postcard, requestCode, callback);
                }
                public void onInterrupt(Throwable exception) {
                    if (null != callback) {
                        callback.onInterrupt(postcard);
                    }
                }
            }
        } else {
            return _navigation(context, postcard, requestCode, callback);
        }
    }
    private Object _navigation(final Context context, final Postcard postcard, final int requestCode, final NavigationCallback callback) {
        final Context currentContext = null == context ? mContext : context;
        //根据 Postcard 类型会有不同的处理
        //我们就只看 activity 的,其他的还有 service, provider, fragment, ContentProvider 等
        //这里有个疑问,结合前面的分析,postcard 的 type 值没见它赋值过啊,难道忽略了哪个步骤?
        //估计在核心部分 completion 里,不急等下去好好拆开看看
        switch (postcard.getType()) {
            case ACTIVITY:
                //同样以下的 destination,携带的数据,action 等都留着等下分析
                final Intent intent = new Intent(currentContext, postcard.getDestination());
                intent.putExtras(postcard.getExtras());
                int flags = postcard.getFlags();
                if (-1 != flags) {
                    intent.setFlags(flags);
                } else if(!(currentContext instanceof Activity)) {
                    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                }
                String action = postcard.getAction();
                if (!TextUtils.isEmpty(action)) {
                    intent.setAction(action);
                }
                //这个 runInMainThread 方法是它自己写的,内部我看了下就是用到了初始化时创建的那个 Handler 对象,来确保在安卓主线程
                runInMainThread(new Runnable() {
                    public void run() {
                        //startActivity 也是它自己写的,里面主要兼容了是否需要考虑返回结果,以及 Activity 的切换动画,以及成功跳转后的一个回调。
                        //所以 Activity 的跳转最终还是系统的方法,不会有别的
                        startActivity(requestCode, currentContext, intent, postcard, callback);
                    }
                });
        }
    }          
    

    Completion 来看下核心部分

    public synchronized static void completion(Postcard postcard) {
        //从 Warehouse.routes 里取出路径相关联的 RouteMeta 对象
        //从前面初始化过程知道,最先执行这里的是创建 interceptorService 的时候,
        //interceptorService 的路径是 /arouter/service/interceptor
        //一开始肯定找不到,routeMeta 为 null
        //----------------------------------------------------------------
        //递归调用回来,这时 Warehouse.routes 里就有 /arouter/service/interceptor 这个路径对应的 RouteMeta 对象了,即 InterceptorServiceImpl 对象,这就能看前面拦截器里面所做的逻辑细节了。
        RouteMeta routeMeta = Warehouse.routes.get(postcard.getPath());
        if (null == routeMeta) {
            //interceptorService 的 group 的值为 arouter,这在前面初始化的时候存进去了
            //所以得到的 groupMeta 对象就是 ARouter$$Group$$arouter 类型
            Class<? extends IRouteGroup> groupMeta = Warehouse.groupsIndex.get(postcard.getGroup());
            IRouteGroup iGroupInstance = groupMeta.getConstructor().newInstance();
            //在这里就会加载 /arouter/service/autowired, /arouter/service/interceptor 两个路径及对应的数据
            iGroupInstance.loadInto(Warehouse.routes);
            //接着在 group 组里移除了 arouter
            Warehouse.groupsIndex.remove(postcard.getGroup());
            //递归调用
            completion(postcard);
        } else {
            //这里一顿赋值,像 interceptorService 在刚才 loadInto 的时候做了初始化
            postcard.setDestination(routeMeta.getDestination());
            postcard.setType(routeMeta.getType());
            postcard.setPriority(routeMeta.getPriority());
            postcard.setExtra(routeMeta.getExtra());
            //对 uri 做了单独处理
            Uri rawUri = postcard.getUri();
            if (null != rawUri) {
                Map<String, String> resultMap = TextUtils.splitQueryParameters(rawUri);
                Map<String, Integer> paramsType = routeMeta.getParamsType();
                if (MapUtils.isNotEmpty(paramsType)) {
                    for (Map.Entry<String, Integer> params : paramsType.entrySet()) {
                        setValue(postcard,
                                 params.getValue(),
                                 params.getKey(),
                                 resultMap.get(params.getKey()));
                    }
                    postcard.getExtras().putStringArray(ARouter.AUTO_INJECT, paramsType.keySet().toArray(new String[]{}));
                }
                postcard.withString(ARouter.RAW_URI, rawUri.toString());
            }
            //下面的代码逻辑和 activity 不相关就不看了
        }
    }
    

    以上是拿 interceptorService 做的分析,如果是 Activity 应该是同理的,

    首先一个 group 对应一组路由(即 Warehouse.routes),刚开始 Activity 的 path 肯定也没有对应的 RouteMeta 于是找到对应 IRouteGroup 对象,通过它去 load 路由列表。不同点在于,ARouterGrouparouter 类是已经在依赖包里的,而用于 Activity 跳转的这些 IRouteGroup 类应该是通过 @Route 注解编译生成的,大概看个长相,

    /**
     * DO NOT EDIT THIS FILE!!! IT WAS GENERATED BY AROUTER. */
    //长的就像这样,不可编辑,因为是框架生成的
    public class ARouter$$Group$$login implements IRouteGroup {
        @Override
        public void loadInto(Map<String, RouteMeta> atlas) {
            atlas.put("/login/fastway/login", RouteMeta.build(RouteType.ACTIVITY, FastLoginActivity.class, "/login/fastway/login", "login", null, -1, -2147483648));
        }
    }
    

    这样一来,同一个 group 下的路径都被加载了。这也看出同组间的页面跳转会比不同组之间的页面跳转省事很多。

    加载了路由列表后,接下去递归的逻辑也和 interceptorService 类似。到这就能解释前面 postcard 的 getType 等值是怎么来的了。

    最后,再来回顾一遍整体逻辑。首先添加依赖进行相关 gradle 配置,接着定义好页面路由路径,配置初始化。框架的加载逻辑是,先通过初始化过程完成路由表组的加载(就是根据 @Route 以及框架预先定义的组进行加载),然后在真正要进行页面跳转时,从缓存路由列表里获取路由信息,找不到则通过事先加载的路由表组进行加载路由列表,找得到则继续组装具体页面相关的一些信息,最后通过系统完成页面跳转。

    题外话

    源码这么看下来,发现 ARouter 和 _ARouter 的关系有点像 ContextWrapper 和 mBase 还有 PhoneWindow 和 mDecor 的关系,一个对外,一个对内。看似工作是 ARouter 做的,实际上都是 _ARouter 做的。

    我觉得多看源码的好处就是可以模仿源码的代码实现,慢慢的逼格就上去了。

    相关文章

      网友评论

          本文标题:ARouter 简单使用及原理

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