一次Java字節碼插樁實戰

理解本文須要必定的Java字節碼指令基礎,能夠閱讀筆者的另外一篇文章: 大話+圖說:Java字節碼指令——只爲讓你懂
利用Android字節碼插樁技術能夠很方便地幫助咱們實現不少手術刀式的代碼設計,如無埋點統計上報、輕量級AOP等。下面咱們就經過一次實戰,把這門技術真正用起來。

奇葩需求

假設有這樣一個需求,咱們須要在本項目工程的全部組件(Activity/Receiver/Service/Provider)的on系列生命週期類方法執行時,調用一個咱們寫好的方法,傳入組件的實例對象,來對組件的相關狀態進行監測,如何實現?
通常的思路有兩種:java

  1. 經過Java繼承體系,爲咱們實現的四大組件分別創建基類,在基類父方法裏對監測方法進行調用。
  2. 經過Android API Hook技術,即經過動態代理等方法替換關鍵節點,抓住組件的節點方法並調用咱們的監測方法。

上面的第一種方法比較麻煩,並且控制力較弱,也沒法顧及咱們所依賴的Jar或者aar中的組件,好比小米推送中自帶的Service和Receiver,是徹底沒法觸及的。第二種方法則比較強大,可是須要考慮兼容性問題,技術實現上的成本也比較高,畢竟有一些生命週期的節點很差找,不免焦頭爛額。android

本文對此的實戰即經過字節碼插樁,在class文件編譯成dex以前(同時也是proguard操做以前),遍歷全部要編譯的class文件並對其中符合條件的方法進行修改,注入咱們要調用的監測方法的代碼,從而實現這個需求。git

HiBeaver 是目前這方面比較完善的字節碼插樁Gradle插件,目前最新的1.2.4版本支持經過通配符或正則表達式的方法來匹配目標類和目標方法,進行方法的批量插樁注入和修改,很是靈活易用。對於相似上文提出的需求,實現起來很是方便,惟一前提的僅僅是:知道全部組件的類的全名就能夠了。github

準備工做

好,基於這些,正式開始實戰,牛刀小試一下:
首先創建一個工程,爲便於演示,咱們引入小米推送(接入方式再也不贅述,詳見小米推送文檔),而後完善代碼到以下狀態:web

圖片描述

MainActivity內容很簡單,註冊了小米推送,有一個TextView點擊後能夠跳轉到SecondActivity,僅此而已。具體以下:正則表達式

圖片描述

SecondActivity中一切從簡:segmentfault

圖片描述

至於DemoMessageReceiver這個類裏徹底依照小米推送接入文檔中的配置,沒有實質改動,再也不貼出。
注意到還有一個MonitorUtil的類,內容以下:閉包

圖片描述

其中的monitorThis的方法就是咱們打算在各個生命週期方法裏插入的調用方法。app

開始實戰

下面咱們就開始實現開頭處提到的需求:經過字節碼插樁的方法,本工程裏的全部組件的生命週期方法return以前調用咱們的monitorThis方法,傳入組件實例等信息做爲參數。ide

首先,要引入HiBeaver插件:
而後在項目的根build.gradle下面增長classpath以下:

圖片描述

classpath 'com.bryansharp:hibeaver:1.2.4'

隨後爲咱們工程的app/build.gradle增長以下配置:

apply plugin: 'hiBeaver'
import com.bryansharp.gradle.hibeaver.utils.MethodLogAdapter
import org.objectweb.asm.ClassVisitor
import org.objectweb.asm.MethodVisitor
import org.objectweb.asm.Opcodes

