前言
之前做内存优化的时候,为了实现对线程的使用监控,借助了一个第三方的hook框架(epic),这个框架可以hook一切java方法,使用也简单,但是最大的问题是它有较严重的兼容性问题,部分机型会出现闪退的现象,这就导致它不能被带到线上使用,只能在线下使用,为了实现在线上监控线程的使用,于是我便开发了BlackHook插件,也可以hook一切java方法,而且很稳定,没有兼容性问题,真是十足的黑科技
简介
BlackHook 是一个实现编译时插桩的gradle插件,基于ASM+Tranfrom实现,理论上可以hook任意一个java方法或者kotlin方法,只要代码对应的字节码可以在编译阶段被Tranfrom扫描到,就可以使用ASM在代码对应的字节码处插入特定字节码,从而hook该方法
优点
- 用DSL(领域特定语言)使用该插件,使用简单,配置灵活,而且插入的字节码可以使用
ASM Bytecode Viewer Support Kotlin 插件自动生成,上手难度低 - 理论上可以hook任意一个java方法,只要代码对应的字节码可以在编译阶段被Tranfrom扫描到
- 基于ASM+Tranfrom实现,在编译阶段直接修改字节码,效率高,没有兼容性问题
使用
在app下面的build.gradle文件添加如下代码
apply plugin: 'com.blackHook'
/**
* 返回hook线程构造函数的字节码,Hook 线程的构造函数,让每次在调用Thread的构造函数的时候就会调用
* ThreadCheck类的 printThread方法,从而在控制台打印线程的构造函数的调用堆栈,这些代码可以借助
* ASM Bytecode Viewer Support Kotlin生成,MethodVisitor是ASM提供的一个类,用于修改字节码
*/
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)
}
/**
* 返回需要被hook的方法,需要被hook的方法是Thread的构造函数
*/
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 {
//表示要处理的数据类型是什么,CLASSES 表示要处理编译后的字节码(可能是 jar 包也可能是目录),RESOURCES 表示要处理的是标准的 java 资源
inputTypes BlackHook.CONTENT_CLASS
//表示Transform 的作用域,这里设置的SCOPE_FULL_PROJECT代表作用域是全工程
scopes BlackHook.SCOPE_FULL_PROJECT
//表示是否支持增量编译,false不支持
isIncremental false
//表示hook的方法
hookMethodList = getHookMethods()
}
以上的代码其实是hook的Thread的构造函数,将ThreadCheck的printThread方法hook到了Thread的构造函数中,每次调用线程的构造函数的时候就会调用ThreadCheck的printThread方法,这个方法会打印出Thread的构造函数的调用堆栈,从而可以在控制台知道哪个页面的哪行代码实例化了Thread,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())
}
}
上面的代码获取了调用堆栈,并且打印到控制台
实现原理
首先它是一个gradle 的自定义Plugin,其次它是通过在编译阶段修改字节码实现Hook,在编译阶段通过Tranfrom扫描所有的字节码,然后根据在使用插件的时候设置的需要被Hook的方法,插入需要被插入的字节码,
需要被插入的字节码也是在使用的时候设置的,例如下面的代码
/**
* 返回hook线程构造函数的字节码,Hook 线程的构造函数,让每次在调用Thread的构造函数的时候就会调用
* ThreadCheck的 printThread方法,从而在控制台打印线程的构造函数的调用堆栈,这些代码可以借助
* ASM Bytecode Viewer Support Kotlin生成,MethodVisitor是ASM提供的一个类,用于修改字节码
*/
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)
}
准备过程
实现这个gradle插件需要我们有足够的预备知识,如下:
- 首先要知道如何使用Android Studio开发Gradle插件
- 了解TransformAPI:Transform API是从Gradle 1.5.0版本之后提供的,它允许第三方在打包Dex文件之前的编译过程中修改java字节码(自定义插件注册的transform会在ProguardTransform和DexTransform之前执行,所以自动注册的类不需要考虑混淆的情况).参考文章有:
- Android 热修复使用Gradle Plugin1.5改造Nuwa插件(主要看前半部分关于TransformAPI的介绍,Nuwa相关的内容可先忽略)
- 字节码修改框架(相比于Javassist框架ASM较难上手,但性能更高,但相学习难度阻挡不了我们对性能的追求):
- ASM英文文档
- ASM API文档
- Android 热修复方案Tinker(七) 插桩实现(主要看关于ASM使用的介绍及与transformAPI的结合)
实现过程
1.自定义gradle plugin
因为这是一个gradle插件,所以需要我们自定义一个gradle的plugin
1. 新建一个模块
在工程中新建一个模块,命名为"buildSrc",注意,一定要命名为buildSrc,否则在工程中必须要将代码发布到本地或者远程maven仓库中才能正常使用,这样调试不方便,如下所示:
[图片上传失败...(image-88703d-1634713340383)]
2. 然后配置gradle脚本,代码如下所示:
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.tools.build:gradle:3.4.1"
implementation 'org.ow2.asm:asm:9.1'
implementation 'org.ow2.asm:asm-commons:9.1'
}
3. 实现Plugin类
新建groovy文件夹,新建BlackHookPlugin类,继承Transform类,实现Plugin接口
[图片上传失败...(image-843eb6-1634713340383)]
BlackHookPlugin代码如下所示:
package com.blackHook.plugin
class BlackHookPlugin extends Transform implements Plugin<Project> {
....此处省略了很多代码
@Override
void apply(Project target) {
println("注册了")
project = target
target.extensions.getByType(BaseExtension).registerTransform(this)
target.extensions.create("blackHook", BlackHook.class)
}
....此处省略了很多代码
}
新建resources文件夹,新建com.blackHook.properties文件,如下所示
[图片上传失败...(image-2c65fe-1634713340383)]
com.blackHook.properties文件的代码如下:
implementation-class=com.blackHook.plugin.BlackHookPlugin
implementation-class的值即是BlackHookPlugin的完整路径,另外,com.blackHook.properties文件的文件名既是使用插件的时候的插件名,如下代码:
apply plugin: 'com.blackHook'
2. 实现BlackHook扩展类
新建BlackHook类,代码如下
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;
}
}
这个类用于接收开发人员使用插件的时候设置的参数和需要被Hook的方法以及参与Hook的字节码,我们在使用blackHook插件的时候可以使用DSL的方式来使用,如下代码所示:
blackHook {
//表示要处理的数据类型是什么,CLASSES 表示要处理编译后的字节码(可能是 jar 包也可能是目录), RESOURCES 表示要处理的是标准的 java 资源
inputTypes BlackHook.CONTENT_CLASS
//表示Transform 的作用域,这里设置的SCOPE_FULL_PROJECT代表作用域是全工程
scopes BlackHook.SCOPE_FULL_PROJECT
//表示是否支持增量编译,false不支持
isIncremental false
//表示hook的方法
hookMethodList = getHookMethods()
}
之所以可以这么做是因为我们在BlackHookPlugin将BlackHook类添加到了target.extensions(扩展属性)中,
如下代码:
class BlackHookPlugin extends Transform implements Plugin<Project> {
@Override
void apply(Project target) {
target.extensions.create("blackHook", BlackHook.class)
}
}
3.开始实现扫描
需要在BlackHookPlugin的transform()方法中扫描全局代码,代码如下:
@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, outputProvider)
}
//遍历jarInputs
input.jarInputs.each { JarInput jarInput ->
//处理jarInputs
handleJarInputs(jarInput, 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.separator + name)
fos.write(code)
fos.close()
}
}
}
//处理完输入文件之后,要把输出给下一个任务
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().endsWith(".jar")) {
//重名名输出文件,因为可能同名,会覆盖
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 tmpFile = new File(jarInput.file.getParent() + File.separator + "classes_temp.jar")
//避免上次的缓存被重复插入
if (tmpFile.exists()) {
tmpFile.delete()
}
JarOutputStream jarOutputStream = new JarOutputStream(new FileOutputStream(tmpFile))
//用于保存
while (enumeration.hasMoreElements()) {
JarEntry jarEntry = (JarEntry) enumeration.nextElement()
String entryName = jarEntry.getName()
ZipEntry zipEntry = new ZipEntry(entryName)
InputStream inputStream = jarFile.getInputStream(jarEntry)
//插桩class
if (entryName.endsWith(".class") && !entryName.startsWith("R$")
&& !"R.class".equals(entryName) && !"BuildConfig.class".equals(entryName)) {
//class文件处理
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()
}
//结束
jarOutputStream.close()
jarFile.close()
def dest = outputProvider.getContentLocation(jarName + md5Name,
jarInput.contentTypes, jarInput.scopes, Format.JAR)
FileUtils.copyFile(tmpFile, dest)
tmpFile.delete()
}
}
扫描的过程中会将扫描到的所有类的信息(包含类名,父类名,方法名等)交给AllClassVisitor类,AllClassVisitor类代码如下所示:
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);
// 新建AllMethodVisitor类,将扫描到类和方法的信息以及BlackHook类存储的参数交给 AllMethodVisitor对象,由AllMethodVisitor来判断是否需要Hook指定的方法
return new AllMethodVisitor(blackHook, mv, access, name, descriptor, className, superClassName);
}
然后在AllClassVisitor类中会将将扫描到的类和方法的信息以及BlackHook扩展类存储的参数交给AllMethodVisitor对象,由AllMethodVisitor来判断是否需要Hook指定的方法,AllMethodVisitor代码如下:
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);
//这里根据开发人员设置的需要hook的方法以及扫描到的方法来判断是否需要hook
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;
}
}
}
}
}
在这个类中根据开发人员调用插件的时候设置的需要hook的方法以及扫描到的方法来判断是否需要hook