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

在前面中我们对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

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

推荐阅读更多精彩内容

  • 在前面文章中对ARouter中页面跳转和源码进行了分析,今天我们来学习下通过URL跳转本地页面的使用和跳转源码分析...
    juexingzhe阅读 7,878评论 4 14
  • 开发一款App,总会遇到各种各样的需求和业务,这时候选择一个简单好用的轮子,就可以事半功倍 前言 上面一段代码,在...
    WangDeFa阅读 65,685评论 44 199
  • 本文章用于记录笔者学习 ARouter 源码的过程,仅供参考,如有错误之处还望悉心指出,一起交流学习。 ARout...
    DevLocke阅读 13,945评论 6 52
  • 如果和互相懂得的人在一起,晴天可以逛城市,游风景,雨天可以逛书店,看画展。 如果和相爱的人在一起,晴天可以一起出门...
    靖理阅读 376评论 0 3
  • 正如别人所说,我是个十足的路痴,在家乡的地下通道会出来不知往哪边走,夜里的城市是另外一副模样……我也不知道是什么让...
    鲤鱼梦阅读 276评论 0 0