04 项目架构-组件化-ARouter

ARouter疑难杂症解析

我们都知道ARouter可以用于组件化各个模块之间的通信和跳转,在使用ARouter过程中,我产生了几个问题:

  1. 如果我们注解相同的path会怎么样?即有一个SecondActivity使用/a/b的path,而另一个ThirdActivity也使用/a/b的path,那么编译通得过吗?如果通得过的话,通过path获取的又是哪一个Activity呢?
  2. 如果不同的module下,有两个Activity是相同的组会怎么样?即module1有一个SecondActivity使用/a/c的path,而module2也有一个ThirdActivity也使用/a/d的path,编译得过吗?
  3. ARouter也可用于获取服务,假设采用通过接口的方式发现服务的话,如果接口不止一个实现,会怎样,会报错吗?
  4. ARouter服务,为什么不能用抽象类继承IProvider然后实现抽象类而只能用接口继承IProvider然后实现该接口?
  5. 每次通过ARouter获取相同的path的服务,获取的都是同一个对象还是不同的对象?
  6. arouter-gradle-plugin的作用是什么?网上说ARouter加入apk后第一次加载会耗时,又是怎么回事?

于是闲的蛋疼的我,看了一下ARouter的源码,写了一个Demo:XRouter,地址。求一波star。

ARouter源码分为几个部分:

  1. 编译期,即从.java文件到.class文件期间:利用apt框架生成各类,主要逻辑在arouter-compiler模块
  2. transform时期(可选,可以加快第一次进入应用速度):作用于class到dex文件时期,用ASM框架插入字节码,模块是arouter-gradle-plugin
  3. 运行时期:在应用运行时,使用ARouter.init()和navigation进行路由跳转,逻辑在arouter-api模块

接下来我们一一分析:

arouter-compiler:编译期生成类文件

在ARouter源码中,arouter-compiler模块用于处理注解。

关于什么是注解,如何定义注解,以及apt框架,可以看一下这篇文章-Android注解快速入门和实用解析

ARouter在编译期,即由.java到.class之前,通过提取注解,生成了我们想要的.java文件。

主要的代码是在arouter-compiler这个module中。我们引入ARouter时用到了annotationProcessor 'com.alibaba:arouter-compiler:latestversion'即是引入的这个module,annotationProcessor的作用是在编译期处理注解,并不会打包进apk。

这个module中,我们主要分析RouteProcessor的代码,其他xxxProcessor原理也是类似,不一一赘述。

RouteProcessor间接继承AbstractProcessor,init方法获取了各类工具,process方法才是重头戏。

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        if (CollectionUtils.isNotEmpty(annotations)) {
            Set<? extends Element> routeElements = roundEnv.getElementsAnnotatedWith(Route.class);
            try {
                this.parseRoutes(routeElements);  //1
            } catch (Exception e) {
            }
            return true;
        }
        return false;
    }
复制代码

都是做一些准备工作,重点在注释1的parseRoutes方法中。

parseRoutes方法,由于比较长,我只截取了一部分:

...//前面经过了很多的工作之后
if (types.isSameType(tm, iProvider)) {   // Its implements iProvider interface himself.
    // This interface extend the IProvider, so it can be used for mark provider
    loadIntoMethodOfProviderBuilder.addStatement(
            "providers.put($S, $T.build($T." + routeMeta.getType() + ", $T.class, $S, $S, null, " + routeMeta.getPriority() + ", " + routeMeta.getExtra() + "))",
            (routeMeta.getRawType()).toString(),
            routeMetaCn,
            routeTypeCn,
            className,
            routeMeta.getPath(),
            routeMeta.getGroup());
} else if (types.isSubtype(tm, iProvider)) {
    // This interface extend the IProvider, so it can be used for mark provider
    loadIntoMethodOfProviderBuilder.addStatement(
            "providers.put($S, $T.build($T." + routeMeta.getType() + ", $T.class, $S, $S, null, " + routeMeta.getPriority() + ", " + routeMeta.getExtra() + "))",
            tm.toString(),    // So stupid, will duplicate only save class name.
            routeMetaCn,
            routeTypeCn,
            className,
            routeMeta.getPath(),
            routeMeta.getGroup());
}
...
复制代码

