I always think bytecode staking is unscalable knowledge, but I am not busy with my work recently, so I decided to learn bytecode staking. Open the articles I have collected for a long time and start to study. The following are the articles I collected before (collect == learn).

  • https://juejin.cn/post/6999646242125529096

  • https://juejin.cn/post/6986848837797658637

  • https://juejin.cn/post/6844904194445426702

  • Juejin. Cn/post / 696579…

To get started, simply insert some code into the Test () method of the test.java class; Because it is the first experience, do not say difficult, how simple how to come. At the beginning, I was also confused. Practice is the best way to master new skills and knowledge, so I went to the blog to experience AMS bytecode staking myself. I’ll create a new project to show bytecode staking, step by step, starting from 0, and inserting some code before and after the test method.

Project github download address

1. Create a project and create a plug-in module

The pluginams Module only keeps the main folder and does not need the resource directory. You need to create a new resource directory, which will be explained later.

2. Build the plugin

'groovy' apply plugin: 'kotlin' 'maven' repositories { mavenCentral() } dependencies { //gradle sdk implementation gradleApi() //groovy sdk Implementation localGroovy() // introduce ams implementation 'org.ow2.asm:asm:7.2' implementation 'org.ow2.asm: asM-Commons :7.2' Implementation 'org.ow2.asm: asM-analysis :7.2' implementation 'org.ow2.asm:asm-util:7.2' implementation 'org. Ow2. Asm: asm - tree: 7.2' implementation 'com. Android. View the build: gradle: 4.1.2', {exclude group:'org.ow2.asm'}} group='com.cb.test.amsplugin' version='1.0.0' // Upload uploadArchives {mavenDeployer {// Upload uploadArchives {repository} uri('./ams_plugin')) } } }Copy the code

Write a Gradle plugin class

The plugin’s main function is to traverse the entire project’s files, including jar packages

``` package com.cb.test.pluginams import com.android.build.api.transform.Format import com.android.build.api.transform.QualifiedContent import com.android.build.api.transform.Transform import com.android.build.api.transform.TransformInvocation import com.android.build.gradle.AppExtension import com.android.build.gradle.internal.pipeline.TransformManager import com.android.utils.FileUtils import org.gradle.api.Plugin import org.gradle.api.Project import org.objectweb.asm.ClassReader import org.objectweb.asm.ClassWriter import java.io.FileOutputStream ``` class TestMethodPlugin : Transform(), Plugin<Project> { override fun apply(target: Project) { val appExtension = target.extensions.getByType(AppExtension::class.java) appExtension.registerTransform(this)  } override fun getName(): String { return "TestMethodPlugin" } override fun getInputTypes(): MutableSet<QualifiedContent.ContentType> { return TransformManager.CONTENT_CLASS } override fun isIncremental(): Boolean { return false } override fun getScopes(): MutableSet<in QualifiedContent.Scope> { return TransformManager.SCOPE_FULL_PROJECT } override fun transform(transformInvocation: TransformInvocation?) { val inputs = transformInvocation?.inputs val out = transformInvocation?.outputProvider Inputs?. ForEach {transformInput - > / / project directory Traverse the project directory transformInput. DirectoryInputs. ForEach {directoryInput - > the if (directoryInput.file.isDirectory) { FileUtils.getAllFiles(directoryInput.file).forEach { val file = it val name = If (name.endswith (".class") && name! = "R.class" && ! name.startsWith("R$") && name ! = "buildconfig.class ") {// find what you need. The class files, Val classPath = file.absolutePath Val cr = ClassReader(file.readbytes ()) val CW = ClassWriter(cr, ClassWriter.COMPUTE_MAXS) val visitor = TestClassVisitor(cw) cr.accept(visitor, ClassReader.SKIP_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)} / / jar package transformInput jarInputs. ForEach {val dest = out? The getContentLocation (it. The name, it contentTypes, it.scopes, Format.JAR ) FileUtils.copyFile(it.file, dest) } } } }Copy the code

After the TestClassVisitor will

4. Name the plug-in

Create a meta-INF folder and create a gradle-plugins folder. Then create a properties file such as com. Cb. Test. Amsplugin. Properties;

Then specify the absolute path of the TestMethodPlugin plugin plugin in the file

implementation-class=com.cb.test.pluginams.TestMethodPlugin
Copy the code

5. Customize a MethodVisitor

package com.cb.test.pluginams import org.objectweb.asm.MethodVisitor import org.objectweb.asm.commons.AdviceAdapter class CustomizeMethodVisitor( api: Int, methodVisitor: MethodVisitor, className: String?, access: Int, name: String?, descriptor: String? ) : AdviceAdapter(api, methodVisitor, access, name, descriptor) { private var mClassName: String? = className private val TAG = "${this.javaClass.simpleName}: Override fun onMethodEnter() {println("$TAG onMethodEnter") super.onmethodenter (); Mv.visitcinsn (" test.class ") mv.visitcinsn ("aaa start") mv.visitMethodInsn( INVOKESTATIC, "android/util/Log", "d", "(Ljava/lang/String; Ljava/lang/String;) I", false) mv.visitinsn (POP)} /** * Insert after method call, note before super.onmethodexit (opcode) */ Override fun onMethodExit(opcode: Int) { mv.visitLdcInsn("Test.class ") mv.visitLdcInsn("aaa end") mv.visitMethodInsn( INVOKESTATIC, "android/util/Log", "d", "(Ljava/lang/String; Ljava/lang/String;) I", false) mv.visitInsn(POP) println("$TAG onMethodExit") super.onMethodExit(opcode) } }Copy the code

The AMS Bytecode Viewer plugin can be found in Android Stuido. The AMS Bytecode Viewer plugin can be found in Android Stuido. The AMS Bytecode Viewer plugin can be found in Android Stuido.

6. Use AMS Bytecode Viewer to generate the corresponding bytecode

Create a new test.java

public class Test {

    public static final String TAG = "Test.class ";

    void test() {
    }

}
Copy the code

Right click code area

then

I’m going to copy the code here, I’m going to use it;

Then, in test.java, write the code you want to insert

public class Test { public static final String TAG = "Test.class "; void test() { Log.d(TAG, "aaa start"); Log.d(TAG, "aaa end"); }}Copy the code

Repeat the steps of AMS Bytecode Viewer, get the bytecode, copy and save it, and use the DIff comparison tool to find out the differences between the two bytecodes, as shown in the figure

Diff comparison tool

Find the differences, copy them,

The first four lines of Log correspond to log.d (TAG, “aaa start”); The next four lines correspond to log.d (TAG, “AAA end”);

This matches step 5.

7. Write ClassVisitor

package com.cb.test.pluginams import org.objectweb.asm.ClassVisitor import org.objectweb.asm.MethodVisitor import org.objectweb.asm.Opcodes class TestClassVisitor(classVisitor: ClassVisitor) : ClassVisitor(Opcodes.ASM7, classVisitor) { private val TAG = "PluginAmsTag: " 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 } override fun visitMethod( methodAccess: Int, methodName: String? , methodDescriptor: String? , signature: String? , exceptions: Array<out String>? ) : MethodVisitor { val methodVisitor = super.visitMethod(methodAccess, methodName, methodDescriptor, signature, Println ("$TAG method = $methodName") println("$TAG className = $className" / / note here is the full path is copied. Com. Cb test. Amstest. Test. The class note cannot be used. Instead of/if (className == "com/cb/test/amstest/ test" && methodName == "test") {// Return our custom MethodVisitor return CustomizeMethodVisitor(api, methodVisitor, className, methodAccess, methodName, methodDescriptor) } return methodVisitor } }Copy the code

The plug-in code is now complete

8. Build plug-ins

Click on uploadArchives and wait for compilation, which will generate the plug-in

9. Introduce plug-ins

Introduce the generated plug-ins in your project’s build.gradle

Buildscript {ext.kotlin_version = "1.5.0" Repositories {Google () mavenCentral() // here maven {url Uri ('/pluginams ams_plugin ')}} dependencies {classpath "com. Android. View the build: gradle: 4.1.2" classpath "Org. Jetbrains. Kotlin: kotlin - gradle - plugin: $kotlin_version" / / here classpath ". Com. Cb test. Amsplugin: pluginams: 1.0.0 "/ / NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files } }Copy the code

Note that com. Cb. Test. Amsplugin: pluginams: 1.0.0 naming rules, is look at generated in the plug-in maven – metadata. ArtifactId groupId + + version of the XML file

<? The XML version = "1.0" encoding = "utf-8"? > <metadata> <groupId>com.cb.test.amsplugin</groupId> <artifactId>pluginams</artifactId> <versioning> < Release >1.0.0</ Release > < Versions > <version>1.0.0</version> </ Versions > <lastUpdated>20220129085728</lastUpdated> </versioning> </metadata>Copy the code

Then add the plugin to build. Gradle in your app

The id naming convention here is just before step 4. Properties

Personally, I feel that the naming rules here are a little disgusting. If the naming is not correct, the compilation will not pass. If you want to try it, you must not name the main steps randomly.

10. Run the validation

Test test = new Test(); Test.test ();Copy the code

Look at the logcat print

The 2022-01-29 17:11:30. 560, 14149-14149 / com. Cb. Test. Amstest D/test. Class: Aaa start 17:11:30 2022-01-29. 560. 14149-14149 / com. Cb test. Amstest D/test. Class: aaa endCopy the code

The test.class file generated by test.java was inserted successfully

The insertion is successful, and now the simple AMS bytecode staking is complete.