您好,登錄后才能下訂單哦!
很多人聽到關(guān)于“插樁”的詞語,第一眼覺得會(huì)很高深,那到底什么是插樁呢?用通俗的話來講,插樁就是將一段代碼通過某種策略插入到另一段代碼,或替換另一段代碼。這里的代碼可以分為源碼和字節(jié)碼,而我們所說的插樁一般指字節(jié)碼插樁。
我們都知道JAVA是面向?qū)ο螅ɡ^承、封裝、多態(tài)),而插樁的意義在于面向切面(AOP)?,可想而知單方面的面向?qū)ο箝_發(fā)有許多的局限性,而結(jié)合面向切面編程可以說補(bǔ)足了我們的這種局限性。舉個(gè)例子:在onClick中一般都要做防抖動(dòng)操作,這樣是為了避免多次打開頁面的問題。一般實(shí)現(xiàn)的話是在每個(gè)onClick實(shí)現(xiàn)第二次點(diǎn)擊的時(shí)候加個(gè)時(shí)間判斷。而插樁的話業(yè)務(wù)端可以不寫任何代碼通過插樁的方法把這個(gè)時(shí)間判斷插入的字節(jié)碼里面。從標(biāo)題名字看Java字節(jié)碼:是Java虛擬機(jī)執(zhí)行的一種虛擬指令格式。通過JVM轉(zhuǎn)換生成機(jī)器指令插樁:是在保證被測程序原有邏輯完整性的基礎(chǔ)上在程序中插入一些探針(又稱為“探測儀”)。
銀行系統(tǒng)會(huì)有一個(gè)取款流程,我們可以把方框里的流程合為一個(gè),另外系統(tǒng)還會(huì)有一個(gè)查詢余額流程,我們先把這兩個(gè)流程放到一起,有沒有發(fā)現(xiàn),這個(gè)兩者有一個(gè)相同的驗(yàn)證流程,我們先把它們?nèi)ζ饋碓僬f下一步,有沒有想過可以把這個(gè)驗(yàn)證用戶的代碼是提取出來,不放到主流程里去呢,這就是AOP的作用了,有了AOP,你寫代碼時(shí)不要把這個(gè)驗(yàn)證用戶步驟寫進(jìn)去,即完全不考慮驗(yàn)證用戶。
什么是AOP:把這些橫跨并嵌入眾多模塊里的功能(如監(jiān)控每個(gè)方法的性能) 集中起來,放到一個(gè)統(tǒng)一的地方來控制和管理能給我?guī)硎裁矗翰恍薷脑创a的情況下給程序動(dòng)態(tài)統(tǒng)一添加功能的一種技術(shù),把散落在程序中的公共部分提取出來,做成切面類,這樣的好處在于,代碼的可重用,一旦涉及到該功能的需求發(fā)生變化,只要修改該代碼就行,否則,你要到處修改,如果只要修改1、2處那還可以接受,萬一有1000處呢。
這是app打包流程的整個(gè)過程而我把這個(gè)打包流程主要分為一下步驟:
aapt來打包資源文件,生成R.java文件
處理AIDL,生成對應(yīng)的.java接口文件
編譯Java文件,生成對應(yīng)的.class文件
把.class文件轉(zhuǎn)化成Davik VM支持的.dex文件
打包生成未簽名的.apk文件。
字節(jié)碼插樁入口:我們知道Android程序從Java源代碼到可執(zhí)行的Apk包主要分析兩個(gè)環(huán)節(jié):
javac:將源文件編譯成class格式的文件
dex:將class格式的文件匯總到dex格式的文件中
我們要想對字節(jié)碼進(jìn)行修改,只需要在javac之后,dex之前對class文件進(jìn)行字節(jié)碼掃描,并按照一定規(guī)則進(jìn)行過濾及修改就可以了,這樣修改過后的字節(jié)碼就會(huì)在后續(xù)的dex打包環(huán)節(jié)被打到apk中,這就是我們的插樁入口。
每個(gè)Transform其實(shí)都是一個(gè)gradle task,Android編譯器中的TaskManager將每個(gè)Transform串連起來,第一個(gè)Transform接收來自javac編譯的結(jié)果,以及已經(jīng)拉取到在本地的第三方依賴(jar. aar),還有resource資源,注意,這里的resource并非android項(xiàng)目中的res資源,而是asset目錄下的資源。這些編譯的中間產(chǎn)物,在Transform組成的鏈條上流動(dòng),每個(gè)Transform節(jié)點(diǎn)可以對class進(jìn)行處理再傳遞給下一個(gè)Transform。我們常見的混淆,Desugar等邏輯,它們的實(shí)現(xiàn)如今都是封裝在一個(gè)個(gè)Transform中,而我們自定義的Transform,會(huì)插入到這個(gè)Transform鏈條的最前面。
對于Android Gradle Plugin 版本在1.5.0及以上的情況,Google官方提供了transformapi用作字節(jié)碼插樁的入口。
implementation?'com.android.tools.build:gradle:1.5.0'復(fù)制代碼
一般使用方法為:extends Transform重寫transform()
需要引入Instrumentation
7.png
通過Java Instrumentation機(jī)制,為獲得插樁入口,對于apk build過程進(jìn)行了兩處插樁(即hook),圖中標(biāo)紅部分:
Instrumentation:指的是可以用獨(dú)立于應(yīng)用程序之外的代理(agent)程序來監(jiān)測和協(xié)助運(yùn)行在JVM上的應(yīng)用程序。這種監(jiān)測和協(xié)助包括但不限于獲取JVM運(yùn)行時(shí)狀態(tài),替換和修改類定義等。
在build進(jìn)程,對ProcessBuilder.start()方法進(jìn)行插樁
ProcessBuilder類是J2SE 1.5在java.lang中新添加的一個(gè)新類,此類用于創(chuàng)建操作系統(tǒng)進(jìn)程,它提供一種啟動(dòng)和管理進(jìn)程的方法,start方法就是開始創(chuàng)建一個(gè)進(jìn)程,對它進(jìn)行插樁,使得通過下面方式啟動(dòng)dx.jar進(jìn)程執(zhí)行dex任務(wù)時(shí):
java??dex.jar??com.android.dx.command.Main??--dex
增加參數(shù)-javaagent agent.jar,使得dex進(jìn)程也可以使用Java Instrumentation機(jī)制進(jìn)行字節(jié)碼插樁
在dex進(jìn)程
對我們的目標(biāo)方法com.android.dx.command.Main.processClasses進(jìn)行字節(jié)碼插入,從而實(shí)現(xiàn)打入apk的每一個(gè)項(xiàng)目中的類都按照我們制定的規(guī)則進(jìn)行過濾及字節(jié)碼修改。
build進(jìn)程使用Instrumentation的方式時(shí)之前敘述過的VirtualMachine.loadAgent方式(方式二),dex進(jìn)程中的方式則是-javaagent agent.jar方式(方式一)。
由此,我們獲得了進(jìn)行字節(jié)碼插樁的入口,下面我們就使用ASM庫的API,對項(xiàng)目中的每一個(gè)類進(jìn)行掃描,過濾,及字節(jié)碼修改。
1、創(chuàng)建一個(gè)Android library Module工程
2、build.gradle改成groovy方式
apply?plugin:?'groovy' ????dependencies?{ ????????compile?gradleApi() ????????compile?localGroovy() ????}復(fù)制代碼
3、新建.groovy類繼承 Plugin并實(shí)現(xiàn)apply方法,注意:類的后綴不再是.java而是.groovy
4、在main下創(chuàng)建resources目錄
5、增加對應(yīng)的maven deployer發(fā)布到本地或遠(yuǎn)程倉庫
6、使用已發(fā)布的倉庫
ASM 是一個(gè) Java 字節(jié)碼操控框架。它能被用來動(dòng)態(tài)生成類或者增強(qiáng)既有類的功能。ASM 可以直接產(chǎn)生二進(jìn)制 class 文件,也可以在類被加載入 Java 虛擬機(jī)之前動(dòng)態(tài)改變類行為。Java class 被存儲(chǔ)在嚴(yán)格格式定義的 .class 文件里,這些類文件擁有足夠的元數(shù)據(jù)來解析類中的所有元素:類名稱、方法、屬性以及 Java 字節(jié)碼(指令)。ASM 從類文件中讀入信息后,能夠改變類行為,分析類信息,甚至能夠根據(jù)用戶要求生成新類。
Framework | First time | Later times |
---|---|---|
Javassist | 257 | 5.2 |
BCEL | 473 | 5.5 |
ASM | 62.4 | 1.1 |
可以使用一個(gè)插件[ASM Bytecode Outline]更有效的用ASM編寫字節(jié)碼
ASM(core api) 按照visitor模式按照class文件結(jié)構(gòu)依次訪問class文件的每一部分,有如下幾個(gè)重要的visitor。
需要?jiǎng)?chuàng)建一個(gè) Cla***eader 對象,將 .class 文件的內(nèi)容讀入到一個(gè)字節(jié)數(shù)組中
然后需要一個(gè) ClassWriter 的對象將操作之后的字節(jié)碼的字節(jié)數(shù)組回寫
需要事件過濾器 ClassVisitor。在調(diào)用 ClassVisitor 的某些方法時(shí)會(huì)產(chǎn)生一個(gè)新的 XXXVisitor 對象,當(dāng)我們需要修改對應(yīng)的內(nèi)容時(shí)只要實(shí)現(xiàn)自己的 XXXVisitor 并返回就可以了
這個(gè)類會(huì)將 .class 文件讀入到 Cla***eader 中的字節(jié)數(shù)組中,它的 accept 方法接受一個(gè) ClassVisitor 實(shí)現(xiàn)類,并按照順序調(diào)用 ClassVisitor 中的方法
ClassWriter 是一個(gè) ClassVisitor 的子類,是和 Cla***eader 對應(yīng)的類,Cla***eader 是將 .class 文件讀入到一個(gè)字節(jié)數(shù)組中,ClassWriter 是將修改后的類的字節(jié)碼內(nèi)容以字節(jié)數(shù)組的形式輸出。
void visit(int version, int access, String name, String signature, String superName, String[] interfaces)
該方法是當(dāng)掃描類時(shí)第一個(gè)調(diào)用的方法,主要用于類聲明使用。下面是對方法中各個(gè)參數(shù)的示意:visit( 類版本 , 修飾符 , 類名 , 泛型信息 , 繼承的父類 , 實(shí)現(xiàn)的接口)* AnnotationVisitor visitAnnotation(String desc, boolean visible)
該方法是當(dāng)掃描器掃描到類注解聲明時(shí)進(jìn)行調(diào)用。下面是對方法中各個(gè)參數(shù)的示意:visitAnnotation(注解類型 , 注解是否可以在 JVM 中可見)。* FieldVisitor visitField(int access, String name, String desc, String signature, Object value)
該方法是當(dāng)掃描器掃描到類中字段時(shí)進(jìn)行調(diào)用。下面是對方法中各個(gè)參數(shù)的示意:visitField(修飾符 , 字段名 , 字段類型 , 泛型描述 , 默認(rèn)值)* MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions)
該方法是當(dāng)掃描器掃描到類的方法時(shí)進(jìn)行調(diào)用。下面是對方法中各個(gè)參數(shù)的示意:visitMethod(修飾符 , 方法名 , 方法簽名 , 泛型信息 , 拋出的異常)* void visitEnd()
該方法是當(dāng)掃描器完成類掃描時(shí)才會(huì)調(diào)用,如果想在類中追加某些方法
MethodVisitor 是一個(gè)抽象類,當(dāng) ASM 的 Cla***eader 讀取到 Method 時(shí)就轉(zhuǎn)入 MethodVisitor 接口處理。
AdviceAdapter 是 MethodVisitor 的子類,使用 AdviceAdapter 可以更方便的修改方法的字節(jié)碼。
其中比較重要的幾個(gè)方法如下:
void visitCode():表示 ASM 開始掃描這個(gè)方法
void onMethodEnter():進(jìn)入這個(gè)方法
void onMethodExit():即將從這個(gè)方法出去
void onVisitEnd():表示方法掃碼完畢
全限定名即為全類名中的“.”,換為“/”,舉例:
類android.widget.AdapterView.OnItemClickListener的全限定名為: android/widget/AdapterView$OnItemClickListener復(fù)制代碼
描述符(descriptors):
1.類型描述符,如下圖所示:
在class文件中類型 boolean用“Z”描述,數(shù)組用“[”描述(多維數(shù)組可疊加),那么我們最常見的自定義引用類型呢?“L全限定名;”.例如:
Android中的android.view.View類,描述符為“Landroid/view/View;”
2.方法描述符的組織結(jié)構(gòu)為:
(參數(shù)類型描述符)返回值描述符復(fù)制代碼復(fù)制代碼
其中無返回值void用“V”代替,舉例:
方法boolean?onGroupClick(ExpandableListView?parent,?View?v,?int?groupPosition,?long?id)? 的描述符如下:(Landroid/widget/ExpandableListView;Landroid/view/View;IJ)Z復(fù)制代碼
對上圖中三個(gè)步驟的詳細(xì)說明:
ASM的ClassVisitor對所有類的class文件進(jìn)行掃描,在visitMethod()方法中判斷是不是BaseActivity,如果是進(jìn)行步驟二,否則終止掃描;
ClassVisitor每掃描到一個(gè)方法時(shí),在visitMethod中進(jìn)行如下判定:
是不是要過濾的<init>方法
如果判定通過,則證明本次掃描到的方法是需要注入字節(jié)碼的方法,然后將
將掃描邏輯交給MethodVisitor,進(jìn)行字節(jié)碼的修改(步驟三)。
假設(shè)待修改的方法如下:
public?int?test()?{??try?{?? ??????Thread.sleep(1000); ??}?catch?(InterruptedException?e)?{ ??????e.printStackTrace(); ??} }
修改之后需要變成:
public?int?test()?{???long?startTime?=?System.currentTimeMillis();???try?{?? ???????Thread.sleep(1000);}???catch?(InterruptedException?e){? ???????e.printStackTrace();?? ???}???long?timing?=?System.currentTimeMillis()?-?startTime; ???BlockManager.timingPage(getLocalClassName(),?timing); }
通過上面實(shí)戰(zhàn)練習(xí),相信你已經(jīng)初步掌握了插樁的基本技術(shù),但是這還遠(yuǎn)遠(yuǎn)不夠;在項(xiàng)目中會(huì)遇到各式各樣的問題,現(xiàn)實(shí)情況可能沒有demo這么簡單;不過沒關(guān)系,大家轉(zhuǎn)發(fā)加關(guān)注,如果在插樁過程中遇到任何問題,都可以留言問我。
覺得文章不錯(cuò)的喜歡的小伙伴可以關(guān)注加轉(zhuǎn)發(fā),歡迎大家前來探討交流,同時(shí),我也非常歡迎大家互相交流技術(shù),共同成長。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。