Recently, the startup time of App needs to be optimized. The existing code has the following problems:
- Threads were not reused (using new Thread\HandlerThread) and too many threads were created
- Use HandlerThread, which is not destroyed after use (Looper waits) and consumes memory
- Start the thread early, but not use it
- Some business parties initialize the business code prematurely, albeit asynchronously, affecting startup time
Due to the above problems, it is necessary to scan the execution of the child thread and the main thread between the cold start of the App and the display of the home page.
The data to be monitored are as follows:
- The number of threads created, including the number and usage
- The execution time of functions such as runnable. Run and Asynctask. doInBackground
Android Studio profiler-> CPU -> Threads shows the number of threads created. However, whenever I open profiler on my phone, it will not work at all.
Based on the above requirements, threaded code is staked using ASM.
- Execution time: Calculating the execution times of run, doInBackground, and so on is easy. You simply add code before and after these methods, and then you can calculate the time.
- Counting the number of threads: Thread is the system code and cannot be counted
Thread.run
Method for piling, also can not beThread.start
Plug in the code, because in the case of thread pools, it’s all system code. Staking can only be done in business code. So consider inserting code in the following code:
- Runnable.run
- AsyncTask.doInBackground
- Callable.call
- Handler. HandleMessage, Handler. Callback. HandleMessage
- Thread.run
- TimerTask.run
All of these methods run on threads, where thread.currentthread () is used to retrieve the currentThread data. This covers most cases, because most threads are created when there is a task, and thread usage and thread creation are at the same time. However, there is a special case of HandlerThread, which can be created first and then wait for the Looper to execute until the task is available. Therefore, if the Looper waits for the task, it will not be counted. Therefore, this case needs to be handled in a special way. We need to scan line by line for new HandlerThread code, and if so, count it into thread creation.
Here, basically finished talking about our thread peg idea. So how do you stake it? Using the ASM library, Gradle’s Transform is used to pile class files before they are packaged into dex. We want to use Gradle plug-ins to plug code, how to do plug-in development? There are two ways:
- This project creates a buildSrc module (the module name is specifically used for plug-in development) for development.
- Independent engineering development
Check out this article for details.
This chapter takes the first option and creates buildSrc in the Demo project. Then create a groovy plug-in file:
class ThreadInjectPlugin implements Plugin<Project> { @Override void apply(Project project) { def android if (project.plugins.hasPlugin(AppPlugin)) { android = project.extensions.getByType(AppExtension) } else { android = Project. Extensions. GetByType (LibraryExtension)} / / processing methods such as runnable android. RegisterTransform (new ThreadRunTransform ()) / / processing new HandlerThread android. RegisterTransform (new HandlerThreadTransform ())}}Copy the code
The first Transform scans the runnable and inserts code before and after the function (onMethodEnter). The second Transform deals specifically with the new HandlerThread.
Look at the ThreadRunTransform:
class ThreadRunTransform extends Transform { @Override String getName() { return "ThreadTransform" } @Override Set<QualifiedContent.ContentType> getInputTypes() { return TransformManager.CONTENT_CLASS } @Override Set<? super QualifiedContent.Scope> getScopes() { return Sets.immutableEnumSet(QualifiedContent.Scope.PROJECT) } @Override boolean isIncremental() { return false } @Override void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException { super.transform(transformInvocation) transformInvocation.inputs.each { TransformInput input -> input.directoryInputs.each { DirectoryInput directoryInput -> transformDirectory(directoryInput, transformInvocation.outputProvider) } } } private void transformDirectory(DirectoryInput directoryInput, TransformOutputProvider outputProvider) { if (directoryInput.file.isDirectory()) { directoryInput.file.eachFileRecurse { File file -> def className = file.name def path = file.path if (isAppClass(className, FileInputStream = new FileInputStream(file.getabsolutePath ()) //------------- Get the class code, scan it through the ClassVisitor, ClassReader ClassReader = new ClassReader(fileInputStream) ClassWriter ClassWriter = new ClassWriter(ClassReader, ClassWriter.COMPUTE_MAXS) ClassVisitor visitor = new ThreadRunVisitor(className, classWriter) classReader.accept(visitor, ClassReader.EXPAND_FRAMES) byte[] code = classWriter.toByteArray(); FileOutputStream fos = new FileOutputStream(file.parentFile.absolutePath + File.separator + className) fos.write(code) Fos. Close () / / -- -- -- -- -- -- -- -- -- -- -- - end point code -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --} the catch (Exception e) {}}}} def dest = outputProvider.getContentLocation(directoryInput.name, directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY) FileUtils.copyDirectory(directoryInput.file, dest) } private boolean isAppClass(String className, Return classname.endswith (".class") &&! className.contains("R\$") && !" R.class".equals(className) && !" BuildConfig.class".equals(className); }}Copy the code
public class ThreadRunVisitor extends ClassVisitor { private String className; private boolean needInject; public ThreadRunVisitor(String className, ClassVisitor classVisitor) { super(Opcodes.ASM5, classVisitor); this.className = className; } @Override public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {// Determine whether to inject this. NeedInject = isInjectClass(className, interfaces, superName); super.visit(version, access, name, signature, superName, interfaces); } @Override public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { MethodVisitor methodVisitor = this.cv.visitMethod(access, name, descriptor, signature, exceptions); boolean isInject = this.needInject && isInjectMethod(name, descriptor); if (! isInject) { return methodVisitor; } return (MethodVisitor) new AdviceAdapter(groovyjarjarasm.asm.Opcodes.ASM5, methodVisitor, access, name, descriptor) { @Override protected void onMethodEnter() { super.onMethodEnter(); / / insert in front of the method you want to insert the code (the code in your app project). This mv. VisitMethodInsn (INVOKESTATIC, "com/example/project/AopUtil", "runStart", "V" (), false); } @override protected void onMethodExit(int opcode) {// Insert the code you want at the end of the method (this code is in your app project) this.mv.visitMethodInsn(INVOKESTATIC, "com/example/project/AopUtil", "runEnd", "()V", false); }}; } public boolean isInjectClass(String className, String[] interfaces, String superName) { if (className == null) return false; . / / 1, support runnable and android OS. The Handler. Callback if (interfaces! = null) { for (String inter : interfaces) { if ("java/lang/Runnable".equals(inter) || "android/os/Handler$Callback".equals(inter)) return true; }} //2, support ExtendsAsyncTask if (" Android/OS /AsyncTask". Equals (superName)) {return true; HandleMessage if (" Android/OS /Handler". Equals (superName)) {return true; } thread. run if (" Java /lang/Thread". Equals (superName)) {return true; } return false; } public boolean isInjectMethod(String methodName, String methodDesc) { if (methodName == null || methodDesc == null) return false; If (methodName.equals("run") && methoddesc.equals ("()V"))) return true; / / 2, extendedAsyncTask. If the doInBackground method (methodName. Equals (" doInBackground ")) return true; If (methodName.equals("handleMessage")) {return methodDesc.equals("(Landroid/os/Message;) V") || methodDesc.equals("(Landroid/os/Message;) Z"); } return false; }}Copy the code
To clarify, in the following code, the new Runnable is compiled as an inner class and two classes, test. class and Test$1.class, are created and scanned respectively:
public class Test{ void test(){ new Thread(new Runnable(){ @override public void run(){ //xxxx } }).start(); }}Copy the code
For the second Transform, it is not clear which method has a new HandlerThread. The first Transform does not match desc by method name. Therefore, it is necessary to use ClassNode to scan and obtain the methods list of this class, and then get the instructions of each method (which are executed after compilation), and analyze whether this statement is included in the instructions.
The code for HandlerThreadTransform is similar to the code for ThreadRunTransform.
private void transformDirectory(DirectoryInput directoryInput, TransformOutputProvider outputProvider) { if (directoryInput.file.isDirectory()) { directoryInput.file.eachFileRecurse { File file -> def className = file.name def path = file.path if (isAppClass(className, path)) { try { FileInputStream fileInputStream = new FileInputStream(file.getAbsolutePath()) ClassReader classReader = new ClassReader(fileInputStream) ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS) ClassVisitor visitor = new HandlerThreadVisitor(classReader,classWriter) classReader.accept(visitor, ClassReader.EXPAND_FRAMES) byte[] code = classWriter.toByteArray(); FileOutputStream fos = new FileOutputStream(file.parentFile.absolutePath + File.separator + className) fos.write(code) fos.close() } catch (Exception e) { e.printStackTrace() } } } } def dest = outputProvider.getContentLocation(directoryInput.name, directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY) FileUtils.copyDirectory(directoryInput.file, dest) }Copy the code
First, in the visit method of HandlerThreadVisitor, the list of methods that need to be staked in the class needs to be scanned. How do you determine if this method needs to be staked? When using ClassNode to parse a class file, you can use classNode.methods to get a list of the class’s methods. Iterate through the list, get MethodNode, and extract the instructions (AbstractInsnNode) that each method should be compiled and executed by the JVM. Each AbstractInsnNode, there will be an int value: opcode (the specific value in the org. Objectweb. Asm. Opcodes), the instructions about the content of the currently executing. For example:
Load variable opcode value: Opcodes.iload (loads variables of type int)=21 opcodes.lload (loads variables of type long)=22 opcodes.fload (loads variables of type float)=23 Opcodes.ISTORE = 54, LSTORE = 55, FSTORE = 56, DSTORE = 57, ASTORE = 58 Opcodes.INVOKEVIRTUAL(invokes static methods) = 184 opcodes. INVOKEVIRTUAL(invokes static methods) =182 Opcodes.INVOKESPECIAL(invokes static methods) Non-static) =183 opcodes.invokedynamic (lambda deicing method, explained later) =186 Create variable opcodes.new (load a constructor for a class)Copy the code
Here’s an example of code:
public void test(){ HandlerThread ht = new HandlerThread("xxx"); ht.start(); // New handlerThread(" XXX ") TypeInsnNode(opCodes :187, Desc: android/OS/HandlerThread LdcInsnNode (opcodes: 18, CST: xx) - load constant MethodInsnNode (opcodes: 183, owner:android/os/HandlerThread, name:<init>, desc:(Ljava/lang/String;) V) -- call the constructor VarInsnNode(opcodes:58, var:1) ---- assign the object created by the constructor above to the second variable (the first is this of the class, var is created in the order in which the variables are created, if the method has arguments, //ht.start() is translated as VarInsnNode(opcodes:25, var:1) ---- loads the second variable, Namely MethodInsnNode thread variables (opcodes: 182, the owner: android/OS/HandlerThread, name: start, desc: () V) - load variable called start method.Copy the code
The monitoring effect code we want is as follows:
public void test(){ HandlerThread ht = new HandlerThread("xx"); ht.start(); AopUtil.addThread(ht); // Insert our own monitor codeCopy the code
The id and name of the Thread are already determined when the Thread is initialized. Therefore, when we detect the execution of thread.start method, we can append the following command to it:
VarInsnNode opcodes: 25, var: (1) - loaded MethodInsnNode thread variables (opcodes: 184, the owner: com/example/project/AopUtil, name:addThread, desc:(Ljava/lang/Thread;) V)Copy the code
Of course, we need to remember which variable is loaded by the compiler before calling the start method, i.e. remember the value of varinsnNode. var when varinsnNode. opcodes is 25 (opcodes.aload), so that we can load this variable later to call our piling method.
So here’s the problem. Sometimes business code is written like this:
new HandlerThread("xxx").start(); This code is translated as the following instruction: TypeInsnNode(opcodes:187, desc:android/os/HandlerThread) LdcInsnNode(opcodes:18, CST: xx) - load constant MethodInsnNode (opcodes: 183, the owner: android/OS/HandlerThread, name: < init >, desc: (Ljava/lang/String;) V) - call constructor MethodInsnNode (opcodes: 182, the owner: android/OS/HandlerThread, name: start, desc: () V) - load variable called start method. The only difference is that opcodes. ASTORE and opcodes. ALOAD are missingCopy the code
At this time, there is no Thread variable in this method, so we need to add the instruction. Before the start instruction, add the create variable (newLocal), store object (opcodes.astore), and read variable (opcodes.aload) instructions. So we need to remember the instruction before the start method. If the instruction is the instruction that calls init constructor, we need to add the above instruction. If the ALOAD instruction is called before the start method, we need to remember the var parameter in the ALOAD instruction.
Take a look at the core code (full code later) :
@Override public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { MethodVisitor methodVisitor = this.cv.visitMethod(access, name, descriptor, signature, exceptions); boolean injectLambda = hasLambda(name); boolean isInject = injectMethods.contains(new Method(name, descriptor)); if (! isInject && ! injectLambda) { return methodVisitor; } return new AdviceAdapter(groovyjarjarasm.asm.Opcodes.ASM5, methodVisitor, access, name, descriptor) { int lastThreadVarIndex = -1; // Remember the position of thread variable String lastThreadInstruction; Override public void visitVarInsn(int opcode, int var) {super.visitvarinsn (opcode, var); if(isInject) { if (opcode == ALOAD) { lastThreadInstruction = VISIT_VAR_INSN_LOAD; lastThreadVarIndex = var; } } } @Override public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) { if (isInject) { if (! THREAD.equals(owner) && ! HANDLER_THREAD.equals(owner)) { super.visitMethodInsn(opcode, owner, name, descriptor, isInterface); return; } if (!" <init>".equals(name) && !" start".equals(name)) { super.visitMethodInsn(opcode, owner, name, descriptor, isInterface); return; } // If ("<init>".equals(name)) {super.visitmethodinsn (opcode, owner, name); descriptor, isInterface); lastThreadInstruction = VISIT_METHOD_THREAD_INIT; } else if (" start ". The equals (name)) {/ / before the first test whether the thread is stored as a local variable if (lastThreadInstruction. Equals (VISIT_METHOD_THREAD_INIT)) { Type threadType = type.getobJectType (" Java /lang/ thread "); lastThreadVarIndex = newLocal(threadType); this.mv.visitVarInsn(ASTORE, lastThreadVarIndex); this.mv.visitVarInsn(ALOAD, lastThreadVarIndex); } // continue to call the start method super.visitMethodinsn (opcode, owner, name, Descriptor, isInterface); If (lastThreadVarIndex > 0) {this.mv.visitVarinsn (ALOAD, lastThreadVarIndex); // this.mv.visitmethodinsn (INVOKEVIRTUAL, owner, "getId", "()J", false); this.mv.visitMethodInsn(INVOKESTATIC, "com/example/project/AopUtil", "addThread", "(Ljava/lang/Thread;) V", false); } } } else { super.visitMethodInsn(opcode, owner, name, descriptor, isInterface); }}}; }Copy the code
Now that most of this is done, let’s move on to ASM’s handling of lambda expressions.
class Java8 { interface Logger { void log(String s); } public static void main(String... args) { sayHi(s -> System.out.println(s)); } private static void sayHi(Logger logger) { logger.log("Hello!" ); }} Public class Java8 {interface Logger {void log(String s); } public static void main(String... SayHi (s -> new Java8$1()); } private static void sayHi(Logger logger) { logger.log("Hello!" ); Static void lambda$main$0(String STR){system.out.println (STR); }} public class Java8$1 implements Java8.logger {public Java8$1(){} @override public void log(String s) Lambda $main$0(s); }}Copy the code
In the main function, there is an opcodes.invokedynamic directive (InvokeDynamicInsnNode). Look at the arguments in this directive:
First, we determine whether the desc in this instruction contains Java /lang/Runnable, and the name is run. If the match is successful, we obtain the real execution function (bsmArgs[1]) after the method is desugared, and add our pegging code to this function. View the specific code:
public class HandlerThreadVisitor extends ClassVisitor { public static final String HANDLER_THREAD = "android/os/HandlerThread"; public static final String THREAD = "java/lang/Thread"; private final String VISIT_VAR_INSN_LOAD = "visitVarInsn-Load"; private final String VISIT_METHOD_THREAD_INIT = "visitMethod-ThreadInit"; private ClassNode classnode; ArrayList<Method> injectMethods = new ArrayList<>(); ArrayList<String> lambdaMethods = new ArrayList<>(); public HandlerThreadVisitor(ClassReader classReader, ClassVisitor classVisitor) { super(Opcodes.ASM5, classVisitor); classnode = new ClassNode(); classReader.accept(classnode, 0); } @Override public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { getInjectMethods(classnode); super.visit(version, access, name, signature, superName, interfaces); } @Override public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { MethodVisitor methodVisitor = this.cv.visitMethod(access, name, descriptor, signature, exceptions); boolean injectLambda = hasLambda(name); boolean isInject = injectMethods.contains(new Method(name, descriptor)); if (! isInject && ! injectLambda) { return methodVisitor; } return new AdviceAdapter(groovyjarjarasm.asm.Opcodes.ASM5, methodVisitor, access, name, descriptor) { int lastThreadVarIndex = -1; String lastThreadInstruction; @Override protected void onMethodEnter() { super.onMethodEnter(); if (injectLambda) { this.mv.visitMethodInsn(INVOKESTATIC, "com/example/project/AopUtil", "runStart", "()V", false); } } @Override protected void onMethodExit(int opcode) { super.onMethodExit(opcode); if (injectLambda) { this.mv.visitMethodInsn(INVOKESTATIC, "com/example/project/AopUtil", "runEnd", "()V", false); } } @Override public void visitVarInsn(int opcode, int var) { super.visitVarInsn(opcode, var); if(isInject) { if (opcode == ALOAD) { lastThreadInstruction = VISIT_VAR_INSN_LOAD; lastThreadVarIndex = var; } } } @Override public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) { if (isInject) { if (! THREAD.equals(owner) && ! HANDLER_THREAD.equals(owner)) { super.visitMethodInsn(opcode, owner, name, descriptor, isInterface); return; } if (!" <init>".equals(name) && !" start".equals(name)) { super.visitMethodInsn(opcode, owner, name, descriptor, isInterface); return; } // If ("<init>".equals(name)) {super.visitmethodinsn (opcode, owner, name); descriptor, isInterface); lastThreadInstruction = VISIT_METHOD_THREAD_INIT; } else if (" start ". The equals (name)) {/ / before the first test whether the thread is stored as a local variable if (lastThreadInstruction. Equals (VISIT_METHOD_THREAD_INIT)) { Type threadType = type.getobJectType (" Java /lang/ thread "); lastThreadVarIndex = newLocal(threadType); this.mv.visitVarInsn(ASTORE, lastThreadVarIndex); this.mv.visitVarInsn(ALOAD, lastThreadVarIndex); } // continue to call the start method super.visitMethodinsn (opcode, owner, name, Descriptor, isInterface); If (lastThreadVarIndex > 0) {this.mv.visitVarinsn (ALOAD, lastThreadVarIndex); this.mv.visitMethodInsn(INVOKESTATIC, "com/example/project/AopUtil", "addThread", "(Ljava/lang/Thread;) V", false); } } } else { super.visitMethodInsn(opcode, owner, name, descriptor, isInterface); }}}; } public void getInjectMethods(ClassNode classnode) { for (MethodNode method : classnode.methods) { for (int i = 0; i < method.instructions.size(); i++) { AbstractInsnNode insnNode = method.instructions.get(i); if (insnNode.getOpcode() == Opcodes.NEW) { TypeInsnNode methodInsnNode = (TypeInsnNode) insnNode; if (HANDLER_THREAD.equals(methodInsnNode.desc) || THREAD.equals(methodInsnNode.desc)) { injectMethods.add(new Method(method.name, method.desc)); }} else if (InvokeDynamicInsnNode instanceof runnable lambda expression if ((InvokeDynamicInsnNode) insnNode).desc.contains("Ljava/lang/Runnable;" ) && ((InvokeDynamicInsnNode) insnNode).name.equals("run")) { lambdaMethods.add(((Handle) ((InvokeDynamicInsnNode) insnNode).bsmArgs[1]).getName()); } } } } } private boolean hasLambda(String name){ for (int i = 0; i < lambdaMethods.size(); i++) { if (lambdaMethods.get(i).contains(name)) { return true; } } return false; } static class Method { String name; String desc; public Method(String name, String desc) { this.name = name; this.desc = desc; } @Override public boolean equals(Object o) { Method temp = (Method) o; return name.equals(temp.name) && desc.equals(temp.desc); }}}Copy the code
I’ve done all the code here. Take a look at our code in AopUtils:
public class AopUtil { static HashSet<Long> allThread = new HashSet<>(); static HashSet<Long> usedThread = new HashSet<>(); static ConcurrentHashMap<String, Long> threadRunStartTime = new ConcurrentHashMap<>(); public static void runStart() { logThreadUsage(Thread.currentThread(), true); threadRunStartTime.put(getKey(), System.currentTimeMillis()); } private static String getKey(){ String stackTrace = Log.getStackTraceString(new Throwable()); stackTrace = stackTrace.split("\n\t")[3]; Return stackTrace.substring(0, stackTrace.indexof ("("))); } public static void runEnd() { String key = getKey(); Long start = threadRunStartTime.get(key); if (start ! = null) { Log.d("ThreadAop-runCost", key + "cost time:" + (System.currentTimeMillis() - start)); threadRunStartTime.remove(key); } } public static void addThread(Thread thread) { logThreadUsage(thread, false); } private static void logThreadUsage(Thread thread, boolean isFromRun) { if (thread.getName().equals("main")) { return; } synchronized (AopUtil.class) { if (usedThread == null) { usedThread = new HashSet<>(); } if (isFromRun) { Log.d("ThreadAop-used1", "thread is used: " + thread.getId() + ", name is " + thread.getName()); usedThread.add(thread.getId()); } if (allThread == null) { allThread = new HashSet<>(); } if (allThread.contains(thread.getId())) { Log.d("ThreadAop", Log.getStackTraceString(new Throwable())); return; } allThread.add(thread.getId()); Log.d("ThreadAop-1", "current size:" + allThread.size() + " add new thread:" + thread.getName() + ", usedThread:" + usedThread.size()); Log.d("ThreadAop", Log.getStackTraceString(new Throwable())); }}}Copy the code
Now the plugin has been developed and introduced into our project:
To start, add the following files to the buildSrc module:
Add the following code to build.gradle in your app module:
Plugins {id 'thread-inject'} or plugins: 'thread-inject'Copy the code
Once the app is packaged and running, you can see the log output of our staked code. This is the end of the code.
Android Studio->Preferences->Plugins. Install the ASM Bytecode Viewer. Restart after installing the Plugins. After restarting Android Studio, go to the project’s build directory, right-click a. Class file, and select: