ARoute初步探究(二):路由定位过程

上一篇文章我们记录了在编译期ARoute将目标注解生成文件保存的过程,这一节我们来探究下在用户使用时ARoute做了什么

两条路径保证

在官方README中我们能看见关于AutoRegister的介绍,借由这个gradle插件能实现路由表的自动加载。这节我们先不讲这个,关心下普通状态下的加载过程。

前期准备工作

初始化

在使用ARoute之前,我们需要进行初始化ARouter.init,初始化期间会将储存信息读取到静态变量中,使我们能顺序完成跳转,因此先来看下初始化的内容:
ARoute的初始化代码:

 public static void init(Application application) {
           // 忽略不重要的过程
            hasInit = _ARouter.init(application);
    }
// _ARouter.init
protected static synchronized boolean init(Application application) {
        // 主要是进行数据仓库初始化
        LogisticsCenter.init(mContext, executor);
}

初始化过程都是基操,维护一个主线程Handler用于线程切换、保存Context 等
接下来我们主要看下LogisticsCenter.init ,这里会进行一次判断

if (ARouter.debuggable() || PackageUtils.isNewVersion(context)) {
       logger.info(TAG, "Run with debug mode or new install, rebuild router map.");
       // These class was generated by arouter-compiler.
       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);    // Save new version name when router map update finishes.
        } else {
           logger.info(TAG, "Load router map from cache.");
           routerMap = new HashSet<>(context.getSharedPreferences(AROUTER_SP_CACHE_KEY, Context.MODE_PRIVATE).getStringSet(AROUTER_SP_KEY_MAP, new HashSet<String>()));
        }
}

这里看到先做了一次预加载,如果是debug模式或者版本号升级的话预先做一次加载。但是使用版本号来判断是否需要重新读取组件,在未升级版本号的情况下可能会出现问题。
getFileNameByPackageName这个函数的目的是读取安装包中现有的class,这些class都是以包名开头的,我们来看下他是如何实现的:

public static Set<String> getFileNameByPackageName(Context context, final String packageName) throws PackageManager.NameNotFoundException, IOException, InterruptedException {
        final Set<String> classNames = new HashSet<>();
        //获取源文件路径,先着重看下这个
        List<String> paths = getSourcePaths(context);
        、、、暂时忽略

public static List<String> getSourcePaths(Context context) throws PackageManager.NameNotFoundException, IOException {
        ApplicationInfo applicationInfo = context.getPackageManager().getApplicationInfo(context.getPackageName(), 0);
        // app 根目录
        File sourceApk = new File(applicationInfo.sourceDir);

        List<String> sourcePaths = new ArrayList<>();
        // 应用存放数据目录
        sourcePaths.add(applicationInfo.sourceDir); //add the default apk path
        //the prefix of extracted file, ie: test.classes
        String extractedFilePrefix = sourceApk.getName() + EXTRACTED_NAME_EXT;
//        如果VM已经支持了MultiDex,就不要去Secondary Folder加载 Classesx.zip了,那里已经么有了
//        通过是否存在sp中的multidex.version是不准确的,因为从低版本升级上来的用户,是包含这个sp配置的
        if (!isVMMultidexCapable()) {
            //the total dex numbers
            int totalDexNumber = getMultiDexPreferences(context).getInt(KEY_DEX_NUMBER, 1);
            File dexDir = new File(applicationInfo.dataDir, SECONDARY_FOLDER_NAME);
            for (int secondaryNumber = 2; secondaryNumber <= totalDexNumber; secondaryNumber++) {
                //for each dex file, ie: test.classes2.zip, test.classes3.zip...
                String fileName = extractedFilePrefix + secondaryNumber + EXTRACTED_SUFFIX;
                File extractedFile = new File(dexDir, fileName);
                if (extractedFile.isFile()) {
                    sourcePaths.add(extractedFile.getAbsolutePath());
                    //we ignore the verify zip part
                } else {
                    throw new IOException("Missing extracted secondary dex file '" + extractedFile.getPath() + "'");
                }
            }
        }
        if (ARouter.debuggable()) { // Search instant run support only debuggable
            sourcePaths.addAll(tryLoadInstantRunDexFile(applicationInfo));
        }
        return sourcePaths;
    }

private static boolean isVMMultidexCapable() {
        boolean isMultidexCapable = false;
        String vmName = null;

        try {
            if (isYunOS()) {    // YunOS需要特殊判断(我怀疑你在打广告)
                vmName = "'YunOS'";
                isMultidexCapable = Integer.valueOf(System.getProperty("ro.build.version.sdk")) >= 21;
            } else {    // 非YunOS原生Android
                vmName = "'Android'";
                String versionString = System.getProperty("java.vm.version");
                if (versionString != null) {
                    Matcher matcher = Pattern.compile("(\\d+)\\.(\\d+)(\\.\\d+)?").matcher(versionString);
                    if (matcher.matches()) {
                        try {
                            int major = Integer.parseInt(matcher.group(1));
                            int minor = Integer.parseInt(matcher.group(2));
                            isMultidexCapable = (major > VM_WITH_MULTIDEX_VERSION_MAJOR)
                                    || ((major == VM_WITH_MULTIDEX_VERSION_MAJOR)
                                    && (minor >= VM_WITH_MULTIDEX_VERSION_MINOR));
                        } catch (NumberFormatException ignore) {
                            // let isMultidexCapable be false
                        }
                    }
                }
            }
        } catch (Exception ignore) {

        }

        Log.i(Consts.TAG, "VM with name " + vmName + (isMultidexCapable ? " has multidex support" : " does not have multidex support"));
        return isMultidexCapable;
    }

代码中会到安装目录下读取apk包( /data/app/com.alibaba.android.arouter.demo-S8YOQ-yxwK8cUikcaWvAVA==/base.apk),这里还有分包的可能

这里的是否支持分包isVMMultidexCapable是通过判断版本号来决定的,version>2.1,这是一个可以学习的点。

拿到apk后,开启CountDownLatch,多线程查询所有文件

List<String> paths = getSourcePaths(context);
        final CountDownLatch parserCtl = new CountDownLatch(paths.size());

        for (final String path : paths) {
            DefaultPoolExecutor.getInstance().execute(new Runnable() {
                @Override
                public void run() {
                    DexFile dexfile = null;

                    try {
                        if (path.endsWith(EXTRACTED_SUFFIX)) {
                            //NOT use new DexFile(path), because it will throw "permission error in /data/dalvik-cache"
                            dexfile = DexFile.loadDex(path, path + ".tmp", 0);
                        } else {
                            dexfile = new DexFile(path);
                        }

                        Enumeration<String> dexEntries = dexfile.entries();
                        while (dexEntries.hasMoreElements()) {
                            String className = dexEntries.nextElement();
                            if (className.startsWith(packageName)) {
                                classNames.add(className);
                            }
                        }
                    } catch (Throwable ignore) {
                        Log.e("ARouter", "Scan map file in dex files made error.", ignore);
                    } finally {
                        if (null != dexfile) {
                            try {
                                dexfile.close();
                            } catch (Throwable ignore) {
                            }
                        }

                        parserCtl.countDown();
                    }
                }
            });
        }

        parserCtl.await();

        Log.d(Consts.TAG, "Filter " 

使用DexFile读取文件,遍历文件中的class,拿到所有包名开头的;
接下来就是根据读到的信息,获取三类我们想要的文件:IRouteRoot、IInterceptorGroup、IProviderGroup 读取进内存;这里我们比较关心的是IRouteRoot,他就是我们上节提到的ARouter$$Root$$app,这里包含了所有我们的组信息:

 for (String className : routerMap) {
                    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);
                    }
                }

