*本篇文章已受权微信公众号 guolin_blog (郭霖)独家发布html
摘要:java
在编译时,扫描即将打包到apk中的全部类,将全部组件类收集起来,经过修改字节码的方式生成注册代码到组件管理类中,从而实现编译时自动注册的功能,不用再关心项目中有哪些组件类了。 特色:不须要注解,不会增长新的类;性能高,不须要反射,运行时直接调用组件的构造方法;能扫描到全部类,不会出现遗漏;支持分级按需加载功能的实现。android
最近在公司作android组件化开发框架的搭建,采用组件总线的方式进行通讯:提供一个基础库,各组件(IComponent接口的实现类)都注册到组件管理类(组件总线:ComponentManager)中,组件之间在同一个app内时,经过ComponentManager转发调用请求来实现通讯(不一样app之间的通讯方式不是本文的主题,暂且略去)。但在实现过程当中遇到了一个问题:git
如何将不一样module中的组件类自动注册到ComponentManager中?github
目前市面上比较经常使用的解决方案是使用annotationProcessor:经过编译时注解动态生成组件映射表代码的方式来实现。但尝试事后发现有问题,由于编译时注解的特性只在源码编译时生效,没法扫描到aar包里的注解(project依赖、maven依赖均无效),也就是说必须每一个module编译时生成本身的代码,而后要想办法将这些分散在各aar种的类找出来进行集中注册。web
ARouter的解决方案是:正则表达式
运行时经过读取全部dex文件遍历每一个entry查找指定包内的全部类名,而后反射获取类对象。这种效率看起来并不高。apache
ActivityRouter的解决方案是(demo中有2个组件名为'app'和'sdk'):api
@Modules({"app", "sdk"})
注解用来标记当前app内有多少组件,根据这个注解生成一个RouterInit类这种方式用一个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中的全部类,并将指定接口的实现类(或指定类的子类)经过字节码操做自动注册到对应的管理类中。尤为适用于命令模式或策略模式下的映射表生成。
在组件化开发框架中,可有助于实现分级按需加载的功能:
build.gradle文件的部份内容以下:
apply 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文件中要添加本地仓库的地址及dependencies
buildscript {
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方法中添加类扫描相关的代码
// 遍历输入文件
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来查看每一个类的父类类名及所实现的接口名称,与配置的信息进行比较,若是符合咱们的过滤条件,则记录下来,在所有扫描完成后将调用这些类的无参构造方法进行注册
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.infoList.each { ext ->
if (shouldProcessThisClassForRegister(ext, name)) {
if (superName != 'java/lang/Object' && !ext.superClassNames.isEmpty()) {
for (int i = 0; i < ext.superClassNames.size(); i++) {
if (ext.superClassNames.get(i) == superName) {
ext.classList.add(name)
return
}
}
}
if (ext.interfaceName && interfaces != null) {
interfaces.each { itName ->
if (itName == ext.interfaceName) {
ext.classList.add(name)
}
}
}
}
}
}
}
复制代码
3.记录目标类所在的文件,由于咱们接下来要修改其字节码,将注册代码插入进去
static 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>
方法),生成的代码是直接调用扫描到的类的无参构造方法,并不是经过反射
import 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)
}
@Override
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;
}
@Override
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)
}
@Override
void visitMaxs(int maxStack, int maxLocals) {
super.visitMaxs(maxStack + 4, maxLocals)
}
}
}
复制代码
5.接收扩展参数,获取须要扫描类的特征及须要插入的代码
找了好久没找到gradle插件接收自定义对象数组扩展参数的方法,因而退一步改用List<Map>
接收后再进行转换的方式来实现,以此来接收多个扫描任务的扩展参数
import 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())
}
}
}
}
复制代码
import java.util.regex.Pattern
/**
* aop的配置信息
* @author billy.qi
* @since 17/3/28 11:48
*/
class RegisterInfo {
static final DEFAULT_EXCLUDE = [
'.*/R(\\$[^/]*)?'
, '.*/BuildConfig$'
]
//如下是可配置参数
String interfaceName = ''
ArrayList<String> superClassNames = []
String initClassName = ''
String initMethodName = ''
String registerClassName = ''
String registerMethodName = ''
ArrayList<String> include = []
ArrayList<String> exclude = []
//如下不是可配置参数
ArrayList<Pattern> includePatterns = []
ArrayList<Pattern> excludePatterns = []
File fileContainsInitClass //initClassName的class文件或含有initClassName类的jar文件
ArrayList<String> classList = new ArrayList<>()
RegisterInfo(){}
boolean validate() {
return interfaceName && registerClassName && registerMethodName
}
//用于在console中输出日志
@Override
String toString() {
StringBuilder sb = new StringBuilder('{')
sb.append('\n\t').append('scanInterface').append('\t\t\t=\t').append(interfaceName)
sb.append('\n\t').append('scanSuperClasses').append('\t\t=\t[')
for (int i = 0; i < superClassNames.size(); i++) {
if (i > 0) sb.append(',')
sb.append(' \'').append(superClassNames.get(i)).append('\'')
}
sb.append(' ]')
sb.append('\n\t').append('codeInsertToClassName').append('\t=\t').append(initClassName)
sb.append('\n\t').append('codeInsertToMethodName').append('\t=\t').append(initMethodName)
sb.append('\n\t').append('registerMethodName').append('\t\t=\tpublic static void ')
.append(registerClassName).append('.').append(registerMethodName)
sb.append('\n\t').append('include').append(' = [')
include.each { i ->
sb.append('\n\t\t\'').append(i).append('\'')
}
sb.append('\n\t]')
sb.append('\n\t').append('exclude').append(' = [')
exclude.each { i ->
sb.append('\n\t\t\'').append(i).append('\'')
}
sb.append('\n\t]\n}')
return sb.toString()
}
void init() {
if (include == null) include = new ArrayList<>()
if (include.empty) include.add(".*") //若是没有设置则默认为include全部
if (exclude == null) exclude = new ArrayList<>()
if (!registerClassName)
registerClassName = initClassName
//将interfaceName中的'.'转换为'/'
if (interfaceName)
interfaceName = convertDotToSlash(interfaceName)
//将superClassName中的'.'转换为'/'
if (superClassNames == null) superClassNames = new ArrayList<>()
for (int i = 0; i < superClassNames.size(); i++) {
def superClass = convertDotToSlash(superClassNames.get(i))
superClassNames.set(i, superClass)
if (!exclude.contains(superClass))
exclude.add(superClass)
}
//interfaceName添加到排除项
if (!exclude.contains(interfaceName))
exclude.add(interfaceName)
//注册和初始化的方法所在的类默认为同一个类
initClassName = convertDotToSlash(initClassName)
//默认插入到static块中
if (!initMethodName)
initMethodName = "<clinit>"
registerClassName = convertDotToSlash(registerClassName)
//添加默认的排除项
DEFAULT_EXCLUDE.each { e ->
if (!exclude.contains(e))
exclude.add(e)
}
initPattern(include, includePatterns)
initPattern(exclude, excludePatterns)
}
private static String convertDotToSlash(String str) {
return str ? str.replaceAll('\\.', '/').intern() : str
}
private static void initPattern(ArrayList<String> list, ArrayList<Pattern> patterns) {
list.each { s ->
patterns.add(Pattern.compile(s))
}
}
}
复制代码
在主app module的build.gradle文件中添加扩展参数,示例以下:
//auto register extension
// 功能介绍:
// 在编译期扫描将打到apk包中的全部类
// 将 scanInterface的实现类 或 scanSuperClasses的子类
// 并在 codeInsertToClassName 类的 codeInsertToMethodName 方法中生成以下代码:
// codeInsertToClassName.registerMethodName(scanInterface)
// 要点:
// 1. codeInsertToMethodName 若未指定,则默认为static块
// 2. codeInsertToMethodName 与 registerMethodName 须要同为static或非static
// 自动生成的代码示例:
/*
在com.billy.app_lib_interface.CategoryManager.class文件中
static
{
register(new CategoryA()); //scanInterface的实现类
register(new CategoryB()); //scanSuperClass的子类
}
*/
apply plugin: 'auto-register'
autoregister {
registerInfo = [
[
'scanInterface' : 'com.billy.app_lib_interface.ICategory'
// scanSuperClasses 会自动被加入到exclude中,下面的exclude只做为演示,其实能够不用手动添加
, 'scanSuperClasses' : ['com.billy.android.autoregister.demo.BaseCategory']
, 'codeInsertToClassName' : 'com.billy.app_lib_interface.CategoryManager'
//未指定codeInsertToMethodName,默认插入到static块中,故此处register必须为static方法
, 'registerMethodName' : 'register' //
, 'exclude' : [
//排除的类,支持正则表达式(包分隔符须要用/表示,不能用.)
'com.billy.android.autoregister.demo.BaseCategory'.replaceAll('\\.', '/') //排除这个基类
]
],
[
'scanInterface' : 'com.billy.app_lib.IOther'
, 'codeInsertToClassName' : 'com.billy.app_lib.OtherManager'
, 'codeInsertToMethodName' : 'init' //非static方法
, 'registerMethodName' : 'registerOther' //非static方法
]
]
}
复制代码
本文介绍了AutoRegister插件的功能及其在组件化开发框架中的应用。重点对其原理作了说明,主要介绍了此插件的实现过程,其中涉及到的技术点有TransformAPI、ASM、groovy相关语法、gradle机制。
本插件的全部代码及其用法demo已开源到github上,欢迎fork、start
接下来就用这个插件来为咱们自动管理注册表吧!