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 是一个 工具类, 常用来获取所有带某个注解的元素如: 所有方法一般流程:
整个过程来说:
- 解析注解
- 构造一个数据结构保存注解中的有效信息
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 可以帮助我们处理大量的样板代码, 可以帮助我们自动化一些配置化的代码。
网友评论