前言
之前做內(nèi)存優(yōu)化的時候愧口,為了實現(xiàn)對線程的使用監(jiān)控,借助了一個第三方的hook框架(epic)授滓,這個框架可以hook一切java方法羞海,使用也簡單,但是最大的問題是它有較嚴重的兼容性問題筛婉,部分機型會出現(xiàn)閃退的現(xiàn)象,這就導致它不能被帶到線上使用癞松,只能在線下使用爽撒,為了實現(xiàn)在線上監(jiān)控線程的使用,于是我便開發(fā)了BlackHook插件响蓉,也可以hook一切java方法硕勿,而且很穩(wěn)定,沒有兼容性問題枫甲,真是十足的黑科技
簡介
BlackHook 是一個實現(xiàn)編譯時插樁的gradle插件源武,基于ASM+Tranfrom實現(xiàn),理論上可以hook任意一個java方法或者kotlin方法想幻,只要代碼對應的字節(jié)碼可以在編譯階段被Tranfrom掃描到粱栖,就可以使用ASM在代碼對應的字節(jié)碼處插入特定字節(jié)碼,從而hook該方法
優(yōu)點
- 用DSL(領域特定語言)使用該插件脏毯,使用簡單闹究,配置靈活,而且插入的字節(jié)碼可以使用
ASM Bytecode Viewer Support Kotlin 插件自動生成食店,上手難度低 - 理論上可以hook任意一個java方法渣淤,只要代碼對應的字節(jié)碼可以在編譯階段被Tranfrom掃描到
- 基于ASM+Tranfrom實現(xiàn),在編譯階段直接修改字節(jié)碼吉嫩,效率高价认,沒有兼容性問題
使用
在app下面的build.gradle文件添加如下代碼
apply plugin: 'com.blackHook'
/**
* 返回hook線程構造函數(shù)的字節(jié)碼,Hook 線程的構造函數(shù)自娩,讓每次在調(diào)用Thread的構造函數(shù)的時候就會調(diào)用
* ThreadCheck類的 printThread方法用踩,從而在控制臺打印線程的構造函數(shù)的調(diào)用堆棧,這些代碼可以借助
* ASM Bytecode Viewer Support Kotlin生成,MethodVisitor是ASM提供的一個類捶箱,用于修改字節(jié)碼
*/
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的構造函數(shù)
*/
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 {
//表示要處理的數(shù)據(jù)類型是什么动漾,CLASSES 表示要處理編譯后的字節(jié)碼(可能是 jar 包也可能是目錄)丁屎,RESOURCES 表示要處理的是標準的 java 資源
inputTypes BlackHook.CONTENT_CLASS
//表示Transform 的作用域,這里設置的SCOPE_FULL_PROJECT代表作用域是全工程
scopes BlackHook.SCOPE_FULL_PROJECT
//表示是否支持增量編譯旱眯,false不支持
isIncremental false
//表示hook的方法
hookMethodList = getHookMethods()
}
以上的代碼其實是hook的Thread的構造函數(shù)晨川,將ThreadCheck的printThread方法hook到了Thread的構造函數(shù)中,每次調(diào)用線程的構造函數(shù)的時候就會調(diào)用ThreadCheck的printThread方法删豺,這個方法會打印出Thread的構造函數(shù)的調(diào)用堆棧共虑,從而可以在控制臺知道哪個頁面的哪行代碼實例化了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())
}
}
上面的代碼獲取了調(diào)用堆棧呀页,并且打印到控制臺
實現(xiàn)原理
首先它是一個gradle 的自定義Plugin妈拌,其次它是通過在編譯階段修改字節(jié)碼實現(xiàn)Hook,在編譯階段通過Tranfrom掃描所有的字節(jié)碼蓬蝶,然后根據(jù)在使用插件的時候設置的需要被Hook的方法尘分,插入需要被插入的字節(jié)碼,
需要被插入的字節(jié)碼也是在使用的時候設置的丸氛,例如下面的代碼
/**
* 返回hook線程構造函數(shù)的字節(jié)碼培愁,Hook 線程的構造函數(shù),讓每次在調(diào)用Thread的構造函數(shù)的時候就會調(diào)用
* ThreadCheck的 printThread方法缓窜,從而在控制臺打印線程的構造函數(shù)的調(diào)用堆棧定续,這些代碼可以借助
* ASM Bytecode Viewer Support Kotlin生成,MethodVisitor是ASM提供的一個類禾锤,用于修改字節(jié)碼
*/
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)
}
準備過程
實現(xiàn)這個gradle插件需要我們有足夠的預備知識私股,如下:
- 首先要知道如何使用Android Studio開發(fā)Gradle插件
- 了解TransformAPI:Transform API是從Gradle 1.5.0版本之后提供的,它允許第三方在打包Dex文件之前的編譯過程中修改java字節(jié)碼(自定義插件注冊的transform會在ProguardTransform和DexTransform之前執(zhí)行,所以自動注冊的類不需要考慮混淆的情況).參考文章有:
- Android 熱修復使用Gradle Plugin1.5改造Nuwa插件(主要看前半部分關于TransformAPI的介紹时肿,Nuwa相關的內(nèi)容可先忽略)
- 字節(jié)碼修改框架(相比于Javassist框架ASM較難上手庇茫,但性能更高,但相學習難度阻擋不了我們對性能的追求):
- ASM英文文檔
- ASM API文檔
- Android 熱修復方案Tinker(七) 插樁實現(xiàn)(主要看關于ASM使用的介紹及與transformAPI的結合)
實現(xiàn)過程
1.自定義gradle plugin
因為這是一個gradle插件螃成,所以需要我們自定義一個gradle的plugin
1. 新建一個模塊
在工程中新建一個模塊旦签,命名為"buildSrc",注意寸宏,一定要命名為buildSrc宁炫,否則在工程中必須要將代碼發(fā)布到本地或者遠程maven倉庫中才能正常使用,這樣調(diào)試不方便氮凝,如下所示:
[圖片上傳失敗...(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. 實現(xiàn)Plugin類
新建groovy文件夾,新建BlackHookPlugin類,繼承Transform類竿秆,實現(xiàn)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. 實現(xiàn)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;
}
}
這個類用于接收開發(fā)人員使用插件的時候設置的參數(shù)和需要被Hook的方法以及參與Hook的字節(jié)碼蕾羊,我們在使用blackHook插件的時候可以使用DSL的方式來使用,如下代碼所示:
blackHook {
//表示要處理的數(shù)據(jù)類型是什么帽驯,CLASSES 表示要處理編譯后的字節(jié)碼(可能是 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.開始實現(xiàn)掃描
需要在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;
}
// 掃描到每個類中的方法的時候會回調(diào)到這個方法
@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類存儲的參數(shù)交給 AllMethodVisitor對象,由AllMethodVisitor來判斷是否需要Hook指定的方法
return new AllMethodVisitor(blackHook, mv, access, name, descriptor, className, superClassName);
}
然后在AllClassVisitor類中會將將掃描到的類和方法的信息以及BlackHook擴展類存儲的參數(shù)交給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);
//這里根據(jù)開發(fā)人員設置的需要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;
}
}
}
}
}
在這個類中根據(jù)開發(fā)人員調(diào)用插件的時候設置的需要hook的方法以及掃描到的方法來判斷是否需要hook