preface

Before doing memory optimization, in order to achieve the use of thread monitoring, with the help of a third party hook framework (epic), the framework can hook all Java method, also easy to use, but the biggest problem is that it has a serious compatibility issues, some models will appear the phenomenon of flash back, as a result, it can not be used to online, It can only be used offline. In order to monitor the use of threads online, I developed BlackHook plug-in, which can hook all Java methods, and it is very stable, no compatibility problems, it is really full of black technology

Introduction to the

BlackHook is a gradle plugin that implements compile-time stapling. Based on ASM+Tranfrom, it can hook any Java method or Kotlin method, as long as the bytecode corresponding to the code can be scanned by Tranfrom at compile time. You can hook the method by using ASM to insert specific bytecodes at the corresponding bytecodes in the code

advantages

  1. Using DSL(Domain specific Language), this plug-in is easy to use, flexible to configure, and the inserted Bytecode can be automatically generated using ASM Bytecode Viewer Support Kotlin plug-in, which is easy to get started
  2. It is theoretically possible to hook any Java method as long as the code’s corresponding bytecode can be scanned by Tranfrom at compile time
  3. Based on ASM+Tranfrom implementation, directly modify bytecode in the compilation stage, high efficiency, no compatibility problems

use

Add the following code to the build.gradle file below your app

