AOP概念以及常见手段(一)

1,186 阅读13分钟

理解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分为两种

  1. 编译期预处理方式,在运行前、编译中把代码处理好:
    • APT
    • Transform
    • AspectJ
  2. 运行时处理,例如动态代理、运行时各种各样的Hook等:
    • Cglib + DexMaker
    • Dexposed
    • Xposed
    • ADocker

下面我们先看一下它们具体都是怎么工作的,鉴于篇幅,这里我们先只看下预编译处理的三种方式。

APT

APT,即Annotation Processor Tools,它用来获取标注某注解的所有类/方法/参数并执行某些操作,一般是生成代码并打入包中,后续通过反射提供给业务代码实用。

  1. 严格的说,按我们上面说的概念,APT其实不算是正统的AOP,因为它需要侵入业务代码,给所有位置手动添加注解;
  2. 宽泛点说,也可以认为是AOP,业务类添加注解可以理解为额外手动创建一个切面。

简单示例

  1. 继承AbstractProcessor,重写process方法,查找到所有标注特定注解的类并缓存;
  2. 通过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之间。

  1. Transform并不是必须的,只要找到class编译为dex的task之前的Task,通过before插入一个自定义task也可以,但使用Transform更简单。
  2. transform也可以直接手写字节码流,不是必须利用ASM和Javassist等class文件修改工具,但如果使用它们会让class修改更为简单;因为ASM与Javassist相比功能更加强大,基本所有的需求都能实现,所以一般我们都是使用ASM。

简单示例

  1. 自定义gradle插件,在buildSrc中编写transform并注册。
class TransformPlugin : Plugin<Project> {
    override fun apply(target: Project) {
        val baseExtension = target.extensions.findByType(BaseExtension::class.java)
        baseExtension?.registerTransform(TestNonIncrementTransform())
    }
}
  1. 创建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)
            }
        }
    }
  1. 遍历路径下以及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()
        }
    }
  1. 使用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时会有ArrayIndexOutOfBoundsExceptionIllegalArgumentException等错误。

ASM版本号最高支持的JDK版本
5.0-5.28
6.09
6.110
6.211
6.2.1-7.012
7.113
7.214

三种优化思路

上面写的示例是最简单的模版,它还有进一步优化空间,一般Transform优化会采用以下三种方式。

缩小transform范围
  1. 通过getInputTypesgetScopesgetReferencedScopes精确控制自己关心的内容;
  2. 在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)
                }
            }
        }
    }

测试结果看到增量编译二次执行时速度提升更加明显:

  1. 非增量transform:
    • clean后第一次编译时间为:1m 35s
    • 第二次编译时间为:1m 13s
  2. 增量transform:
    • clean后第一次编译时间为:1m 57s
    • 第二次编译时间为:56s

踩过的一万个坑

在写demo的过程中遇到了很多坑,可能大多数都比较基础,不过搞明白确实花了不少时间。

gradle插件注册写法与插件配置顺序强相关
  1. 如果我们自己写的插件在BaseExtension插件之后注册的话,即
    apply plugin: 'com.android.application'
    apply plugin: 'xxx'
    
    那么插件就应该这么写,因为xxx在application之后,baseExtension一定可以查找出来。
    override fun apply(target: Project) {
    	val baseExtension = target.extensions.findByType(BaseExtension::class.java)
    	baseExtension?.registerTransform(TestNonIncrementTransform())
    }
    
  2. 如果我们自己写的插件在之前注册的话,即
    apply plugin: 'xxx'
    apply plugin: 'com.android.application'
    
    那么插件就应该这么写,否则会因为xxx注册时application尚未注册导致baseExtension查找为空
    override fun apply(target: Project) {
    	target.afterEvaluate {
    		val baseExtension = it.extensions.findByType(BaseExtension::class.java)
    		baseExtension?.registerTransform(TestNonIncrementTransform())
    	}
    }
    
  3. 那如果我们自己写的插件是在项目级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核心功能为:遍历所有标注某注解的元素,可以动态生成新类提供给运行时调用。那么也就是说它有以下局限性

  1. 不能修改现有代码,只能添加新类或者只遍历元素;
  2. 我们要遍历的元素是后面新写的,或者我们可以介入历史代码手动添加注解。
Transform

Transform可以遍历到工程下的所有代码、资源,能够做到对它们的动态修改或添加,可以说它是万能的,APT的功能它也可以实现。

它的难点在于两点:

  1. 如何找到一个切面,也就是要处理的元素的共同特征;
  2. ASM比较难上手,对字节码知识要求比较高。

以上完整源码请见:github.com/YouCii/Adva…