初始化结束,我们的组信息储存在Warehouse.groupsIndex

路由定位

最简单的形式:

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

build会生成一个PostCard对象
new Postcard(path, group),其中的组信息group也是根据简单的第一个/分割而来
接下来的navigation自然也是交给Postcard来处理了,事实上他只是充当了数据model的作用,保存了组信息,调用转发给了ARoute

ARouter.getInstance().navigation(context, this, -1, callback);

接下来交给数据仓库尝试路由:

 try {
        LogisticsCenter.completion(postcard);
    }

首先会在已经解析的Route信息中查找,如果找不到则到groupsIndex 中找对应的组信息,然后实例化组信息记录类,加载数据:

 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:这里是重新调了一次自身
            }
        }

在找到的情况下,填充自身数据:

// 比较重要的是这个,记录目标class
  postcard.setDestination(routeMeta.getDestination());
  postcard.setType(routeMeta.getType());
  postcard.setPriority(routeMeta.getPriority());
  postcard.setExtra(routeMeta.getExtra());

接下来回到_ARoute中,找到记录就告知上层:

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

然后是实际的跳转:

return _navigation(context, postcard, requestCode, callback);

我们看下Activity的处理情况:

case ACTIVITY:
    // Build intent
    final Intent intent = new Intent(currentContext, postcard.getDestination());
    // 我们在外层调用withString等方法传递参数的时候,就会保存到PostCard的mBundle中
    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.
        // 应该有很多朋友遇到这种奔溃,非Activity源启动需要加上New_Task标志
        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;

这段代码不难看明白,基本是我们平时跳转的基操。

课后题:通过插件扫描dex是怎么实现的

LogisticsCenter有这么一个方法loadRouterMap,我们在调初始化的时候会调用它,所以可否在这里入手呢?
com.alibaba.arouter注册的插件PluginLaunch会执行 RegisterTransform的转换操作,经过层层调用,最终是使用asm来修改现有代码的,RouteMethodVisitor会修改LogisticsCenter. loadRouterMap 在其中注入 LogisticsCenter. register方法,把扫描到的目标类IRoute作为参数注入。

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

推荐阅读更多精彩内容