优秀的编程知识分享平台

网站首页 > 技术文章 正文

阿里ARouter问的几个问题你都会嘛?

nanyue 2024-08-11 20:39:51 技术文章 5 ℃

我们都知道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时期(可选,可以加快第一次进入应用速度):作用于classdex文件时期,用ASM框架插入字节码,模块是arouter-gradle-plugin

3、运行时期:在应用运行时,使用ARouter.init()navigation进行路由跳转,逻辑在arouter-api模块

接下来我们一一分析:

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

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

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

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

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

RouteProcessor间接继承AbstractProcessorinit方法获取了各类工具,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对象,在RouteProcessorinit方法中从环境中拿到,我们可以用它来比较两个接口是否相同或者一个是另一个的子类。

通过调用typesisSameType方法,我们可以比较tmiProvider是否是同样的接口。tm变量是class文件所实现的接口,这个class文件则是有注解修饰的class文件,iProvider则是com\alibaba\android\arouter\facade\template\IProvider.java这个接口。

如果typesIProvider接口,即该类直接实现了IProvider,那么ARouter就利用javapoet生成相应的代码。types.isSubtype(tm, iProvider)的意思是如果typesIProvider的子类,那么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中放入了一个以HelloServicekey,对应的RouteMetavalue的键值对。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会生成这样的类文件,可以看到这时候放入Mapkey就不再是实现的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然后实现该接口,这是因为ARouterRouteProcessorparseRoutes方法中只处理了接口的情况。

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

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

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

1、如果SecondActivityThirdActivity在同一个module下:

RouteProcessor有一个成员变量groupMapgroupMap对生成ARouter$Group$group_name文件起到了非常重要的作用。private Map<String, Set<RouteMeta>> groupMap = new HashMap<>();

groupMapkeystring,即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等文件,那么毫无疑问,会生成两个相同的文件。还是以上面的SecondActivityThirdActivity为例子,那么它们都会生成ARouter$Group$a的文件,那么在合并到dex的时候肯定会出错,事实上也是这样的。

总结一下:

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

  • 如果SecondActivityThirdActivity在同一个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单例,然后调用ARouternavigation方法,ARouternavigation如下:

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处,可见是从WarehouseprovidersIndex获取了serviceName对应的RouteMetaprovidersIndex是一个Map,是在ARouter初始化的时候得到的一个以ClassName作为keyRouteMeta作为valueMap

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

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

由于显然在通过接口获取服务之前ARouter已经初始化完毕,所以这里的meta不为null通过接口的类名拿到了对应的RouteMeta后,然后组成了一个Postcard(跳卡)。

回到_ARouternavigation方法,之后调用了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;
                ....
        }
    }
}

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

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

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

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

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

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

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

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方法的呢?在LogisticsCenterinit方法中。

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) {
        ...
    }
}

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

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

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注册了一个transformImpltransformImpl的类型是RegisterTransform,继承了TransformTransform会在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,调用ScanUtilsscanJar方法,让我们看看是什么:

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通过判断classclassName是否以对应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,用于之后插入字节码做准备。

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

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

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

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方法修改最大操作数栈。

至此,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比较多,会比较耗时。

Tags:

最近发表
标签列表