ARouter解析二:页面跳转源码分析

96
juexingzhe
2017.06.26 23:42* 字数 1674

在前面中我们对ARouter的页面跳转功能的使用有了基本的了解,由于篇幅的原因没有对跳转的源码进行分析,今天我们就来探究一下页面的跳转过程。在看这篇文章之前建议小伙伴们先看下面链接给出的文章好有个整体的了解。
ARouter解析一:基本使用及页面注册源码解析

整个流程示意图如下,接下来我们会对着这个示意图开始开车。


页面跳转流程.png

1.获取ARouter实例

我们先从简单的说起,不带参数的页面跳转,一行代码实现。

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

用户基本需要打交道的接口都在ARouter中,该类使用的是单例模式。使用这个框架的前提就是需要初始化Router,否则会报错。

初始化错误.png

调用ARouter.init(getApplication());进行初始化。单例模式是典型写法,有两个if判断,第一个判断没什么可说的,之后synchronized上锁,再进行判断是否null,这个主要是为了多线程环境保护。

public static ARouter getInstance() {
        if (!hasInit) {
            throw new InitException("ARouter::Init::Invoke init(context) first!");
        } else {
            if (instance == null) {
                synchronized (ARouter.class) {
                    if (instance == null) {
                        instance = new ARouter();
                    }
                }
            }
            return instance;
        }
}

2.构造路由信息的容器Postcard

得到ARouter实例后调用build方法,传入目标页面的path("/test/activity2"),我们来看看build的源码。这里使用的是代理模式,其实是调用_ARouter的build方法,这里需要提的一点是ARouter.init也是调用的_ARouter的init方法,里面主要是做一些映射文件的加载工作。

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

接着往下看,来到_ARouter的build方法,注意路径不能为空,也就是目标页面必须要有注解@Route(path = "/test/activity2")。然后会返回一个Postcard,官方解释A container that contains the roadmap.这是个路由信息的存储器,里面包含页面跳转的所有信息。

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

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

new Postcard(path, group)中,第一个参数就是路径,第二个参数是组别信息。那我们的栗子path = "/test/activity2来说test就是group。这里就需要提下,ARouter框架是分组管理,按需加载。提起来很高深的样子呢!其实解释起来就是,在编译期框架扫描了所有的注册页面/服务/字段/拦截器等,那么很明显运行期不可能一股脑全部加载进来,这样就太不和谐了。所以就分组来管理,ARouter在初始化的时候只会一次性地加载所有的root结点,而不会加载任何一个Group结点,这样就会极大地降低初始化时加载结点的数量。比如某些Activity分成一组,组名就叫test,然后在第一次需要加载组内的某个页面时再将test这个组加载进来。

3.路由信息完善与跳转

ok,我们言归正传,就下来就是一行代码的最后一个方法navigation。这里其实是postcard的navigation方法。

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

最后会来到_ARouter的navigation方法,方法比较长,为了更好的说清今天的主题我做了点手脚删掉一些,不要打我:)我们分成几个步骤,第二个回调的步骤没什么可说的,接下来详细解释下第一和第三步。

1.首先调用LogisticsCenter.completion完成postcard的补充,这个详见后面解析。

2.然后如果有回调函数就进行回调。

3.如果需要拦截,就进行拦截器的处理,否则就调用_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 (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);
        }
        if (!postcard.isGreenChannel()) { //如果需要拦截
        ……
        } else {//不需要拦截
            return _navigation(context, postcard, requestCode, callback);
        }

        return null;
}

3.1.路由信息完善

postcard我们前面说过是所有路由信息的容器,那么到目前为止我们的postcard中只有path和group的信息,目标页面是什么还不知道,是不是我吹牛了?别急,LogisticsCenter.completion就是干这个活的,用来补充postcard信息的。我们看下源码,也是比较长。嘿嘿你猜错了,这个我就不再做删减,原生的,我们一步步来看。