截取的这部分代码和ARouter接口发现服务有关,在types.isSameType(tm, iProvider)的这句话中,types时一个Type对象,在RouteProcessor的init方法中从环境中拿到,我们可以用它来比较两个接口是否相同或者一个是另一个的子类。通过调用types的isSameType方法,我们可以比较tm和iProvider是否是同样的接口。tm变量是class文件所实现的接口,这个class文件则是有注解修饰的class文件,iProvider则是com\alibaba\android\arouter\facade\template\IProvider.java这个接口。如果types是IProvider接口,即该类直接实现了IProvider,那么ARouter就利用javapoet生成相应的代码。types.isSubtype(tm, iProvider)的意思是如果types是IProvider的子类,那么ARouter同样就利用javapoet生成相应的代码。

javapoet是什么?JavaPoet是square推出的开源java代码生成框架,提供Java Api生成.java源文件。ARouter利用javapoet生成了各种路由文件。这样就可以在运行的时候去扫描路由文件所在的路径,获取路由文件的类名,调用相关方法初始化路由表。

这里可能很难理解,再来梳理一下,ARouter发现服务是这样的,如果有一个类HelloService继承IProvider,并且HelloService有path的注解的话,比如:

@Route(path = "/module1/hello")
public class HelloService implements IProvider {
    @Override
    public void init(Context context) {

    }
}
复制代码

那么根据上面的情况,ARouter会生成这样的类文件,module1是模块名,可以看到往传进来的一个Map中放入了一个以HelloService为key,对应的RouteMeta为value的键值对。RouteMeta可以理解为跳转的元信息,可以通过RouteMeta获取跳转的path,group以及对应的目标Class等:

public class ARouter$$Providers$$module1 implements IProviderGroup {
  @Override
  public void loadInto(Map<String, RouteMeta> providers) {
    providers.put("com.example.module1.HelloService", RouteMeta.build(RouteType.PROVIDER, HelloService.class, "/module1/hello", "module1", null, -1, -2147483648));
  }
}
复制代码

然而ARouter还提供了另一种方式,即用一个类比如IUserService继承IProvider接口,然后编写类UserServiceImpl实现IUserService,同时UserServiceImpl用path注解,这时ARouter会生成这样的类文件,可以看到这时候放入Map的key就不再是实现的UserServiceImpl类名,而是其所实现的接口IUserService,这就是ARouter通过接口发现服务的规则,用来实现解耦:

public class ARouter$$Providers$$module1 implements IProviderGroup {
  @Override
  public void loadInto(Map<String, RouteMeta> providers) {
    providers.put("com.example.module1.IUserService", RouteMeta.build(RouteType.PROVIDER, UserServiceImpl1.class, "/u/1", "u", null, -1, -2147483648));

  }
}
复制代码

现在考虑文章开头提到的第3和4个问题。第3个问题是如果接口不止一个实现,会怎样。还是以上面的UserServiceImpl为例子,那么如果还有一个类比如UserServiceImpl2同样集成IUserService,并且UserServiceImpl2同样也有path注解,那么ARouter会生成这样类文件。

public class ARouter$$Providers$$module1 implements IProviderGroup {
  @Override
  public void loadInto(Map<String, RouteMeta> providers) {
    providers.put("com.example.module1.IUserService", RouteMeta.build(RouteType.PROVIDER, UserServiceImpl1.class, "/u/1", "u", null, -1, -2147483648));
    providers.put("com.example.module1.IUserService", RouteMeta.build(RouteType.PROVIDER, UserServiceImpl2.class, "/u/2", "u", null, -1, -2147483648));
  }
}
复制代码

显然,同样实现IUserService会使它们有相同的key,后面put的元素会覆盖掉之前put的元素。经过上面的推论,可以得出如果接口不止一个实现,字母表排在后面的接口会覆盖掉排在前面的接口。

第4个问题:为什么不能用抽象类继承IProvider然后实现抽象类而只能用接口继承IProvider然后实现该接口,这是因为ARouter在RouteProcessor的parseRoutes方法中只处理了接口的情况。

那么第1个和第2个问题是怎样呢?

问题1:如果我们注解相同的path会怎么样?即有一个SecondActivity使用/a/b的path,而另一个ThirdActivity也使用/a/b的path。

我们知道apt框架是分module来进行处理的,因此我们也把问题分为在同一个module下和在不同module下:

  1. 如果SecondActivity和ThirdActivity在同一个module下:

    RouteProcessor有一个成员变量groupMap,groupMap对生成ARouter$$Group$$group_name文件起到了非常重要的作用。

    private Map<String, Set<RouteMeta>> groupMap = new HashMap<>();
    复制代码
    

    groupMap的key是string,即group的名字,value是一个Set,我们都知道Set的一个特性,当试图放入一个Set中已有的元素时,会放入不了,并且不会抛异常。由此我们猜测,如果我们在同一个module中注解相同的path,那么排在字母表后面的元素会无效。即如果有一个SecondActivity使用/a/b的path,而另一个ThirdActivity也使用/a/b的path。那么ARouter生成的group类文件将会是下面这样,ThirdActivity由于没有被添加到Set中因此不会再生成的文件中出现:

    public class ARouter$$Group$$a implements IRouteGroup {
      @Override
      public void loadInto(Map<String, RouteMeta> atlas) {
        atlas.put("/a/b", RouteMeta.build(RouteType.ACTIVITY, SecondActivity.class, "/a/b", "a", null, -1, -2147483648));
      }
    }
    复制代码
    
  2. 如果SecondActivity和ThirdActivity在不同的module下:

    由于apt框架是分module编译,并且每个module都会生成ARouter$$Root$module_name,ARouter$$Group$$group_name等文件,那么毫无疑问,会生成两个相同的文件。还是以上面的SecondActivity和ThirdActivity为例子,那么它们都会生成ARouter$$Group$$a的文件,那么在合并到dex的时候肯定会出错,事实上也是这样的。

总结一下:

对于第1个问题:如果我们注解相同的path会怎么样?即有一个SecondActivity使用/a/b的path,而另一个ThirdActivity也使用/a/b的path。

  • 如果SecondActivity和ThirdActivity在同一个module中,那么字母表排在后面的元素会因为无法添加到Set中而无效。即只生成了SecondActivity的相关路由。
  • 如果在不同的module中,由于apt框架是分module编译,并且每个module都会生成ARouterRoot$module_name,ARouterGroup$$group_name等文件,那么毫无疑问,会生成两个相同的group的路由文件,这时候根本就编译过不了

这也同样解释了第2个问题,因此不再赘述。

对于第5个问题:每次通过ARouter获取相同的path的服务,获取的都是同一个对象还是不同的对象?

则需要在arouter-api模块中去找答案了。

arouter-api:跳转的实现模块

ARouter在运行期间需要用到的类比如ARouter.javaLogisticsCenter.java等都在arouter-api这个模块中。

对于第5个问题:每次通过ARouter获取相同的path的服务,获取的都是同一个对象还是不同的对象?

我们先来考虑通过接口获取服务的情况:

在使用ARouter的时候,我们一般是通过这样的方式通过接口获取服务:

val service2 = ARouter.getInstance().navigation(IUserService::class.java)
复制代码

ARouter.getInstance()显然是通过DCL获取ARouter单例,然后调用ARouter的navigation方法,ARouter的navigation如下:

public <T> T navigation(Class<? extends T> service) {
    return _ARouter.getInstance().navigation(service);
}
复制代码

