In this article
Bytecode piling is implemented using GradlePlugin, Transform, and ASM. The code of GradlePlugin is written by Kotlin, so you can read it easily if you are not familiar with Groovy.
What is bytecode staking
To understand bytecode piling, we should first understand the idea of AOP(Aspect Oriented Programming). In contrast, OOP(object-oriented Programming) mainly encapsulates the entities, attributes and behaviors of business processes, in order to have a clearer and efficient division of logical units. AOP(aspect oriented programming) is to extract the aspects of the business process, which is faced with a step or stage in the process, in order to achieve the isolation effect of low coupling between the parts of the logical process.
Insert position of bytecode
Android developers should be familiar with this image (from Android Developer) :
In Compilers, they are:
Bytecode staking is to modify the.class file to modify the code before the.class file is converted to the.dex.
The business scenario
Bytecode staking can be used for logging, performance statistics, exception handling, modifying tripartite library code, and so on.
Knowledge to master
When using bytecode staking in Android projects, you need to master it
- Knowledge of GradlePlugin
- Understand the Transform API
- Understand the word ASM, Javassist or other related peg framework
Create your GradlePlugin
GradlePlugin can also be found in the official documentation
Three ways to create
- Build. Gradle. Write plugin code directly into the build.gradle of the corresponding model (directly, but only for a single model).
- Write plugin code in buildSrc (scope is the entire project)
- Create plugin module, write resource file xxx.properties file to determine plugin name and file corresponding address (compared to the first two implementation methods are more complex, but can be uploaded to a public warehouse for multi-project reuse)
Mark the location of the module file in the Properties file:
The properties file
Introduced the module name: the properties file filename (example for the com. Shawn. Addlogformethodplugin)
build.gradle
Maven libraries are introduced to facilitate local debugging.
Using the plugin
- In the first case plugin is the default package name and can be referenced directly
- In the second method, the import is based on the package name defined by the user
- The third way is to first import the repository in the project root directory and then reference it
Build. Gradle in the root directory
Inside the model build. Gradle
Understand the Transform
What is the Transform
Starting with 1.5.0-beta1, the Gradle plug-in includes a Transform API that allows third-party plug-ins to operate on compiled class files before converting them to dex files. (This API already existed in 1.4.0-beta2, but has been radically improved in 1.5.0-beta1)
How Transform works
An interceptor similar to OkHttp, in chain of responsibility mode. Each Transform is a Task, and the compiled intermediate is passed across multiple Transform chains. The user defined Transform is executed first in the header. Since the source of each Transform is provided by the previous Transform, if the user defined Transform does not move the corresponding file to the specified output location, The next Transform will not work.
Transform correlation method
getName()
/** * Returns the unique name of the transform. * * This is associated with the type of work that the transform does. It does not have to be * unique per variant. */
@NonNull
public abstract String getName(a);
Copy the code
According to the current name of the Transform, the name will be used to create the directory, it will appear in the app/build/intermediates/transforms directory.
getInputTypes()
/**
* Returns the type(s) of data that is consumed by the Transform. This may be more than
* one type.
*
* <strong>This must be of type {@link QualifiedContent.DefaultContentType}</strong>
*/
@NonNull
public abstract Set<ContentType> getInputTypes(a);
Copy the code
The data types to be processed to determine which types of results we need to modify, such as classes, resource files, etc.
getScopes()
/** * Returns the scope(s) of the Transform. This indicates which scopes the transform consumes. */
@NonNull
public abstract Set<? super Scope> getScopes();
Copy the code
Represents the scope of content that the Transform will operate on
These scopes can be combined freely (e.g. SCOPE_FULL_PROJECT includes scope. PROJECT, scope. SUB_PROJECTS, scope. EXTERNAL_LIBRARIES).
isIncremental()
/** * Returns whether the Transform can perform incremental work. * * If it does, then the TransformInput may contain a list of changed/removed/added files, unless * something else triggers a non incremental run. */
public abstract boolean isIncremental(a);
Copy the code
Whether incremental updates are supported: If true, TransformInput will contain a list of modified files, if false, a full compilation will remove the last output.
Description of related status:
-
NOTCHANGED: No processing is required.
-
REMOVED: Files need to be REMOVED.
-
ADDED, CHANGED: The file needs to be modified normally.
transform()
/**
* Executes the Transform.
*
* <p>The inputs are packaged as an instance of {@link TransformInvocation}
* <ul>
* <li>The <var>inputs</var> collection of {@link TransformInput}. These are the inputs
* that are consumed by this Transform. A transformed version of these inputs must
* be written into the output. What is received is controlled through
* {@link #getInputTypes()}, and {@link #getScopes()}.</li>
* <li>The <var>referencedInputs</var> collection of {@link TransformInput}. This is
* for reference only and should be not be transformed. What is received is controlled
* through {@link #getReferencedScopes()}.</li>
* </ul>
*
* A transform that does not want to consume anything but instead just wants to see the content
* of some inputs should return an empty set in {@link #getScopes()}, and what it wants to
* see in {@link #getReferencedScopes()}.
*
* <p>Even though a transform's {@linkTransform#isIncremental()} returns true, this method may * be receive <code>false</code> in <var>isIncremental</var>. This can be due to * <ul> * <li>a change in secondary files ({@link #getSecondaryFiles()},
* {@link #getSecondaryFileOutputs()}, {@link #getSecondaryDirectoryOutputs()})</li>
* <li>a change to a non file input ({@link#getParameterInputs()})</li> * <li>an unexpected change to the output files/directories. This should not happen unless * tasks are improperly configured and clobber each other's output.</li> * <li>a file deletion that the transform mechanism could not match to a previous input. * This should not happen in most case, except in some cases where dependencies have * changed.</li> * </ul> * In such an event, when <var>isIncremental</var> is false, the inputs will not have any * incremental change information: * <ul> * <li>{@link JarInput#getStatus()} will return {@link Status#NOTCHANGED} even though
* the file may be added/changed.</li>
* <li>{@link DirectoryInput#getChangedFiles()} will return an empty map even though
* some files may be added/changed.</li>
* </ul>
*
* @param transformInvocation the invocation object containing the transform inputs.
* @throws IOException if an IO error occurs.
* @throws InterruptedException
* @throws TransformException Generic exception encapsulating the cause.
*/
public void transform(@NonNull TransformInvocation transformInvocation)
throws TransformException, InterruptedException, IOException {
// Just delegate to old method, for code that uses the old API.
//noinspection deprecation
transform(transformInvocation.getContext(), transformInvocation.getInputs(),
transformInvocation.getReferencedInputs(),
transformInvocation.getOutputProvider(),
transformInvocation.isIncremental());
}
Copy the code
GetInputs () is available here. If getInputs() is consumed, the transform must be output to the next level, otherwise the next level cannot read the compiled middleware. IsIncremental is used for incremental compilation.
override fun transform(transformInvocation: TransformInvocation?). {
valinputs = transformInvocation? .inputsval out= transformInvocation? .outputProvider inputs? .forEach { transformInput ->// Project directory
transformInput.directoryInputs.forEach { directoryInput ->
if (directoryInput.file.isDirectory) {
FileUtils.getAllFiles(directoryInput.file).forEach {
val file = it
val name = file.name
if (name.endsWith(".class") && name ! ="R.class" && !name.startsWith("R\$") && name ! ="BuildConfig.class") {
val classPath = file.absolutePath
val cr = ClassReader(file.readBytes())
val cw = ClassWriter(cr, ClassWriter.COMPUTE_MAXS)
val visitor = AddLogClassVisitor(cw)
cr.accept(visitor, ClassReader.EXPAND_FRAMES)
val byte = cw.toByteArray();
val fos = FileOutputStream(classPath)
fos.write(byte)
fos.close()
}
}
}
val dest = out? .getContentLocation( directoryInput.name, directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY ) FileUtils.copyDirectoryToDirectory(directoryInput.file, dest) }/ / the jar package
transformInput.jarInputs.forEach {
val dest = out? .getContentLocation( it.name, it.contentTypes, it.scopes, Format.JAR ) FileUtils.copyFile(it.file, dest) } } }Copy the code
ASM Practice Scheme
Bytecode related knowledge please check for yourself, this article will not explain too much.
ASM core API
The ASM Core API can mimic the SAX approach to parsing XML files by streaming bytecode files without reading in the entire structure of the class. The advantage is that it is very memory efficient, but difficult to program. However, for performance reasons, programming typically uses the Core API. There are several key classes in the Core API:
- ClassReader: Used to read compiled. Class files.
- ClassWriter: A bytecode file used to rebuild a compiled class to generate a new class.
- A variety of Visitor classes: The CoreAPI works from top to bottom based on the bytecode, with different visitors for different areas of the bytecode file, such as MethodVisitor for accessing methods, FieldVisitor for accessing class variables, AnnotationVisitor for accessing annotations, and so on. To implement AOP, the focus is on using the MethodVisitor.
Situational goal
To add a custom method call to all methods in mainActivity.kt:
- First read the class file you want to modify in the transform() method. Create a ClassVisitor class and pass in a ClassWriter for a bytecode location modification operation, pass in a ClassVisitor for a ClassReader to rebuild a new bytecode file.
val classPath = file.absolutePath
val cr = ClassReader(file.readBytes())
val cw = ClassWriter(cr, ClassWriter.COMPUTE_MAXS)
val visitor = AddLogClassVisitor(cw)
cr.accept(visitor, ClassReader.EXPAND_FRAMES)
val byte = cw.toByteArray()
val fos = FileOutputStream(classPath)
fos.write(byte)
fos.close()
Copy the code
- Write a custom Visitor class that inherits from ClassVisitor to implement method positioning.
- The visit() method gets information about the class:
public void visit(
final int version,
final int access,
final String name,
final String signature,
final String superName,
final String[] interfaces) {
if(cv ! =null) { cv.visit(version, access, name, signature, superName, interfaces); }}Copy the code
- The visitMethod() method traverses all method information to locate methods that need to be modified.
public MethodVisitor visitMethod(
final int access,
final String name,
final String descriptor,
final String signature,
final String[] exceptions) {
if(cv ! =null) {
return cv.visitMethod(access, name, descriptor, signature, exceptions);
}
return null;
}
Copy the code
Find the method of locating a target by name here:
AddLogClassVisitor.kt
class AddLogClassVisitor(classVisitor: ClassVisitor) : ClassVisitor(Opcodes.ASM7, classVisitor) {
private var className: String? = null
override fun visit(
version: Int,
access: Int,
name: String? , signature:String? , superName:String? , interfaces:Array<out String>? {
super.visit(version, access, name, signature, superName, interfaces)
className = name
println("className = $className")}override fun visitMethod(
access: Int,
name: String? , descriptor:String? , signature:String? , exceptions:Array<out String>?: MethodVisitor {
val methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions)
println("method = $name")
if (className.equals("com/shawn/krouter/MainActivity") && name ! ="<init>") {
return if (name == "onCreate") {
AddLogInitMethodVisitor(methodVisitor)
} else {
AddLogMethodVisitor(methodVisitor)
}
}
return methodVisitor
}
}
Copy the code
- Write a custom Visitor class that inherits from MethodVisitor to make bytecode modifications.
- The bytecode in the visitCode() method is inserted before the method is executed.
/** Starts the visit of the method's code, if any (i.e. non abstract method). */
public void visitCode(a) {
if(mv ! =null) { mv.visitCode(); }}Copy the code
- In visitInsn (opcode: Int) method when the opcode = = ARETURN | | opcode = = RETURN, behind the byte code will be inserted into the method of the insert.
/**
* Visits a zero operand instruction.
*
* @param opcode the opcode of the instruction to be visited. This opcode is either NOP,
* ACONST_NULL, ICONST_M1, ICONST_0, ICONST_1, ICONST_2, ICONST_3, ICONST_4, ICONST_5,
* LCONST_0, LCONST_1, FCONST_0, FCONST_1, FCONST_2, DCONST_0, DCONST_1, IALOAD, LALOAD,
* FALOAD, DALOAD, AALOAD, BALOAD, CALOAD, SALOAD, IASTORE, LASTORE, FASTORE, DASTORE,
* AASTORE, BASTORE, CASTORE, SASTORE, POP, POP2, DUP, DUP_X1, DUP_X2, DUP2, DUP2_X1, DUP2_X2,
* SWAP, IADD, LADD, FADD, DADD, ISUB, LSUB, FSUB, DSUB, IMUL, LMUL, FMUL, DMUL, IDIV, LDIV,
* FDIV, DDIV, IREM, LREM, FREM, DREM, INEG, LNEG, FNEG, DNEG, ISHL, LSHL, ISHR, LSHR, IUSHR,
* LUSHR, IAND, LAND, IOR, LOR, IXOR, LXOR, I2L, I2F, I2D, L2I, L2F, L2D, F2I, F2L, F2D, D2I,
* D2L, D2F, I2B, I2C, I2S, LCMP, FCMPL, FCMPG, DCMPL, DCMPG, IRETURN, LRETURN, FRETURN,
* DRETURN, ARETURN, RETURN, ARRAYLENGTH, ATHROW, MONITORENTER, or MONITOREXIT.
*/
public void visitInsn(final int opcode) {
if(mv ! =null) { mv.visitInsn(opcode); }}Copy the code
Here we insert the required bytecode:
AddLogMethodVisitor.kt
class AddLogMethodVisitor(methodVisitor: MethodVisitor) :
MethodVisitor(Opcodes.ASM7, methodVisitor) {
override fun visitCode(a) {
super.visitCode()
// method is inserted before execution
mv.visitLdcInsn("AddLogMethodPlugin")
mv.visitTypeInsn(Opcodes.NEW, "java/lang/StringBuilder")
mv.visitInsn(Opcodes.DUP)
mv.visitMethodInsn(
Opcodes.INVOKESPECIAL,
"java/lang/StringBuilder"."<init>"."()V".false
)
mv.visitLdcInsn("into ")
mv.visitMethodInsn(
Opcodes.INVOKEVIRTUAL,
"java/lang/StringBuilder"."append"."(Ljava/lang/String;) Ljava/lang/StringBuilder;".false
)
mv.visitTypeInsn(Opcodes.NEW, "java/lang/Exception")
mv.visitInsn(Opcodes.DUP)
mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Exception"."<init>"."()V".false)
mv.visitMethodInsn(
Opcodes.INVOKEVIRTUAL,
"java/lang/Exception"."getStackTrace"."()[Ljava/lang/StackTraceElement;".false
)
mv.visitInsn(Opcodes.ICONST_0)
mv.visitInsn(Opcodes.AALOAD)
mv.visitMethodInsn(
Opcodes.INVOKEVIRTUAL,
"java/lang/StackTraceElement"."getMethodName"."()Ljava/lang/String;".false
)
mv.visitMethodInsn(
Opcodes.INVOKEVIRTUAL,
"java/lang/StringBuilder"."append"."(Ljava/lang/String;) Ljava/lang/StringBuilder;".false
)
mv.visitMethodInsn(
Opcodes.INVOKEVIRTUAL,
"java/lang/StringBuilder"."toString"."()Ljava/lang/String;".false
)
mv.visitMethodInsn(
Opcodes.INVOKESTATIC,
"com/shawn/krouter/uitl/UtilsKt"."MyLogD"."(Ljava/lang/String; Ljava/lang/String;) V".false)}override fun visitInsn(opcode: Int) {
// insert after method
super.visitInsn(opcode)
}
}
Copy the code
- The requirements can be implemented by compiling the plugin and incorporating it into the corresponding model.
Viewing the class file, you can see that the bytecode was inserted successfully
The log output
Easily generate insert code
The ASM Bytecode Outline plugin is recommended in many web articles to view bytecodes, but it is currently only installed in Intellij IDEA, not Android Studio. The ASM Bytecode Viewer allows you to insert Bytecode more easily.
- First write the test. Java class, compile it as.class, and then use the ASM Bytecode Viewer plug-in to view the associated ASM code.
Test.java
package com.shawn.krouter;
import android.util.Log;
import com.shawn.krouter.uitl.UtilsKt;
public class Test {
String name = "testFun";
public void TestFun(a){
Log.d("suihw"."lalala"); }}Copy the code
View ASM code through the plug-in:
package asm.com.shawn.krouter;
import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.Attribute;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.ConstantDynamic;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.Handle;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.TypePath;
public class TestDump implements Opcodes {
public static byte[] dump() throws Exception {
ClassWriter classWriter = new ClassWriter(0);
FieldVisitor fieldVisitor;
MethodVisitor methodVisitor;
AnnotationVisitor annotationVisitor0;
classWriter.visit(V1_8, ACC_PUBLIC | ACC_SUPER, "com/shawn/krouter/Test".null."java/lang/Object".null);
classWriter.visitSource("Test.java".null);
{
methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "<init>"."()V".null.null);
methodVisitor.visitCode();
Label label0 = new Label();
methodVisitor.visitLabel(label0);
methodVisitor.visitLineNumber(5, label0);
methodVisitor.visitVarInsn(ALOAD, 0);
methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/Object"."<init>"."()V".false);
methodVisitor.visitInsn(RETURN);
Label label1 = new Label();
methodVisitor.visitLabel(label1);
methodVisitor.visitLocalVariable("this"."Lcom/shawn/krouter/Test;".null, label0, label1, 0);
methodVisitor.visitMaxs(1.1);
methodVisitor.visitEnd();
}
{
methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "TestFun"."()V".null.null);
methodVisitor.visitCode();
Label label0 = new Label();
methodVisitor.visitLabel(label0);
methodVisitor.visitLineNumber(7, label0);
methodVisitor.visitLdcInsn("suihw");
methodVisitor.visitLdcInsn("lalala");
methodVisitor.visitMethodInsn(INVOKESTATIC, "android/util/Log"."d"."(Ljava/lang/String; Ljava/lang/String;) I".false);
methodVisitor.visitInsn(POP);
Label label1 = new Label();
methodVisitor.visitLabel(label1);
methodVisitor.visitLineNumber(8, label1);
methodVisitor.visitInsn(RETURN);
Label label2 = new Label();
methodVisitor.visitLabel(label2);
methodVisitor.visitLocalVariable("this"."Lcom/shawn/krouter/Test;".null, label0, label2, 0);
methodVisitor.visitMaxs(2.1);
methodVisitor.visitEnd();
}
classWriter.visitEnd();
returnclassWriter.toByteArray(); }}Copy the code
- Modify the test. Java class, write the relevant Java code you want to insert, and then compile it into a.class file:
Modified test.java
package com.shawn.krouter;
import android.util.Log;
import com.shawn.krouter.uitl.UtilsKt;
public class Test {
String name = "testFun";
public void TestFun(a){
UtilsKt.MyLogD("suihw"."into " + new Exception().getStackTrace()[0].getMethodName());
Log.d("suihw"."lalala"); }}Copy the code
View ASM code through the plug-in:
package asm.com.shawn.krouter;
import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.Attribute;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.ConstantDynamic;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.Handle;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.TypePath;
public class TestDump implements Opcodes {
public static byte[] dump() throws Exception {
ClassWriter classWriter = new ClassWriter(0);
FieldVisitor fieldVisitor;
MethodVisitor methodVisitor;
AnnotationVisitor annotationVisitor0;
classWriter.visit(V1_8, ACC_PUBLIC | ACC_SUPER, "com/shawn/krouter/Test".null."java/lang/Object".null);
classWriter.visitSource("Test.java".null);
{
methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "<init>"."()V".null.null);
methodVisitor.visitCode();
Label label0 = new Label();
methodVisitor.visitLabel(label0);
methodVisitor.visitLineNumber(7, label0);
methodVisitor.visitVarInsn(ALOAD, 0);
methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/Object"."<init>"."()V".false);
methodVisitor.visitInsn(RETURN);
Label label1 = new Label();
methodVisitor.visitLabel(label1);
methodVisitor.visitLocalVariable("this"."Lcom/shawn/krouter/Test;".null, label0, label1, 0);
methodVisitor.visitMaxs(1.1);
methodVisitor.visitEnd();
}
{
methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "TestFun"."()V".null.null);
methodVisitor.visitCode();
Label label0 = new Label();
methodVisitor.visitLabel(label0);
methodVisitor.visitLineNumber(9, label0);
methodVisitor.visitLdcInsn("suihw");
methodVisitor.visitTypeInsn(NEW, "java/lang/StringBuilder");
methodVisitor.visitInsn(DUP);
methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder"."<init>"."()V".false);
methodVisitor.visitLdcInsn("into ");
methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder"."append"."(Ljava/lang/String;) Ljava/lang/StringBuilder;".false);
methodVisitor.visitVarInsn(ALOAD, 0);
methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Object"."getClass"."()Ljava/lang/Class;".false);
methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Class"."getSimpleName"."()Ljava/lang/String;".false);
methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder"."append"."(Ljava/lang/String;) Ljava/lang/StringBuilder;".false);
methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder"."toString"."()Ljava/lang/String;".false);
methodVisitor.visitMethodInsn(INVOKESTATIC, "com/shawn/krouter/uitl/UtilsKt"."MyLogD"."(Ljava/lang/String; Ljava/lang/String;) V".false);
Label label1 = new Label();
methodVisitor.visitLabel(label1);
methodVisitor.visitLineNumber(10, label1);
methodVisitor.visitLdcInsn("suihw");
methodVisitor.visitLdcInsn("lalala");
methodVisitor.visitMethodInsn(INVOKESTATIC, "android/util/Log"."d"."(Ljava/lang/String; Ljava/lang/String;) I".false);
methodVisitor.visitInsn(POP);
Label label2 = new Label();
methodVisitor.visitLabel(label2);
methodVisitor.visitLineNumber(11, label2);
methodVisitor.visitInsn(RETURN);
Label label3 = new Label();
methodVisitor.visitLabel(label3);
methodVisitor.visitLocalVariable("this"."Lcom/shawn/krouter/Test;".null, label0, label3, 0);
methodVisitor.visitMaxs(3.1);
methodVisitor.visitEnd();
}
classWriter.visitEnd();
returnclassWriter.toByteArray(); }}Copy the code
- Finding the similarities and differences is the relevant bytecode that we need to copy and insert.
eggs
BuildSrc unified versioning
The official introduction
The buildSrc module is executed before the entire project is compiled, before build.gradle is executed at the root of the project, so it can be used for uniform version control and code prompts, which is very handy.
The project structure
build.gradle.kts
Dependencies.kt
Introduce in related modules: