Bytecode staking was originally intended to be an introduction to bytecode staking. However, bytecode staking requires the use of a custom Gradle Plugin, suggesting that this article will not be short.


Before we look at bytecode staking, let’s look at compile staking.

What is a peg

ButterKnife, if you’ve used it before, generates Java files at compile time, and at run time, uses reflection to retrieve the generated class and call its binding methods to bind controls. (what? Haven’t you learned about the ButterKnife principle? Check it out — From Handwritten ButterKnife to Mastering AnnotationProcessor)

Compilation staking basically means modifying existing code or generating new code during compilation.

What is bytecode staking

Bytecode staking is actually a more detailed step than compilation staking. Compilation staking refers to the compilation process, which includes the Java –> class –> dex whole process. Bytecode staking is only for the class step, and the generated class file is modified.

Preface to bytecode staking

First, let’s look at when bytecode staking is used. Learn technology not to show off technology, but to solve business problems.

Let’s imagine a business scenario — we need to log user clicks. What do we do?

  • Call statistics code in each onClick() method? It’s too much work! What’s more, people always forget, it is easy to omit the situation.
  • Create a new click class and use the new click class every time you set a click listener? It’s fine for self-written code, but what about third-party library classes?

This is where bytecode pegs can be used! After a Java file is compiled into a class, you can get the entire class file, including your own code and other library classes. Once you have a class file, you can make batch changes, and Java files are insensitive because we only work with class files.

Before using bytecode pegs, we need to retrieve each class file. In this case, we need to use the custom Transform, which needs to be registered when we customize Gradle Plugin. We need to learn how to customize a Gradle Plugin.

When the instructions are finally written, fasten your seat belts and get ready to Go. Go! Go! Go!

Custom Gradle

Since this is my custom Gradle creation in Android Studio, I need to write a lot of configuration by hand, which will be troublesome, but it doesn’t matter, I have written it, then you can copy it.

Create the Module

First, we need to create a new Module:

Android Studio –> File –> New –> New Module –> Java or Kotlin Library –> click_plugin

Update the build. Gradle

Overwrite the original build.gradle file:

apply plugin: 'groovy' apply plugin: 'maven' dependencies { implementation fileTree(dir: 'libs', includes: [' *. Jar ']) implementation gradleApi () implementation localGroovy () implementation 'com. Android. View the build: gradle: 4.0.0' } // These are the configuration information, which will be used later. Group ='bjsdm.plugin' version='1.0.0' uploadArchives{repositories{mavenDeployer{ // A bjsdm_repo folder will be created and named repository(URL: uri('.. /bjsdm_repo')) } } }Copy the code

The main things to notice are these three configurations:

  • group='bjsdm.plugin'
  • Version = "1.0.0
  • repository(url: uri('.. /bjsdm_repo'))

Create the Plugin

Create the clickplugin.groovy file

The package name error here is ignored, the problem is identified.

package bjsdm.plugin import org.gradle.api.Plugin import org.gradle.api.Project public class ClickPlugin implements Plugin<Project>{@override void apply(Project Project) {println(" successfully configured --------->ClickPlugin")}}Copy the code

configuration

Configure:

implementation-class=bjsdm.plugin.ClickPlugin
Copy the code

This is the class you just wrote. You can name bjsdm.click.properties as you wish, and you will use it later in your configuration.

The deployment of

To deploy the Plugin:

Double-click uploadArchives.

You can see the following files generated in the root directory:

Rely on

Build. Gradle = build. Gradle = build. Gradle = build.

The main thing is the configuration inside the blue box, which is the name of the reminder. If you forget it, you can go back and have a look.