_ARouter是真正实现逻辑的一个类,它的navigation如下:

protected <T> T navigation(Class<? extends T> service) {
    try {
        Postcard postcard = LogisticsCenter.buildProvider(service.getName()); //1
        ....
        LogisticsCenter.completion(postcard);  
        return (T) postcard.getProvider();
    } catch (NoRouteFoundException ex) {
        return null;
    }
}
复制代码

注释1,让我们进去看看干了什么:

    public static Postcard buildProvider(String serviceName) {
        RouteMeta meta = Warehouse.providersIndex.get(serviceName); //1
        if (null == meta) {
            return null;
        } else {
            return new Postcard(meta.getPath(), meta.getGroup());
        }
    }
复制代码

注释1处,可见是从Warehouse的providersIndex获取了serviceName对应的RouteMeta,providersIndex是一个Map,是在ARouter初始化的时候得到的一个以ClassName作为key,RouteMeta作为value的Map。

Warehouse可以立即为数据仓库,是一个包含众多静态Map的一个类。ARouter在初始化的时候将路由表加载到内存,就是将路由表加载到Warehouse的众多Map中。

RouteMeta可以理解为一个的bean,里面存储跳转的path,以及最终跳转的目标className。

由于显然在通过接口获取服务之前ARouter已经初始化完毕,所以这里的meta不为null。

通过接口的类名拿到了对应的RouteMeta后,然后组成了一个Postcard(跳卡)。

回到_ARouter的navigation方法,之后调用了LogisticsCenter.completion(postcard);

看一下completion方法:

public synchronized static void completion(Postcard postcard) {
        ......
            switch (routeMeta.getType()) {
                case PROVIDER:  // if the route is provider, should find its instance
                    // Its provider, so it must implement IProvider
                    Class<? extends IProvider> providerMeta = (Class<? extends IProvider>) routeMeta.getDestination();
                    IProvider instance = Warehouse.providers.get(providerMeta);  //1
                    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;
                    ....
            }
        }
    }
复制代码

省略了一下代码,省略的代码的逻辑就是先去Warehouse的routes这个Map去找有没有相应的RouteMeta,如果没有的话,就去Warehouse的groupsIndex加载该group,然后这个时候routes这个map肯定有相应的RouteMeta了。

然后看注释1,从Warehouse的providers这个map中去获取class对应的对象,如果有的话,就直接使用,如果没有的话,就通过反射创建一个实例,并添加到Warehouse的providers这个map中。所以每次通过ARouter的接口的方式发现服务,每次获取的都是同一个对象。

而如果是通过path的方式呢?由于通过path的方式最终调用的也是completion方法,因此结论和通过接口的方式发现服务相同。

至此第5个问题的结论是:每次通过ARouter的接口的方式发现服务,每次获取的都是同一个对象。

还有一个问题,arouter-gradle-plugin的作用是什么?网上说ARouter加入apk后第一次加载会耗时,又是怎么回事?

arouter-gradle-plugin:作用于class到dex文件时期,用ASM框架插入字节码

arouter-gradle-plugin是一个插件,关于什么是插件,如何定义插件,可以看一下这篇文章:云飞扬-组件化开发(9):自定义Gradle插件(9)

ARouter使用ASM的方式向class文件中插入字节码,关于什么是ASM,可以看一下这篇文章:深入探索编译插桩技术(四、ASM 探秘)

在ARouter的源码中,arouter-gradle-plugin模块的作用是使用ASM的方式往LogisticsCenter的loadRouterMap方法中插入字节码:

LogisticsCener:

    private static void loadRouterMap() {
        registerByPlugin = false;
        //ASM框架往下面插入字节码
        //比如:register("com.alibaba.android.arouter.routes.ARouter$$Root$$arouterapi");
        //比如:register("com.alibaba.android.arouter.routes.ARouter$$Providers$$arouterapi")
    }
