In the AOP tool ASM foundation introductory blog, introduced the basic knowledge of ASM and the overall API structure, so that we have a preliminary understanding of ASM. This post will introduce three cases to deepen your understanding of ASM interfaces.

  1. Delete Logs: Delete all Log output from the project

  2. Add Log: Add Log output to the project

  3. Add a try… Catch exception catching: Add exception steps to all methods of your project

The examples in this article are based on Android projects, so some basic concepts are introduced.

1. Basic concepts

1.1 the Transform API

Android Gradle since version 1.5.0 provides a Transfrom API that allows third-party plugins to manipulate.class files during compilation prior to packaging the dex. In layman’s terms, Android provides an entry point to modify bytecode at compile time.

A Transform is a new Task that executes chained, with the output of the previous Transform as input to the current Transform and its output as input to the next Transform. The input to Transform is represented by TransformInput and contains JarInput and DirectoryInput, and the output is represented by TransformOutputProvider.

1.2 Custom Plug-ins

There are many plugins available in Android, such as the apply plugin: ‘com.android.application’ : represents an App plug-in; Apply Plugin: ‘com.android.library’ : represents a library plug-in. We can also implement a custom plug-in by inheriting Plugin. In the actual business, Plugin + Transform + ASM is usually used to achieve a powerful custom plug-in. There are many blog posts about the implementation of custom plug-ins, but I won’t go into them here.

ASM case source address: github.com/dengshiwei/…

2. Delete logs

In Android development, we often use the Log class for Log output, but some security checks will consider Log output as a risky behavior, so the detected App is required to delete all Log printing. Then we can use ASM technology to implement Log deletion at compile time.

The target

Delete all logs from the Log class in the project.

Train of thought

Since we want to remove all Log output from the project, we need to detect where the Log output is called. As you can see from the AOP Tool ASM Basics post, the MethodVisitor class is used for method access, where the visitMethodInsn interface is a callback to each instruction implemented by the method. We just need to check the Log output method of class D, e, or I in this callback, and then return directly.

Key code implementation
/** * Check the use of Log within the method call and delete */
override fun visitMethodInsn(opcodeAndSource: Int, owner: String? , name:String? , descriptor:String? , isInterface:Boolean) {
    if(Opcodes.ACC_STATIC.and(opcodeAndSource) ! =0 && owner == "android/util/Log" && (name == "d" || name == "i" || name == "e" || name == "w" || name == "v")
            && descriptor == "(Ljava/lang/String; Ljava/lang/String;) I"
    ) {
        /** * the direct return simply removes the call to the Log instruction, but the corresponding current operand stack is not processed */
        return
    }

    super.visitMethodInsn(opcodeAndSource, owner, name, descriptor, isInterface)
}
Copy the code

VisitMethodInsn = visitMethodInsn; visitMethodInsn = visitMethodInsn; visitMethodInsn = visitMethodInsn; visitMethodInsn = visitMethodInsn; Return.

3. Add logs

An interface is called in more than one place, it is necessary to print out the method call order when troubleshooting problems, development did not print this information, manual addition may have omission, so the most convenient to add custom plug-in.

The target

Add a log to print the name of the current method when the method is entered.

Train of thought

Also, to print the call names of all methods, we need to call the Log class when the method comes in to output the Log. As you can see from the AOP Tools ASM Basics post, the AdviceAdapter class provided with ASM can detect access timing for methods.

  • onMethodEnter: when method access begins
  • onMethodExit: at the end of method access

All we need to do is call Log in the onMethodEnter method.

Key code implementation
/** * adds the names of all called methods to log output */
internal class PrintLogInterceptor(varclassName: String? , methodVisitor: MethodVisitor, access:Int, name: String? , descriptor: String?) : AdviceAdapter(PluginConstant.ASM_VERSION, methodVisitor, access, name, descriptor) {override fun onMethodEnter(a) {
        super.onMethodEnter()
        // Add the current class name to the action stack as a TAGmv.visitLdcInsn(StringUtils.getFileName(className!!) )// Add the current method name to the stack for output
        mv.visitLdcInsn(name)
        mv.visitMethodInsn(Opcodes.INVOKESTATIC, "android/util/Log"."d"."(Ljava/lang/String; Ljava/lang/String;) I".false)}}Copy the code

Call visitLdcInsn to push the class and method names to the operand stack, and then call log. d(String tag, String MSG) from visitMethodInsn to output the Log.

The sample

The original code snippet:

public class MainActivity extends AppCompatActivity {
    public void testLog() {
        Log.d("TAG"."dsw");
        Log.i("TAG"."dsw");
        Log.v("TAG"."dsw");
        Log.w("TAG"."dsw");
        Log.e("TAG"."dsw"); }}Copy the code

