1. 什么是插樁?
用通俗的話來講,插樁就是將一段代碼通過某種策略插入到另一段代碼,或替換另一段代碼。這里的代碼可以分為源碼
和字節碼
,而我們所說的插樁一般指字節碼插樁。
下圖是Android開發者常見的一張圖,我們編寫的源碼(.java)通過javac編譯成字節碼(.class),然后通過dx/d8編譯成dex文件(.dex)。
我們下面要講的插樁,就是在.class轉為.dex之前,修改.class文件從而達到修改或替換代碼的目的。
那有人肯定會有這樣的疑問?既然插樁是插入或替換代碼,那為何我不自己直接插入或替換呢?為何還要用這么“復雜”的工具?別著急,第二個問題將會給你答案。
2. 插樁的應用場景有哪些?
技術是服務于業務的,一個無法推進業務進步的技術并不值得我們學習。在上面,我們對插樁的理解是:插入,替換代碼。那么,結合這個核心主線我們來挖掘插樁能被應用的場景有哪些?
-
代碼插入
我們所熟悉的ButterKnife,Dagger這些常用的框架,也是在編譯期間生成了代碼,簡化了程序員的操作。假設有這么一個需求,要監控某些或者所有方法的執行耗時?你會怎么做呢?如果你監控的方法只有十幾個或者幾十個,那么也許通過程序員自身的編碼就能輕松解決;但是如果監控的方法達到百千甚至萬級別,你還通過編碼來解決?那么程序員存在的價值在哪里?面對這樣的重復勞動問題,最先想到的就應該是自動化,也就是我們今天所講的插樁。通過插樁,我們掃描每一個class文件,并針對特定規則進行字節碼修改從而達到監控每個方法耗時的目的。關于如何實現這樣的需求,后面我會詳細講述。
-
代碼替換
如果遇到這么一個需求,需要將項目中所有使用某個方法(如Dialog.show())的地方替換成自己包裝的方法(MyDialog.show()),那么你該如何解決呢?有人會說,直接使用快捷鍵就能全局替換。那么有兩個問題
- 如果有其他類定義了show()方法,并被調用了,直接使用快捷鍵是否會被錯誤替換?
- 如果其他引用包使用了該方法,你怎么替換呢?
沒關系,插樁同樣可以解決你的問題。
綜合上面所說的兩點,其實很多業務場景都使用了插樁技術,比如無痕埋點,性能監控等。
3. 掌握插樁應該具備的基礎知識有哪些?
上面講了插樁的應用場景,是否現在想躍躍欲試呢?別著急,想掌握好插樁技術,練就扎實的插樁功底,我們是需要具備一些基礎知識的。
熟練掌握字節碼相關技術。可參考 一文讓你明白Java字節碼
Gradle自定義插件,直接參考官網 Writing Custom plugins
如果你想運用在Android項目中,那么還需要掌握Transform API,
這是android在將class轉成dex之前給我們預留的一個接口,在該接口中我們可以通過插件形式來修改class文件。字節碼修改工具。如AspectJ,ASM,javasisst。這里我推薦使用ASM,關于ASM相關知識,在下一章我給大家簡單介紹。同樣大家可以參考 Asm官方文檔
groovy語言基礎
如果你具備了上面5塊知識,那么恭喜你,會很順利的完成字節碼插樁技術了。下面,我通過實戰一個很簡單的例子,帶領大家一起領略插樁的風采。
4. 使用ASM進行字節碼插樁
4.1 什么是ASM?
ASM是生成和轉換已編譯的Java類工具,就是我們插樁需要使用的工具。
4.2 兩種API?
ASM提供了兩種API來生成和轉換已編譯類
- 一個是核心API,以基于事件形式來表示類
- 一個是樹API,以基于對象形式來表示類
4.3 基于事件形式
我們通過上面的基礎知識,了解到類的結構,類包含字段,方法,指令等;基于事件的API把類看作是一系列事件來表示,每一個類的事件表示一個類的元素。類似解析XML的SAX
4.4 基于對象形式
基于對象的API將類表示成一棵對象樹,每個對象表示類的一部分。類似解析XML的DOM
4.5 優缺點比較
事件形式 | 對象形式 | |
---|---|---|
內存占用 | 少 | 多 |
實現難度 | 難 | 易 |
通過上面表格,我們清楚的了解到:
- 事件API內存占用少于對象API,因為事件API不需要在內存中創建和存儲對象樹
- 事件API實現難度比對象API大,因為事件API在任意時刻類中只有一個元素可使用,但是對象API能獲得整個類。
那么接下來,我們就通過比較容易實現的對象API入手,一起完成上面的需求。
我們Android的構建工具是Gradle,因此我們結合transform和Gradle插件方式來完成該需求,接下來我們來看看gradle官方提供的3種插件形式
4.6 Gradle插件的3種形式
插件形式 | 說明 |
---|---|
Build script | 直接在build script中寫插件代碼,不可復用 |
buildSrc | 獨立項目結構,只能在本構建體系中復用,無法提供給其他項目 |
Standalone | 獨立項目結構,發布到倉庫,可以復用 |
由于我們是demo,并不需要共享給其他項目,因此采用buildSrc方式即可,但是正常項目中都采用Standalone形式。
5. 實踐
5.1 目標
實現自定義gradle插件,通過ASM實現在MainActivity的onCreate中插入Log打印語句
5.2 自定義Gradle插件實現
gradle實現自定義插件一般有三種方式,考慮到靈活性,我們選擇第三種Standalone方式實現自定義插件
5.2.1 創建module
創建一個新的module,刪除不必要的文件,只留下build.gradle, src/main這兩個文件和文件夾
5.2.2 創建目錄和配置文件
-
創建代碼目錄
在src/main
下創建 java和groovy目錄以及resources/META-INF/gradle-plugins
目錄,META-INF和gradle-plugins均為package
-
創建配置文件
在resources/META-INF/gradle-plugins
下創建xxxx.properties
文件, xxxx為apply plugin時用到的名字,文件中內容為implementation-class = 插件的完整路徑
,這里可以先空著,后面創建了插件后再填入
5.2.3 build.gradle編寫
build.gradle中引入groovy和maven插件,然后引入gradle插件,asm以及gradle api和庫,最后進行編譯
注意:此處的定義的group + 此module名(或者archivesBaseName定義) + version
即是插件的依賴地址
,下方倉庫設置中的pom.groupId, pom.artifactId, pom.version是同樣的效果,同時設置會生成兩個不同地址
apply plugin: 'groovy'
apply plugin: 'maven'
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation gradleApi()
implementation localGroovy()
implementation 'com.android.tools.build:gradle:3.5.3'
}
group='danny.lifecycle.plugin'
version='1.0.0'
uploadArchives {
repositories {
mavenDeployer {
// pom.groupId = 'com.xxx.plugin.gradle' //groupId
// pom.artifactId = 'xxx' //artifactId
// pom.version = '1.0.2' //版本號
//本地的Maven地址設置
repository(url: uri('../asm_lifecycle_repo'))
}
}
}
5.2.4 編寫插件
編譯完成后在groovy中添加package,創建實現Plugin<>接口的類文件,先使用java文件便于包和類的引入,具體實現如下,編寫完成后將.java后綴改成.groovy,這就是自定義插件的入口
public class LifeCyclePlugin implements Plugin<Project> {
@Override
public void apply(Project project) {
}
}
同級目錄下創建繼承Transform的類文件,同樣使用java文件導入包和引用類,具體實現如下,編寫完成后后綴改為.groovy,Transform的作用是可以在項目構建過程中.class文件轉換成.dex文件期間獲取到.class文件進行讀取修改操作
public class LifeCycleTransform extends Transform {
@Override
public String getName() {
return "LifeCycleTransform";
}
@Override
public Set<QualifiedContent.ContentType> getInputTypes() {
return TransformManager.CONTENT_CLASS;
}
@Override
public Set<? super QualifiedContent.Scope> getScopes() {
return TransformManager.PROJECT_ONLY;
}
@Override
public boolean isIncremental() {
return false;
}
@Override
public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
}
}
getName():這里可以指定此task的名字,不過最終名字需要做一些拼接,transformClassesWith名字ForDebug/Release
getInputTypes():處理的文件類型,此處為class文件
getScopes():作用范圍,此處為只處理當前項目文件
isIncremental():是否支持增量編譯
transform(TransformInvocation transformInvocation):主要處理文件和jar包的方法
編寫完transform后,在plugin中進行注冊,AppExtension就是指的build.gradle中的android{}閉包
public class LifeCyclePlugin implements Plugin<Project> {
@Override
public void apply(Project project) {
def extension = project.extensions.getByType(AppExtension)
LifeCycleTransform transform = new LifeCycleTransform();
extension.registerTransform(transform)
}
}
5.2.5 生成倉庫
此時在gradle任務中生成了uploadArchives任務,雙擊后即可生成插件倉庫
5.2.6 自定義gradle插件引入和使用
在項目的根目錄build.gradle中添加倉庫路徑,然后在dependencies中添加classpath,引入插件
buildscript {
repositories {
google()
jcenter()
maven {
url uri('./asm_lifecycle_repo')
}
}
dependencies {
classpath 'com.android.tools.build:gradle:3.5.3'
classpath 'danny.lifecycle.plugin:asm_lifecycle_plugin3:1.0.0'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
接著在要使用的module的build.gradle中通過apply plugin引入插件使用
apply plugin: 'com.android.application'
apply plugin: 'danny.asm.lifecycle'
這樣,就完成了自定義gradle插件的編寫和使用
5.3. ASM實現字節碼插樁
5.3.1 引入ASM
完成自定義gradle插件后,在插件的build.gradle中添加ASM依賴
apply plugin: 'groovy'
apply plugin: 'maven'
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation gradleApi()
implementation localGroovy()
implementation 'com.android.tools.build:gradle:3.5.3'
//ASM相關依賴
implementation 'org.ow2.asm:asm:7.1'
implementation 'org.ow2.asm:asm-commons:7.1'
}
group='danny.lifecycle.plugin'
version='1.0.0'
uploadArchives {
repositories {
mavenDeployer {
// pom.groupId = 'com.xxx.plugin.gradle' //groupId
// pom.artifactId = 'xxx' //artifactId
// pom.version = '1.0.2' //版本號
//本地的Maven地址設置
repository(url: uri('../asm_lifecycle_repo'))
}
}
}
5.3.2 創建Visitor
在自定義插件module中的src/main/java下添加package,然后創建繼承ClassVisitor的類文件,實現visit,visitMethod方法
public class LifecycleClassVisitor extends ClassVisitor {
private String className;
private String superName;
public LifecycleClassVisitor(ClassVisitor classVisitor) {
super(Opcodes.ASM5, classVisitor);
}
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
super.visit(version, access, name, signature, superName, interfaces);
this.className = name;
this.superName = superName;
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
MethodVisitor methodVisitor = cv.visitMethod(access, name, descriptor, signature, exceptions);
if (className.equals("com/example/lifecycledemo/MainActivity") && superName.equals("androidx/appcompat/app/AppCompatActivity")) {
if (name.startsWith("onCreate")) {
return new LifeCycleMethodVisitor(Opcodes.ASM5, methodVisitor, access, name, descriptor, className, superName);
}
}
return methodVisitor;
}
@Override
public void visitEnd() {
super.visitEnd();
System.out.println("ClassVisitor visitEnd()");
}
}
在visit方法中獲取類名,超類名,在visitMethod方法中篩選類名MainActivity,超類AppCompatActivity的文件,接著篩選onCreate方法,最后返回一個繼承自 AdviceAdater的類
public class LifeCycleMethodVisitor extends AdviceAdapter {
private String className;
private String methodName;
protected LifeCycleMethodVisitor(int api, MethodVisitor methodVisitor, int access, String name, String descriptor, String className, String superName) {
super(api, methodVisitor, access, name, descriptor);
this.className = className;
this.superName = superName;
System.out.println("MethodVisitor Constructor");
}
@Override
protected void onMethodEnter() {
super.onMethodEnter();
System.out.println("MethodVisitor visitCode========");
mv.visitLdcInsn("TAG");
mv.visitLdcInsn(className + "---->" + methodName);
mv.visitMethodInsn(Opcodes.INVOKESTATIC, "android/util/Log", "i", "(Ljava/lang/String;Ljava/lang/String;)I", false);
mv.visitInsn(Opcodes.POP);
}
@Override
protected void onMethodExit(int opcode) {
mv.visitLdcInsn("TAG");
mv.visitLdcInsn("this is end");
mv.visitMethodInsn(Opcodes.INVOKESTATIC, "android/util/Log", "i", "(Ljava/lang/String;Ljava/lang/String;)I", false);
mv.visitInsn(Opcodes.POP);
super.onMethodExit(opcode);
}
}
在繼承了AdviceAdapter的類中,實現onMethodEnter
和onMethodExit
方法,對應onCreate方法的開始和結束節點,在這兩個節點通過MethodVisitor的一系列api調用插入Log打印語句
visitLdcInsn(final Object value)對應LDC指令
visitMethodInsn(final int opcode, final String owner, final String name, final String descriptor, final boolean isInterface)是訪問方法指令,此處用到的五個參數
opcode: 對應字節碼指令操作碼,此處傳入了 調用類方法的指令
owner: 方法所在包
name: 方法名
descriptor: 方法描述符,前一個Ljava/lang/String:Ljava/lang/String指明方法有兩個String類型參數,最后的 I 表示方法返回int類型
isInterface: 是否是接口類的實現方法
visitInsn()對應空操作數指令,比如POP, DUP
注:此處也可直接繼承MethodVisitor,實現visitCode方法插入代碼,但要實現在方法結束前插入代碼需要另外實現visitInsn(int opcode)方法,根據opcode == RETURN來判斷指令執行到方法末尾了,插入代碼后再調用super方法即可
5.3.3 讀取class文件數據
在自定義繼承Transform類的transform方法中進行操作,通過getInputs()獲取輸入的class文件和jar包的路徑,outputProvider管理輸出路徑,接著遍歷inputs,directoryInputs獲取到class文件的路徑集合,再次遍歷,篩選出class文件,通過ClassReader進行讀取,ClassWriter進行寫入,將classWriter傳入自定義的ClassVisitor中,接著調用classReader的accept方法正式對class文件進行讀取并調用classVisitor中的方法,比如visit(),visitMethod()等,我們在里面對MainActivity的onCreate方法中加入了一行Log打印語句,然后通過classWriter的toByteArray()方法輸出修改后的class文件btye數組,覆蓋掉原來的class文件,最后將修改后的class文件目錄整個copy新的目錄下,這個新目錄是根據輸入的內容,作用范圍等信息生成的,供下一個Task使用
@Override
public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
Collection<TransformInput> inputs = transformInvocation.getInputs()
TransformOutputProvider outputProvider = transformInvocation.outputProvider
inputs.each {TransformInput input ->
input.directoryInputs.each {DirectoryInput directoryInput ->
File dir = directoryInput.file
if (dir) {
dir.traverse (type: FileType.FILES, nameFilter: ~/.*\.class/) { File file ->
println("find class: " + file.name)
//對class文件進行讀取
ClassReader classReader = new ClassReader(file.bytes)
//對class文件的寫入
ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
//訪問class文件相應的內容,解析到某一個結構就會通知到classVisitor相應的方法
println("before visit")
ClassVisitor visitor = new LifecycleClassVisitor(classWriter)
println("after visit")
//依次調用ClassVisitor接口的各個方法
classReader.accept(visitor, ClassReader.EXPAND_FRAMES)
println("after accept")
//toByteArray方法會將最終修改的字節碼以byte數組形式返回
byte[] bytes = classWriter.toByteArray()
//通過文件流寫入方式覆蓋掉原先的內容,實現class文件的改寫
FileOutputStream fileOutputStream = new FileOutputStream(file.path)
fileOutputStream.write(bytes)
fileOutputStream.close()
}
}
def dest = outputProvider.getContentLocation(directoryInput.name, directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)
FileUtils.copyDirectory(directoryInput.file, dest)
}
}
}
5.3.4 運行
代碼都全部編寫之后,再次點擊uploadArchives任務生成本地倉庫,然后就可以運行項目檢測插樁是否成功了,這是項目中的MainActivity文件,可以看到只在onCreate中第一行打印了一個log
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
Log.i("TAG", "is this the first log?");
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
項目運行后,在LogCat中篩選TAG,可以看到打印語句的輸出,onMethodEnter中插入的語句最先輸出,接著是在onCreate方法開頭的語句,最后是在onMethodExit中插入的語句
5.4. 總結
自定義Gradle插件遵循一定的規則,手動實現幾次就能掌握,插件和ASM的銜接在Transform中完成,插件負責輸入數據,ASM接收數據后進行字節碼修改,最后再重新輸出,ASM的使用主要還是流程和api的掌握,比較不好編寫的是最終插入和修改字節碼的api,如果對字節碼指令不太熟悉的話可以安裝一個ASM Bytecode Viewer插件,將相關操作在java文件中完成,運用插件編譯成字節碼和ASM指令格式,照搬過來就行
轉自:
http://www.lxweimin.com/p/13d18c631549
https://blog.csdn.net/tushiba/article/details/106361871