复制代码

那么什么时候调用loadRouterMap方法的呢?在LogisticsCenter的init方法中。

    public synchronized static void init(Context context, ThreadPoolExecutor tpe) throws HandlerException {
        mContext = context;
        executor = tpe;
        try {
            loadRouterMap();  //1
            if (registerByPlugin) {   //2
            } else {
                Set<String> routerMap;
                // It will rebuild router map every times when debuggable.
                if (ARouter.debuggable() || PackageUtils.isNewVersion(context)) {
                    routerMap = ClassUtils.getFileNameByPackageName(mContext, ROUTE_ROOT_PAKCAGE);   //3
                    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 {
                    routerMap = new HashSet<>(context.getSharedPreferences(AROUTER_SP_CACHE_KEY, Context.MODE_PRIVATE).getStringSet(AROUTER_SP_KEY_MAP, new HashSet<String>()));
                }
                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);
                    }
                }
            }
        } catch (Exception e) {
            ...
        }
    }
复制代码

LogisticsCenter的init方法会在ARouter初始化的时候调用,用于将路由表加载到内存当中。

注释1调用了loadRouterMap方法,loadRouterMap方法被插入字节码,那么会调用register方法,与此同时registerByPlugin这个boolean值会被设置为true。因此,在注释2处,如果registerByPlugin为false,则说明没有被插入字节码,将进入注释3。那么则会通过扫描指定包名下面的所有className,去进行相关初始化,具体的逻辑在arouter-api模块的ClassUtils的getFileNameByPackageName方法中:

ClassUtils.getFileNameByPackageName:

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);
    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) {
                } finally {
                    if (null != dexfile) {
                        try {
                            dexfile.close();
                        } catch (Throwable ignore) {
                        }
                    }
                    parserCtl.countDown();
                }
            }
        });
    }
    parserCtl.await();
    return classNames;
}
复制代码

可以看到,采用了一个闭锁CountDownLatch,开线程池去扫描各个dex文件指定包名下的className。主线程必须等待所有dex文件被扫描完毕,如果有多个dex文件的话可能会比较耗时。不过由于ARouter对此作了缓存,被找到的所有的className会被缓存到sp中,下一次加载的时候就直接从缓存中取,只有第一次进入应用会比较耗时。

而arouter-gradle-plugin的目的就是连这第一次进入应用的耗时都要减少。

如何分析着手arouter-gradle-plugin这个模块呢?arouter-gradle-plugin首先是一个插件,分析一个插件那么得从它的插件类着手。

arouter-gradle-plugin模块首先从PluginLaunch这个类入手:

public class PluginLaunch implements Plugin<Project> {

    @Override
    public void apply(Project project) {
        def isApp = project.plugins.hasPlugin(AppPlugin)
        if (isApp) {
            def android = project.extensions.getByType(AppExtension)
            def transformImpl = new RegisterTransform(project)
            //init arouter-auto-register settings
            ArrayList<ScanSetting> list = new ArrayList<>(3)
            list.add(new ScanSetting('IRouteRoot'))
            list.add(new ScanSetting('IInterceptorGroup'))
            list.add(new ScanSetting('IProviderGroup'))
            RegisterTransform.registerList = list
            //register this plugin
            android.registerTransform(transformImpl)  //1
        }
    }

}
复制代码

可以看到是一个典型的插件,并且判断了只有application 模块才需要运行相关的逻辑。

注释1注册了一个transformImpl,transformImpl的类型是RegisterTransform,继承了Transform。Transform会在gradle构建过程中,从class文件到dex文件期间,对class文件或资源文件做相关的修改。

RegisterTransform这个类关键点是在transform方法:

