摘要
在编译时,扫描即将打包到apk中的所有类,将所有组件类收集起来,通过修改字节码的方式生成注册代码到组件管理类中,从而实现编译时自动注册的功能,不用再关心项目中有哪些组件类了。
特点:不需要注解,不会增加新的类;性能高,不需要反射,运行时直接调用组件的构造方法;能扫描到所有类,不会出现遗漏;支持分级按需加载功能的实现。
前言
最近在公司做android组件化开发框架的搭建,采用组件总线的方式进行通信:提供一个基础库,各组件(IComponent接口的实现类)都注册到组件管理类(组件总线:ComponentManager)中,组件之间在同一个app内时,通过ComponentManager转发调用请求来实现通信(不同app之间的通信方式不是本文的主题,暂且略去)。但在实现过程中遇到了一个问题:
如何将不同module中的组件类自动注册到ComponentManager中?
目前市面上比较常用的解决方案是使用annotationProcessor:通过编译时注解动态生成组件映射表代码的方式来实现。但尝试过后发现有问题,因为编译时注解的特性只在源码编译时生效,无法扫描到aar包里的注解(project依赖、maven依赖均无效),也就是说必须每个module编译时生成自己的代码,然后要想办法将这些分散在各aar种的类找出来进行集中注册。
ARouter的解决方案是:
- 每个module都生成自己的java类,这些类的包名都是’com.alibaba.android.arouter.routes’
- 然后在运行时通过读取每个dex文件中的这个包下的所有类通过反射来完成映射表的注册,详见ClassUtils.java源码
运行时通过读取所有dex文件遍历每个entry查找指定包内的所有类名,然后反射获取类对象。这种效率看起来并不高。
ActivityRouter的解决方案是(demo中有2个组件名为’app’和’sdk’):
- 在主app module中有一个
@Modules({"app", "sdk"})
注解用来标记当前app内有多少组件,根据这个注解生成一个RouterInit类 - 在RouterInit类的init方法中生成调用同一个包内的RouterMapping_app.map
- 每个module生成的类(RouterMapping_app.java 和 RouterMapping_sdk.java)都放在com.github.mzule.activityrouter.router包内(在不同的aar中,但包名相同)
- 在RouterMapping_sdk类的map()方法中根据扫描到的当前module内所有路由注解,生成了调用Routers.map(…)方法来注册路由的代码
- 在Routers的所有api接口中最终都会触发RouterInit.init()方法,从而实现所有路由的映射表注册
这种方式用一个RouterInit类组合了所有module中的路由映射表类,运行时效率比扫描所有dex文件的方式要高,但需要额外在主工程代码中维护一个组件名称列表注解: @Modules({“app”, “sdk”})
有没有一种方式可以更高效地管理这个列表呢?
联想到之前用ASM框架自动生成代码的方式做了个AndAop插件用于自动插入指定代码到任意类的任意方法中,于是写了一个自动生成注册组件的gradle插件。
大致思路是:在编译时,扫描所有类,将符合条件的类收集起来,并通过修改字节码生成注册代码到指定的管理类中,从而实现编译时自动注册的功能,不用再关心项目中有哪些组件类了。不会增加新的class,不需要反射,运行时直接调用组件的构造方法。
性能方面:由于使用效率更高的ASM框架来进行字节码分析和修改,并过滤掉android/support
包中的所有类(还支持设置自定义的扫描范围),经公司项目实测,未代码混淆前所有dex文件总计12MB左右,扫描及代码插入的总耗时在2s-3s之间,相对于整个apk打包所花3分钟左右的时间来说可以忽略不计(运行环境:MacBookPro 15吋高配 Mid 2015)。
开发完成后,考虑到这个功能的通用性,于是升级组件扫描注册插件为通用的自动注册插件AutoRegister,支持配置多种类型的扫描注册,使用方式见github中的README文档。此插件现已用到组件化开发框架: CC中
升级后,AutoRegister插件的完整功能描述是:
在编译期扫描即将打包到apk中的所有类,并将指定接口的实现类(或指定类的子类)通过字节码操作自动注册到对应的管理类中。尤其适用于命令模式或策略模式下的映射表生成。
在组件化开发框架中,可有助于实现分级按需加载的功能:
- 在组件管理类中生成组件自动注册的代码
- 在组件框架第一次被调用时加载此注册表
- 若组件中有很多功能提供给外部调用,可以将这些功能包装成多个Processor,并将它们自动注册到组件中进行管理
- 组件被初次调用时再加载这些Processor
实现过程
第一步:准备工作
- 首先要知道如何使用Android Studio开发Gradle插件
- 了解TransformAPI:Transform API是从Gradle 1.5.0版本之后提供的,它允许第三方在打包Dex文件之前的编译过程中修改java字节码(自定义插件注册的transform会在ProguardTransform和DexTransform之前执行,所以自动注册的类不需要考虑混淆的情况).参考文章有:
- Android 热修复使用Gradle Plugin1.5改造Nuwa插件(主要看前半部分关于TransformAPI的介绍,Nuwa相关的内容可先忽略)
- 字节码修改框架(相比于Javassist框架ASM较难上手,但性能更高,但相学习难度阻挡不了我们对性能的追求):
- ASM英文文档
- ASM API文档
- Android 热修复方案Tinker(七) 插桩实现(主要看关于ASM使用的介绍及与transformAPI的结合)
第二步:构建插件工程
- 按照如何使用Android Studio开发Gradle插件文章中的方法创建好插件工程并发布到本地maven仓库(我是放在工程根目录下的一个文件夹中),这样我们就可以在本地快速调试了
build.gradle文件的部分内容如下: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
69apply plugin: 'groovy'
apply plugin: 'maven'
dependencies {
compile gradleApi()
compile localGroovy()
}
repositories {
mavenCentral()
}
dependencies {
compile 'com.android.tools.build:gradle:2.2.0'
}
//加载本地maven私服配置(在工程根目录中的local.properties文件中进行配置)
Properties properties = new Properties()
properties.load(project.rootProject.file('local.properties').newDataInputStream())
def artifactory_user = properties.getProperty("artifactory_user")
def artifactory_password = properties.getProperty("artifactory_password")
def artifactory_contextUrl = properties.getProperty("artifactory_contextUrl")
def artifactory_snapshot_repoKey = properties.getProperty("artifactory_snapshot_repoKey")
def artifactory_release_repoKey = properties.getProperty("artifactory_release_repoKey")
def maven_type_snapshot = true
// 项目引用的版本号,比如compile 'com.yanzhenjie:andserver:1.0.1'中的1.0.1就是这里配置的。
def artifact_version='1.0.1'
// 唯一包名,比如compile 'com.yanzhenjie:andserver:1.0.1'中的com.yanzhenjie就是这里配置的。
def artifact_group = 'com.billy.android'
def artifact_id = 'autoregister'
def debug_flag = true //true: 发布到本地maven仓库, false: 发布到maven私服
task sourcesJar(type: Jar) {
from project.file('src/main/groovy')
classifier = 'sources'
}
artifacts {
archives sourcesJar
}
uploadArchives {
repositories {
mavenDeployer {
//deploy到maven仓库
if (debug_flag) {
repository(url: uri('../repo-local')) //deploy到本地仓库
} else {//deploy到maven私服中
def repoKey = maven_type_snapshot ? artifactory_snapshot_repoKey : artifactory_release_repoKey
repository(url: "${artifactory_contextUrl}/${repoKey}") {
authentication(userName: artifactory_user, password: artifactory_password)
}
}
pom.groupId = artifact_group
pom.artifactId = artifact_id
pom.version = artifact_version + (maven_type_snapshot ? '-SNAPSHOT' : '')
pom.project {
licenses {
license {
name 'The Apache Software License, Version 2.0'
url 'http://www.apache.org/licenses/LICENSE-2.0.txt'
}
}
}
}
}
}
根目录的build.gradle文件中要添加本地仓库的地址及dependencies1
2
3
4
5
6
7
8
9
10
11
12
13
14buildscript {
repositories {
maven{ url rootProject.file("repo-local") }
maven { url 'http://maven.aliyun.com/nexus/content/groups/public/' }
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.0.0-beta6'
classpath 'com.github.dcendents:android-maven-gradle-plugin:1.4.1'
classpath 'com.billy.android:autoregister:1.0.1'
}
}
2.在Transform类的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// 遍历输入文件
inputs.each { TransformInput input ->
// 遍历jar
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)
//遍历jar的字节码类文件,找到需要自动注册的component
if (CodeScanProcessor.shouldProcessPreDexJar(src.absolutePath)) {
CodeScanProcessor.scanJar(src, dest)
}
FileUtils.copyFile(src, dest)
project.logger.info "Copying\t${src.absolutePath} \nto\t\t${dest.absolutePath}"
}
// 遍历目录
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(file.isFile()){
CodeScanProcessor.checkInitClass(path, new File(dest.absolutePath + File.separator + path))
if (CodeScanProcessor.shouldProcessClass(path)) {
CodeScanProcessor.scanClass(file)
}
}
}
project.logger.info "Copying\t${directoryInput.file.absolutePath} \nto\t\t${dest.absolutePath}"
// 处理完后拷到目标文件
FileUtils.copyDirectory(directoryInput.file, dest)
}
}
CodeScanProcessor是一个工具类,其中CodeScanProcessor.scanJar(src, dest)
和CodeScanProcessor.scanClass(file)
分别是用来扫描jar包和class文件的
扫描的原理是利用ASM的ClassVisitor来查看每个类的父类类名及所实现的接口名称,与配置的信息进行比较,如果符合我们的过滤条件,则记录下来,在全部扫描完成后将调用这些类的无参构造方法进行注册
1 | static void scanClass(InputStream inputStream) { |
3.记录目标类所在的文件,因为我们接下来要修改其字节码,将注册代码插入进去1
2
3
4
5
6
7
8
9static void checkInitClass(String entryName, File file) {
if (entryName == null || !entryName.endsWith(".class"))
return
entryName = entryName.substring(0, entryName.lastIndexOf('.'))
RegisterTransform.infoList.each { ext ->
if (ext.initClassName == entryName)
ext.fileContainsInitClass = file
}
}
4.扫描完成后,开始修改目标类的字节码(使用ASM的MethodVisitor来修改目标类指定方法,若未指定则默认为static块,即<clinit>
方法),生成的代码是直接调用扫描到的类的无参构造方法,并非通过反射
- class文件: 直接修改此字节码文件(其实是重新生成一个class文件并替换掉原来的文件)
- jar文件:复制此jar文件,找到jar包中目标类所对应的JarEntry,修改其字节码,然后替换原来的jar文件
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
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174import org.apache.commons.io.IOUtils
import org.objectweb.asm.*
import java.util.jar.JarEntry
import java.util.jar.JarFile
import java.util.jar.JarOutputStream
import java.util.zip.ZipEntry
/**
*
* @author billy.qi
* @since 17/3/20 11:48
*/
class CodeInsertProcessor {
RegisterInfo extension
private CodeInsertProcessor(RegisterInfo extension) {
this.extension = extension
}
static void insertInitCodeTo(RegisterInfo extension) {
if (extension != null && !extension.classList.isEmpty()) {
CodeInsertProcessor processor = new CodeInsertProcessor(extension)
File file = extension.fileContainsInitClass
if (file.getName().endsWith('.jar'))
processor.insertInitCodeIntoJarFile(file)
else
processor.insertInitCodeIntoClassFile(file)
}
}
//处理jar包中的class代码注入
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 (isInitClass(entryName)) {
println('codeInsertToClassName:' + 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
}
boolean isInitClass(String entryName) {
if (entryName == null || !entryName.endsWith(".class"))
return false
if (extension.initClassName) {
entryName = entryName.substring(0, entryName.lastIndexOf('.'))
return extension.initClassName == entryName
}
return false
}
/**
* 处理class的注入
* @param file class文件
* @return 修改后的字节码文件内容
*/
private byte[] insertInitCodeIntoClassFile(File file) {
def optClass = new File(file.getParent(), file.name + ".opt")
FileInputStream inputStream = new FileInputStream(file)
FileOutputStream outputStream = new FileOutputStream(optClass)
def bytes = referHackWhenInit(inputStream)
outputStream.write(bytes)
inputStream.close()
outputStream.close()
if (file.exists()) {
file.delete()
}
optClass.renameTo(file)
return bytes
}
//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)
}
MethodVisitor visitMethod(int access, String name, String desc,
String signature, String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions)
if (name == extension.initMethodName) { //注入代码到指定的方法之中
boolean _static = (access & Opcodes.ACC_STATIC) > 0
mv = new MyMethodVisitor(Opcodes.ASM5, mv, _static)
}
return mv
}
}
class MyMethodVisitor extends MethodVisitor {
boolean _static;
MyMethodVisitor(int api, MethodVisitor mv, boolean _static) {
super(api, mv)
this._static = _static;
}
void visitInsn(int opcode) {
if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN)) {
extension.classList.each { name ->
if (!_static) {
//加载this
mv.visitVarInsn(Opcodes.ALOAD, 0)
}
//用无参构造方法创建一个组件实例
mv.visitTypeInsn(Opcodes.NEW, name)
mv.visitInsn(Opcodes.DUP)
mv.visitMethodInsn(Opcodes.INVOKESPECIAL, name, "<init>", "()V", false)
//调用注册方法将组件实例注册到组件库中
if (_static) {
mv.visitMethodInsn(Opcodes.INVOKESTATIC
, extension.registerClassName
, extension.registerMethodName
, "(L${extension.interfaceName};)V"
, false)
} else {
mv.visitMethodInsn(Opcodes.INVOKESPECIAL
, extension.registerClassName
, extension.registerMethodName
, "(L${extension.interfaceName};)V"
, false)
}
}
}
super.visitInsn(opcode)
}
void visitMaxs(int maxStack, int maxLocals) {
super.visitMaxs(maxStack + 4, maxLocals)
}
}
}
5.接收扩展参数,获取需要扫描类的特征及需要插入的代码
找了很久没找到gradle插件接收自定义对象数组扩展参数的方法,于是退一步改用List<Map>
接收后再进行转换的方式来实现,以此来接收多个扫描任务的扩展参数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
45import org.gradle.api.Project
/**
* aop的配置信息
* @author billy.qi
* @since 17/3/28 11:48
*/
class AutoRegisterConfig {
public ArrayList<Map<String, Object>> registerInfo = []
ArrayList<RegisterInfo> list = new ArrayList<>()
Project project
AutoRegisterConfig(){}
void convertConfig() {
registerInfo.each { map ->
RegisterInfo info = new RegisterInfo()
info.interfaceName = map.get('scanInterface')
def superClasses = map.get('scanSuperClasses')
if (!superClasses) {
superClasses = new ArrayList<String>()
} else if (superClasses instanceof String) {
ArrayList<String> superList = new ArrayList<>()
superList.add(superClasses)
superClasses = superList
}
info.superClassNames = superClasses
info.initClassName = map.get('codeInsertToClassName') //代码注入的类
info.initMethodName = map.get('codeInsertToMethodName') //代码注入的方法(默认为static块)
info.registerMethodName = map.get('registerMethodName') //生成的代码所调用的方法
info.registerClassName = map.get('registerClassName') //注册方法所在的类
info.include = map.get('include')
info.exclude = map.get('exclude')
info.init()
if (info.validate())
list.add(info)
else {
project.logger.error('auto register config error: scanInterface, codeInsertToClassName and registerMethodName should not be null\n' + info.toString())
}
}
}
}
1 | import java.util.regex.Pattern |
第三步: 在application中配置自动注册插件所需的相关扩展参数
在主app module的build.gradle文件中添加扩展参数,示例如下:
1 | //auto register extension |
总结
本文介绍了AutoRegister插件的功能及其在组件化开发框架中的应用。重点对其原理做了说明,主要介绍了此插件的实现过程,其中涉及到的技术点有TransformAPI、ASM、groovy相关语法、gradle机制。
本插件的所有代码及其用法demo已开源到github上,欢迎fork、start
接下来就用这个插件来为我们自动管理注册表吧!