public synchronized static void completion(Postcard postcard) {
        if (null == postcard) {
            throw new NoRouteFoundException(TAG + "No postcard!");
        }

        RouteMeta routeMeta = Warehouse.routes.get(postcard.getPath());
        if (null == routeMeta) {    // Maybe its does't exist, or didn't load.
            Class<? extends IRouteGroup> groupMeta = Warehouse.groupsIndex.get(postcard.getGroup());  // Load route meta.
            if (null == groupMeta) {
                throw new NoRouteFoundException(TAG + "There is no route match the path [" + postcard.getPath() + "], in group [" + postcard.getGroup() + "]");
            } else {
                // Load route and cache it into memory, then delete from metas.
                try {
                    if (ARouter.debuggable()) {
                        logger.debug(TAG, String.format(Locale.getDefault(), "The group [%s] starts loading, trigger by [%s]", postcard.getGroup(), postcard.getPath()));
                    }

                    IRouteGroup iGroupInstance = groupMeta.getConstructor().newInstance();
                    iGroupInstance.loadInto(Warehouse.routes);
                    Warehouse.groupsIndex.remove(postcard.getGroup());

                    if (ARouter.debuggable()) {
                        logger.debug(TAG, String.format(Locale.getDefault(), "The group [%s] has already been loaded, trigger by [%s]", postcard.getGroup(), postcard.getPath()));
                    }
                } catch (Exception e) {
                    throw new HandlerException(TAG + "Fatal exception when loading group meta. [" + e.getMessage() + "]");
                }

                completion(postcard);   // Reload
            }
        } else {
            postcard.setDestination(routeMeta.getDestination());
            postcard.setType(routeMeta.getType());
            postcard.setPriority(routeMeta.getPriority());
            postcard.setExtra(routeMeta.getExtra());

            Uri rawUri = postcard.getUri();
            if (null != rawUri) {   // Try to set params into bundle.
                Map<String, String> resultMap = TextUtils.splitQueryParameters(rawUri);
                Map<String, Integer> paramsType = routeMeta.getParamsType();

                if (MapUtils.isNotEmpty(paramsType)) {
                    // Set value by its type, just for params which annotation by @Param
                    for (Map.Entry<String, Integer> params : paramsType.entrySet()) {
                        setValue(postcard,
                                params.getValue(),
                                params.getKey(),
                                resultMap.get(params.getKey()));
                    }

                    // Save params name which need autoinject.
                    postcard.getExtras().putStringArray(ARouter.AUTO_INJECT, paramsType.keySet().toArray(new String[]{}));
                }

                // Save raw uri
                postcard.withString(ARouter.RAW_URI, rawUri.toString());
            }

            switch (routeMeta.getType()) {
                case PROVIDER:  // if the route is provider, should find its instance
                    // Its provider, so it must be implememt IProvider
                    Class<? extends IProvider> providerMeta = (Class<? extends IProvider>) routeMeta.getDestination();
                    IProvider instance = Warehouse.providers.get(providerMeta);
                    if (null == instance) { // There's no instance of this provider
                        IProvider provider;
                        try {
                            provider = providerMeta.getConstructor().newInstance();
                            provider.init(mContext);
                            Warehouse.providers.put(providerMeta, provider);
                            instance = provider;
                        } catch (Exception e) {
                            throw new HandlerException("Init provider failed! " + e.getMessage());
                        }
                    }
                    postcard.setProvider(instance);
                    postcard.greenChannel();    // Provider should skip all of interceptors
                    break;
                case FRAGMENT:
                    postcard.greenChannel();    // Fragment needn't interceptors
                default:
                    break;
            }
        }
}

仓库查找页面结点
首先根据路径信息到Warehouse仓库中查找路由节点信息,其实就是几个Map,包含有根节点/拦截器和组别等。

