Author: Lance Teacher, enjoy learning class

Reprint please state the source!

First, what is pile insertion

In the hot Fix Solution published by Qzone, the Javaassist library is used to insert a code into the constructor of the class to solve the CLASS_ISPREVERIFIED problem. Including the realization of Instant Run and the thermal repair meituan Robus based on Instant Run, all use the piling technology.

Staking is inserting or replacing a piece of code. Bytecode piling, as its name implies, is the operation of modifying the Class file and modifying or enhancing the original code logic after the source code is compiled into bytecode (Class) and before dex is generated under Android.

We need to check the method execution time. If each method needs to be added manually, we need to delete the corresponding code one by one when it is not needed. One or two methods are ok, but if there are 10 or 20 methods, it will be much trouble! So annotations can be used to mark the methods that need to be staked, combined with compiled bytecode manipulation to help us automatically insert and turn off the staked when not needed. This AOP thinking lets us focus only on the poked code itself.

Bytecode manipulation framework

As mentioned above, Qzone uses Javaassist for bytecode piling. Besides Javaassist, there is a more widely used ASM framework, which is also a bytecode manipulation framework. Instant Run and AspectJ use ASM to achieve their respective functions.

JSON data, which we are familiar with, is text-based, and we can easily generate and modify JSON data by knowing its rules. Similarly, Class bytecodes have their own rules (formats). Manipulating JSON makes it very easy to generate and modify JSON data with GSON. Bytecode Class can also be modified with Javassist/ASM.

The bytecode manipulation framework is used to generate or modify Class files, so in Android the bytecode framework itself does not need to be packaged into APK, only the generated/modified classes need to be packaged into APK. It works after the Class is generated in the Android packaging process above and before the dex is packaged.

3. Use of ASM

Because ASM has better performance and more flexible lines than Javassist, this article will focus on using ASM. We can test bytecode modification in A Java application before we actually use Android.

3.1. Introduce ASM into AS

ASM can be imported directly from the JCenter () repository, so you can search through :bintray.com/

Click the workpiece marked in the figure to enter, you can see that the latest official version is: 7.1.

Therefore, we can add AS:

Also, note that we introduced the testImplementation, which means that we can only use this framework in Our Java unit tests and has no impact on our dependencies in Android.

Android projects that use Gradle in AS automatically create Java unit tests and Android unit tests. The test code is in Test and androidTest respectively.

3.2. Prepare piles to be inserted Class

Create a Java class under test/ Java:

public class InjectTest {
       public static void main(String[] args) {
    }
}
Copy the code

Since we are working with bytecode staking, we can go to test/ Java and compile this class using Javac to generate the corresponding class file.

javac InjectTest.java
Copy the code

3.3. Pile insertion

Since the main method does not have any output code, we type javaInjectTest that executing this Class will not produce any output. Next, we use ASM to insert into the main method the log output from the initial diagram that records the execution time of the function.

Write test methods in unit tests