Plugins: 'bjsdm.click' BuildScript {repositories{Google () jCenter () // Custom plugins repository maven {url '.. /bjsdm_repo'} dependencies {classpath 'bjsdm.plugin:click_plugin:1.0.0'}}Copy the code

test

Use the build command to test:

./gradlew clean assembledebug
Copy the code

Successful output! Gradle Plugin created successfully!

A custom Transform

Create the clickTransform.groovy file:

Override method description

  • getName(): Sets the name.
  • getInputTypes(): Filters file types. Fill in any type, return all files of that type. There are two default types:
    • QualifiedContent.DefaultContentType.CLASSES: class File type.
    • QualifiedContent.DefaultContentType.RESOURCES: Resource file type.
  • getScopes(): Used to specify the scope of a search:
    • QualifiedContent.Scope.PROJECT: the main Project.
    • QualifiedContent.Scope.SUB_PROJECTS: Other Modules.
    • QualifiedContent.Scope.EXTERNAL_LIBRARIES: External library.
    • QualifiedContent.Scope.TESTED_CODE: The test code for the current variable, including the dependency library.
    • QualifiedContent.Scope.PROVIDED_ONLY: local or remote dependencies.
    • QualifiedContent.Scope.PROJECT_LOCAL_DEPS: local dependencies of the main Project, containing local JARS,Have been abandoned, the use ofQualifiedContent.Scope.EXTERNAL_LIBRARIESInstead of
    • QualifiedContent.Scope.SUB_PROJECTS_LOCAL_DEPS: local dependencies for other modules, containing local jars,Have been abandoned, the use ofQualifiedContent.Scope.EXTERNAL_LIBRARIESInstead of
  • isIncremental(): Indicates whether incremental compilation is supported.

With this in mind, we can improve clickTransform.groovy:

public class ClickTransform extends Transform {
    @Override
    String getName() {
        return "ClickTransform"
    }
    @Override
    Set<QualifiedContent.ContentType> getInputTypes() {
        return TransformManager.CONTENT_CLASS
    }
    @Override
    Set<? super QualifiedContent.Scope> getScopes() {
        return TransformManager.SCOPE_FULL_PROJECT
    }
    @Override
    boolean isIncremental() {
        return false
    }
}
Copy the code

Among them:

  • TransformManager.CONTENT_CLASS: immutableset. of(CLASSES)QualifiedContent.DefaultContentType.CLASSESPut it in Set.
  • TransformManager.SCOPE_FULL_PROJECT: immutableset. of(scope. PROJECT, scope. SUB_PROJECTS, scope. EXTERNAL_LIBRARIES).

The preceding two parameters can be set based on different scenarios.

All class files can be transformed. How else to do bytecode staking.

transform()

Exactly, so we also need to rewrite the Transform () method of Transform :(old version)

    @Override
    void transform(Context context, Collection<TransformInput> inputs, Collection<TransformInput> referencedInputs,
                   TransformOutputProvider outputProvider, boolean isIncremental)
            throws IOException, TransformException, InterruptedException {
        super.transform(context, inputs, referencedInputs, outputProvider, isIncremental)
    }
Copy the code

However, the above method has been deprecated and is now recommended :(new version)

    @Override
    void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        super.transform(transformInvocation)
    }
Copy the code

The difference between the two versions is that all arguments from the old version are encapsulated in the new version of TransformInvocation, but the new version still calls the old method by default:

    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

The transform() method is like a processing channel: we put the inputs in: inputs; after processing, we place them in the outputProvider.

  • inputs: Data is sent in two formats:
    • Jar package format. Participate in the compilation as a JAR package, such as a dependent JAR package.
    • Directory format. Participate in compiling as source code, such as the code we write on a project.
  • outputProvider: Output directory to which modified files are copied.

Be sure to override transform() because the transform() method inside transform is an empty method:

    @Deprecated
    @SuppressWarnings("UnusedParameters")
    public void transform(
            @NonNull Context context,
            @NonNull Collection<TransformInput> inputs,
            @NonNull Collection<TransformInput> referencedInputs,
            @Nullable TransformOutputProvider outputProvider,
            boolean isIncremental) throws IOException, TransformException, InterruptedException {
    }
Copy the code

This is equivalent to putting something in the transform() processing channel, but nothing comes out because the modified file is not copied to the output directory.

So, for now, let’s implement a basic function that prints the name of the thing we put in:

@Override void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {/ / gain entry to traverse the def transformInputs = transformInvocation. Inputs transformInputs. Each {TransformInput TransformInput - > / / traverse jar package transformInput jarInputs. Each {JarInput JarInput - > println (" JarInput: "Def enumeration = new JarFile(jarinput.file).entries() while" + jarInput) // Decompress JarFile def enumeration = new JarFile(jarinput.file).entries() while (enumeration. HasMoreElements ()) {/ / get the contents of the jar inside def entry = enumeration. The nextElement () println (" jarInput File: "+ entry. Name)}} / / directory traversal transformInput directoryInputs. Each {DirectoryInput DirectoryInput - > Println (" directoryInputs: "+ directoryInput) / / access catalog file directoryInput. Inside the file. The eachFileRecurse {file file - > println (" directoryInputs file: " + file.name) } } } }Copy the code

registered

We registered our Transform with the ClickPlugin:

@ Override void the apply (Project Project) {println (" configuration success -- -- -- -- -- -- -- -- -- > ClickPlugin ") Project. The android. RegisterTransform (new ClickTransform()) }Copy the code

Double-click uploadArchives to deploy and run the./gradlew clean assembledebug command:


Because there is so much output, I will not intercept all of it, but only part of it, obviously with output:

  • jarInput
  • jarInput File
  • directoryInputs
  • directoryInputs File

At this point, you can finally breathe a sigh of relief. The basic process has been worked out, leaving only the bytecode changes and the modified files in the output directory. Here, I will only show you how to change the class file under directory, but the class file of the JAR package is similarly changed, so it is interesting to explore further.

Bytecode processing

For bytecode processing, the ASM tool is used, mainly using its three classes:

  • ClassReader: reads and parses.class files.
  • ClassVisitor: Is responsible for accessing elements in the.class file, for example when a method is read, the corresponding MethodVisitor is called internally. Obvious division of labor.
  • ClassWriter: Generates a bytecode utility class that outputs the bytecode as a byte array.

Build. Gradle (:click_plugin) dependency to build. Gradle (:click_plugin)

Implementation 'org.ow2. ASM: ASM :9.1' implementation 'org.ow2. ASM: asM-Commons :9.1'Copy the code

ClassVisitor

We need to create a ClassVisitor to filter the class file and modify the method read for methods that meet the criteria.

Clickclassvisitor.java, stored in:

public class ClickClassVisitor extends ClassVisitor { public ClickClassVisitor(ClassVisitor classVisitor) { super(Opcodes.ASM4, classVisitor); } @Override public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { MethodVisitor methodVisitor = cv.visitMethod(access, name, descriptor, signature, exceptions); If (name.startswith ("onClick")) {system.out.println ("onClick"); Return new ClickMethodVisitor(methodVisitor); } return methodVisitor; }}Copy the code

MethodVisitor

Before we can insert code, we need to know the code we are inserting:

Log.e("TAG", "CLICK")
Copy the code

But when implemented, it is actually divided into three parts:

  • “TAG” –> Use the A symbol
  • “CLICK” –> use B notation
  • e(Log, A, B)

Why did I pull out the string?

That’s because a string in a bytecode structure is actually a table, a table with a constant pool:

For details on the bytecode structure, see Bytecode Structure Analysis.

As for the bytecode that this code actually compiles, we can look at it by decompiling. Kotlin –> Show Kotlin Bytecode:

With that in mind, we can continue writing clickMethodVisitor.java:

public class ClickMethodVisitor extends MethodVisitor { public ClickMethodVisitor(MethodVisitor methodVisitor) { super(Opcodes.ASM9, methodVisitor); } @Override public void visitCode() { super.visitCode(); mv.visitLdcInsn("TAG"); mv.visitLdcInsn("CLICK"); mv.visitMethodInsn(Opcodes.INVOKESTATIC, "android/util/Log", "e", "(Ljava/lang/String; Ljava/lang/String;) I", false); mv.visitInsn(Opcodes.POP); }}Copy the code

There is an easier way to install the ASM Bytecode Viewer Support Kotlin plugin, which can be found in Android Studio.

Install and restart Android Studio, right-click file –> Asm Bytecode Viewer:

Modify transform() method:

@Override void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {/ / gain entry to traverse the def transformInputs = transformInvocation. Inputs / / get the output directory def transformOutputProvider = TransformInvocation. OutputProvider transformInputs. Each {TransformInput TransformInput - > / / traverse the jar package TransformInput. JarInputs. Each {JarInput JarInput - > / / the jar package copy directly to the output directory File dest = transformOutputProvider.getContentLocation(jarInput.name, jarInput.contentTypes, jarInput.scopes, Format.JAR) FileUtils.copyFile(jarInput.file, Dest)} / / directory traversal transformInput directoryInputs. Each {DirectoryInput DirectoryInput - > / / access class files in directory DirectoryInput. File. EachFileRecurse {file file - > if (file. AbsolutePath. EndsWith (" class ")) {/ / analytical def read for class files Def ClassWriter = new ClassWriter(classReader) def ClassWriter = new ClassWriter(classReader, Def classVisitor = new ClickClassVisitor(classWriter) // Start reading classReader.accept(classVisitor, Def bytes = classwriter.tobytearray () def outputStream = new FileOutputStream(file.path) outputStream.write(bytes) outputStream.close()}} // Copy the Directory file to the output Directory file dest = transformOutputProvider.getContentLocation(directoryInput.name, directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY) FileUtils.copyDirectory(directoryInput.file, dest) } } }Copy the code

Look at the code, in fact, are annotations and copy code, not difficult to understand.

That’s pretty much the end of it, but don’t forget to include a click-listening event:

class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) tv_click.setOnClickListener { Log.e("TAG", "Normal click events ")}}}Copy the code

Double-click uploadArchives to deploy.

Run…

Finally finished…

GitHub address: github.com/bjsdm/TestC…