ARouter源码解析(五)

栏目: IOS · Android · 发布时间: 5年前

内容简介:arouter-gradle-plugin version : 1.0.2AutoRegister :在本系列的第一篇中讲过,ARouter 可以通过扫描 dex 文件中 class 的全类名,来加载 compiler 生成的路由类。但这种方式影响性能,并且效率也不高。所以在 ARouter v1.3.0 之后的版本中,加入了自动注册的方式进行路由表的加载,自动注册可以缩短初始化时间,解决应用加固导致无法直接访问 dex 文件从而初始化失败的问题。

arouter-gradle-plugin version : 1.0.2

AutoRegister : https://github.com/luckybilly/AutoRegister

前言

在本系列的第一篇中讲过,ARouter 可以通过扫描 dex 文件中 class 的全类名,来加载 compiler 生成的路由类。但这种方式影响性能,并且效率也不高。所以在 ARouter v1.3.0 之后的版本中,加入了自动注册的方式进行路由表的加载,自动注册可以缩短初始化时间,解决应用加固导致无法直接访问 dex 文件从而初始化失败的问题。

那么自动注册到底是什么东东,为什么有这么强大的能力呢?

那么接下来,我们就来分析分析。

预先需要了解的知识点:

  • 自定义 gradle plugin
  • gradle transform api
  • 使用 asm 实现字节码插桩

arouter-register

arouter-register 的入口就在 PluginLaunch

public class PluginLaunch implements Plugin<Project> {

    @Override
    public void apply(Project project) {
        def isApp = project.plugins.hasPlugin(AppPlugin)
        //only application module needs this plugin to generate register code
        if (isApp) {
            Logger.make(project)

            Logger.i('Project enable arouter-register plugin')

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

}

从上面的代码可知:

  • 只在 application module (一般都是 app module)生成自动注册的代码;
  • 初始化了自动注册的设置,这样自动注册就知道需要注册 IRouteRoot IInterceptorGroup IProviderGroup 这三者;
  • 注册 RegisterTransform ,字节码插桩将在 RegisterTransform 中完成;

可以看出,重点就在 RegisterTransform 里面。那我们重点就关注下 RegisterTransform 的代码,这里就贴出 transform 方法的源码了。(关于 Transform 的 InputTypes 和 Scopes 知识点在这就不讲了,如有需要了解的同学可以看 Android 热修复使用Gradle Plugin1.5改造Nuwa插件

class RegisterTransform extends Transform {


	@Override
	void transform(Context context, Collection<TransformInput> inputs
	               , Collection<TransformInput> referencedInputs
	               , TransformOutputProvider outputProvider
	               , boolean isIncremental) throws IOException, TransformException, InterruptedException {
	
	    Logger.i('Start scan register info in jar file.')
	
	    long startTime = System.currentTimeMillis()
	    boolean leftSlash = File.separator == '/'
	
	    inputs.each { TransformInput input ->
	
	        // 扫描所有的 jar 文件
	        input.jarInputs.each { JarInput jarInput ->
	            String destName = jarInput.name
	            // rename jar files
	            def hexName = DigestUtils.md5Hex(jarInput.file.absolutePath)
	            if (destName.endsWith(".jar")) {
	                destName = destName.substring(0, destName.length() - 4)
	            }
	            // 输入的 jar 文件
	            File src = jarInput.file
	            // 输出的 jar 文件
	            File dest = outputProvider.getContentLocation(destName + "_" + hexName, jarInput.contentTypes, jarInput.scopes, Format.JAR)
	
	            // 扫描 jar 文件,查找实现 IRouteRoot IInterceptorGroup IProviderGroup 接口的类,并且找到 LogisticsCenter 在哪个 jar 文件中
	            // 不扫描 com.android.support 开头的 jar
	            if (ScanUtil.shouldProcessPreDexJar(src.absolutePath)) {
	                // ScanUtil.scanJar 的代码就不详细展开了,感兴趣的同学可以自己去看下
	                ScanUtil.scanJar(src, dest)
	            }
	            FileUtils.copyFile(src, dest)
	
	        }
	        // 扫描所有的 class 文件,查找实现 IRouteRoot IInterceptorGroup IProviderGroup 接口的类
	        // 和扫描 jar 做差不多类似的工作。不同的点就是不用再去找 LogisticsCenter 类
	        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("\\\\", "/")
	                }
	                // 只处理 com/alibaba/android/arouter/routes/ 开头的 class
	                if(file.isFile() && ScanUtil.shouldProcessClass(path)){
	                    ScanUtil.scanClass(file)
	                }
	            }
	
	            // copy to dest
	            FileUtils.copyDirectory(directoryInput.file, dest)
	        }
	    }
	
	    Logger.i('Scan finish, current cost time ' + (System.currentTimeMillis() - startTime) + "ms")
	    
	    // 这里开始字节码插桩操作
	    if (fileContainsInitClass) {
	        // 遍历之前找的 IRouteRoot IInterceptorGroup IProviderGroup
	        registerList.each { ext ->
	            Logger.i('Insert register code to file ' + fileContainsInitClass.absolutePath)
	
	            if (ext.classList.isEmpty()) {
	                Logger.e("No class implements found for interface:" + ext.interfaceName)
	            } else {
	                ext.classList.each {
	                    Logger.i(it)
	                }
	                // 对 LogisticsCenter.class 做字节码插桩
	                RegisterCodeGenerator.insertInitCodeTo(ext)
	            }
	        }
	    }
	
	    Logger.i("Generate code finish, current cost time: " + (System.currentTimeMillis() - startTime) + "ms")
	}

}