/** * fis = new FileInputStream ("xxxxx/test/java/InjectTest.class"); / * * * * 2, perform analysis and pile / / / the class reading and analysis of the bytecode engine ClassReader cr = new ClassReader (fis); ClassWriter CW = new ClassWriter(ClassWriter.COMPUTE_FRAMES); Cr. accept(new ClassAdapterVisitor(CW), classreader.expand_frames); [] newClassBytes = cw.tobytearray (); File file = new File("xxx/test/java2/");
file.mkdirs();
FileOutputStream fos = new  FileOutputStream
("xxx/test/java2/InjectTest.class");
fos.write(newClassBytes);
fos.close();
Copy the code

We won’t discuss the design of the ASM framework itself here. The code above takes the class generated in the previous step, and when ASM completes the staking, prints the result to the test/java2 directory. The key point lies in the second step, how to insert the pile.

The class data is handed to the ClassReader and parsed, similar to XML parsing, in an event-driven form to the ClassAdapterVisitor, the first parameter to accept.

public  class  ClassAdapterVisitor  extends  ClassVisitor {
	public  ClassAdapterVisitor(ClassVisitor cv) {
		super(Opcodes.ASM7, cv);
	}
	@Override
	public  MethodVisitor visitMethod(int access, String name, String desc, String signature,
	String[] exceptions) {
		System.out.println(Methods: "" + name + "Signature." + desc);
		MethodVisitor mv = super.visitMethod(access, name, desc, signature,
		exceptions);
		returnnew MethodAdapterVisitor(api,mv, access, name, desc); }}Copy the code

The analysis results are obtained through the ClassAdapterVisitor, where a class has methods, annotations, properties, and so on, So ClassReader will call the visitXX methods in the ClassAdapterVisitor that correspond to visitMethod, visitAnnotation, visitField.

Our goal is to do a function peg, so override the visitMethod method method, where we return a MethodVisitor method parser object. A method’s parameters, annotations, and method body need to be analyzed and processed in the MethodVisitor.

package com.enjoy.asminject.example; import com.enjoy.asminject.ASMTest; import org.objectweb.asm.AnnotationVisitor; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Type; import org.objectweb.asm.commons.AdviceAdapter; import org.objectweb.asm.commons.Method; /** * AdviceAdapter: Subclass * extends methodVisitor, */ public class adapterVisitor extends AdviceAdapter {private Boolean inject; protected MethodAdapterVisitor(int api, MethodVisitor methodVisitor, int access, String name, String descriptor) { super(api, methodVisitor, access, name, descriptor); } /** * the annotation above the analysis method * is here?? * <p> * Determine if the current method uses injectTime. If it does, we need to pin the method. * * @param desc * @param visible * @return
*/
	@Override
	public  AnnotationVisitor visitAnnotation(String desc, Boolean visible) {
		if (Type.getDescriptor(ASMTest.class).equals(desc)) {
			System.out.println(desc);
			inject = true;
		}
		return  super.visitAnnotation(desc, visible);
	}
	private  int start;
	@Override
	protected  void onMethodEnter() {
		super.onMethodEnter();
		if(inject) {// What happens after execution? Log to a local variable invokeStatic(type.getType ("Ljava/lang/System;"),
			new  Method("currentTimeMillis"."()J")); start = newLocal(Type.LONG_TYPE); // create a local variable of type LONG // record the result of method execution to the local variable storeLocal(start); } } @Override protected void onMethodExit(int opcode) { super.onMethodExit(opcode);if (inject){
			invokeStatic(Type.getType("Ljava/lang/System;"),
			new  Method("currentTimeMillis"."()J"));
			int end = newLocal(Type.LONG_TYPE);
			storeLocal(end);
			getStatic(Type.getType("Ljava/lang/System;"),"out",Type.getType("Ljava/io" +
			"/PrintStream;")); // Allocate memory and push dUP to the top of the stack so that the following INVOKESPECIAL knows whose constructor to execute to create a StringBuilder newInstance(type.getType ("Ljava/lang/StringBuilder;"));
			dup();
			invokeConstructor(Type.getType("Ljava/lang/StringBuilder;"),new  Method("<init>"."()V"));
			visitLdcInsn("execute:");
			invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"),new  Method("append"."(Ljava/lang/String;) Ljava/lang/StringBuilder;")); / / subtraction loadLocal (end); loadLocal(start); math(SUB,Type.LONG_TYPE); invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"),new  Method("append"."(J)Ljava/lang/StringBuilder;"));
			invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"),new  Method("toString"."()Ljava/lang/String;"));
			invokeVirtual(Type.getType("Ljava/io/PrintStream;"),new  Method("println"."(Ljava/lang/String;) V")); }}}Copy the code

MethodAdapterVisitor descends from AdviceAdapter, which is a subclass of MethodVisitor that wraps instruction insertion methods, making it more intuitive and simple.

In the code above, onMethodEnter calls back when it enters a method, so inserting instructions into the method is adding some code at the beginning of the whole method. We need to insert longs= System.currentTimemillis () in this method; . In onMethodExit the output code is inserted at the end of the method.

@Override
protected  void onMethodEnter() {
	super.onMethodEnter();
	if(inject) {// What happens after execution? Log to a local variable invokeStatic(type.getType ("Ljava/lang/System;"),
		new  Method("currentTimeMillis"."()J")); start = newLocal(Type.LONG_TYPE); // create a local variable of type LONG // record the result of method execution to the local variable storeLocal(start); }}Copy the code

How do I write this code? Longs = system.currentTimemillis (); The relative instruction of this code. We can start by writing a code

void test(){// Insert code long s = system.currentTimemillis (); /** * method implementation code.... Long e = system.currentTimemillis (); System.out.println("execute:"+(e-s) +" ms.");
}
Copy the code

Then compile to Class using Javac and view bytecode instructions using javap -C. Plug-ins can also be used to view, so we do not need to manually execute various commands.

After the installation is complete, you can right click on the source code of the class that needs to be staked:

Click the ASM Bytecode Viewer and it will pop up

So line 20: Longs = System.currentTimemillis (); It contains two instructions: INVOKESTATIC and LSTORE.

Back to the onMethodEnter method

@Override
protected  void onMethodEnter() {
	super.onMethodEnter();
	if(inject) {//invokeStatic directive invokeStatic(type.getType ()"Ljava/lang/System;"),
		new  Method("currentTimeMillis"."()J")); // Create a local variable of Type LONG start = newLocal(type.long_type); // the store directive stores the result of method execution from the operand stack to the local variable storeLocal(start); }}Copy the code

OnMethodExit also writes code according to instructions. Finally, after the insertion, we can obtain the modified class data.

Iv. Implementation in Android

In Android, the first problem we need to consider is how to get all the Class files to determine whether we need to pile. Transform does just that.

Follow me and share more technical dry goods