void transform(Context context, Collection<TransformInput> inputs
               , Collection<TransformInput> referencedInputs
               , TransformOutputProvider outputProvider
               , boolean isIncremental) throws IOException, TransformException, InterruptedException {
    boolean leftSlash = File.separator == '/'
    inputs.each { TransformInput input ->    //1
        input.jarInputs.each { JarInput jarInput ->
            String destName = jarInput.name
            def hexName = DigestUtils.md5Hex(jarInput.file.absolutePath)
            if (destName.endsWith(".jar")) {
                destName = destName.substring(0, destName.length() - 4)
            }
            File src = jarInput.file
            File dest = outputProvider.getContentLocation(destName + "_" + hexName, jarInput.contentTypes, jarInput.scopes, Format.JAR)
            if (ScanUtil.shouldProcessPreDexJar(src.absolutePath)) {
                ScanUtil.scanJar(src, dest)         //2
            }
            FileUtils.copyFile(src, dest)
        }
        // scan class files
        input.directoryInputs.each { DirectoryInput directoryInput ->
            File dest = outputProvider.getContentLocation(directoryInput.name, directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)
            String root = directoryInput.file.absolutePath
            if (!root.endsWith(File.separator))
                root += File.separator
            directoryInput.file.eachFileRecurse { File file ->
                def path = file.absolutePath.replace(root, '')
                if (!leftSlash) {
                    path = path.replaceAll("\\\\", "/")
                }
                if(file.isFile() && ScanUtil.shouldProcessClass(path)){
                    ScanUtil.scanClass(file)   //3
                }
            }
            // copy to dest
            FileUtils.copyDirectory(directoryInput.file, dest)
        }
    }
    if (fileContainsInitClass) {
        registerList.each { ext ->
            if (ext.classList.isEmpty()) {
            } else {
                RegisterCodeGenerator.insertInitCodeTo(ext)  //4
            }
        }
    }
}
复制代码

transform有两种inputs,一种是jar,一种是Directory,因此注释1对这两种inputs做了区分。接下来看注释2,调用ScanUtils的scanJar方法,让我们看看是什么:

    static void scanJar(File jarFile, File destFile) {
        if (jarFile) {
            def file = new JarFile(jarFile)
            Enumeration enumeration = file.entries()
            while (enumeration.hasMoreElements()) {
                JarEntry jarEntry = (JarEntry) enumeration.nextElement()
                String entryName = jarEntry.getName()
                if (entryName.startsWith(ScanSetting.ROUTER_CLASS_PACKAGE_NAME)) {  //1
                    InputStream inputStream = file.getInputStream(jarEntry)
                    scanClass(inputStream)   //2
                    inputStream.close()
                } else if (ScanSetting.GENERATE_TO_CLASS_FILE_NAME == entryName) {  
                    // mark this jar file contains LogisticsCenter.class
                    // After the scan is complete, we will generate register code into this file
                    RegisterTransform.fileContainsInitClass = destFile  //3
                }
            }
            file.close()
        }
    }
复制代码

注释1通过判断class的className是否以对应ROUTER_CLASS_PACKAGE开头,ROUTER_CLASS_PACKAGE开头的话证明就是ARouter在注解编译期间产生的类文件,然后调用scanClass方法:

static void scanClass(InputStream inputStream) {
    ClassReader cr = new ClassReader(inputStream)
    ClassWriter cw = new ClassWriter(cr, 0)
    ScanClassVisitor cv = new ScanClassVisitor(Opcodes.ASM5, cw)
    cr.accept(cv, ClassReader.EXPAND_FRAMES)
    inputStream.close()
}
static class ScanClassVisitor extends ClassVisitor {
    ScanClassVisitor(int api, ClassVisitor cv) {
        super(api, cv)
    }
    void visit(int version, int access, String name, String signature,
               String superName, String[] interfaces) {
        super.visit(version, access, name, signature, superName, interfaces)
        RegisterTransform.registerList.each { ext ->
            if (ext.interfaceName && interfaces != null) {
                interfaces.each { itName ->
                    if (itName == ext.interfaceName) {
                        //fix repeated inject init code when Multi-channel packaging
                        if (!ext.classList.contains(name)) {
                            ext.classList.add(name)
                        }
                    }
                }
            }
        }
    }
}
复制代码

