background
With the increasing focus on APM (Application Performance Management) in the project, such as Debug logs, runtime monitoring will be gradually added to the source code, with the increase of functions, The monitoring logging code in some way interferes with or even interferes with the reading of business code, so I looked for ways to automate logging into code, and “staking” came to mind. Essentially, the idea is AOP, dynamically injecting code at compile or run time. This article takes a look at the process of modifying bytecode at compile time, inserting logging code before and after method execution, and does some preliminary testing.
An overview of the
After handing over the background, first docking next to say the content to do a brief description. Because it’s compile time, the first thing you need to do is find a point in time during compile time, which is the Transform part of the title; After finding the “crime” location, the next is the “crime”, this is the choice of compiled.class bytecode to start, the tool to be introduced in the second half of ASM. At this point, I hope readers can have a preliminary impression of what this article is about.
Transform
The above first
Before Android Gradle 1.5, there was a separate Transform API. Since version 2.0, it has been incorporated directly into the Gradle API.
Gradle 1.5:
The Compile 'com. Android. The tools.build:transfrom-api:1.5. 0'Copy the code
Gradle 2.0 starts:
implementation 'com. Android. Tools. Build: gradle - API: 3.0.1'
Copy the code
Each Transform is actually a Gradle Task. They are chained together, with the output of the previous one as the input of the next. Our custom Transform is executed first as the first task.
This article defines Gradle plug-ins based on buildSrc, which is sufficient because it will only be used in Demo projects. One thing to note is that the buildSrc approach requires that the name of the Library Module be buildSrc.
Without further ado, get straight to the picture above:
buildSrc module:
apply plugin: AsmPlugin
Copy the code
Finally, add the buildSrc Module to settings.gradle
include ':app'.':buildSrc'
Copy the code
At this point, we have a custom plug-in that is very rudimentary. We just print “Hello Gradle Plugin” on the console. Let’s compile it to see if it works.
An AsmTransform will be defined and registered with the AsmPlugin, which will be posted in the introduction to ASM.
ASM
How do you change the bytecode when you have a chance to do something? At this point the artifact ASM came out.
ASM is a fully functional Java bytecode operation and analysis framework. It can be used to dynamically generate classes or enhance the functionality of existing classes. ASM can either generate binary class files directly or dynamically change the behavior of classes before they are loaded into the Java virtual machine.
More details can be found at [ASM’s website](https://asm.ow2.io/).
The latest version was 7.0 when I wrote the Demo.
ASM provides a Visitor based API that separates the logic for reading and writing classes by providing a ClassReader that reads the class bytecode and passes it to the class Visitor interface. The ClassVisitor interface provides many Visitor methods, such as visit Class, visit Method, and so on, just as a ClassReader visits each instruction of the Class bytecode with a ClassVisitor.
Reading is not enough; if we want to modify the bytecode, ClassWriter comes into play. ClassWriter actually inherits from the ClassVisitor, and all it does is hold the bytecode information and eventually export it, so if we can proxy the ClassWriter interface, we can interfere with the resulting bytecode.
Okay, let’s cut the crap and get right to the code.
Take a look at the structure of the plug-in directory
BuildSrc builds. Gradle with ASM dependencies
/ / ASM related
implementation 'org. Ow2. Asm: asm: 7.1'
implementation 'org. Ow2. Asm: asm - util: 7.1'
implementation 'org. Ow2. Asm: asm - Commons: 7.1'
Copy the code
Let’s take a look at the AsmTransform
import com.android.build.api.transform.DirectoryInput
import com.android.build.api.transform.Format
import com.android.build.api.transform.JarInput
import com.android.build.api.transform.QualifiedContent
import com.android.build.api.transform.Transform
import com.android.build.api.transform.TransformException
import com.android.build.api.transform.TransformInput
import com.android.build.api.transform.TransformInvocation
import com.android.build.api.transform.TransformOutputProvider
import com.android.build.gradle.internal.pipeline.TransformManager
import me.sure.asm.TestMethodClassAdapter
import org.apache.commons.io.FileUtils
import org.gradle.api.Project
import org.objectweb.asm.ClassReader
import org.objectweb.asm.ClassWriter
class AsmTransform extends Transform {
Project project
AsmTransform(Project project) {
this.project = project
}
@Override
void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
super.transform(transformInvocation)
println("===== ASM Transform =====")
println("${transformInvocation.inputs}")
println("${transformInvocation.referencedInputs}")
println("${transformInvocation.outputProvider}")
println("${transformInvocation.incremental}")
// Whether the current compilation is incremental
boolean isIncremental = transformInvocation.isIncremental()
// Consumer input, from which you can get the JAR package and the class folder path. Need to output to the next task
Collection<TransformInput> inputs = transformInvocation.getInputs()
// Reference input, no output required.
Collection<TransformInput> referencedInputs = transformInvocation.getReferencedInputs()
//OutputProvider manages the output path. If the consumer input is empty, you will find OutputProvider == null
TransformOutputProvider outputProvider = transformInvocation.getOutputProvider()
for (TransformInput input : inputs) {
for (JarInput jarInput : input.getJarInputs()) {
File dest = outputProvider.getContentLocation(
jarInput.getFile().getAbsolutePath(),
jarInput.getContentTypes(),
jarInput.getScopes(),
Format.JAR)
// Copy the modified bytecode to dest to achieve the purpose of intervening bytecode during compilation
transformJar(jarInput.getFile(), dest)
}
for (DirectoryInput directoryInput : input.getDirectoryInputs()) {
println("== DI = " + directoryInput.file.listFiles().toArrayString())
File dest = outputProvider.getContentLocation(directoryInput.getName(),
directoryInput.getContentTypes(), directoryInput.getScopes(),
Format.DIRECTORY)
// Copy the modified bytecode to dest to achieve the purpose of intervening bytecode during compilation
//FileUtils.copyDirectory(directoryInput.getFile(), dest)
transformDir(directoryInput.getFile(), dest)
}
}
}
@Override
String getName(a) {
return AsmTransform.simpleName
}
@Override
Set<QualifiedContent.ContentType> getInputTypes() {
return TransformManager.CONTENT_CLASS
}
@Override
Set<? super QualifiedContent.Scope> getScopes() {
return TransformManager.SCOPE_FULL_PROJECT
}
@Override
boolean isIncremental(a) {
return true
}
private static void transformJar(File input, File dest) {
println("=== transformJar ===")
FileUtils.copyFile(input, dest)
}
private static void transformDir(File input, File dest) {
if (dest.exists()) {
FileUtils.forceDelete(dest)
}
FileUtils.forceMkdir(dest)
String srcDirPath = input.getAbsolutePath()
String destDirPath = dest.getAbsolutePath()
println("=== transform dir = " + srcDirPath + "," + destDirPath)
for (File file : input.listFiles()) {
String destFilePath = file.absolutePath.replace(srcDirPath, destDirPath)
File destFile = new File(destFilePath)
if (file.isDirectory()) {
transformDir(file, destFile)
} else if (file.isFile()) {
FileUtils.touch(destFile)
transformSingleFile(file, destFile)
}
}
}
private static void transformSingleFile(File input, File dest) {
println("=== transformSingleFile ===")
weave(input.getAbsolutePath(), dest.getAbsolutePath())
}
private static void weave(String inputPath, String outputPath) {
try {
FileInputStream is = new FileInputStream(inputPath)
ClassReader cr = new ClassReader(is)
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES)
TestMethodClassAdapter adapter = new TestMethodClassAdapter(cw)
cr.accept(adapter, 0)
FileOutputStream fos = new FileOutputStream(outputPath)
fos.write(cw.toByteArray())
fos.close()
} catch (IOException e) {
e.printStackTrace()
}
}
}
Copy the code
Our InputTypes are CONTENT_CLASS, indicating a class file, The primary function of the transform method SCOPE_FULL_PROJECT is to store Inputs in the location provided by the outProvider. The generated locations are shown below:
Compared to the code, there are two main transform methods, one transformJar is a simple copy, and the other transformSingleFile, where we use ASM to modify the bytecode. Looking at the Weave method, you can see that we read the input stream from the inputPath with a ClassReader, and encapsulate it with a Adapter before ClassWriter. Let’s see what adapter does.
public class TestMethodClassAdapter extends ClassVisitor implements Opcodes {
public TestMethodClassAdapter(ClassVisitor classVisitor) {
super(ASM7, classVisitor);
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
return (mv == null)?null : newTestMethodVisitor(mv); }}Copy the code
This adapter receives a classVisitor as input (that is, a ClassWriter), which is accessed using the custom TestMethodVisitor in the visitMethod method. Now look at TestMethodVisitor:
public class TestMethodVisitor extends MethodVisitor {
public TestMethodVisitor(MethodVisitor methodVisitor) {
super(ASM7, methodVisitor);
}
@Override
public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) {
System.out.println("== TestMethodVisitor, owner = " + owner + ", name = " + name);
// Prints before the method is executed
mv.visitLdcInsn(" before method exec");
mv.visitLdcInsn(" [ASM 测试] method in " + owner + " ,name=" + name);
mv.visitMethodInsn(INVOKESTATIC,
"android/util/Log"."i"."(Ljava/lang/String; Ljava/lang/String;) I".false);
mv.visitInsn(POP);
super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
// Prints after the method is executed
mv.visitLdcInsn(" after method exec");
mv.visitLdcInsn(" method in " + owner + " ,name=" + name);
mv.visitMethodInsn(INVOKESTATIC,
"android/util/Log"."i"."(Ljava/lang/String; Ljava/lang/String;) I".false); mv.visitInsn(POP); }}Copy the code
TestMethodVisitor overrides the visitMethodInsn method by inserting “bytecodes” before and after the default method that approximate Bytecode, which you can think of as a BytecODE in ASM format. What we did was to output two logs:
Log.i("before method exec"."[ASM test] method in" + owner + ", name=" + name);
Log.i("after method exec"."method in" + owner + ", name=" + name);
Copy the code
That’s what a lot of long-winded writing is all about. It’s too much trouble to write. Don’t worry, ASM provides a plug-in that can convert the source code into ASM Bytecode. Address in [here] (https://plugins.jetbrains.com/plugin/5918-asm-bytecode-outline)
Find an easy way to try it, as shown below:
Finally, let’s see what the compiled asmtest.class looks like
conclusion
So far, the process of modifying bytecode during compilation by means of Transform + ASM has been introduced. Because it is only a preliminary study, there are still many details to be optimized from the actual application. I hope this article can provide a little preliminary convenience for friends who are interested in this kind of method.
The resources
Google. Making. IO/android – gra…
asm.ow2.io/
Asm. Ow2. IO/asm4 – guide….
Quinnchen. Me / 2018/09/13 /…
Plugins.jetbrains.com/plugin/5918…
www.sensorsdata.cn/blog/201812…