RouteMeta routeMeta = Warehouse.routes.get(postcard.getPath());
class Warehouse {
    // Cache route and metas
    static Map<String, Class<? extends IRouteGroup>> groupsIndex = new HashMap<>();
    static Map<String, RouteMeta> routes = new HashMap<>();

    // Cache provider
    static Map<Class, IProvider> providers = new HashMap<>();
    static Map<String, RouteMeta> providersIndex = new HashMap<>();

    // Cache interceptor
    static Map<Integer, Class<? extends IInterceptor>> interceptorsIndex = new UniqueKeyTreeMap<>("More than one interceptors use same priority [%s]");
    static List<IInterceptor> interceptors = new ArrayList<>();
}

一开始肯定是没有这个节点信息的,所以需要到Warehouse.groupsIndex中找到组别的信息,这里就是test.

Class<? extends IRouteGroup> groupMeta = Warehouse.groupsIndex.get(postcard.getGroup());  // Load route meta.

然后通过反射加载这一组类别的映射关系,就是前面提到的按需加载。然后从仓库中删除这个组别信息节点,防止重复加载。可以看见编译期间已经组成了RouteMeta这个结点信息,包含有目标页面,类型,路径,组别,参数,优先级等信息。

 IRouteGroup iGroupInstance = groupMeta.getConstructor().newInstance();
iGroupInstance.loadInto(Warehouse.routes);
Warehouse.groupsIndex.remove(postcard.getGroup());

我们再看下生成的映射关系文件ARouter$$Group$$test长什么样.

ARouter$$Group$$test.png

接下来会递归调用completion(postcard),现在routeMeta就不为空了,会走到else中,首先给postcard补充信息,有了这些信息postcard就可以愉快的工作了。我们这个栗子中type很明显是activity,所以就走到default中break出来了。

3.2. _navigation跳转

绕了一大圈终于要进行跳转了aaa!我们来看下怎么跳转的,可以先猜下,无法也是startActivity,orz。来到ACTIVITY分支,从postcard中拿到目标页面Test2Activity.class然后组成intent,然后putExtras,如果是startActivityForResult,这里面就有参数。如果context不是activity,那么就需要另起一个栈Intent.FLAG_ACTIVITY_NEW_TASK进行activity的展示。接下来通过handler发送启动activity的任务。终于找到了熟悉的ActivityCompat.startActivityActivityCompat.startActivityForResult,真是泪流满面。后面就顺理成章了。

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

                // 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;
            case PROVIDER:
                return postcard.getProvider();
            case BOARDCAST:
            case CONTENT_PROVIDER:
            case FRAGMENT:
                Class fragmentMeta = postcard.getDestination();
                try {
                    Object instance = fragmentMeta.getConstructor().newInstance();
                    if (instance instanceof Fragment) {
                        ((Fragment) instance).setArguments(postcard.getExtras());
                    } else if (instance instanceof android.support.v4.app.Fragment) {
                        ((android.support.v4.app.Fragment) instance).setArguments(postcard.getExtras());
                    }

                    return instance;
                } catch (Exception ex) {
                    logger.error(Consts.TAG, "Fetch fragment instance error, " + TextUtils.formatStackTrace(ex.getStackTrace()));
                }
            case METHOD:
            case SERVICE:
            default:
                return null;
        }

        return null;
}

4.总结

页面跳转的源码基本就是这些内容了,分享内容只是以页面跳转不带参数为栗子,其实带参数和页面跳转动画设置都是一样的,信息都在postcard中,在LogisticsCenter.completion进行构造,依此类推。可以看出整个框架分层仔细,各个层之间分工明确。与编译期间映射关系打交道的工作都下层到LogisticsCenter,与用户打交道的API都在ARouter中。学习一个框架最好也可以学习下设计方法,提升内功。

后面当然还有解析三,会分享下url跳转的使用和源码分析等内容,欢迎关注哦。

你们的赞是我坚持的最大动力,谢谢!

欢迎关注公众号:JueCode

Android开发之路
Web note ad 1