1 什么是插樁护赊?
聽到關(guān)于“插樁”的詞語惠遏,第一眼覺得會很高深,那到底什么是插樁呢骏啰?用通俗的話來講节吮,插樁就是將一段代碼通過某種策略插入到另一段代碼,或替換另一段代碼判耕。這里的代碼可以分為源碼和字節(jié)碼透绩,而我們所說的插樁一般指字節(jié)碼插樁。
圖1是Android開發(fā)者常見的一張圖壁熄,我們編寫的源碼(.java)通過javac編譯成字節(jié)碼(.class)帚豪,然后通過dx/d8編譯成dex文件。
我們下面要講的插樁草丧,就是在.class轉(zhuǎn)為.dex之前狸臣,修改.class文件從而達到修改或替換代碼的目的。
那有人肯定會有這樣的疑問昌执?既然插樁是插入或替換代碼烛亦,那為何我不自己直接插入或替換呢诈泼?為何還要用這么“復(fù)雜”的工具?別著急煤禽,第二個問題將會給你答案铐达。
2 插樁的應(yīng)用場景有哪些?
技術(shù)是服務(wù)于業(yè)務(wù)的檬果,一個無法推進業(yè)務(wù)進步的技術(shù)并不值得我們學(xué)習(xí)瓮孙。在上面,我們對插樁的理解是:插入选脊,替換代碼杭抠。那么,結(jié)合這個核心主線我們來挖掘插樁能被應(yīng)用的場景有哪些知牌?
-
代碼插入
我們所熟悉的ButterKnife祈争,Dagger這些常用的框架斤程,也是在編譯期間生成了代碼角寸,簡化了程序員的操作。假設(shè)有這么一個需求忿墅,要監(jiān)控某些或者所有方法的執(zhí)行耗時扁藕?你會怎么做呢?如果你監(jiān)控的方法只有十幾個或者幾十個疚脐,那么也許通過程序員自身的編碼就能輕松解決亿柑;但是如果監(jiān)控的方法達到百千甚至萬級別,你還通過編碼來解決棍弄?那么程序員存在的價值在哪里望薄?面對這樣的重復(fù)勞動問題,最先想到的就應(yīng)該是自動化呼畸,也就是我們今天所講的插樁痕支。通過插樁,我們掃描每一個class文件蛮原,并針對特定規(guī)則進行字節(jié)碼修改從而達到監(jiān)控每個方法耗時的目的卧须。關(guān)于如何實現(xiàn)這樣的需求,后面我會詳細講述儒陨。
-
代碼替換
如果遇到這么一個需求花嘶,需要將項目中所有使用某個方法(如Dialog.show())的地方替換成自己包裝的方法(MyDialog.show()),那么你該如何解決呢蹦漠?有人會說椭员,直接使用快捷鍵就能全局替換。那么有兩個問題
1 如果有其他類定義了show()方法笛园,并被調(diào)用了隘击,直接使用快捷鍵是否會被錯誤替換容劳?
2 如果其他引用包使用了該方法,你怎么替換呢闸度?
沒關(guān)系竭贩,插樁同樣可以解決你的問題。
綜合上面所說的兩點莺禁,其實很多業(yè)務(wù)場景都使用了插樁技術(shù)留量,比如無痕埋點,性能監(jiān)控等哟冬。
3 掌握插樁應(yīng)該具備的基礎(chǔ)知識有哪些楼熄?
熟練掌握字節(jié)碼相關(guān)技術(shù)『葡浚可參考 一文讓你明白Java字節(jié)碼
Gradle自定義插件可岂,直接參考官網(wǎng) Writing Custom plugins
如果你想運用在Android項目中,那么還需要掌握Transform API,
這是android在將class轉(zhuǎn)成dex之前給我們預(yù)留的一個接口翰灾,在該接口中我們可以通過插件形式來修改class文件缕粹。字節(jié)碼修改工具。如AspectJ纸淮,ASM平斩,javasisst。這里我推薦使用ASM咽块,關(guān)于ASM相關(guān)知識绘面,在下一章我給大家簡單介紹。同樣大家可以參考 Asm官方文檔
groovy語言基礎(chǔ)
如果你具備了上面5塊知識侈沪,那么恭喜你揭璃,會很順利的完成字節(jié)碼插樁技術(shù)了充石。下面蝶俱,我通過實戰(zhàn)一個很簡單的例子,帶領(lǐng)大家一起領(lǐng)略插樁的風(fēng)采荠商。
4 使用ASM進行字節(jié)碼插樁
1 什么是ASM?
ASM是生成和轉(zhuǎn)換已編譯的Java類工具皆撩,就是我們插樁需要使用的工具扣墩。
2 兩種API?
ASM提供了兩種API來生成和轉(zhuǎn)換已編譯類扛吞,一個是核心API呻惕,以基于事件形式來表示類;另一個是樹API滥比,以基于對象形式來表示類亚脆。
3 基于事件形式
我們通過上面的基礎(chǔ)知識,了解到類的結(jié)構(gòu)盲泛,類包含字段濒持,方法键耕,指令等;基于事件的API把類看作是一系列事件來表示柑营,每一個類的事件表示一個類的元素屈雄。類似解析XML的SAX
4 基于對象形式
基于對象的API將類表示成一棵對象樹,每個對象表示類的一部分官套。類似解析XML的DOM
5 優(yōu)缺點比較
通過上面表格酒奶,我們清楚的了解到:
- 事件API內(nèi)存占用少于對象API,因為事件API不需要在內(nèi)存中創(chuàng)建和存儲對象樹
- 事件API實現(xiàn)難度比對象API大奶赔,因為事件API在任意時刻類中只有一個元素可使用惋嚎,但是對象API能獲得整個類。
那么接下來站刑,我們就通過比較容易實現(xiàn)的對象API入手另伍,一起完成上面的需求。
我們Android的構(gòu)建工具是Gradle绞旅,因此我們結(jié)合transform和Gradle插件方式來完成該需求摆尝,接下來我們來看看gradle官方提供的3種插件形式
6 Gradle插件的3種形式
ASM
ASM是一種基于java字節(jié)碼層面的代碼分析和修改工具,ASM的目標是生成玻靡,轉(zhuǎn)換和分析已編譯的java class文件结榄,可使用ASM工具讀/寫/轉(zhuǎn)換JVM指令集。通俗點講就是來處理javac編譯之后的class文件
Java字節(jié)碼
Java字節(jié)碼是Java虛擬機執(zhí)行的一種指令格式囤捻。通俗來講字節(jié)碼就是經(jīng)過javac命令編譯之后生成的Class文件。Class文件包含了Java虛擬機指令集和符號表以及若干其他的輔助信息邻寿。Class是一組以8位字節(jié)為基礎(chǔ)單位的二進制文件蝎土。各個數(shù)據(jù)項目嚴格按照順序緊湊的排列在Class文件之中。中間沒有任何分隔符绣否,這使得整個Class文件中存儲的內(nèi)容幾乎全是程序運行時的必要數(shù)據(jù)誊涯。
class文件有固定的結(jié)構(gòu),保留了幾乎所有的源代碼文件中的符號蒜撮。class文件的結(jié)構(gòu):
- 描述類的modifier暴构,name,父類段磨,接口和注釋
- 描述類中變量的modfier取逾,名字,類型和注釋
- 描述類中方法和構(gòu)造函數(shù)的modifier苹支,名字參數(shù)類型砾隅,返回類型,注釋等信息债蜜,當(dāng)然也包含已編譯成java字節(jié)碼指令序列的方法具體內(nèi)容
- class文件的靜態(tài)池區(qū)域晴埂,用來保存所有的數(shù)字究反,字符串,類型的常量儒洛,這些常量只被定義過一次且被其他class中區(qū)域所引用
一個Java文件編譯之后可能對應(yīng)多個class文件精耐。
字節(jié)碼描述符
類
Class文件中使用全限定名來表示一個類的引用,即把類名所有“.”換成了“/”琅锻,例如:
android.content.Context在class中文android/content/Context-
數(shù)據(jù)類型
數(shù)據(jù)類型黍氮、方法的參數(shù)列表(包括數(shù)量、類型以及順序)和返回值以及代表無返回值的void類型都用一個大寫字符來表示
如:
String[] -> [Ljava/lang/String;
int[][] -> [[I; 方法
方式使用(), 按照參數(shù)列表浅浮,返回值的順序表示沫浆。 例如:
void init() -> ()V
void test(object) -> (Ljava/lang/object;)V
String[] getArray(String s) -> (Ljava/lang/String;)[Ljava/lang/String;
1. 采用groovy創(chuàng)建插件
一,新建一個Java Library module
滚秩,在新建的javaModule
中专执,刪除 src->main 下面的java
目錄,新建一個groovy
目錄在groovy
目錄下創(chuàng)建類似java
的package
實際上是groovy
語言的包郁油。
注意:如果是kotlin 語言建立java的目錄本股,java語言也是java目錄,根據(jù)不同的語言來建立文件夾
這邊是groovy
語言就用groovy
目錄
二桐腌,在 src->main
下面創(chuàng)建一個 resources
目錄拄显,在resources
目錄下依次創(chuàng)建META-INF/gradle-plugins
目錄,最后在該目錄下創(chuàng)建一個名為 com.hm.plugin.lifecycle.properties
的文本文件案站,文件名是你要定義的插件名躬审,按需自定義即可。最后的工程結(jié)構(gòu)如圖所示:
注意: 這邊的META-INF/gradle-plugins
不是一個文件夾名字叫META-INF/gradle-plugins
而是META-INF
文件夾下有個gradle-plugins
文件夾
修改module的build.gradle文件蟆盐,引入groovy插件等:
apply plugin: 'java-library'
apply plugin: 'groovy'
apply plugin: 'maven'
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
compile gradleApi()
compile localGroovy()
compile 'com.android.tools.build:transform-api:1.5.0'
compile 'com.android.tools.build:gradle:3.0.1'
}
sourceCompatibility = "1.7"
targetCompatibility = "1.7"
//通過maven將插件發(fā)布到本地的腳本配置承边,根據(jù)自己的要求來修改
uploadArchives {
repositories.mavenDeployer {
pom.version = '1.0.0'
pom.artifactId = 'hmlifecyclepluginlocal'
pom.groupId = 'com.heima.iou'
repository(url: "file:///Users/hjy/.m2/repository/")
}
}
注意:根據(jù)不同語言依賴不同的語言插件
apply plugin:'java-library'
apply plugin:'kotlin'
apply plugin:'groovy'
apply plugin:'maven'
如果是kotlin語言編寫插件,就要用apply plugin:'kotlin'
這里有幾點需要說明的是:
- 通常都是采用groovy語言來創(chuàng)建gradle plugin的石挂,groovy是兼容java的博助,你完全可以采用java來編寫插件。關(guān)于groovy語言痹愚,了解一些基礎(chǔ)語法就足夠支撐我們?nèi)ゾ帉懖寮恕?/li>
- 在 src/main/resources/META-INF/gradle-plugins目錄下定義插件聲明富岳,*.properties文件的文件名就是插件名稱。
2. 實現(xiàn)Plugin接口
要編寫一個插件是很簡單的拯腮,只需實現(xiàn)Plugin接口即可窖式。
package com.hm.iou.lifecycle.plugin
import com.android.build.gradle.AppExtension
import org.gradle.api.Plugin
import org.gradle.api.Project
class LifeCyclePlugin implements Plugin<Project>{
@Override
void apply(Project project) {
println "------LifeCycle plugin entrance-------"
}
}
接著在com.hm.plugin.lifecycle.properties文件里增加配置:
implementation-class=com.hm.iou.lifecycle.plugin.LifeCyclePlugin
其中implementation-class的值為Plugin接口的實現(xiàn)類的全限定類名,至此為止一個最簡單的插件編寫好了疾瓮,它的功能很簡單脖镀,僅僅是在控制臺打印一句文本而已。
插件的發(fā)布
我們通過maven將該插件發(fā)布到本地的maven倉庫里,發(fā)布成功后蜒灰,我們在app module里引入該插件弦蹂,修改app module目錄下的build.gradle文件,增加如下配置
apply plugin: 'com.android.application'
//引入自定義插件强窖,插件名與前面的*.properties文件的文件名是一致的
apply plugin: 'com.hm.plugin.lifecycle'
buildscript {
repositories {
google()
jcenter()
//自定義插件maven地址凸椿,替換成你自己的maven地址
maven { url 'file:///Users/hjy/.m2/repository/' }
}
dependencies {
//通過maven加載自定義插件
classpath 'com.heima.iou:hmlifecyclepluginlocal:1.0.0'
}
}
我們build一下工程,在Gradle Console里會打印出"------LifeCycle plugin entrance-------"來翅溺,這說明我們的自定義插件成功了脑漫。
講到這里可以看到,按這個步驟實現(xiàn)一個gradle插件是很簡單的咙崎,它并沒有我們想象中那么高深莫測优幸,你也可以自豪地說我會制作gradle插件了。
3. Gradle Transform
然而前面這個插件并沒有什么卵用褪猛,它僅僅只是在編譯時网杆,在控制臺打印一句話而已。那么怎么通過插件在打包前去掃描所有的class文件呢伊滋,幸運的是官方給我們提供了Gradle Transform
技術(shù)碳却,簡單來說就是能夠讓開發(fā)者在項目構(gòu)建階段即由class到dex轉(zhuǎn)換期間修改class文件,Transform階段會掃描所有的class文件和資源文件笑旺,具體技術(shù)我這里不詳細展開昼浦,下面通過偽代碼部分說下我的思路。
public class CustomTransform extends Transform {
@Override
public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
super.transform(transformInvocation);
//當(dāng)前是否是增量編譯(由isIncremental() 方法的返回和當(dāng)前編譯是否有增量基礎(chǔ))
boolean isIncremental = transformInvocation.isIncremental();
//消費型輸入筒主,可以從中獲取jar包和class文件夾路徑关噪。需要輸出給下一個任務(wù)
Collection<TransformInput> inputs = transformInvocation.getInputs();
//OutputProvider管理輸出路徑,如果消費型輸入為空物舒,你會發(fā)現(xiàn)OutputProvider == null
TransformOutputProvider outputProvider = transformInvocation.getOutputProvider();
for(TransformInput input : inputs) {
for(JarInput jarInput : input.getJarInputs()) {
File dest = outputProvider.getContentLocation(
jarInput.getFile().getAbsolutePath(),
jarInput.getContentTypes(),
jarInput.getScopes(),
Format.JAR);
//將修改過的字節(jié)碼copy到dest色洞,就可以實現(xiàn)編譯期間干預(yù)字節(jié)碼的目的了
FileUtils.copyFile(jarInput.getFile(), dest);
}
for(DirectoryInput directoryInput : input.getDirectoryInputs()) {
File dest = outputProvider.getContentLocation(directoryInput.getName(),
directoryInput.getContentTypes(), directoryInput.getScopes(),
Format.DIRECTORY);
//將修改過的字節(jié)碼copy到dest,就可以實現(xiàn)編譯期間干預(yù)字節(jié)碼的目的了
FileUtils.copyDirectory(directoryInput.getFile(), dest);
}
}
}
@Override
public String getName() {
return "CustomTransform";
}
@Override
public boolean isIncremental() {
return true; //是否開啟增量編譯
}
@Override
public Set<QualifiedContent.ContentType> getInputTypes() {
return TransformManager.CONTENT_CLASS;
}
@Override
public Set<? super QualifiedContent.Scope> getScopes() {
return TransformManager.SCOPE_FULL_PROJECT;
}
}
Transform兩個過濾緯度
ContentType冠胯,數(shù)據(jù)類型,有CLASSES和RESOURCES兩種锦针。
其中的CLASSES包含了源項目中的.class文件和第三方庫中的.class文件荠察。
RESOURCES僅包含源項目中的.class文件。
對應(yīng)getInputTypes() 方法奈搜。
Scope悉盆,表示要處理的.class文件的范圍,主要有
PROJECT馋吗, SUB_PROJECTS焕盟,EXTERNAL_LIBRARIES等。
對應(yīng)getScopes() 方法宏粤。
什么是增量編譯
我理解的增量編譯:
1脚翘、基于Task的上次輸出快照和這次輸入快照對比灼卢,如果相同,則跳過相應(yīng)任務(wù)来农;
2鞋真、基于Task本身是否支持增量更新。
3.4沃于、增量編譯實驗
3.4.1涩咖、Transform 的isIncremental()返回true。
@Override
public boolean isIncremental() {
return true;
}
(1)繁莹、clean之后檩互,第一次編譯,即使Transform里面isIncremental()返回true咨演,Transform開啟了增量編譯闸昨,此時對Transform來說仍然不是增量編譯, transform方法中isIncremental = false雪标;
(2)零院、不做任何改變直接進行第二次編譯,Transform別標記為up-to-date村刨,被跳過執(zhí)行告抄;
(3)、修改一個文件中代碼嵌牺,進行第三次編譯打洼,此時對Transform來說是增量編譯,transform方法中isIncremental = true逆粹。
3.4.2募疮、Transform 的isIncremental()返回false。
@Override
public boolean isIncremental() {
return false;
}
(1)僻弹、clean之后阿浓,第一次編譯,此時對Transform來說不是增量編譯蹋绽, transform方法中isIncremental = false芭毙;
(2)、不做任何改變直接進行第二次編譯卸耘,Transform別標記為up-to-date退敦,被跳過執(zhí)行;
(3)蚣抗、修改一個文件中代碼侈百,進行第三次編譯,此時對Transform來說不是增量編譯,transform方法中isIncremental = false钝域。
結(jié)論:1讽坏、一次編譯對Transform來說是否是增量編譯取決于兩個方面:
(1)、當(dāng)前編譯是否有增量基礎(chǔ)网梢;
(2)震缭、當(dāng)前Transform是否開啟增量編譯。
結(jié)論:2战虏、不管Transform是否開啟增量編譯拣宰,若TransformTask的當(dāng)前輸入快照和上次輸出快照相同,則跳過當(dāng)前TransformTask烦感。
2.5巡社、支持增量編譯
Transform支持增量編譯分為兩步:
(1)重寫Transform的接口方法:isIncremental(),返回true手趣。
@Override
public boolean isIncremental() {
return true;
}
2)判斷當(dāng)前編譯對于Transform是否是增量編譯:
如果不是增量編譯晌该,就按照前面的方式,依次處理所有的class文件绿渣;
(比如說clean之后的第一次編譯沒有增量基礎(chǔ)朝群,即使Transform的isIncremental放回true,當(dāng)前編譯對Transform仍然不是增量編譯中符,所有需要依次處理所有的class文件)
如果是增量編譯姜胖,根據(jù)每個文件的Status,處理文件:
如果文件有改變淀散,就按照前面的方式右莱,去處理這個問題。
如果文件沒有改變档插,就不需要進行處理慢蜓,因為在輸出目錄已經(jīng)有一個上次處理過的class文件了
(NOTCHANGED: 當(dāng)前文件不需處理,甚至復(fù)制操作都不用郭膛;
ADDED晨抡、CHANGED: 正常處理,輸出給下一個任務(wù)则剃;
REMOVED: 移除outputProvider獲取路徑對應(yīng)的文件凄诞。)
注意:當(dāng)前編譯對于Transform是否是增量編譯受兩個方面的影響:
(1)isIncremental() 方法的返回值;
(2)當(dāng)前編譯是否有增量基礎(chǔ)忍级;(clean之后的第一次編譯沒有增量基礎(chǔ),之后的編譯有增量基礎(chǔ))
增量的時間縮短為全量的速度提升了3倍多伪朽,而且這個速度優(yōu)化會隨著工程的變大而更加顯著轴咱。
2.6、支持并發(fā)編譯
private WaitableExecutor waitableExecutor = WaitableExecutor.useGlobalSharedThreadPool();
//異步并發(fā)處理jar/class
waitableExecutor.execute(() -> {
bytecodeWeaver.weaveJar(srcJar, destJar);
return null;
});
waitableExecutor.execute(() -> {
bytecodeWeaver.weaveSingleClassToFile(file, outputFile, inputDirPath);
return null;
});
//等待所有任務(wù)結(jié)束
waitableExecutor.waitForTasksWithQuickFail(true);
為什么要等待所有任務(wù)結(jié)束?
如果不等待朴肺,主線程就會進入下一個任務(wù)的處理窖剑,可能當(dāng)前的任務(wù)的處理工作還沒完成。
并發(fā)Transform和非并發(fā)Transform下戈稿,編譯速度提高了80%西土。
修改Plugin接口實現(xiàn)類,在插件中注冊該Transfrom:
class LifeCyclePlugin implements Plugin<Project>{
@Override
void apply(Project project) {
println "------LifeCycle plugin entrance-------"
def android = project.extensions.getByType(AppExtension)
android.registerTransform(new LifeCycleTransform(project))
}
}
運行完以后鞍盗,在引用這個插件的目錄下的build目錄下會看到如下這個目錄
注意:在transform
中的transform(TransformInvocation transformInvocation)``` 這個方法中如果啥都不寫會報這個錯誤
就是因為在這個方法中必須要實現(xiàn)如下內(nèi)容
這個方法對應(yīng)的就是下面這個目錄文件需了,/intermediates/transforms/LifeCycleTransform/debug
這個目錄
如果沒有寫會是這個樣子的
這樣就會導(dǎo)致截屏2020-07-22 下午3.38.19.png
,你如果之前沒有寫截屏2020-07-22 下午3.45.29.png
這個代碼般甲,導(dǎo)致了上邊的錯誤肋乍,但是如果你在transform 中已經(jīng)寫了這段代碼,還是報截屏2020-07-22 下午3.38.19.png
這個錯誤
那是因為敷存,你沒有刪除app的目錄下的build的目錄墓造,
clean project下,就把app的build的目錄和project的build的目錄都就清空了锚烦,這樣運行觅闽,就沒有截屏2020-07-22 下午3.38.19.png
這個錯誤,
/intermediates/transforms/LifeCycleTransform/debug
這個目錄下涮俄,如下
在app->build->intermediates->transforms中蛉拙,可以看到所有的Transform,包括我們剛才自定義的Transform禽拔。從上圖中可以看到刘离,這里的0.jar、1.jar睹栖、2.jar等等硫惕,都是通過outputProvider.getContentLocation()方法來生成的,這個Transform目錄下的class文件野来、jar包等恼除,會當(dāng)做下一個Transform的inputs傳遞過去。
asm的原理:
ASM Core API可以類比解析XML文件中的SAX方式曼氛,不需要把這個類的整個結(jié)構(gòu)讀取進來豁辉,就可以用流式的方法來處理字節(jié)碼文件。好處是非常節(jié)約內(nèi)存舀患,但是編程難度較大徽级。然而出于性能考慮,一般情況下編程都使用Core API聊浅。在Core API中有以下幾個關(guān)鍵類:
? ClassReader:用于讀取已經(jīng)編譯好的.class文件餐抢。
? ClassWriter:用于重新構(gòu)建編譯后的類现使,如修改類名、屬性以及方法旷痕,也可以生成新的類的字節(jié)碼文件碳锈。
? 各種Visitor類:如上所述,CoreAPI根據(jù)字節(jié)碼從上到下依次處理欺抗,對于字節(jié)碼文件中不同的區(qū)域有不同的Visitor售碳,比如用于訪問方法的MethodVisitor、用于訪問類變量的FieldVisitor绞呈、用于訪問注解的AnnotationVisitor等贸人。為了實現(xiàn)AOP,重點要使用的是MethodVisitor报强。
4. 通過ASM動態(tài)修改字節(jié)碼
到現(xiàn)在灸姊,我們只剩下最后一步了,那就是如何注入代碼了秉溉。ASM 是一個 Java 字節(jié)碼操控框架力惯,它能被用來動態(tài)生成類或者增強既有類的功能。我這里對ASM不做詳細介紹了召嘶,主要是介紹使用ASM動態(tài)注入代碼的思路父晶。
首先,我們修改一下AppLifeCycleManager類弄跌,增加動態(tài)注入字節(jié)碼的入口方法:
/**
* 通過插件加載 IAppLike 類
*/
private static void loadAppLike() {
}
//通過反射去加載 IAppLike 類的實例
private static void registerAppLike(String className) {
if (TextUtils.isEmpty(className))
return;
try {
Object obj = Class.forName(className).getConstructor().newInstance();
if (obj instanceof IAppLike) {
APP_LIKE_LIST.add((IAppLike) obj);
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 初始化
*
* @param context
*/
public static void init(Context context) {
//通過插件加載 IAppLike 類
loadAppLike();
Collections.sort(APP_LIKE_LIST, new AppLikeComparator());
for (IAppLike appLike : APP_LIKE_LIST) {
appLike.onCreate(context);
}
}
相比之前甲喝,這里增加了一個loadAppLike()方法,在init()方法調(diào)用時會先執(zhí)行铛只。通過前面Transform步驟之后埠胖,我們現(xiàn)在的目標是把代碼動態(tài)插入到loadAppLike()方法里,下面這段代碼是我們期望插入后的結(jié)果:
private static void loadAppLike() {
registerAppLike("com.hm.iou.lifecycle.apt.proxy.Heima$$ModuleAAppLike$$Proxy");
registerAppLike("com.hm.iou.lifecycle.apt.proxy.Heima$$ModuleBAppLike$$Proxy");
registerAppLike("com.hm.iou.lifecycle.apt.proxy.Heima$$ModuleCAppLike$$Proxy");
registerAppLike("com.hm.iou.lifecycle.apt.proxy.Heima$$ModuleDAppLike$$Proxy");
}
這樣在初始化時淳玩,就已經(jīng)知道要加載哪些生命周期類直撤,來看看具體實現(xiàn)方法,關(guān)于ASM不了解的地方蜕着,需要先搞清楚其使用方法再來閱讀:
class AppLikeCodeInjector {
//掃描出來的所有 IAppLike 類
List<String> proxyAppLikeClassList
AppLikeCodeInjector(List<String> list) {
proxyAppLikeClassList = list
}
void execute() {
println("開始執(zhí)行ASM方法======>>>>>>>>")
File srcFile = ScanUtil.FILE_CONTAINS_INIT_CLASS
//創(chuàng)建一個臨時jar文件谋竖,要修改注入的字節(jié)碼會先寫入該文件里
def optJar = new File(srcFile.getParent(), srcFile.name + ".opt")
if (optJar.exists())
optJar.delete()
def file = new JarFile(srcFile)
Enumeration<JarEntry> enumeration = file.entries()
JarOutputStream jarOutputStream = new JarOutputStream(new FileOutputStream(optJar))
while (enumeration.hasMoreElements()) {
JarEntry jarEntry = enumeration.nextElement()
String entryName = jarEntry.getName()
ZipEntry zipEntry = new ZipEntry(entryName)
InputStream inputStream = file.getInputStream(jarEntry)
jarOutputStream.putNextEntry(zipEntry)
//找到需要插入代碼的class,通過ASM動態(tài)注入字節(jié)碼
if (ScanUtil.REGISTER_CLASS_FILE_NAME == entryName) {
println "insert register code to class >> " + entryName
ClassReader classReader = new ClassReader(inputStream)
// 構(gòu)建一個ClassWriter對象承匣,并設(shè)置讓系統(tǒng)自動計算棧和本地變量大小
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS)
ClassVisitor classVisitor = new AppLikeClassVisitor(classWriter)
//開始掃描class文件
classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES)
byte[] bytes = classWriter.toByteArray()
//將注入過字節(jié)碼的class蓖乘,寫入臨時jar文件里
jarOutputStream.write(bytes)
} else {
//不需要修改的class,原樣寫入臨時jar文件里
jarOutputStream.write(IOUtils.toByteArray(inputStream))
}
inputStream.close()
jarOutputStream.closeEntry()
}
jarOutputStream.close()
file.close()
//刪除原來的jar文件
if (srcFile.exists()) {
srcFile.delete()
}
//重新命名臨時jar文件韧骗,新的jar包里已經(jīng)包含了我們注入的字節(jié)碼了
optJar.renameTo(srcFile)
}
//插入字節(jié)碼的邏輯嘉抒,都在這個類里面
class AppLikeClassVisitor extends ClassVisitor {
AppLikeClassVisitor(ClassVisitor classVisitor) {
super(Opcodes.ASM5, classVisitor)
}
@Override
MethodVisitor visitMethod(int access, String name,
String desc, String signature,
String[] exception) {
println "visit method: " + name
MethodVisitor mv = super.visitMethod(access, name, desc, signature, exception)
//找到 AppLifeCycleManager里的loadAppLike()方法,我們在這個方法里插入字節(jié)碼
if ("loadAppLike" == name) {
mv = new LoadAppLikeMethodAdapter(mv, access, name, desc)
}
return mv
}
}
class LoadAppLikeMethodAdapter extends AdviceAdapter {
LoadAppLikeMethodAdapter(MethodVisitor mv, int access, String name, String desc) {
super(Opcodes.ASM5, mv, access, name, desc)
}
@Override
protected void onMethodEnter() {
super.onMethodEnter()
println "-------onMethodEnter------"
//遍歷插入字節(jié)碼袍暴,其實就是在 loadAppLike() 方法里插入類似registerAppLike("");的字節(jié)碼
proxyAppLikeClassList.forEach({proxyClassName ->
println "開始注入代碼:${proxyClassName}"
def fullName = ScanUtil.PROXY_CLASS_PACKAGE_NAME.replace("/", ".") + "." + proxyClassName.substring(0, proxyClassName.length() - 6)
println "full classname = ${fullName}"
mv.visitLdcInsn(fullName)
mv.visitMethodInsn(INVOKESTATIC, "com/hm/lifecycle/api/AppLifeCycleManager", "registerAppLike", "(Ljava/lang/String;)V", false);
})
}
@Override
protected void onMethodExit(int opcode) {
super.onMethodExit(opcode)
println "-------onMethodEnter------"
}
}
}
最后重新編譯插件再運行众眨,驗證結(jié)果握牧。
這里有個比較困難的地方,就是需要使用ASM編寫class字節(jié)碼娩梨。我這里推薦一個比較好用的方法:
- 將要注入的java源碼先寫出來;
- 通過javac編譯出class文件览徒;
- 通過asm-all.jar反編譯該class文件狈定,可得到所需的ASM注入代碼;
執(zhí)行命令如下:
java -classpath "asm-all.jar" org.objectweb.asm.util.ASMifier com/hm/lifecycle/api/AppLifeCycleManager.class
從中找到loadAppLike()方法字節(jié)碼處习蓬,這樣通過ASM注入代碼就比較簡單了:
{
mv = cw.visitMethod(ACC_PRIVATE + ACC_STATIC, "loadAppLike", "()V", null, null);
mv.visitCode();
mv.visitLdcInsn("com.hm.iou.lifecycle.apt.proxy.Heima$$ModuleAAppLike$$Proxy");
mv.visitMethodInsn(INVOKESTATIC, "com/hm/lifecycle/api/AppLifeCycleManager", "registerAppLike", "(Ljava/lang/String;)V", false);
mv.visitInsn(RETURN);
mv.visitMaxs(1, 0);
mv.visitEnd();
}
注意:
每個module的build的目錄纽什,每一次build的時候,都會生成一個build的目錄
transform 每一次的調(diào)試躲叼,用println來調(diào)試芦缰,會發(fā)現(xiàn)運行了一次transform,第二次在運行程序枫慷,就會發(fā)現(xiàn)看不到Log了让蕾,如果你要看Log,就要把依賴插件的Module的build的目錄刪除掉或听,才可以看到Log 或者clean project
TransformInput:所謂Transform就是對輸入的class文件轉(zhuǎn)變成目標字節(jié)碼文件探孝,TransformInput就是這些輸入文件的抽象。目前它包括兩部分:DirectoryInput集合與JarInput集合
誉裆。DirectoryInput:它代表著以源碼方式參與項目編譯的所有目錄結(jié)構(gòu)及其目錄下的源碼文件顿颅,可以借助于它來修改輸出文件的目錄結(jié)構(gòu)、已經(jīng)目標字節(jié)碼文件
JarInput:它代表著以jar包方式參與項目編譯的所有本地jar包或遠程jar包足丢,可以借助于它來動態(tài)添加jar包粱腻。
TransformOutputProvider:它代表的是Transform的輸出,例如可以通過它來獲取輸出路徑
getName:用于指明本Transform的名字斩跌,也是代表該Transform的task的名字
ContentType绍些,數(shù)據(jù)類型,
有CLASSES和RESOURCES兩種滔驶。
其中的CLASSES包含了源項目中的.class文件和第三方庫中的.class文件遇革。
RESOURCES僅包含源項目中的.class文件
getInputTypes:用于指明Transform的輸入類型,可以作為輸入過濾的手段揭糕。在TransformManager定義了如下的幾種類型:
dir.traverse(type: FileType.FILES,nameFilter:~/.*\.class/) { file ->
System.out.println("find class:" + file.name)
}
這個是遍歷目錄 File.traverse File里邊的方法
~/.*\.class/
這個是正則表達式
- 遍歷文件夾
def dir = new File("/")
//eachFile()方法返回該目錄下的所有文件和子目錄萝快,不遞歸
dir.eachFile { file ->
println file.name
}
dir.eachFileMatch(~/.*\.txt/) {file ->
println file.name
}
- 遞歸遍歷文件夾
def dir = new File("/")
//dir.eachFileRecurse()方法會遞歸顯示該目錄下所有的文件和目錄
dir.eachFileRecurse { file ->
println file.name
}
dir.eachFileRecurse(FileType.FILES) { file ->
println file.name
}
- 一些更復(fù)雜的遍歷方法你可以使用traverse方法,但需要你設(shè)置一個特殊的標志指示如何遍歷:
dir.traverse { file ->
//如果當(dāng)前文件是一個目錄且名字是bin著角,則停止遍歷
if (file.directory && file.name=='bin') {
FileVisitResult.TERMINATE
//否則打印文件名字并繼續(xù)
} else {
println file.name
FileVisitResult.CONTINUE
}
}
- gradlew命令:在MacOs系統(tǒng)揪漩,在Android studio下面的終端,使用./gradlew aR命令打包的時候吏口,
gradle明明一般是./gradlew +參數(shù)奄容, gradlew代表 gradle wrapper冰更,意思是gradle的一層包裝,大家可以理解為在這個項目本地就封裝了gradle昂勒,即gradle wrapper蜀细, 在gradle/wrapper/gralde-wrapper.properties文件中聲明了它指向的目錄和版本。只要下載成功即可用grdlew wrapper的命令代替全局的gradle命令戈盈。
./gradlew -v 版本號
./gradlew clean 清除app目錄下的build文件夾
./gradlew build 檢查依賴并編譯打包
./gradlew tasks 列出所有task
這里注意的是 ./gradlew build 命令把debug奠衔、release環(huán)境的包都打出來,如果正式發(fā)布只需要打Release的包塘娶,該怎么辦呢归斤,下面介紹一個很有用的命令 assemble, 如:
./gradlew assembleDebug 編譯并打Debug包
./gradlew assembleRelease 編譯并打Release的包
除此之外刁岸,assemble還可以和productFlavors結(jié)合使用:
./gradlew installRelease Release模式打包并安裝
./gradlew uninstallRelease 卸載Release模式包
-
android 的文件夾開頭為小寫字母
包名都是小寫字母的
如果文件夾開頭是大寫的話脏里,會找不到dataBinding
androidStudio 調(diào)試插件開發(fā)
1、創(chuàng)建remote調(diào)試任務(wù):
選擇 Eidt Configurations
點左上角的 + 號虹曙,選擇 remote迫横。Name可以隨意命名,其他配置可以不用動根吁,端口就5005员淫,點ok關(guān)閉
配置完以后
2、打開Terminal
窗口(一般在底下的工具欄上)击敌,在當(dāng)前的工程目錄下介返,輸入 (注意:在android studio的窗口下的Terminal中執(zhí)行
)
執(zhí)行如下命令:
./gradlew assembleDebug -Dorg.gradle.daemon=false -Dorg.gradle.debug=true。
assembleDebug 可以為其他的構(gòu)建命令沃斤,但參數(shù)-Dorg.gradle.daemon=false -Dorg.gradle.debug=true要有圣蝎。
如果報:-bash: ./gradlew: Permission denied
修改權(quán)限:chmod +x gradlew
在Terminal
的命令中點回車后,會出現(xiàn) To honour the JVM settings for this build a new JVM will be forked
. 這行提示衡瓶,并且會一直停在這里徘公,說明在等待調(diào)試
開始調(diào)試
這時候選擇第二步中創(chuàng)建的remote
任務(wù),并使用調(diào)試啟動(下圖最右邊的調(diào)試按鈕) 就是選擇 debug
調(diào)試 這個時候打開Terminal
就可以看到了調(diào)試的過程
(run
是插入的一些知識)
這邊run的按鈕哮针,運行的assembleDebug
/assembleRelease
執(zhí)行的是 assembleRelease
還是 assembleDebug
實際是由 build variants
設(shè)置的類型決定的关面。
運行是沒有調(diào)試功能的,直接運行轉(zhuǎn)手機
debug
:直接段點調(diào)試
attach debug
:
想象一下下面的場景:你的APK如果已經(jīng)運行在普通模式(非Debug)的情況下十厢,你突然想Debug等太,而又不想重新運行浪費時間,該怎么辦呢蛮放?
這邊的普通模式
缩抡,就是點擊了run
按鈕
普通模式下想設(shè)置斷點進行調(diào)試可不可以呢?
當(dāng)然是可以的包颁,不僅可以瞻想,這種方式已經(jīng)漸漸替代了原先的方案伐脖,畢竟很方便煞茫,不是嗎?那具體要怎么做呢
二窄驹、點擊Attach調(diào)試
attach process
到指定進程和媳,條件觸發(fā)之后就可以直接進入調(diào)試模式
jclasslib bytecode viewer 字節(jié)碼查看
jclasslib bytecode viewer 是一個可以可視化已編譯Java類文件和所包含的字節(jié)碼的工具篡帕。 另外屑迂,它還提供一個庫和屎,可以讓開發(fā)人員讀寫Java類文件和字節(jié)碼
2.3 安裝和使用
2.3.1 安裝
建議直接通過idea的插件庫搜索安裝然后重啟即可春瞬,下面我已經(jīng)安裝過了柴信。
點擊 Install安裝宽气,安裝后點擊 Restart IDE 重啟 IDEA
這邊注意,用這個插件看字節(jié)碼萄涯,要是java文件或者groovy文件绪氛,kotlin文件看不到涝影,kotlin文件需要dex反編譯成class文件燃逻,用jd-gui.jar
查看
2.3.2使用
使用時直接選擇 View --> Show Bytecode With jclasslib
注意:如果是自己項目的源碼需要先編譯
jclasslib窗口(查看字節(jié)碼)
可以查看基本信息伯襟、常量池姆怪、接口、屬性俺附、函數(shù)等信息昙读。
主要優(yōu)點:
1 不需要使用javap指令膨桥,使用簡單
2 點擊字節(jié)碼指令可以跳轉(zhuǎn)到 java虛擬機規(guī)范對應(yīng)的章節(jié)。
比如我們想了解 putstatic 的含義艺沼,可以點擊該指令
如何查看插樁后字節(jié)碼的效果呢?(反編譯)
就是把字節(jié)碼插樁完以后蕴掏,在apk中找到那個文件進行反編譯
1、dex2jar 官方下載地址
作用:將APK直接解壓后挽荡,目錄下包含的一個classes.dex文件反編譯為classes-dex2jar.jar文件定拟。
2逗嫡、jd-gui.jar 官方下載地址
作用:直接查看classes-dex2jar.jar文件驱证。
方法:
將dex2jar.jar解壓成文件夾
將test.apk后綴名修改為.rar然后解壓(.apk 也可以直接解壓)
將test.apk解壓后的目錄下包含的classes.dex文件復(fù)制到dex2jar解壓后的文件夾中
(classes.dex文件與d2j-dex2jar.bat文件同在一個目錄中)
打開cmd命令編輯器
進入classes.dex文件與d2j-dex2jar.bat所在文件目錄
輸入命令sh d2j-dex2jar.sh classes.dex
此時可以看到目錄中多出了classes-dex2jar.jar文件
報錯命令如下:
d2j-dex2jar.sh: line 36: ./d2j_invoke.sh: Permission denied
解決方案:sudo chmod +x d2j_invoke.sh
3逆瑞、jd-gui(這個工具是把class文件翻譯成基本的代碼)
雙擊運行 jd-gui-1.4.0.jar 文件祈远,
將.jar文件拖到工作區(qū)即可打開车份。
asm bytecode 查看扫沼,就是asm中插入代碼用的
查看 Bytecode 方法
查看 Java 代碼:
通過 ASM Bytecode Outline 插件生成代碼
1缎除、在 Android Studio 中安裝 ASM Bytecode Outline 插件器罐;
2、安裝后铸董,在編譯器中粟害,點擊右鍵悲幅,選擇 Show Bytecode outLine; 3卓鹿、在 ASM 標簽中選擇 ASMified减牺,即可在右側(cè)看到當(dāng)前類對應(yīng)的 ASM 代碼存谎。
查看 kotlin 代碼:
通過 AS 自帶的工具查看
路徑:AS 導(dǎo)航欄-Tools-kotlin-show kotlin bytecode
java 用ASM Bytecode Outline 查看asmBytecode
kotlin 用自帶AS 導(dǎo)航欄-Tools-kotlin-show kotlin bytecode
javap指令查看字節(jié)碼(和jclasslib
效果一樣)
javap -v xx.class
借鑒連接