scanClass主要是判断是否class实现了相关的接口,所谓的相关的接口则是IRouteRootIInterceptorGroupIProviderGroup这几个接口,如果实现了这几个接口的任意一个接口(实际上不应该也不允许实现多个)就把它的className加入到相应的classList,用于之后插入字节码做准备。

让我们回到ScanUtils的scanJar方法,注释3找到了fileContainsInitClass,所谓的fileContainsInitClass即LogisticsCenter.java这个类,之后ARouter插入字节码便是往LogisticsCenter的loadRouterMap方法中插入字节码。

回到RegisterTransform类的transform方法,注释3:DirectoryInputs也是和jarInputs一样,需要扫描。

接着来到注释4,如果找到了fileContainsInitClass,即LogisticsCenter这个类,那么则调用RegisterCodeGenerator的insertInitCodeTo方法:

static void insertInitCodeTo(ScanSetting registerSetting) {
    if (registerSetting != null && !registerSetting.classList.isEmpty()) {
        RegisterCodeGenerator processor = new RegisterCodeGenerator(registerSetting)
        File file = RegisterTransform.fileContainsInitClass
        if (file.getName().endsWith('.jar'))
            processor.insertInitCodeIntoJarFile(file)
    }
}
复制代码

insertInitCodeTo进行了相关初始化和判空,调用了insertInitCodeIntoJarFile方法:

    private File insertInitCodeIntoJarFile(File jarFile) {
        if (jarFile) {
            def optJar = new File(jarFile.getParent(), jarFile.name + ".opt")
            if (optJar.exists())
                optJar.delete()
            def file = new JarFile(jarFile)
            Enumeration enumeration = file.entries()
            JarOutputStream jarOutputStream = new JarOutputStream(new FileOutputStream(optJar))
            while (enumeration.hasMoreElements()) {
                JarEntry jarEntry = (JarEntry) enumeration.nextElement()
                String entryName = jarEntry.getName()
                ZipEntry zipEntry = new ZipEntry(entryName)
                InputStream inputStream = file.getInputStream(jarEntry)
                jarOutputStream.putNextEntry(zipEntry)
                if (ScanSetting.GENERATE_TO_CLASS_FILE_NAME == entryName) {   //1
                    def bytes = referHackWhenInit(inputStream)    
                    jarOutputStream.write(bytes)
                } else {
                    jarOutputStream.write(IOUtils.toByteArray(inputStream))
                }
                inputStream.close()
                jarOutputStream.closeEntry()
            }
            jarOutputStream.close()
            file.close()
            if (jarFile.exists()) {
                jarFile.delete()
            }
            optJar.renameTo(jarFile)
        }
        return jarFile
    }
复制代码

重点看注释1,如果类名为GENERATE_TO_CLASS_FILE_NAME,则说明是LogisticsCenter这个类,然后调用referHackWhenInit方法,往LogisticsCenter这个类中插入字节码:

private byte[] referHackWhenInit(InputStream inputStream) {
    ClassReader cr = new ClassReader(inputStream)
    ClassWriter cw = new ClassWriter(cr, 0)
    ClassVisitor cv = new MyClassVisitor(Opcodes.ASM5, cw)
    cr.accept(cv, ClassReader.EXPAND_FRAMES)
    return cw.toByteArray()
}
复制代码

这里才是最终插入ASM字节码实现逻辑。可以看到是个典型的ASM代码,MyClassVisitor对输入流进行处理,MyClassVisitor如下:

