The content of this article is very long, involving more knowledge points, suggest [collection] or [patience to watch], and follow the article to achieve their own ASM pile step by step!

Github: Portal

One, foreword

In Android development, if you want to use ASM library to develop your own bytecode staking library, you need Hook Android compilation process, Class/lib file traversal and manipulation based on Gradle API (developed based on Groovy language).

Hook Android Transform stage:

TransformClassesWithDexBuilderForXXX (XXX is buildTypes corresponding environment)

Check out Google’s official Gradle-API Form for Android

Module & configuration

2.1. Create a module

Create a new “Module” based on Android Studio, select “Java Library”, enter the Module name and package name, and finish.

2.2 dependency Configuration

  • First of all, we are developing based on Gradle (Groovy), so we need to introduce a “Groovy plugin”.
  • Second, we need to rely on the “ASM” library;
  • Third, we need to use the “Gradle API”;

OK, the configuration is as follows:

apply plugin: 'groovy' apply plugin: 'java' dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation localGroovy() implementation localGroovy( Implementation 'org.ow2.asm:asm:9.0' implementation 'org.ow2.asm: asM-Commons :9.0 Implementation 'com. Android. Tools. Build: gradle: 3.5.0'} sourceCompatibility = "8" targetCompatibility = "8"Copy the code

2.3 Gradle API: Plugin

  • Create a “groovy” directory in the main directory;
  • Create package name;
  • Create a Groovy class;

The code screenshot is as follows:

This is our entry point to the whole process, but, you see a problem? Our AopAsmPlugin class has a wavy line indicating that it is not referenced.

2.4 Gradle Plugin configuration

In the “main” directory, create the “Resources” directory, and then create the “meta-INF” directory, and then create the “gradle-plugins” directory. Then create a properties file with a custom name and fill in the following:

We see that once the properties file is defined, our entry class “AopAsmPlugin” is automatically referenced.

2.5. Inherit Android Build API: Transform

To inherit Transform, you must implement the following four methods

package com.chris.aop.asm.plugin; 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.TransformInvocation; import com.android.build.gradle.internal.pipeline.TransformManager; import java.io.IOException; import java.util.Set; public class AopAsmTransform extends Transform { @Override public String getName() { return "AopAsmPlugin"; } @Override public Set<QualifiedContent.ContentType> getInputTypes() { return TransformManager.CONTENT_CLASS; } @Override public Set<? super QualifiedContent.Scope> getScopes() { return TransformManager.SCOPE_FULL_PROJECT; } @Override public boolean isIncremental() { return true; } @override public void transform(TransformInvocation TransformInvocation) throws TransformException, InterruptedException, IOException {// Override this method to Hook class -> dex. Otherwise in. / build/intermediates/transforms/dexBuilder directory, is an empty directory / / can be used before and after contrast dexBuilder and not to use the plugin output}}Copy the code

2.6 Gradle Plugin register custom Transform

Plug-in entry registers custom Transform

// AopAsmPlugin.groovy package com.chris.aop.asm.plugin import com.android.build.gradle.AppExtension import org.gradle.api.Plugin import org.gradle.api.Project class AopAsmPlugin implements Plugin<Project> { @Override void apply(Project project) { if (! project.plugins.hasPlugin("com.android.application")) { throw new Exception("AopAsmPlugin must run at application") } / * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * register the Transform *****************************************************************************/ def extension = project.extensions.getByType(AppExtension) extension.registerTransform(new AopAsmTransform()) } }Copy the code
  • Custom Transform Overloads the Transform method
// AopAsmTransform.java package com.chris.aop.asm.plugin; 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.TransformInvocation; import com.android.build.gradle.internal.pipeline.TransformManager; import java.io.IOException; import java.util.Set; public class AopAsmTransform extends Transform { @Override public String getName() { return "AopAsmPlugin"; } @Override public Set<QualifiedContent.ContentType> getInputTypes() { return TransformManager.CONTENT_CLASS; } @Override public Set<? super QualifiedContent.Scope> getScopes() { return TransformManager.SCOPE_FULL_PROJECT; } @Override public boolean isIncremental() { return true; } @Override public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException { System.out.println("=========> " + System.currentTimeMillis()); Class -> dex -> dex -> dex -> dex -> dex Otherwise in. / build/intermediates/transforms/dexBuilder directory, is an empty directory / / can be used before and after contrast dexBuilder and not to use the plugin output}}Copy the code

3. Look into transform

3.1, QualifiedContent

This interface defines an input type (ContentType) and a Scope. It has two subinterfaces:

  • DirectoryInput
  • JarInput

These two subinterfaces represent two types of files: directory /Class files, Jar/AAR packages; The input source (XXXInput) depends on your ContentType + Scope definition.

package com.android.build.api.transform; import com.android.annotations.NonNull; import java.io.File; import java.util.Set; public interface QualifiedContent { interface ContentType { ....... } enum DefaultContentType implements ContentType { ....... } interface ScopeType { ....... } enum Scope implements ScopeType { ....... } /** * returns the name of the input source, cannot be trusted (because it will be used at different stages of the transform) */ @nonNULL String getName(); @NonNull File getFile(); /** * Define the type of the input stream (Class/Jar/AAR, or Resources) * However, even if you specify the type of the desired transform, you may actually return files containing other types:  * * Even though this may return only {RESOURCES} or {CLASSES}, the actual content (the folder * or the jar) may contain files representing other content types. This is because the * transform mechanism avoids duplicating files around to remove unwanted types for performance. */ @NonNull Set<ContentType> getContentTypes(); @NonNull Set<? super Scope> getScopes(); }Copy the code

3.1.1, QualifiedContent ContentType

public interface QualifiedContent { /** * A content type that is requested through the transform API. */ interface ContentType { String name(); int getValue(); } enum implements ContentType {// implements ContentType; Directory CLASSES(0x01), /** The content is standard Java resources. */ resources (0x02); }... }Copy the code

3.1.2, QualifiedContent. Scope

public interface QualifiedContent { /** * Definition of a scope. */ interface ScopeType { String name(); int getValue(); } enum Scope implements ScopeType {/** ScopeType */ PROJECT(0x01), /** SUB_PROJECTS(0x04), /** External libraries jar/aar */ EXTERNAL_LIBRARIES(0x10), /** Test code for the current environment variant, /** Local or remote dependencies that are provided-only */ PROVIDED_ONLY(0x40), EXTERNAL_LIBRARIES */ @deprecated PROJECT_LOCAL_DEPS(0x02), /** * Subproject jar/ AAR, Use EXTERNAL_LIBRARIES */ @deprecated SUB_PROJECTS_LOCAL_DEPS(0x08) instead; }}Copy the code

3.2, the Transform. Transform

This method is the handling of the intermediate result conversion, and the intermediate information (data) is passed through the TransformInvocation object.

A Transform that processes build artifacts. Transform

For each added transform, a new task is created. The action of adding a transform takes care of handling dependencies between the tasks.This is done based on what the transform processes. The output of the transform becomes consumable by other transforms and these tasks get automatically linked together.

For each transform that is added, a Gradle Task is created. The output of a Trasform, which will be consumed by the next Transform, is automatically linked together.

Transform is a “chain of transformations” :

There are multiple transforms involved in the compilation process: If you use multiple Gradle plugins in your project, each Plugin has a Hook Transform method, and each Plugin must be responsible for its own function. Therefore, there are multiple Transform chains.

  • TransformInvocation
/** * An invocation object used to pass of pertinent information for a * Transform#transform(TransformInvocation) call. */ public interface TransformInvocation {/** * transform Context */ @nonnull Context getContext(); /** * input source (XXXInput) : DirectoryInput/JarInput */ @nonnull Collection<TransformInput> getInputs(); */ @nonnull Collection<TransformInput> getReferencedInputs(); /** * Only secondary files that this * transform can handle incrementally will be part of this change set. Returns a list of files changed since the last compilation. */ @NonNull Collection<SecondaryInput> getSecondaryInputs(); /** * @transformOutputProvider getOutputProvider(); /** * support incremental */ Boolean isIncremental(); }Copy the code

So much for Transform for now, or we’ll get off topic.

Four, to do a good job must first sharpen its device: ASM Bytecode Viewer

  • IDE: Default Android Studio
  • Installing the IDE plug-in

Select the first one to install, and when it is installed, “RestartIDE” will be prompted. Note:

  1. The second one doesn’t need to be installed, because Kotlin comes with “Show Kotlin Bytecode”;
  2. I tried the second plugin, and the result was the same under Kotlin code;
  3. Do not install both plug-ins at the same time, otherwise AS (mine is 3.5) will not start (you have to manually delete one in the directory);
  • ASM Bytecode Viewer
    1. Open any Java file;
    2. Right-click IDE editor -> select “ASM Bytecode Viewer”;
    3. The plug-in then determines:
    • If the file already has a Class file, load the display directly;
    • If no, Recompile first, then load the display;

Five, pile insertion actual combat

We have the ASM Plugin tool, but we don’t understand Java bytecode! It doesn’t matter, for don’t understand, we can learn practice, by modifying Java source code, and then look at ASM compiled bytecode, before and after comparison, can find the difference, we just need to write down the difference, through our own custom plug-in tools, insert into the source file Class file corresponding place.

5.1. Save the status before the modification

How to preserve it? There are too many methods, either Copy & Paste, or screenshot, and so on…..

PNG -2b9c12-1612181084651-0

This is the modified Java source code (left) and the corresponding ASM bytecode (right).

5.2. Add an entry/exit timestamp

After modification & comparison, we found two differences from the original, the only difference between the two changes is the printed string, everything else is the same:

LDC "Chris" NEW java/lang/StringBuilder DUP INVOKESPECIAL java/lang/StringBuilder.<init> ()V LDC "onCreate.onEnter timestamp = " // onEnter / onExit INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;) Ljava/lang/StringBuilder; INVOKESTATIC java/lang/System.currentTimeMillis ()J INVOKEVIRTUAL java/lang/StringBuilder.append (J)Ljava/lang/StringBuilder; INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String; INVOKESTATIC android/util/Log.e (Ljava/lang/String; Ljava/lang/String;) I POPCopy the code

But know the instruction set, still can’t write, how to do? ASM library even code generated for us, directly use the line (but it is recommended that you learn the basic instruction set, do not rely too much on tools) :

Restore Java source code and start writing our Gradle Plugin.

5.3. Path of the compiled product

Before we actually start writing the plug-in, we need to know where our Class and Dex files will end up, so we can see if we’ve manipulated bytecode staking correctly.

  • For Java files, the Class path is:

. / app/build/intermediates/javac/environmental variations/classes directory

  • For Kotlin files, the Class path is:

/app/build/ TMP /kotlin-classes/ environment variants /

  • The Android Gradle Plugin then merges all the classes files into different dex files:

. / app/build/intermediates/transforms/environmental variations/directory

OK, next, we really need to implement our custom plug-in!

5.4. Custom plug-in: AopAsmTransform

We first tried to print the input source, the code and log are as follows (how to use the plugin will be explained later, don’t worry!). :

public class AopAsmTransform extends Transform { @Override public void transform(TransformInvocation TransformInvocation) throws TransformException, InterruptedException, IOException {// null method, Delete can also be super.transform(transformInvocation); System. The out. Println (" [AOP ASM 】 -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- begin "); for (TransformInput input : transformInvocation.getInputs()) { for (DirectoryInput di : input.getDirectoryInputs()) { System.out.println("DirectoryInput = " + di.getFile().getAbsolutePath()); } for (JarInput ji : input.getJarInputs()) { System.out.println("JarInput = " + ji.getFile().getAbsolutePath()); }} System. Out. Println (" [AOP ASM 】 -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- end -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- "); }}Copy the code

When App module is compiled, Build layout log:

As you can see, there are two input sources, JarInput and DirectoryInput, with their absolute paths.

If careful students, should find, AS IDE at the bottom of the prompt:

Session ‘app’: Installation did not succeed. The application could not be installed: INSTALL_FAILED_INVALID_APK

We switched to the Run plane and looked at the Transforms path

DexBuilder directory is empty, there is no dex file, so there is no APK file.


Remember what I said earlier? \color{red}{remember what I said earlier? }

Transform is a link in the chain of intermediates, and its output affects the input of the next Transform, and the Transform method is an empty function. If we don’t set TrannsformOutputProvider correctly, The next link “Task: app: transformClassesWithDexBuilderForDebug” cannot be classes into dex file, will eventually lead to unable to make the apk.

5.4.1 Ensure that the input source can be placed in the specified directory

Where will the fetch file be placed? We can’t write our own dead path!

Actually, file, transformInvocation object already told us, through our getOutputProvider. GetContentLocation, introduced to correct contentType, scope and format, Get the correct absolute path to the destination directory:

public class AopAsmTransform extends Transform { @Override public void transform(TransformInvocation TransformInvocation) throws TransformException, InterruptedException, IOException {// null method, Delete can also be super.transform(transformInvocation); System. The out. Println (" [AOP ASM 】 -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- begin -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - "); TransformOutputProvider provider = transformInvocation.getOutputProvider(); for (TransformInput input : transformInvocation.getInputs()) { for (DirectoryInput di : input.getDirectoryInputs()) { copyQualifiedContent(provider, di, null, Format.DIRECTORY); } for (JarInput ji : input.getJarInputs()) { copyQualifiedContent(provider, ji, getUniqueName(ji.getFile()), Format.JAR); }} System. Out. Println (" [AOP ASM 】 -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- end -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- "); } / * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Duplicate the name of the output file, because it may have the same name (N classes.jar), and will be overwritten ***********************************************************************************************/ private String getUniqueName(File jar) { String name = jar.getName(); String suffix = ""; if (name.lastIndexOf(".") > 0) { suffix = name.substring(name.lastIndexOf(".")); name = name.substring(0, name.lastIndexOf(".")); } String hexName = DigestUtils.md5Hex(jar.getAbsolutePath()); return String.format("%s_%s%s", name, hexName, suffix); } private void copyQualifiedContent(TransformOutputProvider provider, QualifiedContent file, String fileName, Format format) throws IOException { boolean useDefaultName = fileName == null; File dest = provider.getContentLocation(useDefaultName ? file.getName() : fileName, file.getContentTypes(), file.getScopes(), format); if (! dest.exists()) { dest.mkdirs(); dest.createNewFile(); } if (useDefaultName) { FileUtils.copyDirectory(file.getFile(), dest); } else { FileUtils.copyFile(file.getFile(), dest); }}}Copy the code

Compile again and the result is as follows:

It can compile, pass, and install the application successfully. Although successful, however, there are many more JAR files in the directory. Compare our image in “5.3” to the __content__.json file:

As you can see from the Build log in 5.4, these jar packages are all external dependencies. Some of these external dependencies are kotlin’s and some are in the Maven/JCenter repository.

5.4.2 Recursively traverse the Class file

Since this Demo does not deal with manipulating Class files in Jar packages, we will focus on Class files in DirectoryInput (Class operations in Jar packages are similar). Because Java package names are closely related to the directory level, we need to go down the directory level to iterate through the Class files that we really need to peg. Some of the Class files might be resources, compilations, configurations, etc., so we need to filter those out.

Java public class TransConstant {// Configure filter file information public static final String[] CLASS_FILE_IGNORE = {"R.class", "R$", "Manifest", "BuildConfig"}; }Copy the code
public class AopAsmTransform extends Transform { / * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * according to the input directory, Traverse the Class files needed to pile * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * / private void doDirectoryInputTransform(DirectoryInput input) { List<File> files = new ArrayList<>(); listFiles(files, input.getFile()); // TODO: the actual Class file to pile...... } private void listFiles(List<File> list, File file) { if (file == null) { return; } if (file.isDirectory()) { File[] files = file.listFiles(); if (files == null || files.length == 0) { return; } for (File f : files) { listFiles(list, f); } } else if (needTrack(file.getName())) { list.add(file); } } private boolean needTrack(String name) { boolean ret = false; if (name.endsWith(".class")) { int len = TransConstant.CLASS_FILE_IGNORE.length; int i = 0; while (i < len) { if (name.contains(TransConstant.CLASS_FILE_IGNORE[i])) { break; } i ++; } if (i == len) { ret = true; } } return ret; }}Copy the code

Six, everything is ready, just to pile

6.1. Understand the ASM framework

Before piling, we need to know a little bit about the ASM framework (I’ll write a separate article on Class data structures later), but here’s how it works:

  1. IO reads the Class file;
  2. Create ClassReader instance based on IO stream;
  3. Create a ClassWriter instance to modify the bytecode;
  4. Create ClassVisitor instance based on ClassWriter;
  5. Trigger a ClassReader object to parse the Class information;

As you can see from the above steps, ASM uses the visitor pattern to read and parse Class information. Here is the access/traversal process:

6.2. Inherit the ClassVisitor class

The ClassVisitor that ASM provides is an abstract class that needs to be inherited; For the above process, if we need to read or modify a certain piece of code, such as Annotation, Field, Method, etc., we need to overload the corresponding Method. This is for Method, so we need to override the visitMethod Method and use the Method visitor class. In ASM, this class is the AdviceAdapter abstract class, and we inherit it. And we need to reload the “onMethodEnter” and “onMethodExit” methods to implement our requirements.

6.2.1. Customize ClassVisitor inheritance classes

// ClassVisitorAdapter.java public class ClassVisitorAdapter extends ClassVisitor { private String clazzName; public ClassVisitorAdapter(int api, ClassVisitor classVisitor) { super(api, classVisitor); } @Override public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { super.visit(version, access, name, signature, superName, interfaces); this.clazzName = name; } @Override public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { MethodVisitor methodVisitor = cv.visitMethod(access, name, descriptor, signature, exceptions); return new MethodAdviceAdapter(api, methodVisitor, access, name, descriptor, this.clazzName); }}Copy the code

6.2.2. Customize MethodVisitor to inherit the class

// MethodAdviceAdapter.java public class MethodAdviceAdapter extends AdviceAdapter { private String qualifiedName; private String clazzName; private String methodName; private int access; private String desc; protected MethodAdviceAdapter(int api, MethodVisitor methodVisitor, int access, String name, String descriptor, String clazzName) { super(api, methodVisitor, access, name, descriptor); this.qualifiedName = clazzName.replaceAll("/", "."); this.clazzName = clazzName; this.methodName = name; this.access = access; this.desc = descriptor; } @Override protected void onMethodEnter() { enter(); } @Override protected void onMethodExit(int opcode) { exit(opcode); {} private void enter () System. The out. Println (" "MethodAdviceAdapter. Enter" = > "+ clazzName + + methodName +", ", " + access + ", " + desc); } private void exit(int opcode) {system.out.println (" [methodadviceadapter.exit] => "+ opcode); }}Copy the code

Compile and test our output:

> Task: app: transformClassesWithAopAsmPluginForDebug [AOP ASM 】 -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- begin -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - = > com/MethodAdviceAdapter. Enter 】 【 Chris/aop/MainActivity, < init >, 1, () V = > 177 MethodAdviceAdapter. Exit 】 【 = > com/MethodAdviceAdapter. Enter 】 【 Chris/aop/MainActivity, onCreate, 4, (Landroid/os/Bundle;) V [methodAdviceadapter. exit] => 177 /Users/chris/Desktop/Source/AOPDemo/app/build/intermediates/javac/debug/classes/com/chris/aop/MainActivity.class/MainAct Ivity. Class = > com/MethodAdviceAdapter. Enter 】 【 Chris/aop/SecondActivity, onCreate, 4, (Landroid/OS/Bundle;) V = > MethodAdviceAdapter. Exit 】 【 177 = > com/MethodAdviceAdapter. Enter 】 【 Chris/aop/SecondActivity, < init >, 1, (a) V = > MethodAdviceAdapter. Exit 】 【 177 = > MethodAdviceAdapter. Enter 】 【 com/Chris/aop/SecondActivity, _$_findCachedViewById, 1, (I)Landroid/view/View; MethodAdviceAdapter. Exit = > 176 】 【 MethodAdviceAdapter. Enter] = > com/Chris/aop/SecondActivity, _$_clearFindViewByIdCache, 1, ()V [methodAdviceadapter.exit] => 177 /Users/chris/Desktop/Source/AOPDemo/app/build/tmp/kotlin-classes/debug/com/chris/aop/SecondActivity.class/SecondActivity .class [AOP ASM 】 -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- end -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -Copy the code
  • For Java source files: we have only one method, and when compiled, we have two methods, one of which is the default constructor;
  • For the Kotlin source file: we only have one method. When compiled, we have four methods, one is the default constructor, and two are Kotlin synthesized methods, which quickly find the view control by its ID and clear memory on exit.

6.3. Insert the print log

public class MethodAdviceAdapter extends AdviceAdapter { private String qualifiedName; private String clazzName; private String methodName; private int access; private String desc; protected MethodAdviceAdapter(int api, MethodVisitor methodVisitor, int access, String name, String descriptor, String clazzName) { super(api, methodVisitor, access, name, descriptor); this.qualifiedName = clazzName.replaceAll("/", "."); this.clazzName = clazzName; this.methodName = name; this.access = access; this.desc = descriptor; } @Override protected void onMethodEnter() { enter(); } @Override protected void onMethodExit(int opcode) { exit(opcode); } private void enter() {// Skip if (methodname.equals ("<init>")) {return; } mv.visitLdcInsn("Chris"); mv.visitTypeInsn(NEW, "java/lang/StringBuilder"); mv.visitInsn(DUP); mv.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false); mv.visitLdcInsn(qualifiedName + ".onCreate.onEnter timestamp = "); mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;) Ljava/lang/StringBuilder;" , false); mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false); mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(J)Ljava/lang/StringBuilder;" , false); mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;" , false); mv.visitMethodInsn(INVOKESTATIC, "android/util/Log", "e", "(Ljava/lang/String; Ljava/lang/String;) I", false); mv.visitInsn(POP); } private void exit(int opcode) { if (methodName.equals("<init>")) { return; } mv.visitLdcInsn("Chris"); mv.visitTypeInsn(NEW, "java/lang/StringBuilder"); mv.visitInsn(DUP); mv.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false); mv.visitLdcInsn(qualifiedName+ ".onCreate.onExit timestamp = "); mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;) Ljava/lang/StringBuilder;" , false); mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false); mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(J)Ljava/lang/StringBuilder;" , false); mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;" , false); mv.visitMethodInsn(INVOKESTATIC, "android/util/Log", "e", "(Ljava/lang/String; Ljava/lang/String;) I", false); mv.visitInsn(POP); }}Copy the code

Recompile to see if the compiled Class file inserts the bytecode we wrote

Let’s see if Kotlin does the same

As we can imagine, Kotlin also inserts perfectly, and if you look carefully at the “_$_findCachedViewById” method, our logging code is inserted correctly before return.

Run the APP and check Logcat:

7. Use AopAsmPlugin for APP

7.1. Use Gradle + Maven plugin to package and publish

See Custom Gradle Plugin Remote Publishing to learn how to make local Maven packages and publish them to the JCenter central repository.

7.2. Configure top-level build.gradle dependencies

// Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { Ext.kotlin_version = '1.3.72' Repositories {Google () jCenter () Maven {URL uri('./repo')}} Dependencies {classpath 'com. Android. Tools. Build: gradle: 3.5.0' classpath "org. Jetbrains. Kotlin: kotlin - gradle - plugin: $kotlin_version" classpath 'com. Jfrog. Bintray. Gradle: gradle bintray - plugin: 1.8.0 comes with' the classpath 'com. Chris. Aop: aop - asm - gradle - plugin: 1.0.0' / / adds dependent}}  apply from: rootProject.file('gradle/project-mvn-config.gradle') allprojects { repositories { google() jcenter() maven { url uri('./repo') } } tasks.withType(Javadoc) { options.addStringOption('Xdoclint:none', '-quiet') options.addStringOption('encoding', 'UTF-8') } } task clean(type: Delete) { delete rootProject.buildDir }Copy the code

7.3. Configure build.gradle for App

apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'Kotlin-android-extensions' apply plugin: 'com.chris.aop.asm-plugin' // Apply plugin android {..... } dependencies {.... }Copy the code

Eight, summary

This article is just a prelude to let you know that Java bytecode modification, in fact, under the existing tools, everything becomes very simple and easy; Of course, when we actually modify Bytecode, we will also encounter a variety of strange encounter, at this time, we need to modify the source code, and use “ASM Bytecode Viewer” to view, do repeated comparison, find the correct solution.

Github: Portal