*本篇文章已受權微信公衆號 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
接下來就用這個插件來爲咱們自動管理註冊表吧!