hiBeaver {
    modifyMatchMaps = [
            //類名稱匹配規則,*表示任意長度任意字符,|爲分隔符,能夠理解爲或
            '*Activity|*Receiver|*Service|!android*': [
                    //方法名匹配規則與類名相似,同時也支持正則表達式匹配(須要加r:);adapter後爲一個閉包,進行具體的修改
                    ['methodName': 'on**', 'methodDesc': null, 'adapter': {
                        //下面這些爲閉包傳入的參數,能夠幫助咱們進行方法過濾,以及根據方法參數來調整字節碼修改方式
                        ClassVisitor cv, int access, String name, String desc, String signature, String[] exceptions ->
                            //這裏咱們有了ClassVisitor實例,其實能夠爲類添加新的方法。
                            MethodVisitor methodVisitor = cv.visitMethod(access, name, desc, signature, exceptions);
                            MethodVisitor adapter = new MethodLogAdapter(methodVisitor) {

                                @Override
                                void visitCode() {
                                    super.visitCode();
                                    //實例對象入棧
                                    methodVisitor.visitVarInsn(Opcodes.ALOAD, 0);
                                    //下面兩句咱們將方法的名稱和描述做爲常量入棧
                                    methodVisitor.visitLdcInsn(name);
                                    methodVisitor.visitLdcInsn(desc);
                                    //調用咱們的靜態方法
                                    methodVisitor.visitMethodInsn(Opcodes.INVOKESTATIC,
                                            //下面這個MethodLogAdapter.className2Path(String)爲
                                            // hibeaver插件提供的方法,能夠將類名轉爲路徑名
                                            MethodLogAdapter.className2Path("bruce.com.testhibeaver.MonitorUtil"),
                                            "monitorThis", "(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)V");
                                }
                            }
                            return adapter;
                    }]
            ]
    ]
}

HiBeaver在類名和方法名的匹配上很是靈活,能夠很是方便地實現批量匹配,除了完整匹配外,還支持通配符匹配和正則表達式匹配兩種模式。通配符匹配模式中主要可使用兩種符號,即 | 和表示任意長度(>0)的任意字符,而|表示分隔符,這裏能夠理解爲或。所以,上面的:

*Activity|*Receiver|*Service

能夠理解爲,匹配任意全類名以Activity、Receiver或Service結尾的類。

通常來說,咱們的Android組件在命名上都會聽從這個規範,即組件類名以相應的組件名結尾,對於個別不聽從這個原則的,也能夠經過|分隔符來把特殊狀況歸入進去。

除此以外,若是存在更復雜的匹配規則,上述通配符已經沒法知足,hiBeaver也支持正則表達式進行全類名匹配,只須要在表達式前加上「r:」就能夠。好比:

r:.*D[a-zA-Z]*Client

表示匹配符合「.*D[a-zA-Z]*Client」這個正則表達式的類名。

更進一步地,HiBeaver 將來 還將支持根據類的繼承關係進行匹配,好比:

>ext>android.support.v4.app.FragmentActivity

表示匹配全部繼承android.support.v4.app.FragmentActivity的類,而:

>imp>android.os.Handler.Callback

表示匹配全部實現android.os.Handler.Callback接口的類。
不過,目前這兩個特性尚未支持,僅提上了其項目的issue中。
回到剛剛的配置中,下面的methodName方法的匹配規則與類名匹配用法同樣,**和*是同樣的效果,on**即表示名字以on開頭的方法。
好了,編譯運行工程,過程當中在Gradle Console中能夠看到hibeaver進行字節碼插樁輸出以下(局部):

圖片描述

程序運行起來,插樁成功,成功調用了monitorThis方法,但赫然發現輸出以下:

圖片描述

調用了三個onCreate和若干的onCreateView!這是爲何?咱們的MainActivity也沒有這個onCreateView的方法啊!

結合以前Gradle編譯日誌,在仔細一琢磨,忽然明白了:

圖片描述
圖片描述
圖片描述

原來,咱們的*Activity規則會匹配全部的Activity結尾的類,包括一些android v4支持包中的類,什麼AppCompatActivity、FragmentActivity等繼承鏈上的Activity統統被hook了一遍,難怪會有那麼多輸出了,可辛苦了咱們的monitorThis方法。

既然如此,如何是好?針對於當前的需求,咱們固然不想匹配v4包裏的組件類。

所幸的是,HiBeaver中還有另外一種排除匹配,運用!符號改造以下便可:

*Activity|*Receiver|*Service|!android*

這樣就表示,匹配前三種之一(或的關係)且不匹配第四個android*的全類名。
改好後,再次運行,並點擊跳轉到SecondActivity:

圖片描述

能夠看到log輸出一會兒少多了,證實沒有再注入v4包裏的類,同時,小米的組件也被正常注入了,我把網斷掉,能夠看到小米的Receiver被喚起:

圖片描述

再開啓調試,打開網,斷點也能夠正常進入:

圖片描述

同時,每次HiBeaver進行字節碼插樁後還會把修改過、實際使用的字節碼保存到build/HiBeaver目錄下,以便於查看:

clipboard.png

以下圖爲修改後的MainActivity類:

clipboard.png

修改後的小米推送裏的某Receiver:

clipboard.png

這樣,不管是進行節點控制仍是研究其運行機制都大大地方便了。

HiBeaver
相關文章
相關標籤/搜索