class MyClassVisitor extends ClassVisitor {
    MyClassVisitor(int api, ClassVisitor cv) {
        super(api, cv)
    }
    void visit(int version, int access, String name, String signature,
               String superName, String[] interfaces) {
        super.visit(version, access, name, signature, superName, interfaces)
    }
    @Override
    MethodVisitor visitMethod(int access, String name, String desc,
                              String signature, String[] exceptions) {
        MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions)
        //generate code into this method
        if (name == ScanSetting.GENERATE_TO_METHOD_NAME) {
            mv = new RouteMethodVisitor(Opcodes.ASM5, mv)
        }
        return mv
    }
}
class RouteMethodVisitor extends MethodVisitor {
    RouteMethodVisitor(int api, MethodVisitor mv) {
        super(api, mv)
    }
    @Override
    void visitInsn(int opcode) {
        //generate code before return
        if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN)) {
            extension.classList.each { name ->
                name = name.replaceAll("/", ".")
                mv.visitLdcInsn(name)//类名
                // generate invoke register method into LogisticsCenter.loadRouterMap()
                mv.visitMethodInsn(Opcodes.INVOKESTATIC
                        , ScanSetting.GENERATE_TO_CLASS_NAME
                        , ScanSetting.REGISTER_METHOD_NAME
                        , "(Ljava/lang/String;)V"
                        , false)
            }
        }
        super.visitInsn(opcode)
    }
    @Override
    void visitMaxs(int maxStack, int maxLocals) {
        super.visitMaxs(maxStack + 4, maxLocals)
    }
}
复制代码

RouteMethodVisitor关键在visitInsn方法,判断opCode是否是返回的操作码,如果是的话在这之前插入各类字节码,由于修改了操作数栈,因此也需要重写visitMaxs方法修改最大操作数栈。

由于本文不是专门讲ASM的文章,因此ASM的分析就此略过(其实作者也不会)

让我们看看ASM插入后的效果如图(apk经过反编译后展示的代码,实际上插入的是字节码:dex2jar+jd-gui):

image-20210323104318680

至此,arouter-gradle-plugin的逻辑梳理完成。

总结

至此,我们回答了文章开头提出的几个问题:

  1. 如果我们注解相同的path会怎么样?即有一个SecondActivity使用/a/b的path,而另一个ThirdActivity也使用/a/b的path,那么编译通得过吗?如果通得过的话,通过path获取的又是哪一个Activity呢?

    答:如果在相同的module中,由于ARouter源码中使用的是Set,那么获取的是字母表排在前面的元素。如果在不同的module中,编译不过(和问题2一样)。

  2. 如果不同的module下,有两个Activity是相同的组会怎么样?即module1有一个SecondActivity使用/a/c的path,而module2也有一个ThirdActivity也使用/a/d的path,编译得过吗?

    答:编译不过。由于都生成了相同的group文件,合并dex的时候会报错。

  3. ARouter也可用于获取服务,假设采用通过接口的方式发现服务的话,如果接口不止一个实现,会怎样,会报错吗?

    答:如果接口不止一个实现,并且接口的实现都用path注释的话,字母表排在后面的接口会覆盖掉排在前面的接口。

  4. ARouter服务,为什么不能用抽象类继承IProvider然后实现抽象类而只能用接口继承IProvider然后实现该接口?

    答:ARouter只处理了接口的情况,没有处理抽象类。

  5. 每次通过ARouter获取相同的path的服务,获取的都是同一个对象还是不同的对象?

    答:每次通过ARouter的接口的方式发现服务,每次获取的都是同一个对象。

  6. arouter-gradle-plugin的作用是什么?网上说ARouter加入apk后第一次加载会耗时,又是怎么回事?

    答:arouter-gradle-plugin是一个插件,被ARouter用来加快应用安装后第一次进入时的速度。如果使用插件的话,那么会ASM直接插入字节码,省去了运行时需要扫描指定包名下面的所有className所造成的耗时。网上说ARouter加入apk后第一次加载会耗时,这是指的是没有使用arouter插件的时候,在第一次进入apk时,主线程必须等待子线程去扫描指定包名下面的所有className,如果class比较多,会比较耗时。

推荐阅读更多精彩内容