理解AOP是个啥
AOP即面向切片编程,通过编译期预处理或者运行时动态代理的方式,不侵入各模块具体业务代码,在某一切面上实现对某一类问题的统一处理。
可以这么理解,OOP是纵向在子类与父类之间处理逻辑的分层,而AOP是横向处理,不限于满足继承关系的一系列类,只要可以找到切面就可以统一处理。其概念的核心点在于不侵入各模块具体业务代码,否则也可以把在基类中添加统一逻辑看做一个切面,但一般这种情况都不叫AOP,而是OOP的继承特性。
AOP是一种思想,不限于语言、框架,只要满足以上概念就可以认为是AOP,例如我们都比较熟悉的JDK提供的动态代理Proxy.newProxyInstance + InvocationHandler
// 集中配置某Activity内所有xml中定义ImageView的默认显示图片
fun replaceAllImage(context: Context) {
try {
val layoutInflater = LayoutInflater.from(context)
val mFactory2: Field = LayoutInflater::class.java.getDeclaredField("mFactory2")
mFactory2.isAccessible = true
val oldField = mFactory2.get(layoutInflater)
val hookFactory2 = Proxy.newProxyInstance(context.javaClass.classLoader,
arrayOf<Class<*>>(LayoutInflater.Factory2::class.java)) { _, method, args ->
val result = method.invoke(oldField, *args)
if (result is ImageView) {
result.setImageResource(R.drawable.immersive)
}
return@newProxyInstance result
}
mFactory2.set(layoutInflater, hookFactory2)
} catch (exception: Exception) {
ToastUtils.showShortToast("hook失败: $exception")
}
}
再比如Application中注册的ActivityLifecycleCallbacks
// 集中监听所有Activity创建
registerActivityLifecycleCallbacks(object :ActivityLifecycleCallbacks{
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
Log.d("LifecycleCallbacks", "${activity.componentName}.onActivityCreated")
replaceAllImage(activity)
}
}
以上两段代码可以在不侵入Activity具体代码的前提下给所有Activity布局内的所有ImageView添加默认图,虽然它们都是SDK内置的API,不是什么高深的框架,但也都属于AOP的范畴。
常见AOP手段
AOP分为两种
- 编译期预处理方式,在运行前、编译中把代码处理好:
- APT
- Transform
- AspectJ
- 运行时处理,例如动态代理、运行时各种各样的Hook等:
- Cglib + DexMaker
- Dexposed
- Xposed
- ADocker
下面我们先看一下它们具体都是怎么工作的,鉴于篇幅,这里我们先只看下预编译处理的三种方式。
APT
APT,即Annotation Processor Tools,它用来获取标注某注解的所有类/方法/参数并执行某些操作,一般是生成代码并打入包中,后续通过反射提供给业务代码实用。
- 严格的说,按我们上面说的概念,APT其实不算是正统的AOP,因为它需要侵入业务代码,给所有位置手动添加注解;
- 宽泛点说,也可以认为是AOP,业务类添加注解可以理解为额外手动创建一个切面。
简单示例
- 继承
AbstractProcessor,重写process方法,查找到所有标注特定注解的类并缓存; - 通过
processingEnv.filer输出一个类文件。
@AutoService(Processor::class)
class RouterProcessor : AbstractProcessor() {
private var generateContent = ""
override fun process(typeElementSet: MutableSet<out TypeElement>, roundEnvironment: RoundEnvironment): Boolean {
if (roundEnvironment.processingOver()) {
// 第二步,生成文件
generateFile()
} else {
// 第一步,遍历注解类并缓存在list中
for (typeElement in typeElementSet) {
val elements = roundEnvironment.getElementsAnnotatedWith(typeElement) ?: continue
for (element in elements) {
if (element is Symbol.ClassSymbol) {
generateContent += element.fullname.toString() + "|"
}
}
}
}
return false
}
private fun generateFile() {
try {
val source = processingEnv.filer.createSourceFile(ROUTER_CLASS_NAME)
val writer: Writer = source.openWriter()
writer.write(
"""
package com.youcii.advanced;
/**
* Created by APT on ${Date()}.
*/
public class RouteList {
public static final String $ROUTER_FIELD_NAME = \"$generateContent\";
}
"""
)
writer.flush()
writer.close()
} catch (ignore: IOException) {
print("写入失败$ignore")
}
}
override fun getSupportedSourceVersion(): SourceVersion {
return SourceVersion.latestSupported()
}
override fun getSupportedAnnotationTypes(): MutableSet<String> {
return LinkedHashSet<String>().apply {
add(Router::class.java.canonicalName)
}
}
companion object {
/**
* 生成的类全名
*/
const val ROUTER_CLASS_NAME = "com.youcii.advanced.RouteList"
/**
* 生成的类内数据存储变量名
*/
const val ROUTER_FIELD_NAME = "list"
}
}
另外,生成类文件时也可以使用JavaPoet库,例如想要生成该类:
package com.youcii.advanced;
import java.lang.String;
/**
* Created by APT on Fri Feb 19 14:58:56 CST 2021.
*/
public class RouteList {
/**
* 存储Router列表,并用|分割
*/
public static final String list = "xxx";
}
使用JavaPoet库的写法为:
private fun generateFileWithJavaPoet() {
val listField = FieldSpec.builder(String::class.java, ROUTER_FIELD_NAME)
.addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL)
.addJavadoc("存储Router列表,并用|分割\n")
.initializer("\"$generateContent\"")
.build()
val resultClass = TypeSpec.classBuilder("RouteList")
.addModifiers(Modifier.PUBLIC)
.addJavadoc("Created by APT on ${Date()}.\n")
.addField(listField)
.build()
val javaFile = JavaFile.builder("com.youcii.advanced", resultClass)
.build()
try {
val source = processingEnv.filer.createSourceFile(ROUTER_CLASS_NAME)
val writer: Writer = source.openWriter()
javaFile.writeTo(writer)
writer.flush()
writer.close()
} catch (ignore: IOException) {
print("写入失败$ignore")
}
}
个人感觉JavaPoet写起来比较繁琐,而且可读性也很差,不如单独在一个文件写完之后直接复制。而且它只能生成java文件,不能生成kotlin。
原理
为什么我们定义一个Processor配置就可以被gradle自动处理到呢?
APT技术是SPI(Service Provider Interface 服务动态提供接口)的一种应用,核心类是ServiceLoader,在所有的java项目中都可以使用。具体流程为:进行javac编译时java Compiler会执行ServiceLoader.load(XXX.class)方法,内部会固定去resource/META-INF/services路径下查找指定XXX.class的全包名文件,并反射构建文件内部声明的所有子类,然后分别执行XXX.class内的唯一接口方法。
对于SPI的特殊应用APT来说,这些步骤是在名为kaptKotlin的gradle task中执行的。
多轮处理机制
APT处理在同一个Processor对象中会执行多轮process方法,通过RoundEnvironment指定具体待处理元素,例如第一轮会传入该module下所有的待检测元素
最后一轮会传入空,并且通过
processingOver标记为已经处理完毕
AutoService
另外简单介绍一下我们写APT时经常会用到的AutoService库,该库提供了@AutoService(Processor.class)注解避免手动配置resource/META-INFO/services步骤。
它的原理也是利用了APT:它会遍历添加@AutoService的所有类,自动在build/resources/main/META-INF/services/中生成指定的包名文件,并在内部写入了当前的注解类。
为什么AutoService内APT处理完成后还会继续处理我们自定义的APT呢?是因为APT会执行很多遍的原因么?其实是因为gradle的编译顺序是按照依赖顺序依次处理,AutoService作为被依赖的三方库会优先编译,其生成的Processor后续会在项目内部module的kapt task中自行被调用到。
Gradle Transform API
Transform是我们平时使用最多的一种AOP方式,它是android-build-tool提供的一个gradle插件,用于在class编译为dex前修改class文件,具体的执行时机为:compile task与d8 task之间。
- Transform并不是必须的,只要找到class编译为dex的task之前的Task,通过before插入一个自定义task也可以,但使用Transform更简单。
- transform也可以直接手写字节码流,不是必须利用ASM和Javassist等class文件修改工具,但如果使用它们会让class修改更为简单;因为ASM与Javassist相比功能更加强大,基本所有的需求都能实现,所以一般我们都是使用ASM。
简单示例
- 自定义gradle插件,在buildSrc中编写transform并注册。
class TransformPlugin : Plugin<Project> {
override fun apply(target: Project) {
val baseExtension = target.extensions.findByType(BaseExtension::class.java)
baseExtension?.registerTransform(TestNonIncrementTransform())
}
}
- 创建Transform子类
abstract class BaseTransform : Transform(),重写以下方法:
/**
* 当前Transform在列表中存储的名称
*/
override fun getName(): String {
return javaClass.name
}
/**
* 不支持增量编译处理
*/
override fun isIncremental(): Boolean {
return false
}
/**
* 过滤维度一: 输入类型
* CLASSES--代码
* RESOURCES--既不是代码也不是android项目中的res资源,而是asset目录下的资源
*
* 其实上面两个只是暴露给我们的, 另外还有仅Android内部Plugin可用的类型:
* DEX, NATIVE_LIBS, CLASSES_ENHANCED, DATA_BINDING, DEX_ARCHIVE, DATA_BINDING_BASE_CLASS_LOG
*/
override fun getInputTypes(): MutableSet<QualifiedContent.ContentType> {
return TransformManager.CONTENT_CLASS
}
/**
* 过滤维度二: 要处理的.class文件的范围. 如果仅仅只读的话需要在此方法中返回空, 使用getReferencedScopes指定读取的对象
*
* 标准作用域:
* <code>
* enum Scope implements ScopeType {
* // 仅最外层主工程
* PROJECT(0x01),
* // 主工程下的各个module
* SUB_PROJECTS(0x04),
* // lib中引用的jar, implement引入的三方库
* EXTERNAL_LIBRARIES(0x10),
* // Code that is being tested by the current variant, including dependencies
* TESTED_CODE(0x20),
* // Local or remote dependencies that are provided-only
* PROVIDED_ONLY(0x40),
* }
* </code>
*
* 额外作用域:
* <code>
* public enum InternalScope implements QualifiedContent.ScopeType {
* // Scope to package classes.dex files in the main split APK in InstantRun mode. All other classes.dex will be packaged in other split APKs.
* MAIN_SPLIT(0x10000),
* // Only the project's local dependencies (local jars). This is to be used by the library plugin, only (and only when building the AAR).
* LOCAL_DEPS(0x20000),
* // 包括dynamic-feature modules
* FEATURES(0x40000),
* }
* </code>
*/
override fun getScopes(): MutableSet<in QualifiedContent.Scope> {
return TransformManager.SCOPE_FULL_PROJECT
}
重点方法transform
/**
* 1. 如果消费了getInputs()的输入,则transform后必须再输出给下一级
* 2. 如果不希望做任何修改, 应该使用getReferencedScopes指定读取的对象, 并在getScopes中返回空。
* 3. 是否增量编译要以transformInvocation.isIncremental()为准, 如果isIncremental==false则Input#getStatus()极可能不准确
*/
@Throws(TransformException::class, InterruptedException::class, IOException::class)
final override fun transform(transformInvocation: TransformInvocation) {
super.transform(transformInvocation)
// 非增量编译必须先清除之前所有的输出, 否则 transformDexArchiveWithDexMergerForDebug
if (!transformInvocation.isIncremental) {
transformInvocation.outputProvider.deleteAll()
}
val outputProvider = transformInvocation.outputProvider
transformInvocation.inputs.forEach { input ->
input.jarInputs.forEach { jarInput ->
handleJarInput(jarInput)
val dest = outputProvider.getContentLocation(jarInput.file.absolutePath, jarInput.contentTypes, jarInput.scopes, Format.JAR)
FileUtils.copyFile(jarInput.file, dest)
}
input.directoryInputs.forEach { directoryInput ->
handleDirectoryInput(directoryInput.file)
val dest = outputProvider.getContentLocation(directoryInput.name, directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)
FileUtils.copyDirectory(directoryInput.file, dest)
}
}
}
- 遍历路径下以及jar包内的class,后续交给
handleFileBytes处理。
/**
* 两种方式
* 1. 解压缩, 修改完后再重新压缩
* 2. 直接通过JarFile进行遍历, 先写入一个新文件中, 再替换原jar
*/
final override fun handleJarInput(jarInput: JarInput) {
val oldPath = jarInput.file.absolutePath
val oldJarFile = JarFile(jarInput.file)
val newPath = oldPath.substring(0, oldPath.lastIndexOf(".")) + ".bak"
val newFile = File(newPath)
val newJarOutputStream = JarOutputStream(FileOutputStream(newFile))
oldJarFile.entries().iterator().forEach {
newJarOutputStream.putNextEntry(ZipEntry(it.name))
val inputStream = oldJarFile.getInputStream(it)
// 修改逻辑
if (it.name.startsWith("com")) {
val oldBytes = IOUtils.readBytes(inputStream)
newJarOutputStream.write(handleFileBytes(oldBytes))
}
// 不做改动, 原版复制
else {
IOUtils.copy(inputStream, newJarOutputStream)
}
newJarOutputStream.closeEntry()
inputStream.close()
}
newJarOutputStream.close()
oldJarFile.close()
jarInput.file.delete()
newFile.renameTo(jarInput.file)
}
/**
* 对于类的修改, 可以把 new bytes 直接写回原文件
* 注意: 必须递归到file, 不能处理路径
*/
final override fun handleDirectoryInput(inputFile: File) {
if (inputFile.isDirectory) {
inputFile.listFiles()?.forEach {
handleDirectoryInput(it)
}
} else if (inputFile.absolutePath.contains("com/youcii")) {
val inputStream = FileInputStream(inputFile)
val oldBytes = IOUtils.readBytes(inputStream)
inputStream.close()
val newBytes = handleFileBytes(oldBytes)
// 注意!! 实例化FileOutputStream时会清除掉原文件内容!!!!
val outputStream = FileOutputStream(inputFile)
outputStream.write(newBytes)
outputStream.close()
}
}
- 使用ASM处理class类
fun handleFileBytes(oldBytes: ByteArray): ByteArray {
return try {
val classReader = ClassReader(oldBytes)
val classWriter = ClassWriter(ClassWriter.COMPUTE_MAXS)
val classVisitor = getClassVisitor(classWriter)
classReader.accept(classVisitor, Opcodes.ASM5)
classWriter.toByteArray()
} catch (e: ArrayIndexOutOfBoundsException) {
oldBytes
} catch (e: IllegalArgumentException) {
oldBytes
}
}
abstract fun getClassVisitor(classWriter: ClassWriter): ClassVisitor
ASM
ASM是一种操作字节码的工具,采用访问者模式处理class文件内的所有元素。想要更多了解可以看这个:史上最通俗易懂的ASM教程
ASM Bytecode Outline插件
使用这个插件可以帮助我们查看字节码,并直接生成ASM代码。使用技巧:
ASM如何引入?
与Transform一样,ASM也是在build.gradle中com.android.tools.build:gradle一起引入的,不需要我们单独引入。像我们App中3.3.2版本引入的ASM就是6.0,如果想特殊指定版本的话可以使用exclude
implementation 'org.ow2.asm:asm:7.0'
...
implementOnly 'com.android.tools.build:gradle:3.3.2', {
exclude group:'org.ow2.asm'
}
不过需要注意的是,ASM版本对JDK版本有要求,在大量使用java8的情况下最低也要是5.0以上,否则ASM在实例化ClassReader时会有ArrayIndexOutOfBoundsException、IllegalArgumentException等错误。
| ASM版本号 | 最高支持的JDK版本 |
|---|---|
| 5.0-5.2 | 8 |
| 6.0 | 9 |
| 6.1 | 10 |
| 6.2 | 11 |
| 6.2.1-7.0 | 12 |
| 7.1 | 13 |
| 7.2 | 14 |
三种优化思路
上面写的示例是最简单的模版,它还有进一步优化空间,一般Transform优化会采用以下三种方式。
缩小transform范围
- 通过
getInputTypes,getScopes,getReferencedScopes精确控制自己关心的内容; - 在transform之前通过配置关注类/方法列表进一步缩小transform处理范围。 这一种优化与业务强相关,没有通用性。
并发编译
在处理transformInvocation.inputs.jarInputs/directoryInputs的每一个input时可以采用线程池并发处理,从而减少整体执行时间。SDK已经给我们提供了一个WaitableExecutor类,不仅可以提供了线程池的基本功能,也封装了各任务执行顺序的控制。
/**
* 并发处理线程池
*/
private val waitableExecutor = WaitableExecutor.useGlobalSharedThreadPool()
final override fun transform(transformInvocation: TransformInvocation) {
super.transform(transformInvocation)
transformInvocation.inputs.forEach { input ->
input.jarInputs.forEach { jarInput ->
waitableExecutor.execute {
...
}
}
// 可选配置,在waitableDirExecutor执行完毕之后再执行后面的同步代码
// 如果jarInputs与directoryInputs互不相关的话就不需要这一句
// 当然也可以使用 waitForTasksWithQuickFail
this.waitableDirExecutor.waitForAllTasks()
input.directoryInputs.forEach { directoryInput ->
waitableExecutor.execute {
...
}
}
}
// 保证所有任务全部执行完毕再执行后续transform, 传参true表示: 如果其中一个Task抛出异常时终止其他task
waitableExecutor.waitForTasksWithQuickFail<Any>(true)
}
增量编译
增量编译可以跳过大多数没有改动的jar、directory文件的处理从而大幅节省编译时间。核心点是判断inputFile的修改状态,根据不同的状态执行不同的处理。
- NOTCHANGED: 不需要处理,因为存在缓存,所以也无需复制;
- ADDED:正常处理、复制
- REMOVED:需要删除掉outputProvider下的对应缓存文件
- CHANGED:需要先删除对应缓存文件,再正常处理、复制,可以理解为REMOVED+ADDED
final override fun transform(transformInvocation: TransformInvocation) {
super.transform(transformInvocation)
// 非增量编译必须先清除之前所有的输出, 否则 transformDexArchiveWithDexMergerForDebug
if (!transformInvocation.isIncremental) {
transformInvocation.outputProvider.deleteAll()
}
val outputProvider = transformInvocation.outputProvider
transformInvocation.inputs.forEach { input ->
input.jarInputs.forEach { jarInput ->
val dest = outputProvider.getContentLocation(jarInput.file.absolutePath, jarInput.contentTypes, jarInput.scopes, Format.JAR)
// 判断是否增量
if (transformInvocation.isIncremental) {
handleIncrementalJarInput(jarInput, dest)
} else {
handleNonIncrementalJarInput(jarInput, dest)
}
}
input.directoryInputs.forEach { directoryInput ->
val dest = outputProvider.getContentLocation(directoryInput.name, directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)
// 判断是否增量
if (transformInvocation.isIncremental) {
handleIncrementalDirectoryInput(directoryInput, dest)
} else {
handleNonIncrementalDirectoryInput(directoryInput.file)
FileUtils.copyDirectory(directoryInput.file, dest)
}
}
}
}
/**
* 增量处理JarInput
*/
private fun handleIncrementalJarInput(jarInput: JarInput, dest: File) {
when (jarInput.status) {
Status.NOTCHANGED -> {
}
Status.ADDED -> {
handleNonIncrementalJarInput(jarInput, dest)
}
Status.REMOVED -> {
if (dest.exists()) {
FileUtils.forceDelete(dest)
}
}
Status.CHANGED -> {
if (dest.exists()) {
FileUtils.forceDelete(dest)
}
handleNonIncrementalJarInput(jarInput, dest)
}
}
}
/**
* 增量处理类修改
*/
private fun handleIncrementalDirectoryInput(directoryInput: DirectoryInput, dest: File) {
val srcDirPath = directoryInput.file.absolutePath
val destDirPath = dest.absolutePath
directoryInput.changedFiles.forEach { (inputFile, status) ->
val destFilePath = inputFile.absolutePath.replace(srcDirPath, destDirPath)
val destFile = File(destFilePath)
when (status) {
Status.NOTCHANGED -> {
}
Status.ADDED -> {
handleNonIncrementalDirectoryInput(inputFile)
FileUtils.copyFile(inputFile, destFile)
}
Status.REMOVED -> {
if (destFile.exists()) {
FileUtils.forceDelete(destFile)
}
}
Status.CHANGED -> {
if (dest.exists()) {
FileUtils.forceDelete(dest)
}
handleNonIncrementalDirectoryInput(inputFile)
FileUtils.copyFile(inputFile, destFile)
}
}
}
}
测试结果看到增量编译二次执行时速度提升更加明显:
- 非增量transform:
- clean后第一次编译时间为:1m 35s
- 第二次编译时间为:1m 13s
- 增量transform:
- clean后第一次编译时间为:1m 57s
- 第二次编译时间为:56s
踩过的一万个坑
在写demo的过程中遇到了很多坑,可能大多数都比较基础,不过搞明白确实花了不少时间。
gradle插件注册写法与插件配置顺序强相关
- 如果我们自己写的插件在BaseExtension插件之后注册的话,即
那么插件就应该这么写,因为xxx在application之后,baseExtension一定可以查找出来。apply plugin: 'com.android.application' apply plugin: 'xxx'override fun apply(target: Project) { val baseExtension = target.extensions.findByType(BaseExtension::class.java) baseExtension?.registerTransform(TestNonIncrementTransform()) } - 如果我们自己写的插件在之前注册的话,即
那么插件就应该这么写,否则会因为xxx注册时application尚未注册导致baseExtension查找为空apply plugin: 'xxx' apply plugin: 'com.android.application'override fun apply(target: Project) { target.afterEvaluate { val baseExtension = it.extensions.findByType(BaseExtension::class.java) baseExtension?.registerTransform(TestNonIncrementTransform()) } } - 那如果我们自己写的插件是在项目级build.gradle中写的呢?也需要使用afterEvaluate,这种情况下xxx插件也是在application插件之前应用的,与直接在module中先xxx再application的情况一致。
这几种gradle插件写法与引入顺序一一对应,其他组合均会失败。
编译失败:transformDexArchiveWithDexMergerForDebug
这是因为旧数据未清除导致,如果是非增量编译必须先清除之前所有的输出。
final override fun transform(transformInvocation: TransformInvocation) {
super.transform(transformInvocation)
if (!transformInvocation.isIncremental) {
transformInvocation.outputProvider.deleteAll()
}
...
}
编译失败:Invalid empty classfile
此错误是指写入的新类没有内容,是因为在修改原文件内容时inputStream、outputStream使用不正确导致:实例化FileOutputStream时会清除掉原文件内容,所以必须先读出数据,再实例化。
// 1. 错误
val inputStream = FileInputStream(inputFile)
val outputStream = FileOutputStream(inputFile)
...
// 2. 正确
val inputStream = FileInputStream(inputFile)
val oldBytes = IOUtils.readBytes(inputStream)
...
val outputStream = FileOutputStream(inputFile)
outputStream.write(handleFileBytes(oldBytes))
如何选型?
根据上面的介绍,可以总结出它们的核心场景如下:
APT
APT核心功能为:遍历所有标注某注解的元素,可以动态生成新类提供给运行时调用。那么也就是说它有以下局限性:
- 不能修改现有代码,只能添加新类或者只遍历元素;
- 我们要遍历的元素是后面新写的,或者我们可以介入历史代码手动添加注解。
Transform
Transform可以遍历到工程下的所有代码、资源,能够做到对它们的动态修改或添加,可以说它是万能的,APT的功能它也可以实现。
它的难点在于两点:
- 如何找到一个切面,也就是要处理的元素的共同特征;
- ASM比较难上手,对字节码知识要求比较高。
以上完整源码请见:github.com/YouCii/Adva…