上面代码的逻辑很清晰,按照之前设置好的 IRouteRoot IInterceptorGroup IProviderGroup 这三个接口,然后扫描整个项目的代码,分别找到这三者各自的实现类,然后加入到集合中。最后在 LogisticsCenter 中实现字节码插桩。

我们来详细看下 RegisterCodeGenerator.insertInitCodeTo(ext) 的代码

static void insertInitCodeTo(ScanSetting registerSetting) {
    if (registerSetting != null && !registerSetting.classList.isEmpty()) {
        RegisterCodeGenerator processor = new RegisterCodeGenerator(registerSetting)
        // RegisterTransform.fileContainsInitClass 就是包含了 LogisticsCenter.class 的那个 jar 文件
        File file = RegisterTransform.fileContainsInitClass
        if (file.getName().endsWith('.jar'))
        		 // 开始处理
            processor.insertInitCodeIntoJarFile(file)
    }
}

插入的操作在 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))
        // 遍历 jar 文件中的 class
        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)
            // 如果是 LogisticsCenter.class 的话
            if (ScanSetting.GENERATE_TO_CLASS_FILE_NAME == entryName) {

                Logger.i('Insert init code to class >> ' + entryName)
                // 插桩操作
                def bytes = referHackWhenInit(inputStream)
                jarOutputStream.write(bytes)
            } else {
                jarOutputStream.write(IOUtils.toByteArray(inputStream))
            }
            inputStream.close()
            jarOutputStream.closeEntry()
        }
        jarOutputStream.close()
        file.close()
        // 把字节码插桩的 jar 替换掉原来旧的 jar 文件
        if (jarFile.exists()) {
            jarFile.delete()
        }
        optJar.renameTo(jarFile)
    }
    return jarFile
}

字节码插桩的代码还在 referHackWhenInit 方法中。

//refer hack class when object init
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()
}

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)
        // 对 loadRouterMap 这个方法进行代码插入
        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) {
        // 插入的代码在 return 之前
        if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN)) {
            extension.classList.each { name ->
                name = name.replaceAll("/", ".")
                mv.visitLdcInsn(name)//这里的name就是之前扫描出来的 IRouteRoot IInterceptorGroup IProviderGroup 实现类名
                // 生成 LogisticsCenter.register(name) 代码
                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)
    }
}

最终,生成的代码会像下面所示:

private static void loadRouterMap() {
    registerByPlugin = false;
    //auto generate register code by gradle plugin: arouter-auto-register
    // looks like below:
    register("com.alibaba.android.arouter.routes.ARouter$$Root$$app");
    register("com.alibaba.android.arouter.routes.ARouter$$Interceptors$$app");
    register("com.alibaba.android.arouter.routes.ARouter$$Group$$arouter");
}

那么顺便来跟踪一下 register 方法的代码,看看里面是如何完成路由表注册的。

private static void register(String className) {
    if (!TextUtils.isEmpty(className)) {
        try {
            Class<?> clazz = Class.forName(className);
            Object obj = clazz.getConstructor().newInstance();
            if (obj instanceof IRouteRoot) {
                registerRouteRoot((IRouteRoot) obj);
            } else if (obj instanceof IProviderGroup) {
                registerProvider((IProviderGroup) obj);
            } else if (obj instanceof IInterceptorGroup) {
                registerInterceptor((IInterceptorGroup) obj);
            } else {
                logger.info(TAG, "register failed, class name: " + className
                        + " should implements one of IRouteRoot/IProviderGroup/IInterceptorGroup.");
            }
        } catch (Exception e) {
            logger.error(TAG,"register class error:" + className);
        }
    }
}

// 注册 IRouteRoot 类型
private static void registerRouteRoot(IRouteRoot routeRoot) {
    markRegisteredByPlugin();
    if (routeRoot != null) {
        routeRoot.loadInto(Warehouse.groupsIndex);
    }
}

// 注册 IInterceptorGroup 类型
private static void registerInterceptor(IInterceptorGroup interceptorGroup) {
    markRegisteredByPlugin();
    if (interceptorGroup != null) {
        interceptorGroup.loadInto(Warehouse.interceptorsIndex);
    }
}

// 注册 IProviderGroup 类型
private static void registerProvider(IProviderGroup providerGroup) {
    markRegisteredByPlugin();
    if (providerGroup != null) {
        providerGroup.loadInto(Warehouse.providersIndex);
    }
}

// 标记通过gradle plugin完成自动注册
private static void markRegisteredByPlugin() {
    if (!registerByPlugin) {
        registerByPlugin = true;
    }
}

这样相比之下,自动注册的方式确实比扫描 dex 文件更高效,扫描 dex 文件是在 app 运行时操作的,这样会影响 app 的性能,对用户造成不好的体验。而自动注册是在 build 的时候完成字节码插桩的,对运行时不产生影响。

学了今天这招,以后 compiler 生成的代码需要注册的步骤都可以通过自动注册来完成了,赞一个:+1:

番外

之前看到自动注册这么神奇,所以想看下插入字节码之后 LogisticsCenter 代码的效果,所以反编译了一下 ARouter demo apk,可以看到 LogisticsCenter.smali 的 loadRouterMap 方法:

.method private static loadRouterMap()V
    .locals 1

    .line 64
    const/4 v0, 0x0

    sput-boolean v0, Lcom/alibaba/android/arouter/core/LogisticsCenter;->registerByPlugin:Z

    .line 69
    const-string v0, "com.alibaba.android.arouter.routes.ARouter$$Root$$modulejava"

    invoke-static {v0}, Lcom/alibaba/android/arouter/core/LogisticsCenter;->register(Ljava/lang/String;)V

    const-string v0, "com.alibaba.android.arouter.routes.ARouter$$Root$$modulekotlin"

    invoke-static {v0}, Lcom/alibaba/android/arouter/core/LogisticsCenter;->register(Ljava/lang/String;)V

    const-string v0, "com.alibaba.android.arouter.routes.ARouter$$Root$$arouterapi"

    invoke-static {v0}, Lcom/alibaba/android/arouter/core/LogisticsCenter;->register(Ljava/lang/String;)V

    const-string v0, "com.alibaba.android.arouter.routes.ARouter$$Root$$app"

    invoke-static {v0}, Lcom/alibaba/android/arouter/core/LogisticsCenter;->register(Ljava/lang/String;)V

    const-string v0, "com.alibaba.android.arouter.routes.ARouter$$Interceptors$$modulejava"

    invoke-static {v0}, Lcom/alibaba/android/arouter/core/LogisticsCenter;->register(Ljava/lang/String;)V

    const-string v0, "com.alibaba.android.arouter.routes.ARouter$$Interceptors$$app"

    invoke-static {v0}, Lcom/alibaba/android/arouter/core/LogisticsCenter;->register(Ljava/lang/String;)V

    const-string v0, "com.alibaba.android.arouter.routes.ARouter$$Providers$$modulejava"

    invoke-static {v0}, Lcom/alibaba/android/arouter/core/LogisticsCenter;->register(Ljava/lang/String;)V

    const-string v0, "com.alibaba.android.arouter.routes.ARouter$$Providers$$modulekotlin"

    invoke-static {v0}, Lcom/alibaba/android/arouter/core/LogisticsCenter;->register(Ljava/lang/String;)V

    const-string v0, "com.alibaba.android.arouter.routes.ARouter$$Providers$$arouterapi"

    invoke-static {v0}, Lcom/alibaba/android/arouter/core/LogisticsCenter;->register(Ljava/lang/String;)V

    const-string v0, "com.alibaba.android.arouter.routes.ARouter$$Providers$$app"

    invoke-static {v0}, Lcom/alibaba/android/arouter/core/LogisticsCenter;->register(Ljava/lang/String;)V

    return-void
.end method

确实符合我们的预期啊,真好!

References


以上所述就是小编给大家介绍的《ARouter源码解析(五)》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

CSS基础教程

CSS基础教程

Simon Collison / 刘志忠 等 / 人民邮电出版社 / 2007 / 49.00元

《CSS 基础教程》是优秀的CSS 入门书,重点讲述了如何使用CSS 实现基于Web 标准的网站开发,实现网站内容和表现相分离。《CSS 基础教程》包括两部分。第一部分介绍了CSS 的基础知识和基本概念,再利用CSS 分别对网页创建中的一些基本元素加上样式,包括:文本、图像、列表、链接、表格、表单等。第二部分主要讨论了基于CSS 来实现网页基本布局的相关概念与技术,包括浮动、流体布局等。《CSS ......一起来看看 《CSS基础教程》 这本书的介绍吧!

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具