Practice Sample to Run Asm Plugin Enhancement Practice

Practice Sample to Run ASM Plugin Enhancement Practice #

Hello, I’m Sun Pengfei.

In the previous issue of this column, Shao Wen talked about three methods of compiler instrumentation: AspectJ, ASM, and ReDex, as well as their application scenarios. After learning about them, are you eager to apply them to actual work? However, I also understand that many students have limited exposure to instrumentation and rarely use it in their work. Because this technology is so important and can achieve many functions, I still hope that you can master it as much as possible through a combination of theory and practice. Therefore, today I have arranged an “enhancement training” for you. I hope you can take advantage of the momentum, maintain the continuity of learning, and apply the theoretical knowledge from the previous issue to today’s instrumentation exercise.

To minimize the difficulty of getting started, I will provide detailed operating steps. I believe that as long as you follow them and combine them with the learning from the previous issue of this column, you will definitely be able to grasp the essence of instrumentation.

ASM Code Enhancement Practice #

In the previous issue, Eateeer mentioned a tool in the comments, which I am also using to help me understand ASM. Installing the “ASM Bytecode Outline” is also very simple, just search for it in the Plugins section of Android Studio.

The ASM Bytecode Outline plugin can quickly display the bytecode representation of the current editing class and show the ASM code that generates this class. You can right-click in the Android Studio source code compilation box and select “Show Bytecode Outline” to view the decompiled bytecode on the right.

Taking the SampleApplication class as an example in today’s enhancement exercise, the specific bytecode is shown in the image below.

In addition to the bytecode mode, the ASM Bytecode Outline also has an “ASMified” mode, which shows how the SampleApplication class should be constructed using ASM code.

Now let’s deepen our understanding of using ASM through two examples.

1. Measuring Method Execution Time with ASM Code Instrumentation

Our first exercise today is to measure the execution time of each method using ASM. How do we do this? Please don’t worry, let’s take the SampleApplication class as an example again. As shown in the image below, you can first manually write down the code you want to implement before and after instrumentation.

How do we convert this “diff” code into ASM code? The ASM Bytecode Outline has a very powerful feature: it can display the code differences between two consecutive modifications, so we can clearly see how the modified code is presented in the bytecode.

The “onCreate” method in the “ASMified” mode shows the code difference before and after modification, which is the ASM code we need to add. Before actually starting to implement instrumentation, we need to understand the usage of several classes in the Core API of ASM source code, such as ClassReader, ClassWriter, ClassVisitor, etc.

To use ASM, we need to first read the raw bytecode of the Class file with ClassReader, and then modify it using the ClassWriter class based on different Visitor classes. Note that COMPUTE_MAXS and EXPAND_FRAMES are parameters that need special attention.

ClassReader classReader = new ClassReader(is);
// COMPUTE_MAXS indicates that ASM automatically computes the maximum local variables and the maximum stack size
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
ClassVisitor classVisitor = new TraceClassAdapter(Opcodes.ASM5, classWriter);
// EXPAND_FRAMES indicates that stack map frames should be expanded while reading the class. This is necessary when using AdviceAdapter.
classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES);

If we want to measure the execution time of each method, we can use AdviceAdapter to achieve this. It provides the onMethodEnter() and onMethodExit() methods, which are very suitable for method instrumentation before and after execution. For a specific implementation, you can refer to the implementation of TraceClassAdapter in today’s enhancement exercise:

private int timeLocalIndex = 0;
@Override
protected void onMethodEnter() {
    mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
    timeLocalIndex = newLocal(Type.LONG_TYPE); // This is a feature provided by LocalVariablesSorter, which can reuse previous local variables as much as possible
    mv.visitVarInsn(LSTORE, timeLocalIndex);
}

@Override
protected void onMethodExit(int opcode) {
    mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
    mv.visitVarInsn(LLOAD, timeLocalIndex);
    mv.visitInsn(LSUB); // The value is now at the top of the stack
    mv.visitVarInsn(LSTORE, timeLocalIndex); // Because this value will be used later, it is saved to the local variable table first
    int stringBuilderIndex = newLocal(Type.getType("java/lang/StringBuilder"));
    mv.visitTypeInsn(Opcodes.NEW, "java/lang/StringBuilder");
    mv.visitInsn(Opcodes.DUP);
    mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
    mv.visitVarInsn(Opcodes.ASTORE, stringBuilderIndex); // Need to store the top of the stack stringbuilder, otherwise it will not be found later
    mv.visitVarInsn(Opcodes.ALOAD, stringBuilderIndex);
    mv.visitLdcInsn(className + "." + methodName + " time:");
    mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
    mv.visitInsn(Opcodes.POP); // Pop the return value of the append method from the stack
    mv.visitVarInsn(Opcodes.ALOAD, stringBuilderIndex);
    mv.visitVarInsn(Opcodes.LLOAD, timeLocalIndex);
    mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(J)Ljava/lang/StringBuilder;", false);
    mv.visitInsn(Opcodes.POP); // Pop the return value of the append method from the stack
    mv.visitLdcInsn("Geek");
    mv.visitVarInsn(Opcodes.ALOAD, stringBuilderIndex);
    mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
    mv.visitMethodInsn(Opcodes.INVOKESTATIC, "android/util/Log", "d", "(Ljava/lang/String;Ljava/lang/String;)I", false);//Note: The Log.d method has a return value, so it needs to be popped out
    mv.visitInsn(Opcodes.POP);//After inserting bytecode, it is necessary to ensure that the stack is clean and does not affect the original logic, otherwise it will throw exceptions and affect the bytecode processing of other frameworks
}