Look at the code snippet in the processed.class file:

public class MainActivity extends AppCompatActivity {
    public String testString(String var1, String var2) {
        Log.d("MainActivity"."testString");
        int var3 = 5 / 0;
        return "HelloWorld"; }}Copy the code

You can see that the log output is already inserted when the method is first entered.

4. Add a try… Catch Exception

The target

Add a try… to all methods in your project. Catch block, and invoke Exception. PrintStackTrace output log.

Train of thought

Add all methods to try… Catch block, which means that the whole implementation of the method is in the try block, and then we just insert the implementation of the catch block. VisitTryCatchBlock (Final Label Start, Final Label End, Final Label Handler, Final String type) is used to generate a try… Catch block, where start indicates the actual position, end indicates the end position, handler indicates the start position of Exception, and tpye indicates the type of Exception parameter. So we call visitLabel to define the start location before the method access begins, and visitLable to define the end location before the method ends. One detail to note is that the catch block needs to add the return value of the exception based on the return value type of the method.

Key code implementation
class TryCatchInterceptor(methodVisitor: MethodVisitor, access: Int, name: String? .var descriptor: String?) :
        AdviceAdapter(PluginConstant.ASM_VERSION, methodVisitor, access, name, descriptor) {
    private val labelStart = Label()
    private val labelEnd = Label()
    private val labelTarget = Label()
    override fun onMethodEnter(a) {
        // Define the starting position
        mv.visitLabel(labelStart)
        / / try... Catch block
        mv.visitTryCatchBlock(labelStart, labelEnd, labelTarget, "java/lang/Exception")}override fun visitMaxs(maxStack: Int, maxLocals: Int) {
        // Define where normal code ends
        mv.visitLabel(labelEnd)
        // Define where the catch block starts
        mv.visitLabel(labelTarget)
        val local1 = newLocal(Type.getType("Ljava/lang/Exception"))
        mv.visitVarInsn(Opcodes.ASTORE, local1)
        mv.visitVarInsn(Opcodes.ALOAD, local1)
        / / output ex. PrintStackTrace
        mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/Exception"."printStackTrace"."()V".false)
        // Determine the return type of the method
        mv.visitInsn(getReturnCode(descriptor = descriptor))
        super.visitMaxs(maxStack, maxLocals)
    }

    /** * gets the corresponding return value */
    private fun getReturnCode(descriptor: String?).: Int {
        return when(descriptor!! .subSequence(descriptor.indexOf(")") + 1, descriptor.length)) {
            "V" -> Opcodes.RETURN
            "I"."Z"."B"."C"."S" -> {
                mv.visitInsn(Opcodes.ICONST_0)
                Opcodes.IRETURN
            }
            "D" -> {
                mv.visitInsn(Opcodes.DCONST_0)
                Opcodes.DRETURN
            }
            "J" -> {
                mv.visitInsn(Opcodes.LCONST_0)
                Opcodes.LRETURN
            }
            "F" -> {
                mv.visitInsn(Opcodes.FCONST_0)
                Opcodes.FRETURN
            }
            else -> {
                mv.visitInsn(Opcodes.ACONST_NULL)
                Opcodes.ARETURN
            }
        }
    }
}
Copy the code
case

Original code snippet:

public class MainActivity extends AppCompatActivity {
    public void testLog() {
        Log.d("TAG"."dsw");
        Log.i("TAG"."dsw");
        Log.v("TAG"."dsw");
        Log.w("TAG"."dsw");
        Log.e("TAG"."dsw"); }}Copy the code

Look at the code snippet in the processed.class file:

public class MainActivity extends AppCompatActivity {
    public String testString(String var1, String var2) {
        try {
            int var3 = 5 / 0;
            return "HelloWorld";
        } catch (Exception var5) {
            var5.printStackTrace();
            return null; }}}Copy the code

Add a try… Catch block is a little bit more complicated, so some of you should be confused? I don’t know how to write bytecode. ASM Bytecode Outline is a tool that can be installed directly in the IDE, and then right-click on the corresponding file to Show Bytecode Outline.

  • Bytecode: indicates the corresponding.classBytecode file
  • ASMified: Indicates use.ASMThe corresponding code when the framework generates bytecode
  • Groovified: Corresponds to.classBytecode instruction

5. To summarize

Bytecode enhancement technology can be used to dynamically modify bytecode at compile time, typically for scenarios such as burying and staking, and can also be used to locate and fix on-line problems and reduce redundant code during development to improve development efficiency.