美文网首页
AOP : APT 和 ASM 纺织代码

AOP : APT 和 ASM 纺织代码

作者: 壹零二肆 | 来源:发表于2022-03-23 20:57 被阅读0次

    AOP 中, 我们以处理阶段为划分产生了很多可选的技术手段:

    • java 源代码阶段 (apt 、 ksp、 java)
    • class 字节码阶段 (asm javaassist)
    • dex 阶段 (tinker)

    apt 处理的是 java 源代码文件,项目中若有很多类具有相似的样板代码, 可以考虑将这些样板代码在编译期间进行处理。 常常会搭配 javapoet 来编译期间生成一些样板类, 解放手工

    asm 处理的是 class 文件, 比如做一些代码插桩, 映射采集 等字节码增强和生成

    apt

    apt 简单来说做的工作: 通过输入(java文件), 找到带有需要处理的注解的元素, 读取这些注解的信息, 为后续的 代码植入做准备。

    apt 是 gradle build 阶段一个 task 触发的

    正常执行下 app:assembleDebug 触发的 gradle task 如下:

    Starting Gradle Daemon...
    Gradle Daemon started in 1 s 286 ms
    > Task :annotation:compileKotlin UP-TO-DATE
    > Task :annotation:compileJava UP-TO-DATE
    > Task :annotation:compileGroovy NO-SOURCE
    > Task :annotation:processResources UP-TO-DATE
    > Task :annotation:classes UP-TO-DATE
    > Task :annotation:inspectClassesForKotlinIC UP-TO-DATE
    > Task :annotation:jar UP-TO-DATE
    > Task :app:preBuild UP-TO-DATE
    > Task :app:preDebugBuild UP-TO-DATE
    > Task :app:compileDebugAidl NO-SOURCE
    > Task :app:compileDebugRenderscript NO-SOURCE
    > Task :app:generateDebugBuildConfig UP-TO-DATE
    > Task :app:checkDebugAarMetadata UP-TO-DATE
    > Task :app:generateDebugResValues UP-TO-DATE
    > Task :app:generateDebugResources UP-TO-DATE
    > Task :app:mergeDebugResources UP-TO-DATE
    > Task :app:createDebugCompatibleScreenManifests UP-TO-DATE
    > Task :app:extractDeepLinksDebug UP-TO-DATE
    > Task :app:processDebugMainManifest UP-TO-DATE
    > Task :app:processDebugManifest UP-TO-DATE
    > Task :app:processDebugManifestForPackage UP-TO-DATE
    > Task :app:processDebugResources UP-TO-DATE
    > Task :app:kaptGenerateStubsDebugKotlin UP-TO-DATE
    > Task :app:kaptDebugKotlin UP-TO-DATE
    > Task :app:compileDebugKotlin UP-TO-DATE
    > Task :app:javaPreCompileDebug UP-TO-DATE
    > Task :app:compileDebugJavaWithJavac UP-TO-DATE
    > Task :app:compileDebugSources UP-TO-DATE
    > Task :app:mergeDebugNativeDebugMetadata NO-SOURCE
    > Task :app:mergeDebugShaders UP-TO-DATE
    > Task :app:compileDebugShaders NO-SOURCE
    > Task :app:generateDebugAssets UP-TO-DATE
    > Task :app:mergeDebugAssets UP-TO-DATE
    > Task :app:compressDebugAssets UP-TO-DATE
    > Task :app:processDebugJavaRes NO-SOURCE
    > Task :app:mergeDebugJavaResource UP-TO-DATE
    > Task :app:checkDebugDuplicateClasses UP-TO-DATE
    > Task :app:desugarDebugFileDependencies UP-TO-DATE
    > Task :app:mergeExtDexDebug UP-TO-DATE
    > Task :app:dexBuilderDebug UP-TO-DATE
    > Task :app:mergeProjectDexDebug UP-TO-DATE
    > Task :app:mergeLibDexDebug UP-TO-DATE
    > Task :app:mergeDebugJniLibFolders UP-TO-DATE
    > Task :app:mergeDebugNativeLibs NO-SOURCE
    > Task :app:stripDebugDebugSymbols NO-SOURCE
    > Task :app:validateSigningDebug UP-TO-DATE
    > Task :app:writeDebugAppMetadata UP-TO-DATE
    > Task :app:writeDebugSigningConfigVersions UP-TO-DATE
    > Task :app:packageDebug UP-TO-DATE
    > Task :app:assembleDebug UP-TO-DATE
    

    Task :app:kaptGenerateStubsDebugKotlin UP-TO-DATE
    Task :app:kaptDebugKotlin UP-TO-DATE

    就是 apt 的位置, apt 后才会生成 class 文件, 进一步dex , 最后 package

    具体 apt 的代码都写在 AbstractProcessor 的实现类中

    该类中主要常用的几个元素
        override fun init(processingEnvironment: ProcessingEnvironment?) {
            super.init(processingEnvironment)
            mTypeUtil = processingEnvironment?.getTypeUtils()
            mElementUtil = processingEnvironment?.getElementUtils()
            mFiler = processingEnvironment?.getFiler()
            mMessager = processingEnvironment?.getMessager()
        }
    
        override fun process(
            set: MutableSet<out TypeElement>,
            processingEnvironment: RoundEnvironment
        ): Boolean {
            // 具体 apt 代码
    }
    

    process 中, 可以根据 RoundEnvironment 可以取到所有带有某个注释的 类、接口、方法

    TypeElement 是 类/接口 Elements 是一个 工具类, 常用来获取所有带某个注解的元素如: 所有方法

    一般流程:


    整个过程来说:

      1. 解析注解
      1. 构造一个数据结构保存注解中的有效信息

    javapoet

    习惯语法即可

    首先写一个 javaFile涉及到的核心步骤:

    • 类相关 TypeSpec
    • 构造函数 MethodSpec
    • 成员变量 FieldSpec
    • 方法 MethodSpec
    • 注解 AnnotationSpec

    具体如何使用可以直接参考:
    https://blog.csdn.net/qq_17766199/article/details/112429217

    不再赘述

             val genClass =
                    TypeSpec.classBuilder(element.simpleName.toString() + "$\$Impl")
                        .addSuperinterface(ClassName.get(element))
                        .addModifiers(Modifier.PUBLIC)
    
                for (field in fields) {
                    genClass.addField(field)
                }
                for (method in methods) {
                    genClass.addMethod(method)
                }
    
                JavaFile.builder(
                    mElementUtil!!.getPackageOf(element).qualifiedName.toString(),
                    genClass.build()
                )
                    .addFileComment("Generated code")
                    .build()
                    .writeTo(mFiler)
    

    实践

    • app 模块
    • annotation模块

    具体build.gradle 可以参考 github:

    注解代码:

    package com.example.perla
    
    import com.example.annotation.*
    
    @Man(name = "jackie", age = 1, coutry = JackCountry::class)
    interface Jackie : IFigher {
    
        @Body(weight = 200, height = 200)
        fun body()
    
        @GetCE(algorithm = Algorithm::class)
        fun ce(): Int
    
        @GetInstance
        fun instance(): IFigher
    }
    
    class Algorithm : IAlgorithm {
        override fun ce(figher: IFigher): Int {
            return -1
        }
    }
    
    class JackCountry : ICountry {
        override fun name(): String {
            return "China"
        }
    
    }
    
    

    注解生成代码:

    // Generated code
    package com.example.perla;
    
    import com.example.annotation.IAlgorithm;
    import com.example.annotation.IFigher;
    import java.lang.Override;
    import java.lang.String;
    import java.lang.System;
    
    public class Jackie$$Impl implements Jackie {
      private String mKey;
    
      private String name;
    
      private int age;
    
      private String country;
    
      private int weight;
    
      private int height;
    
      private IAlgorithm algorithm;
    
      public Jackie$$Impl(String key) {
        mKey = key;
        name = "jackie";
        age = 1;
        country = new JackCountry().name();
        algorithm = new Algorithm();
      }
    
      @Override
      public void body() {
        weight = 200;
        height = 200;
      }
    
      @Override
      public int ce() {
        if (algorithm != null) {
          return algorithm.ce(instance());
        }
        return weight  + height;
      }
    
      @Override
      public IFigher instance() {
        return new Jackie$$Impl(String.valueOf(System.currentTimeMillis()));
      }
    }
    
    

    核心代码:

    PerlaProcessor.kt

    package com.example.annotation
    
    
    import com.google.auto.common.AnnotationMirrors
    import com.google.auto.common.MoreElements
    import com.google.auto.service.AutoService
    import com.squareup.javapoet.*
    import javax.annotation.processing.*
    import javax.lang.model.SourceVersion
    import javax.lang.model.element.Element
    import javax.lang.model.element.ElementKind
    import javax.lang.model.element.Modifier
    import javax.lang.model.element.TypeElement
    import javax.lang.model.util.Elements
    import javax.lang.model.util.Types
    
    
    @AutoService(Processor::class)
    @SupportedSourceVersion(SourceVersion.RELEASE_11)
    @SupportedOptions()
    @SupportedAnnotationTypes("*")
    class PerlaProcessor : AbstractProcessor() {
    
        private var mTypeUtil: Types? = null
        private var mElementUtil: Elements? = null
        private var mFiler: Filer? = null
        private var mMessager: Messager? = null
        private val aptSourceBook = HashMap<TypeElement, AptManInfo>()
    
    
        override fun init(processingEnvironment: ProcessingEnvironment?) {
            super.init(processingEnvironment)
            mTypeUtil = processingEnvironment?.getTypeUtils()
            mElementUtil = processingEnvironment?.getElementUtils()
            mFiler = processingEnvironment?.getFiler()
            mMessager = processingEnvironment?.getMessager()
    
        }
    
        override fun process(
            set: MutableSet<out TypeElement>,
            processingEnvironment: RoundEnvironment
        ): Boolean {
    
    
            try {
    
                for (element in processingEnvironment.getElementsAnnotatedWith(Man::class.java)) {
                    parseAnnotation(aptSourceBook, element as TypeElement)
                }
    
    
                write()
    
    
            } catch (ex: Exception) {
    
            }
            return true
        }
    
        private fun write() {
    
    
            for ((element, info) in aptSourceBook) {
    
                val fields = ArrayList<FieldSpec>()
                val methods = ArrayList<MethodSpec>()
    
                val keyField = FieldSpec.builder(ClassName.get(String::class.java), "mKey")
                    .addModifiers(Modifier.PRIVATE).build()
    
                val nameField = FieldSpec.builder(String::class.java, "name")
                    .addModifiers(Modifier.PRIVATE)
                    .build()
    
                val ageField = FieldSpec.builder(Int::class.java, "age")
                    .addModifiers(Modifier.PRIVATE)
                    .build()
    
                val countryField = FieldSpec.builder(String::class.java, "country")
                    .addModifiers(Modifier.PRIVATE)
                    .build()
    
                val weightField = FieldSpec.builder(Int::class.java, "weight")
                    .addModifiers(Modifier.PRIVATE)
                    .build()
    
                val heightField = FieldSpec.builder(Int::class.java, "height")
                    .addModifiers(Modifier.PRIVATE)
                    .build()
    
    
                val algorithmField = FieldSpec.builder(IAlgorithm::class.java, "algorithm")
                    .addModifiers(Modifier.PRIVATE)
                    .build()
    
    
                fields.add(keyField)
                fields.add(nameField)
                fields.add(ageField)
                fields.add(countryField)
                fields.add(weightField)
                fields.add(heightField)
                fields.add(algorithmField)
    
                val constructor =
                    MethodSpec.constructorBuilder()
                        .addModifiers(Modifier.PUBLIC)
                        .addParameter(ClassName.get(String::class.java), "key")
                        .addStatement("mKey = key")
                        .addStatement("name = \$S", info.name)
                        .addStatement("age = \$L", info.age)
                        .addStatement("country = new \$T().name()", info.country)
                        .addStatement("algorithm = new \$T()", info.algorithm)
    
    
                val body =
                    MethodSpec.methodBuilder("body")
                        .addAnnotation(Override::class.java)
                        .addModifiers(Modifier.PUBLIC)
    
                info.bodyInfo?.let {
                    body.addStatement("weight = \$L", it.weight)
                    body.addStatement("height = \$L", it.height)
                }
    
                val ce =
                    MethodSpec.methodBuilder("ce")
                        .addAnnotation(Override::class.java)
                        .addModifiers(Modifier.PUBLIC)
                        .returns(TypeName.INT)
                        .beginControlFlow("if (algorithm != null)")
                        .addStatement("return algorithm.ce(instance())")
                        .endControlFlow()
                        .addStatement("return weight  + height")
    
                val getInstance =
                    MethodSpec.methodBuilder("instance")
                        .addAnnotation(Override::class.java)
                        .addModifiers(Modifier.PUBLIC)
                        .returns(ClassName.get(IFigher::class.java))
                        .addStatement(
                            "return new \$T(String.valueOf(\$T.currentTimeMillis()))",
                            ClassName.bestGuess(element.simpleName.toString() + "$\$Impl"),
                            System::class.java
                        )
    
    
    
    
                methods.add(constructor.build())
                methods.add(body.build())
                methods.add(ce.build())
                methods.add(getInstance.build())
    
    
                val genClass =
                    TypeSpec.classBuilder(element.simpleName.toString() + "$\$Impl")
                        .addSuperinterface(ClassName.get(element))
                        .addModifiers(Modifier.PUBLIC)
    
                for (field in fields) {
                    genClass.addField(field)
                }
                for (method in methods) {
                    genClass.addMethod(method)
                }
    
                JavaFile.builder(
                    mElementUtil!!.getPackageOf(element).qualifiedName.toString(),
                    genClass.build()
                )
                    .addFileComment("Generated code")
                    .build()
                    .writeTo(mFiler)
            }
        }
    
        private fun parseAnnotation(
            aptSourceBook: java.util.HashMap<TypeElement, AptManInfo>,
            element: TypeElement
        ) {
    
            val aptManInfo = AptManInfo()
            val annotationInfo = element.getAnnotation(Man::class.java)
            aptManInfo.apply {
                name = annotationInfo.name
                age = annotationInfo.age
                country = getAnnotationClassName(element, Man::class.java, "coutry")?.toString()
                    ?.let { ClassName.bestGuess(it) }
            }
            aptSourceBook[element] = aptManInfo
    
            val methods = mElementUtil!!.getAllMembers(element)
                .filter {
                    it.kind == ElementKind.METHOD &&
                            MoreElements.isAnnotationPresent(it, GetInstance::class.java) ||
                            MoreElements.isAnnotationPresent(it, GetCE::class.java) ||
                            MoreElements.isAnnotationPresent(
                                it,
                                Body::class.java
                            )
    
                }.map { MoreElements.asExecutable(it) }.groupBy {
                    when {
                        MoreElements.isAnnotationPresent(it, Body::class.java) -> Body::class.java
                        MoreElements.isAnnotationPresent(
                            it,
                            GetInstance::class.java
                        ) -> GetInstance::class.java
                        MoreElements.isAnnotationPresent(it, GetCE::class.java) -> GetCE::class.java
                        else -> Any::class.java
                    }
                }
    
            methods[Body::class.java]?.forEach {
                val body = it.getAnnotation(Body::class.java)
                aptManInfo.bodyInfo = BodyInfo().apply {
                    weight = body.weight
                    height = body.height
                }
            }
    
            methods[GetInstance::class.java]?.forEach {
                val instance = it.getAnnotation(GetInstance::class.java)
                aptManInfo.getInstance = instance
            }
    
    
            methods[GetCE::class.java]?.forEach {
                aptManInfo.algorithm =
                    getAnnotationClassName(it, GetCE::class.java, "algorithm").toString()
                        .let { ClassName.bestGuess(it) }
            }
    
    
        }
    
        private fun getAnnotationClassName(
            element: Element,
            key1: Class<out Annotation>,
            key: String
        ): Any? {
            return MoreElements.getAnnotationMirror(element, key1)
                .orNull()?.let {
                    AnnotationMirrors.getAnnotationValue(it, key)?.value
                }
        }
    }
    

    asm

    apt 主要处理 java 文件, asm 处理 class 文件。
    asm 也会搭配 gradle plugin 来进行一些代码增强,代码生成。

    asm 主要是解决如何拿到 class 文件然后进行代码增强
    参考:
    https://tech.meituan.com/2019/09/05/java-bytecode-enhancement.html

    举两个实例来看 asm 的使用:

    • 字节码插桩
    • 映射收集

    插桩

    假设我们需要写一个 trace 插桩

    在函数出入口调用 Trace.beginSection 和 end 就可采集 Trace 数据事后使用 perfetto进行分析

    下面是 Recyclerview 中的一个 trace 方法:

       TraceCompat.beginSection(TRACE_SCROLL_TAG);
            fillRemainingScrollValues(mState);
    
            int consumedX = 0;
            int consumedY = 0;
            if (dx != 0) {
                consumedX = mLayout.scrollHorizontallyBy(dx, mRecycler, mState);
            }
            if (dy != 0) {
                consumedY = mLayout.scrollVerticallyBy(dy, mRecycler, mState);
            }
    
            TraceCompat.endSection();
    

    插桩后结合 systrace 统计的图,perfetto工具查看到的效果

    如果不会使用 systrace 可以查看文章https://mp.weixin.qq.com/s/9dexhnWuWIopdhdU_aKkZw

    这里避免 代码中手动每个函数调用 Trace.beginSection 采用字节码插桩来在 gradle plugin 中批处理添加插桩代码

    下面是method-trace 插件的具体开发过程:

    目录架构:

    MethodTracePlugin

    package com.ss.android.ugc.bytex.method_trace
    
    import com.android.build.gradle.AppExtension
    import com.ss.android.ugc.bytex.common.CommonPlugin
    import com.ss.android.ugc.bytex.common.flow.main.Process
    import com.ss.android.ugc.bytex.common.visitor.ClassVisitorChain
    import com.ss.android.ugc.bytex.pluginconfig.anno.PluginConfig
    import org.gradle.api.Project
    import org.objectweb.asm.ClassReader
    
    
    @PluginConfig("bytex.method-trace")
    class MethodTracePlugin : CommonPlugin<MethodTraceExtension, MethodTraceContext>() {
        override fun getContext(
                project: Project,
                android: AppExtension,
                extension: MethodTraceExtension
        ): MethodTraceContext {
            return MethodTraceContext(project, android, extension)
        }
    
        override fun transform(relativePath: String, chain: ClassVisitorChain): Boolean {
            chain.connect(MethodTraceClassVisitor(context, extension))
            return super.transform(relativePath, chain)
        }
    
        override fun flagForClassReader(process: Process?): Int {
            return ClassReader.SKIP_DEBUG or ClassReader.SKIP_FRAMES or ClassReader.EXPAND_FRAMES
        }
    }
    

    MethodTraceExtension可读取如下build.gradle中配置

    // apply ByteX宿主
    apply plugin: 'bytex'
    ByteX {
        enable pluginEnable
        enableInDebug pluginEnableInDebug
        logLevel pluginLogLevel
    }
    
    apply plugin: 'bytex.method-trace'
    
    MethodTracePlugin {
        enable pluginEnable
        enableInDebug pluginEnableInDebug
        whiteList = ['com/gongshijie']
    }
    
    package com.ss.android.ugc.bytex.method_trace;
    
    import com.ss.android.ugc.bytex.common.BaseExtension;
    
    import java.util.ArrayList;
    import java.util.List;
    
    public class MethodTraceExtension extends BaseExtension {
    
        private List<String> whiteList = new ArrayList<>();
    
    
        @Override
        public String getName() {
            return "MethodTracePlugin";
        }
    
        public List<String> getWhiteList() {
            return whiteList;
        }
    
        public void setWhiteList(List<String> whiteList) {
            this.whiteList = whiteList;
        }
    }
    
    

    TraceMethodVisitor

    package com.ss.android.ugc.bytex.method_trace
    
    import org.objectweb.asm.MethodVisitor
    import org.objectweb.asm.commons.AdviceAdapter
    
    class TraceMethodVisitor(private var context: MethodTraceContext,
                             private var className: String, api: Int, mv: MethodVisitor?,
                             access: Int, var methodName: String?, desc: String?
    ) : AdviceAdapter(api, mv, access, methodName, desc) {
    
    
        override fun onMethodEnter() {
            super.onMethodEnter()
    
            context.logger.i("TraceMethodVisitor", "----插桩----className: $className  methodName: ${methodName}------")
    
            if (methodName != null) {
                mv.visitLdcInsn("$className#$methodName");
                mv.visitMethodInsn(INVOKESTATIC, "com/ss/android/ugc/bytex/method_trace_lib/MyTrace", "beginSection", "(Ljava/lang/String;)V", false);
            }
        }
    
        override fun onMethodExit(opcode: Int) {
            super.onMethodExit(opcode)
            mv.visitMethodInsn(INVOKESTATIC, "com/ss/android/ugc/bytex/method_trace_lib/MyTrace", "endSection", "()V", false);
    
        }
    }
    

    映射采集

    apt 和 asm 往往会搭配起来使用

    比如我们各个模块内部,可以根据注解生成一些映射关系(apt), 后面再通过 asm 来跨模块收集这些映射关系

    目录架构

    熟悉的环境工作不再赘述, 核心部分就是 采集各模块 apt 生成的文件映射关系, 然后 asm 增强到 一个 class 文件内。

    这样的处理在很多框架中都可以找到。

    class ManCollectTransform(val project: Project, val appPlugin: AppPlugin?) : Transform() {
        override fun getName(): String {
            return "ManCollectTransform"
        }
    
        override fun getInputTypes(): MutableSet<QualifiedContent.ContentType> {
            return TransformManager.CONTENT_CLASS
        }
    
        override fun getScopes(): MutableSet<in QualifiedContent.Scope> {
            return TransformManager.SCOPE_FULL_PROJECT
        }
    
        override fun isIncremental(): Boolean {
            return false
        }
    
    
        override fun transform(transformInvocation: TransformInvocation?) {
            val map = HashMap<String, String>()
            transformInvocation?.inputs?.forEach { it ->
    
                it.jarInputs.forEach { jarInput ->
                    val jarFile = JarFile(jarInput.file)
                    val entries = jarFile.entries()
                    for (entry in entries) {
                        if (entry.name.endsWith("$\$Impl.class")) {
                            val inputStream = jarFile.getInputStream(entry)
                            val reader = ClassReader(inputStream)
                            val classNode = ClassNode(ASM5)
                            reader.accept(classNode, ClassReader.SKIP_DEBUG)
                            map.put(classNode.interfaces.first(), classNode.name)
                            inputStream.close()
                        }
                    }
                }
    
                it.directoryInputs.forEach { dirInput ->
                    project.fileTree(dirInput.file).forEach {
                        if (it.absolutePath.endsWith("$\$Impl.class")) {
                            val inputStream = FileInputStream(it)
                            val reader = ClassReader(inputStream)
                            val classNode = ClassNode(Opcodes.ASM5)
                            reader.accept(classNode, ClassReader.SKIP_DEBUG)
                            if (classNode.interfaces.isNotEmpty()) {
                                map.put(classNode.interfaces.first(), classNode.name)
                            }
                            inputStream.close()
                        }
                    }
                }
            }
    
            println("输出映射关系")
            for((k, v) in map) {
                println("""映射关系采集: $k : $v""")
            }
    
            transformInvocation?.inputs?.forEach { it ->
                it.jarInputs.forEach { jarInput ->
                    val jarFile = JarFile(jarInput.file)
                    val manFinderEntry = jarFile.getJarEntry("com/example/mancollect_api/ManFinder.class")
                    val dest = transformInvocation.outputProvider.getContentLocation(jarInput.name, jarInput.contentTypes, jarInput.scopes, Format.JAR)
                    if (manFinderEntry != null) {
                        val inputStream = jarFile.getInputStream(manFinderEntry)
                        val reader = ClassReader(inputStream)
                        val writer = ClassWriter(ClassWriter.COMPUTE_FRAMES)
                        val vis = ManFinderClassAdapter(writer, map)
                        reader.accept(vis, ClassReader.SKIP_DEBUG)
                        inputStream.close()
                    }
                }
            }
        }
    }
    

    总之 , apt 和 asm 可以帮助我们处理大量的样板代码, 可以帮助我们自动化一些配置化的代码。

    相关文章

      网友评论

          本文标题:AOP : APT 和 ASM 纺织代码

          本文链接:https://www.haomeiwen.com/subject/mfjhjrtx.html