前言
我前一篇介绍偶尔ARouter的初始化过程已经导航过程。在初始化话的时候我们知道需要遍历所有的dex 文件找到编译期间生成的类,这个过程比较耗费时间,因此ARouter 提供了一个gradle 插件在编译期间遍历所以的class 文件,找到动态生成的类,然后动态修改字节码插入部分代码,避免了程序运行期间查找,提高了效率。本篇就来研究器实现方式。
一、动态生成的代码
我们通过Route 注解了一个名为TestActivity的页面,在编译期间会动态生成一些类,关于动态生成的过程这里不做介绍,感兴趣的可以查阅android 编译时注解处理器的相关知识。
首先TestActivity 是loginmodule 下面的一个类。
生成的类一
/**
* DO NOT EDIT THIS FILE!!! IT WAS GENERATED BY AROUTER. */
public class ARouter$$Root$$loginmodule implements IRouteRoot {
@Override
public void loadInto(Map<String, Class<? extends IRouteGroup>> routes) {
routes.put("test", ARouter$$Group$$test.class);
}
}
ARouter$$Group$$moduleNmme 代表的一个module。 一个module 里面可以存在多个分组。这里就只有一个test 组。
生成的类二
/**
* DO NOT EDIT THIS FILE!!! IT WAS GENERATED BY AROUTER. */
public class ARouter$$Group$$test implements IRouteGroup {
@Override
public void loadInto(Map<String, RouteMeta> atlas) {
atlas.put("/test/testActivity", RouteMeta.build(RouteType.ACTIVITY, TestActivity.class, "/test/testactivity", "test", null, -1, -2147483648));
}
}
ARouter$$Group$$groupName 表示一个组,一组内可能存在多个路由。组的名称就是注解path里面第一个/与第二个/之间的之间的部分。
加入还存在一个@Route(path = "/debug/debugActivity") 那么就会存在一个debug 组。
一、动态插入代码
下图是android 的打包过程(图片来源与网上)
这整个过程由android 插件完成。
apply plugin: 'com.android.application'
在打包的流程中,android 插件给用户提供了对应的API去访问编译之后的class文件,这个API 就是
Transform ,这一应用现在主要集中在字节码查找、代码注入等。
每个 Transform 都是一个 gradle task, 将 class 文件、本地依赖的 jar, aar 和 resource 资源统一处理。每个 Transform 在处理完之后交给下一个 Transform。如果是用户自定义的 Transform 会插在队列的最前面。
使用方式
首先在项目的gradle 文件下面添加依赖
classpath "com.alibaba:arouter-register:1.0.2"
在App模块下面使用插件
apply plugin: 'com.alibaba.arouter'
apply plugin: 'com.alibaba.arouter' 实际就是找到对应的插件对象然后调用这个对象的apply方法。
具体的讲就是调用PluginLaunch 的apply 方法
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)
//注册我们的transform任务,
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)
}
}
}
这里主要是查找注册的android 插件,然后调用registerTransform 注册用户自定义的Transform。这里注册了一个RegisterTransform对象。
RegisterTransform.groovy
@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 ->
// scan all jars,遍历所有的依赖。
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)
}
// input file
File src = jarInput.file
// output file 获取输出文件地址,在build文件夹下面
File dest = outputProvider.getContentLocation(destName + "_"
+ hexName, jarInput.contentTypes, jarInput.scopes, Format.JAR)
//scan jar file to find classes
if (ScanUtil.shouldProcessPreDexJar(src.absolutePath)) {
ScanUtil.scanJar(src, dest)
}
FileUtils.copyFile(src, dest)
}
// scan class files
//这里是我们自己编写的源文件。
//DirectoryInput 包含全部的编译后的class文件,
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
//遍历全部的class文件
directoryInput.file.eachFileRecurse { File file ->
def path = file.absolutePath.replace(root, '')
if (!leftSlash) {
path = path.replaceAll("\\\\", "/")
}
//判断是不是com/alibaba/android/arouter/routes/包下面的类文件
if(file.isFile() && ScanUtil.shouldProcessClass(path)){
//扫描文件
ScanUtil.scanClass(file)
}
}
// copy to dest
FileUtils.copyDirectory(directoryInput.file, dest)
}
}
//fileContainsInitClass 是com.alibaba:arouter-api 里面的LogisticsCenter类
if (fileContainsInitClass) {
registerList.each { ext ->
if (ext.classList.isEmpty()) {
} else {
ext.classList.each {
Logger.i(it)
}
//开始植入代码
RegisterCodeGenerator.insertInitCodeTo(ext)
}
}
}
Logger.i("Generate code finish, current cost time: " + (System.currentTimeMillis() - startTime) + "ms")
}
这里分为两个部分
第一部分是遍历所有的jar 文件,实际就是遍历所有的依赖,然后调用ScanUtil.scanJar(src, dest)扫描这个jar 文件里面的class文件。scanJar 主要是找到处于com/alibaba/android/arouter/routes/
包下面的类并查看该类时候实现了IRouteRoot,IInterceptorGroup或者IProviderGroup接口,如果实现了某个接口就将该文件保存在对应的ScanSetting内
除此之外 ,scanJar还会找到com/alibaba/android/arouter/core/ 包下面的LogisticsCenter类。LogisticsCenter 就是需要插入代码的类。
第二部分是扫描模块下面用户自己的类,查找处于com/alibaba/android/arouter/routes/包下面的类,同时扫描类,将类添加到对应的ScanSetting对象内。
最终调用RegisterCodeGenerator.insertInitCodeTo(ext) 去处理这些类。
其参数ext 在PluginLaunch 里面创建的ScanSetting,每一个ScanSetting内部的classList列表保存的就是实现对应接口的类。如ScanSetting('IRouteRoot') 里面保存着实现了IRouteRoot 接口的类。
RegisterCodeGenerator.groovy
、、入口函数
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)
}
}
RegisterTransform.fileContainsInitClass 依赖'com.alibaba:arouter-api:1.4.0'对应的jar 文件。
private File insertInitCodeIntoJarFile(File jarFile) {
if (jarFile) {
def optJar = new File(jarFile.getParent(), jarFile.name + ".opt")
if (optJar.exists())
optJar.delete()
//用来拷贝jarFile,
def file = new JarFile(jarFile)
Enumeration enumeration = file.entries()
JarOutputStream jarOutputStream = new JarOutputStream(new
FileOutputStream(optJar))
//遍历jar 包下的文件
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 类
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文件
if (jarFile.exists()) {
jarFile.delete()
}
//使用我们修改之后的jar文件替换原先的jar文件,
//这样最终打包的Jar文件就是这个被修改了的Jar文件。
optJar.renameTo(jarFile)
}
return jarFile
}
这里主要是找到LogisticsCenter 类,然后往这类类里面插入部分代码,插入代码之后将就的Jar 包删除,使用包含插入代码的新的Jar 包。
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 是一个字节码操作库,它可以直接修改已经存在的 class 文件或者生成 class 文件。 ASM 提供了一系列便捷的功能来操作字节码内容,与其它字节码的操作框架相比(例如 AspectJ),ASM 更加偏向于底层,直接操作字节码,在设计上更小、更快,性能上更好,而且几乎可以修改任意字节码。
lass 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
//是不是loadRouterMap方法
if (name == ScanSetting.GENERATE_TO_METHOD_NAME) {
mv = new RouteMethodVisitor(Opcodes.ASM5, mv)
}
return mv
}
}
ClassVisitor 用于访问类,对于类里面定义的方法 都会被传给 visitMethod 来处理。
对于其他的方法都忽略,直到找到需要loadRouterMap 方法。
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)
}
}
extension 是ScanSetting,extension.classList保存扫描时候找到的类。
mv.visitLdcInsn(name) 相当于设置方法的参数,name 实际是一个类的全名。
mv.visitMethodInsn 就是调用LogisticsCenter的静态方法register。
LogisticsCenter.java
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);
}
}
}
register 主要是通过反射创建对象,然后根据类的类型分别处理。
总结
至此,关于ARouter的动态插入代码的实现这里就介绍完了,这里是将查找相关类的过程放到了编译中,避免在运行期间查找进而浪费时间。