apply plugin: 'com.blackHook' /** * returns the bytecode to the hook Thread constructor, which calls printThread every time the Thread constructor is called. To print the thread's constructor call stack on the console, this code can be generated with the help of * ASM Bytecode Viewer Support Kotlin, MethodVisitor is a class that ASM provides, Void createHookThreadByteCode(MethodVisitor MV, String className) {mv.visitTypeInsn(opcodes.new, "com/quwan/tt/asmdemoapp/ThreadCheck") mv.visitInsn(Opcodes.DUP) mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "com/quwan/tt/asmdemoapp/ThreadCheck", "<init>", "()V", false) mv.visitLdcInsn(className) mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "com/quwan/tt/asmdemoapp/ThreadCheck", "printThread", "(Ljava/lang/String;) V", false)} /** * returns the method that needs to be hooked, */ List<HookMethod> getHookMethods() {List<HookMethod> hookMethodList = new ArrayList<>() hookMethodList.add(new HookMethod("java/lang/Thread", "<init>", "()V", { MethodVisitor mv -> createHookThreadByteCode(mv, "Java /lang/Thread")})) return hookMethodList} blackHook {// indicate the data type to process, CLASSES indicate the compiled bytecode to process (could be jar packages or directories), InputTypes BlackHook.CONTENT_CLASS specifies the scope of the Transform. Scopes BlackHook.SCOPE_FULL_PROJECT // Indicates whether incremental compilation is supported. IsIncremental false // hookMethodList = getHookMethods()}Copy the code

ThreadCheck’s printThread constructor is called every time the Thread’s constructor is called. This method prints out the call stack for Thread’s constructor so that you can see on the console which line of code on which page instantiated Thread, as shown in the code for ThreadCheck

class ThreadCheck { var isCanAppendLog = false private val tag = "====>ThreadCheck" fun printThread(name : String){ println("====>printThread:${name}") val es = Thread.currentThread().stackTrace val normalInfo = StringBuilder("  \nThreadTrace:") .append("\nthreadName:${name}") .append("\n====================================threadTraceStart=======================================") for (e in es) {  if (e.className == "dalvik.system.VMStack" && e.methodName == "getThreadStackTrace") { isCanAppendLog = false } if (e.className.contains("ThreadCheck") && e.methodName == "printThread") { isCanAppendLog = true } else { if (isCanAppendLog) { normalInfo.append("\n${e.className}(lineNumber:${e.lineNumber})") } } } normalInfo.append("\n=====================================threadTraceEnd=======================================") Log.i(tag, normalInfo.toString()) } }Copy the code

The above code takes the call stack and prints it to the console

Realize the principle of

First of all, it is a gradle custom Plugin. Second, it implements Hook by modifying bytecodes at compile stage. During compile stage, Tranfrom scans all bytecodes and inserts the bytecodes that need to be inserted according to the method that needs to be hooked set at the time of using the plug-in. The bytecodes that need to be inserted are also set at use, as shown in the following code

/** * return the bytecode of the hook Thread constructor, so that the printThread method of ThreadCheck is called every time the Thread constructor is called, and the call stack of the Thread constructor is printed on the console. This code can be generated using the * ASM Bytecode Viewer Support Kotlin, MethodVisitor is a class that ASM provides, Void createHookThreadByteCode(MethodVisitor MV, String className) {mv.visitTypeInsn(opcodes.new, "com/quwan/tt/asmdemoapp/ThreadCheck") mv.visitInsn(Opcodes.DUP) mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "com/quwan/tt/asmdemoapp/ThreadCheck", "<init>", "()V", false) mv.visitLdcInsn(className) mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "com/quwan/tt/asmdemoapp/ThreadCheck", "printThread", "(Ljava/lang/String;) V", false) }Copy the code

Preparation process

To implement this gradle plugin, we need to have enough preliminary knowledge as follows:

  • How to use Android Studio to develop Gradle plug-ins
  • Understanding TransformAPI: The Transform API is from Gradle Provided after version 1.5.0, it allows third parties to modify Java bytecode during compilation prior to packaging Dex files (custom plug-in registered transforms are executed before ProguardTransform and DexTransform, so auto-registered classes don’t need to worry about confusion). Reference articles are:
    • The Android hotfix uses Gradle Plugin1.5 to transform the Nuwa plugin.
  • Bytecode modification framework (ASM is more difficult to learn than Javassist framework, but it has higher performance, but the difficulty of learning does not stop us from pursuing performance) :
    • ASM English Documentation
    • ASM API documentation
    • Tinker is a hot fix for Android. The Tinker is a hot fix for Android. Tinker is a hot fix for Android.

The implementation process

1. Customize gradle Plugin

Since this is a Gradle plugin, we need to customize a Gradle plugin

1. Create a module

Build a new module in your project and name it “buildSrc”. Note that the name must be buildSrc, otherwise the project must publish the code to the local or remote Maven repository to work properly.

2. Then configure the Gradle script as follows:

plugins { id 'java-library' id 'maven' id 'groovy' } java { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 } dependencies { implementation gradleApi()//gradle sdk implementation LocalGroovy () implementation "com. Android. View the build: gradle: 3.4.1 track" implementation "org. Ow2. Asm: asm: 9.1 'implementation 'org. Ow2. Asm: asm - Commons: 9.1'}Copy the code

3. Implement the Plugin class

Create groovy folder, create BlackHookPlugin class, inherit Transform class, implement Plugin interface

The BlackHookPlugin code looks like this:

package com.blackHook.plugin class BlackHookPlugin extends Transform implements Plugin<Project> { .... A lot of code omitted here @override void apply(Project target) {println(" registered ") Project = target target.extensions.getByType(BaseExtension).registerTransform(this) target.extensions.create("blackHook", BlackHook.class) } .... A lot of code omitted here}Copy the code

Create a new resources folder and a new com.blackhook.properties file, as shown below

The code for the com.blackhook.properties file is as follows:

implementation-class=com.blackHook.plugin.BlackHookPlugin
Copy the code

The implementation-class value is the full path of the BlackHookPlugin, and the name of the com.blackhook. properties file is the name of the plug-in when it is used, as follows:

apply plugin: 'com.blackHook'
Copy the code

2. Implement BlackHook extension classes

Create a new BlackHook class with the following code

public class BlackHook { Closure methodHooker; List<HookMethod> hookMethodList = new ArrayList<>(); public static final String CONTENT_CLASS = "CONTENT_CLASS"; public static final String CONTENT_JARS = "CONTENT_JARS"; public static final String CONTENT_RESOURCES = "CONTENT_RESOURCES"; public static final String SCOPE_FULL_PROJECT = "SCOPE_FULL_PROJECT"; public static final String PROJECT_ONLY = "PROJECT_ONLY"; String inputTypes = CONTENT_CLASS; String scopes = SCOPE_FULL_PROJECT; boolean isNeedLog = false; boolean isIncremental = false; public Closure getMethodHooker() { return methodHooker; } public void setMethodHooker(Closure methodHooker) { this.methodHooker = methodHooker; } public List<HookMethod> getHookMethodList() { return hookMethodList; } public void setHookMethodList(List<HookMethod> hookMethodList) { this.hookMethodList = hookMethodList; } public String getInputTypes() { return inputTypes; } public void setInputTypes(String inputTypes) { this.inputTypes = inputTypes; } public String getScopes() { return scopes; } public void setScopes(String scopes) { this.scopes = scopes; } public boolean getIsIncremental() { return isIncremental; } public void setIsIncremental(boolean incremental) { isIncremental = incremental; } public boolean getIsNeedLog() { return isNeedLog; } public void setIsNeedLog(boolean needLog) { isNeedLog = needLog; }}Copy the code

This class is used to receive the parameters set by the developer when using the plug-in, the methods that need to be hooked and the bytecodes that participate in the Hook. We can use it in the way of DSL when using the blackHook plug-in, as shown in the following code:

BlackHook {// represents what type of data to process, CLASSES represents compiled bytecode (which could be jar packages or directories), InputTypes BlackHook.CONTENT_CLASS specifies the scope of the Transform. Scopes BlackHook.SCOPE_FULL_PROJECT // Indicates whether incremental compilation is supported. IsIncremental false // hookMethodList = getHookMethods()}Copy the code

We can do this because we added the BlackHook class to the Target.Extensions in BlackHookPlugin as follows:

class BlackHookPlugin extends Transform implements Plugin<Project> {
    @Override
    void apply(Project target) {
        target.extensions.create("blackHook", BlackHook.class)
    }
}
Copy the code

3. The scan starts

The global code needs to be scanned in the BlackHookPlugin’s transform() method as follows:

@Override void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException { Collection<TransformInput> inputs = transformInvocation.inputs TransformOutputProvider outputProvider = transformInvocation.outputProvider if (outputProvider ! = null) { outputProvider.deleteAll() } if (blackHook == null) { blackHook = new BlackHook() blackHook.methodHooker = project.extensions.blackHook.methodHooker blackHook.isNeedLog = project.extensions.blackHook.isNeedLog for (int i = 0; i < project.extensions.blackHook.hookMethodList.size(); i++) { HookMethod hookMethod = new HookMethod() hookMethod.className = project.extensions.blackHook.hookMethodList.get(i).className hookMethod.methodName = project.extensions.blackHook.hookMethodList.get(i).methodName hookMethod.descriptor = project.extensions.blackHook.hookMethodList.get(i).descriptor hookMethod.createBytecode = project.extensions.blackHook.hookMethodList.get(i).createBytecode blackHook.hookMethodList.add(hookMethod) } } inputs.each { input -> input.directoryInputs.each { directoryInput -> handleDirectoryInput(directoryInput, JarInputs input. JarInputs. Each {jarInputs input -> jarInputs handleJarInputs(jarInputs, outputProvider) } } super.transform(transformInvocation) } void handleDirectoryInput(DirectoryInput directoryInput, TransformOutputProvider outputProvider) { if (directoryInput.file.isDirectory()) { directoryInput.file.eachFileRecurse {  file -> String name = file.name if (name.endsWith(".class") && ! name.startsWith("R$drawable") && !" R.class".equals(name) && !" BuildConfig.class".equals(name)) { ClassReader classReader = new ClassReader(file.bytes) ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS) ClassVisitor classVisitor = new AllClassVisitor(classWriter, blackHook) classReader.accept(classVisitor, EXPAND_FRAMES) byte[] code = classWriter.toByteArray() FileOutputStream fos = new FileOutputStream( File. ParentFile. AbsolutePath + file. The separator + name) fos. Write (code) fos. Close ()}}} / / after processing the input file, The output to the next task def dest = outputProvider. GetContentLocation (directoryInput. Name, directoryInput contentTypes, directoryInput.scopes, Format.DIRECTORY) FileUtils.copyDirectory(directoryInput.file, dest) } void handleJarInputs(JarInput jarInput, TransformOutputProvider outputProvider) {if (jarInput. File. GetAbsolutePath (). The endsWith (" jar ")) {/ / rename the output file, because may be the same name, will be covered def jarName = jarInput.name def md5Name = DigestUtils.md5Hex(jarInput.file.getAbsolutePath()) if (jarName.endsWith(".jar")) { jarName = jarName.substring(0, jarName.length() - 4) } JarFile jarFile = new JarFile(jarInput.file) Enumeration enumeration = jarFile.entries() File Separator + "classes_temp.jar") // Avoid the previous cache to be inserted repeatedly if (tmpfile.exists ()) {tmpfile.delete ()} JarOutputStream JarOutputStream = new JarOutputStream(new FileOutputStream(tmpFile)) // To save while (enumeration.hasMoreElements()) { JarEntry jarEntry = (JarEntry) enumeration.nextElement() String entryName = jarEntry.getName() ZipEntry zipEntry = new ZipEntry(entryName) InputStream inputStream = Class if (entryname.endswith (".class") &&! entryName.startsWith("R$") && !" R.class".equals(entryName) && !" BuildConfig class ". The equals (entryName)) {/ / class file handling jarOutputStream. PutNextEntry (zipEntry) ClassReader ClassReader = new  ClassReader(IOUtils.toByteArray(inputStream)) ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS) ClassVisitor cv = new AllClassVisitor(classWriter, blackHook) classReader.accept(cv, EXPAND_FRAMES) byte[] code = classWriter.toByteArray() jarOutputStream.write(code) } else { jarOutputStream.putNextEntry(zipEntry) jarOutputStream.write(IOUtils.toByteArray(inputStream)) } JarOutputStream. CloseEntry ()} / / end jarOutputStream. Close () jarFile. Close () def dest = outputProvider.getContentLocation(jarName + md5Name, jarInput.contentTypes, jarInput.scopes, Format.JAR) FileUtils.copyFile(tmpFile, dest) tmpFile.delete() } }Copy the code

During the scan, all the scanned class information (including class name, parent class name, method name, etc.) will be handed to the AllClassVisitor class. The code of the AllClassVisitor class is as follows:

public class AllClassVisitor extends ClassVisitor { private String className; private BlackHook blackHook; private String superClassName; public AllClassVisitor(ClassVisitor classVisitor, BlackHook blackHook) { super(ASM6, classVisitor); this.blackHook = blackHook; } @Override public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { super.visit(version, access, name, signature, superName, interfaces); className = name; superClassName = superName; } @override public MethodVisitor visitMethod(int Access, String name, String descriptor, String signature, String[] exceptions) { MethodVisitor mv = cv.visitMethod(access, name, descriptor, signature, exceptions); // Create a new AllMethodVisitor class and give the AllMethodVisitor object the information scanned to the classes and methods and the parameters stored by the BlackHook class, Return new AllMethodVisitor(blackHook, mv, Access, name, descriptor, className, superClassName); }Copy the code

The AllClassVisitor class then passes the scanned class and method information and the parameters stored by the BlackHook extension class to the AllMethodVisitor object. The AllMethodVisitor determines whether the Hook specified method is needed. AllMethodVisitor code looks like this:

class AllMethodVisitor extends AdviceAdapter { private final String methodName; private final String className; private BlackHook blackHook; private String superClassName; protected AllMethodVisitor(BlackHook blackHook, org.objectweb.asm.MethodVisitor methodVisitor, int access, String name, String descriptor, String className, String superClassName) { super(ASM5, methodVisitor, access, name, descriptor); this.blackHook = blackHook; this.methodName = name; this.className = className; this.superClassName = superClassName; } @Override protected void onMethodEnter() { super.onMethodEnter(); } @Override public void visitMethodInsn(int opcode, String owner, String methodName, String descriptor, boolean isInterface) { super.visitMethodInsn(opcode, owner, methodName, descriptor, isInterface); if (blackHook.isNeedLog) { System.out.println("====>methodInfo:" + "className:" + owner + ",methodName:" + methodName + ",descriptor:" + descriptor); } if (blackHook ! = null && blackHook.hookMethodList ! = null && blackHook.hookMethodList.size() > 0) { for (int i = 0; i < blackHook.hookMethodList.size(); i++) { HookMethod hookMethod = blackHook.hookMethodList.get(i); / / here according to the needs of developers set the hook method and the method of scanning to judge whether need to hook the if ((owner) equals (hookMethod. ClassName) | | superClassName.equals(hookMethod.className) || className.equals(hookMethod.className)) && methodName.equals(hookMethod.methodName) && descriptor.equals(hookMethod.descriptor)) { hookMethod.createBytecode.call(mv); break; } } } } }Copy the code

This class determines whether a hook is needed based on the methods that the developer sets and scans when calling the plug-in

4. The source code

Github.com/18824863285…