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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| public class PluginLaunch implements Plugin<Project> {
@Override public void apply(Project project) { def isApp = project.plugins.hasPlugin(AppPlugin) if (isApp) { Logger.make(project)
Logger.i('Project enable arouter-register plugin')
def android = project.extensions.getByType(AppExtension) def transformImpl = new RegisterTransform(project)
ArrayList<ScanSetting> list = new ArrayList<>(3) list.add(new ScanSetting('IRouteRoot')) list.add(new ScanSetting('IInterceptorGroup')) list.add(new ScanSetting('IProviderGroup')) RegisterTransform.registerList = list android.registerTransform(transformImpl) } }
}
|
从上面的代码可知:
- 只在 application module (一般都是 app module)生成自动注册的代码;
- 初始化了自动注册的设置,这样自动注册就知道需要注册 IRouteRoot IInterceptorGroup IProviderGroup 这三者;
- 注册 RegisterTransform ,字节码插桩将在 RegisterTransform 中完成;
可以看出,重点就在 RegisterTransform 里面。那我们重点就关注下 RegisterTransform 的代码,这里就贴出 transform 方法的源码了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84
| 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 -> 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) } FileUtils.copyFile(src, dest) } 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) } } FileUtils.copyDirectory(directoryInput.file, dest) } } Logger.i('Scan finish, current cost time ' + (System.currentTimeMillis() - startTime) + "ms") if (fileContainsInitClass) { 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) } RegisterCodeGenerator.insertInitCodeTo(ext) } } } Logger.i("Generate code finish, current cost time: " + (System.currentTimeMillis() - startTime) + "ms") }
}
|
上面代码的逻辑很清晰,按照之前设置好的 IRouteRoot IInterceptorGroup IProviderGroup 这三个接口,然后扫描整个项目的代码,分别找到这三者各自的实现类,然后加入到集合中。最后在 LogisticsCenter 中实现字节码插桩。
我们来详细看下 RegisterCodeGenerator.insertInitCodeTo(ext) 的代码
1 2 3 4 5 6 7 8 9 10
| 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) } }
|
插入的操作在 insertInitCodeIntoJarFile 中实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| 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) {
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() if (jarFile.exists()) { jarFile.delete() } optJar.renameTo(jarFile) } return jarFile }
|
字节码插桩的代码还在 referHackWhenInit 方法中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
| 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) 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) { if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN)) { extension.classList.each { name -> name = name.replaceAll("/", ".") mv.visitLdcInsn(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) } }
|
最终,生成的代码会像下面所示:
1 2 3 4 5 6 7 8
| private static void loadRouterMap() { registerByPlugin = false; 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 方法的代码,看看里面是如何完成路由表注册的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
| 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); } } }
private static void registerRouteRoot(IRouteRoot routeRoot) { markRegisteredByPlugin(); if (routeRoot != null) { routeRoot.loadInto(Warehouse.groupsIndex); } }
private static void registerInterceptor(IInterceptorGroup interceptorGroup) { markRegisteredByPlugin(); if (interceptorGroup != null) { interceptorGroup.loadInto(Warehouse.interceptorsIndex); } }
private static void registerProvider(IProviderGroup providerGroup) { markRegisteredByPlugin(); if (providerGroup != null) { providerGroup.loadInto(Warehouse.providersIndex); } }
private static void markRegisteredByPlugin() { if (!registerByPlugin) { registerByPlugin = true; } }
|
这样相比之下,自动注册的方式确实比扫描 dex 文件更高效,扫描 dex 文件是在 app 运行时操作的,这样会影响 app 的性能,对用户造成不好的体验。而自动注册是在 build 的时候完成字节码插桩的,对运行时不产生影响。
学了今天这招,以后 compiler 生成的代码需要注册的步骤都可以通过自动注册来完成了,赞一个👍
番外
之前看到自动注册这么神奇,所以想看下插入字节码之后 LogisticsCenter 代码的效果,所以反编译了一下 ARouter demo apk,可以看到 LogisticsCenter.smali 的 loadRouterMap 方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
| .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
|