The specific implementation is similar to what we see in ASM Bytecode Outline, but here we need to pay attention to the use of local variables. In the example, an important superclass of AdviceAdapter called LocalVariablesSorter is used, which provides a very useful method called newLocal. It can allocate an index for a local variable without the need for the user to consider the allocation and overwriting of local variables.

Another thing to note is that we need to check whether the inserted code leaves unused data at the top of the stack, and if so, it needs to be consumed or popped out. Otherwise, it will cause exceptions in the subsequent code.

This way, we can quickly complete this large segment of bytecode.

2. Replace all new Thread in the project

Another exercise today is to replace all new Thread in the project with the CustomThread class in our project. In practice, you can use this method to add statistical code in the CustomThread to calculate the running time of each thread.

However, this is a relatively tricky situation, and you can think in advance about what situations you might encounter. Similarly, we look at the differences in bytecode using ASM Bytecode Outline by modifying the Thread object in the startThread method of MainActivity here:

InvokeVirtual is called based on the newly created object, so we only need to replace the new object creation process. Here, we need to handle two instructions: a new instruction and an InvokeSpecial instruction. In most cases, these two instructions appear in pairs, but in some special cases, a pre-existing object is directly passed from another location and the constructor is forcibly called.

We need to handle this special case, so in the example, we need to check whether new and InvokeSpecial appear in pairs.

 private boolean findNew = false;//Flag indicating whether the new instruction was encountered
        @Override
        public void visitTypeInsn(int opcode, String s) {
            if (opcode == Opcodes.NEW && "java/lang/Thread".equals(s)) {
                findNew = true;//Encounter a new instruction
                mv.visitTypeInsn(Opcodes.NEW, "com/sample/asm/CustomThread");//Replace the class name of the new instruction
                return;
            }
            super.visitTypeInsn(opcode, s);
        }

        @Override
        public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
            //Exclude CustomThread itself
            if ("java/lang/Thread".equals(owner) && !className.equals("com/sample/asm/CustomThread") && opcode == Opcodes.INVOKESPECIAL && findNew) {
                findNew= false;
                mv.visitMethodInsn(opcode, "com/sample/asm/CustomThread", name, desc, itf);//Replace the class name in INVOKESPECIAL, keep other parameters the same as before
                return;
            }
            super.visitMethodInsn(opcode, owner, name, desc, itf);
        }

The form of the new instruction is relatively special. For example, we may encounter the following case:

new A(new B(2));

The bytecode is as follows, and you will find that two new instructions are chained together.

NEW A
    DUP
    NEW B
    DUP
    ICONST_2
    INVOKESPECIAL B.<init> (I)V
    INVOKESPECIAL A.<init> (LB;)V

Although the ASM Bytecode Outline tool can help us complete many ASM requirements in many scenarios, when dealing with bytecode, we still need to consider many possible situations. You need to pay attention to the characteristics of each instruction. Therefore, in slightly more complex situations, we still need to have some understanding of ASM bytecode and some utility classes in the ASM source code, and we need a lot of practice. After all, practice is the most important thing.

Finally, here’s another question for you to think about: how can we add a try-catch block to a method? You can try implementing it in today’s reinforced exercise code based on the plugin example I provided.

Reinforced exercise code: https://github.com/AndroidAdvanceWithGeektime/Chapter-ASM

Special Surprise #

By reaching this point, I believe you must agree that it is not easy to become an expert in Android development. It takes persistence in learning, practicing, organizing, and sharing. However, some students have managed to persevere.

Do you remember our promise in the guide? We said that we would select students who have actively participated in learning and sharing their experiences and reward them with a ticket to the 2019 GMTC conference. Today, we are fulfilling that promise by giving away one ticket, worth 4800 yuan, to @唯鹿. He not only submitted his homework but also shared the process and experiences of implementing each sample exercise on his blog. He has been consistent in his efforts. I have included the link to his blog posts in the manuscript. If you have any questions about the previous sample exercises, you can refer to the implementation process by @唯鹿.

There are still some GMTC tickets left. Give yourself a chance to advance. There is still time.

Mini programs, Flutter, Mobile AI, Engineering, Performance Optimization… Where is the next stop for the Big Front-End? The 2019 GMTC Global Front-End Technology Conference will be held in Beijing in June. Top front-end experts from Google, BAT, Meituan, JD, Didi, and other leading companies will meet with you face-to-face to discuss the latest trends and best practices in front-end technology. Currently, there is a hot ongoing promotion with a minimum discount of 30% on tickets. We are also still recruiting speakers and topics. Click on the image below to learn more about the conference!