前言
在编译时,扫描即将打包到apk中的所有类,将所有组件类收集起来,通过修改字节码的方式生成注册代码到组件管理类中,从而实现编译时自动注册的功能,不用再关心项目中有哪些组件类了。 特点:不需要注解,不会增加新的类;性能高,不需要反射,运行时直接调用组件的构造方法;能扫描到所有类,不会出现遗漏;支持分级按需加载功能的实现。--来源框架作者齐翊
alibaba/ARouter的 gradle 路由收集插件也是这位作者写的,这个两个插件代码大同小异。
让我们从路由查找,路由写入,路由缓存,组件隔离区分析吧。
废话不说,开始分析。
路由查找
了解TransformAPI:Transform API是从Gradle 1.5.0版本之后提供的,它允许第三方在打包Dex文件之前的编译过程中修改java字节码(自定义插件注册的transform会在ProguardTransform和DexTransform之前执行,所以自动注册的类不需要考虑混淆的情况). 了解TransformAPI:Transform API是从Gradle 1.5.0版本之后提供的,它允许第三方在打包Dex文件之前的编译过程中修改java字节码(自定义插件注册的transform会在ProguardTransform和DexTransform之前执行,所以自动注册的类不需要考虑混淆的情况).
来让我们看看路由是从哪里开始收集的;去收集什么,我们从RegisterTransform这个类开始。
收集的就是实现于IComponent这个接口的类。重点关注一下。
public interface IComponent {
String getName();
boolean onCall(CC cc);
}
遍历输入文件:
1,遍历jar,input.jarInputs.each
,哇。这么简单就能得到我们项目的jar文件了。
2,遍历目录,input.directoryInputs.each
这一部分是class文件。也很简单得到。
我们先跳过扫描缓存部分,下面说缓存的时再分析。
CodeScanner scanProcessor = new CodeScanner(extension.list, cacheMap)
// 遍历输入文件
inputs.each { TransformInput input ->
// 遍历jar
input.jarInputs.each { JarInput jarInput ->
//把发生变动的类,把这个类的接口,从缓存中移除。
if (jarInput.status != Status.NOTCHANGED && cacheMap) {
cacheMap.remove(jarInput.file.absolutePath)
}
//扫描jar
scanJar(jarInput, outputProvider, scanProcessor)
}
// 遍历目录
input.directoryInputs.each { DirectoryInput directoryInput ->
long dirTime = System.currentTimeMillis()
def root = scanClass(outputProvider, directoryInput, scanProcessor)
long scanTime = System.currentTimeMillis()
println "${PLUGIN_NAME} cost time: ${System.currentTimeMillis() - dirTime}, scan time: ${scanTime - dirTime}. path=${root}"
}
}
遍历目录class
先说遍历class吧,扫描class比扫描jar,要少一步jarFile的分析,后面都是一样的。后面是调用scanProcessor.scanClass
去扫描class,让我们进去核心地带scanClass
遨游一番。
这块可能需要你懂一点ASM,不懂也没事,后面找个中文的ASM api看看就好,不复杂。这里我重点关注一下ScanClassVisitor
。
boolean scanClass(InputStream inputStream, String filePath) {
int api = getAsmApiLevel()
try {
ClassReader cr = new MyClassReader(inputStream)
ClassWriter cw = new ClassWriter(cr, 0)
//重点关注
ScanClassVisitor cv = new ScanClassVisitor(api, cw, filePath)
cr.accept(cv, ClassReader.EXPAND_FRAMES)
inputStream.close()
return cv.found
}
ScanClassVisitor
到了核心地带,桃花深处,ScanClassVisitor
中的visit
方法了,让我拨开接口扫描的奥秘。黎明就在眼前...
这里我们分三步走:
第一步:shouldProcessThisClassForRegister,
是否过滤找个类,找个是可以配置的,也是为了减少扫描的时间,比如一些第三方库,什么okhttp,android自带的就没必要扫描了。节省体力,不,是节省时间的开销。
第二步:superClassNames
,这是也是可以配置的,就是接口的父类。如你用一个BaseComponent
,实现于IComponent
,把一些公用的方法写在里面。但这里也不是重点。
void visit(int version, int access, String name, String signature,
String superName, String[] interfaces) {
super.visit(version, access, name, signature, superName, interfaces)
//省了抽象类、接口、非public等类无法调用其无参构造方法的代码
infoList.each { ext ->
if (shouldProcessThisClassForRegister(ext, name)) {
def interfaceName = ext.interfaceName
if (superName != 'java/lang/Object' && !ext.superClassNames.isEmpty(){
for (int i = 0; i < ext.superClassNames.size(); i++) {
if (ext.superClassNames.get(i) == superName) {
gotOne(interfaceName, name, ext)
return
}
}
}
if (interfaceName && interfaces != null) {
interfaces.each { itName ->
//最核心的重点
if (itName == interfaceName) {
gotOne(interfaceName, name, ext)
}
}
}
}
}
}
第三步:敲黑板
,最核心的重点来了,if (itName == interfaceName)
就这一步,判断类是否有实现IComponent
。有就记录起来ext.classList.add(className)
,这里class 是一个set list列表,扫描结束后,需要把这些对象注入到管理类 就是fileContainsInitClass
。稍后下面组件接口注入说吧,addToCacheMap
添加到缓存,稍微留意一下这里,后面的缓存会说到这里。
到此类的扫描接口就结束了
void gotOne(String interfaceName, String className, RegisterInfo ext) {
ext.classList.add(className) //需要把对象注入到管理类 就是fileContainsInitClass
found = true
//添加到缓存
addToCacheMap(interfaceName, className, filePath)
}
遍历jar
遍历jar,就是多了一步jarFile
的处理,后面还是跟扫描class一样的流程,还是调用scanClass
。这里就过了。
//扫描jar包
boolean scanJar(File jarFile, File destFile) {
....
def srcFilePath = jarFile.absolutePath
def file = new JarFile(jarFile)
Enumeration enumeration = file.entries()
while (enumeration.hasMoreElements()) {
JarEntry jarEntry = (JarEntry) enumeration.nextElement()
String entryName = jarEntry.getName()
//support包不扫描
if (entryName.startsWith("android/support"))
break
checkInitClass(entryName, destFile, srcFilePath)
//是否要过滤这个类,这个可配置
if (shouldProcessClass(entryName)) {
InputStream inputStream = file.getInputStream(jarEntry)
//还是调用 scanClass。
scanClass(inputStream, jarFile.absolutePath)
inputStream.close()
}
}
....
return true
}
扫描到组件的写入
实现了IComponent接口的组件会通过ASM写入到ComponentManager
这个类下面。最终是被初始化添加到ConcurrentHashMap
里面,像这样:
class ComponentManager{
static {
registerComponent(new DynamicComponentOption());
//加载类时自动调用初始化:注册所有组件
//通过插件生成组件注册代码
//生成的代码如下:
registerComponent(new ComponentA());
registerComponent(new ComponentB());
//初始化到ConcurrentHashMap
}
}
接着我来看看是如何写入的,激动人心的时刻到来了,辛苦了半天。
注意这一句代码, RegistryCodeGenerator.insertInitCodeTo(ext)
,接着我们继续方法往里面走吧。
extension.list.each { ext ->
if (ext.fileContainsInitClass) {
println('')
println("insert register code to file:" + ext.fileContainsInitClass.absolutePath)
if (ext.classList.isEmpty()) {
project.logger.error("No class implements found for interface:" + ext.interfaceName)
} else {
ext.classList.each {
println(it)
}
//注入组件
RegistryCodeGenerator.insertInitCodeTo(ext)
}
} else {
project.logger.error("The specified register class not found:" + ext.registerClassName)
}
}
我们还是挑软骨头啃,看注入class的这一部分,RegistryCodeGenerator
的generateCodeIntoClassFile
,经过层层关卡,我们会来到这个MyMethodVisitor
类中的visitInsn
。核心注入就在这里了。若未指定则默认为static块
,即<clinit>
方法),生成的代码是直接调用扫描到的类的无参构造方法。
@Override
void visitInsn(int opcode) {
if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN)) {
extension.classList.each { name ->
if (!_static) {
//加载this
mv.visitVarInsn(Opcodes.ALOAD, 0)
}
String paramType
if (extension.paramType == RegisterInfo.PARAM_TYPE_CLASS){
mv.visitLdcInsn(Type.getType("L${name};"))
paramType = 'java/lang/Class'
} else if (extension.paramType == RegisterInfo.PARAM_TYPE_CLASS_NAME){
mv.visitLdcInsn(name.replaceAll("/", "."))
paramType = 'java/lang/String'
} else {
//用无参构造方法创建一个组件实例
mv.visitTypeInsn(Opcodes.NEW, name)
mv.visitInsn(Opcodes.DUP)
mv.visitMethodInsn(Opcodes.INVOKESPECIAL, name, "<init>", "()V", false)
paramType = extension.interfaceName
}
int methodOpcode = _static ? Opcodes.INVOKESTATIC : Opcodes.INVOKESPECIAL
//调用注册方法将组件实例注册到组件库中
mv.visitMethodInsn(methodOpcode
, extension.registerClassName
, extension.registerMethodName
, "(L${paramType};)V"
, false)
}
}
super.visitInsn(opcode)
}
这些一大堆乱七八糟的东西java/lang/Class, java/lang/String等 一定奇怪吧!因为我们用的ASM是操作字节码的,所以这里你需要对JAVA的字节码有一定的了解。再了解一ASM 的api,那就容易理解了。
到此核心部分已经结束了
下面的组件扫描缓存和组件隔离是加分项了,来,让我拖着疲惫的身躯继续吧,其实我不累,只是我身体累而已。
组件缓存
组件缓存是使用官方提供的增量编译提供的数据,进行差异缓存的。真的是天然的优势。看看增量编译文件的几个状态。
public enum Status {
/**
* The file was not changed since the last build.
没改变
*/
NOTCHANGED,
/**
* The file was added since the last build.
新增
*/
ADDED,
/**
* The file was modified since the last build.
改变
*/
CHANGED,
/**
* The file was removed since the last build.
删除
*/
REMOVED;
}
从RegisterTransform
方法的transform
开始看。
如果开启缓存模式,就去 RegisterCache.getRegisterCacheFile
获取缓存文件,这个文件会生成在build
下的一个cc-register
目录下,名为register-cache.json
,具体大家看看这个文件辅助类RegisterCache
。如果缓存文件为空,就去全局扫描。
if (cacheEnabled) { //开启了缓存
gson = new Gson()
cacheFile = RegisterCache.getRegisterCacheFile(project)
if (clearCache && cacheFile.exists())
cacheFile.delete()
cacheMap = RegisterCache.readToMap(cacheFile, new
TypeToken<HashMap<String,ScanHarvest>>() {
}.getType())
if (cacheMap.isEmpty()) {
// 全局扫描
isAllScan = true
}
}
缓存接口的信息,className类名,interfaceName接口名,isInitClass是否是初始化类。processName目前没有用到,应该是备用。
把这些信息都读取到cacheMap
里面,这个map
key
是文件的绝对路径。这些信息用于后面文件写入。
/**
* 已扫描到接口或者codeInsertToClassName jar的信息
*/
class ScanHarvest {
List<Harvest> harvestList = new ArrayList<>()
class Harvest {
String className
String interfaceName
boolean isInitClass
String processName
}
}
这里我依旧从 RegisterTransform scanClass 的缓存开始说起:
def scanClass(TransformOutputProvider outputProvider, DirectoryInput directoryInput, CodeScanner scanProcessor) {
....
// changedFiles 为空 或者 关闭缓存
if (directoryInput.changedFiles.isEmpty() || !cacheEnabled || isAllScan) {
//遍历目录下的每个文件
directoryInput.file.eachFileRecurse { File file ->
scanClassFile(file, root, leftSlash, scanProcessor, dest)
}
} else {
//移除发生改变的缓存
directoryInput.changedFiles.each { fileList ->
cacheMap.remove(fileList.key.absolutePath)
}
cacheMap.each { cache ->
if (cache.key.endsWith(".class")) {
def path = cache.key.replace(root, '')
scanProcessor.hitCache(new File(cache.key), new File(dest, path))
}
}
//扫描发生改变的文件
directoryInput.changedFiles.each { fileList ->
def file = fileList.key
if (fileList.value == Status.CHANGED || fileList.value == Status.ADDED) {
scanClassFile(file, root, leftSlash, scanProcessor, dest)
}
}
}
// 处理完后拷到目标文件
FileUtils.copyDirectory(directoryInput.file, dest)
return root
}
这里我们分五个步骤:
第一步:如果 changedFiles 为空 或者 关闭缓存,就全局扫描。
第二步:移除发生改变的缓存
第三步:查看class是否可以命中缓存,有就添加class list 和 设置fileContainsInitClass。这里大家可以去看看这个scanProcessor.hitCache方法。
第四步:扫描发生改变的文件,和scanClass一样。
第五步:处理完后拷到目标文件, FileUtils.copyDirectory(directoryInput.file, dest)
scanJar的缓存就不说了,因为跟scanClass是差不多的。
那么这些缓存是如何保存下来的?
这个就得关心一下我们上面scanClass
的流程了,就在scanClass
的第三步。可以回头看看。这里不说了。详细代码在CodeScanner
的内部类ScanClassVisitor
的visit
方法 中。
void gotOne(String interfaceName, String className, RegisterInfo ext) {
ext.classList.add(className) //需要把对象注入到管理类 就是fileContainsInitClass
found = true
//添加到缓存
addToCacheMap(interfaceName, className, filePath)
}
到此组件接口缓存就讲完了,下面说说组件之间的代码隔离。
组件之间的隔离
我们可以在项目的demo中的build.gradle文件,看到依赖: 'demo_component_a',addComponent
是一个自定义依赖,这个在写代码时,代码是隔离的,只有打包时候才会依赖进去。
dependencies {
//Notice:组件之间不要互相依赖,只在主app module依赖其它组件
addComponent 'demo_component_a'
addComponent 'demo_component_kt'
}
具体是如何管理的实现的,具体代码在这个类ProjectModuleManager
, addComponentDependencyMethod
方法。核心在于这个一行代码。project.dependencies.add(dependencyMode, realDependency)
。
def dependencyMode = GradleVersion.version(project.gradle.gradleVersion) >= GradleVersion.version('4.1') ? 'api' : 'compile'
if (realDependency) {
//通过参数传递的依赖方式,如:
// project(':moduleName')
// 或
// 'com.billy.demo:demoA:1.1.0'
project.dependencies.add(dependencyMode, realDependency)
到此,全文结束了,谢谢大家。
项目中cc-register这个文件夹为gradle插件,就是文中讨论分析的。
参考
ASM API文档
齐翊 AutoRegister: 一种更高效的组件自动注册方案 juejin.cn/post/684490…