字節(jié)碼插樁--你也可以輕松掌握

1 什么是插樁?

聽到關于“插樁”的詞語遵岩,第一眼覺得會很高深崇渗,那到底什么是插樁呢?用通俗的話來講鸠按,插樁就是將一段代碼通過某種策略插入到另一段代碼,或替換另一段代碼饶碘。這里的代碼可以分為源碼和字節(jié)碼目尖,而我們所說的插樁一般指字節(jié)碼插樁。
圖1是Android開發(fā)者常見的一張圖扎运,我們編寫的源碼(.java)通過javac編譯成字節(jié)碼(.class)瑟曲,然后通過dx/d8編譯成dex文件。


圖1:Java-字節(jié)碼-dex豪治,圖片來自-極客時間

我們下面要講的插樁洞拨,就是在.class轉為.dex之前,修改.class文件從而達到修改或替換代碼的目的负拟。
那有人肯定會有這樣的疑問烦衣?既然插樁是插入或替換代碼,那為何我不自己直接插入或替換呢?為何還要用這么“復雜”的工具花吟?別著急秸歧,第二個問題將會給你答案。

2 插樁的應用場景有哪些衅澈?

技術是服務于業(yè)務的键菱,一個無法推進業(yè)務進步的技術并不值得我們學習。在上面矾麻,我們對插樁的理解是:插入纱耻,替換代碼。那么险耀,結合這個核心主線我們來挖掘插樁能被應用的場景有哪些弄喘?

  • 代碼插入

我們所熟悉的ButterKnife,Dagger這些常用的框架甩牺,也是在編譯期間生成了代碼蘑志,簡化了程序員的操作。假設有這么一個需求贬派,要監(jiān)控某些或者所有方法的執(zhí)行耗時急但?你會怎么做呢?如果你監(jiān)控的方法只有十幾個或者幾十個搞乏,那么也許通過程序員自身的編碼就能輕松解決波桩;但是如果監(jiān)控的方法達到百千甚至萬級別,你還通過編碼來解決请敦?那么程序員存在的價值在哪里镐躲?面對這樣的重復勞動問題,最先想到的就應該是自動化侍筛,也就是我們今天所講的插樁萤皂。通過插樁,我們掃描每一個class文件匣椰,并針對特定規(guī)則進行字節(jié)碼修改從而達到監(jiān)控每個方法耗時的目的裆熙。關于如何實現(xiàn)這樣的需求,后面我會詳細講述禽笑。

  • 代碼替換

如果遇到這么一個需求入录,需要將項目中所有使用某個方法(如Dialog.show())的地方替換成自己包裝的方法(MyDialog.show()),那么你該如何解決呢佳镜?有人會說僚稿,直接使用快捷鍵就能全局替換。那么有兩個問題
1 如果有其他類定義了show()方法邀杏,并被調用了贫奠,直接使用快捷鍵是否會被錯誤替換?
2 如果其他引用包使用了該方法望蜡,你怎么替換呢唤崭?
沒關系,插樁同樣可以解決你的問題脖律。
綜合上面所說的兩點谢肾,其實很多業(yè)務場景都使用了插樁技術,比如無痕埋點小泉,性能監(jiān)控等芦疏。

3 掌握插樁應該具備的基礎知識有哪些?

上面講了插樁的應用場景微姊,是否現(xiàn)在想躍躍欲試呢酸茴?別著急,想掌握好插樁技術兢交,練就扎實的插樁功底薪捍,我們是需要具備一些基礎知識的。

  • 熟練掌握字節(jié)碼相關技術配喳±掖可參考 一文讓你明白Java字節(jié)碼

  • Gradle自定義插件,直接參考官網(wǎng) Writing Custom plugins

  • 如果你想運用在Android項目中晴裹,那么還需要掌握Transform API,
    這是android在將class轉成dex之前給我們預留的一個接口被济,在該接口中我們可以通過插件形式來修改class文件。

  • 字節(jié)碼修改工具涧团。如AspectJ只磷,ASM,javasisst少欺。這里我推薦使用ASM喳瓣,關于ASM相關知識,在下一章我給大家簡單介紹赞别。同樣大家可以參考 Asm官方文檔

  • groovy語言基礎
    如果你具備了上面5塊知識畏陕,那么恭喜你,會很順利的完成字節(jié)碼插樁技術了仿滔。下面惠毁,我通過實戰(zhàn)一個很簡單的例子,帶領大家一起領略插樁的風采崎页。

4 使用ASM進行字節(jié)碼插樁

1 什么是ASM?

ASM是生成和轉換已編譯的Java類工具鞠绰,就是我們插樁需要使用的工具。

2 兩種API飒焦?

ASM提供了兩種API來生成和轉換已編譯類蜈膨,一個是核心API屿笼,以基于事件形式來表示類;另一個是樹API翁巍,以基于對象形式來表示類驴一。

3 基于事件形式

我們通過上面的基礎知識,了解到類的結構灶壶,類包含字段肝断,方法,指令等驰凛;基于事件的API把類看作是一系列事件來表示胸懈,每一個類的事件表示一個類的元素。類似解析XML的SAX

4 基于對象形式

基于對象的API將類表示成一棵對象樹恰响,每個對象表示類的一部分趣钱。類似解析XML的DOM

5 優(yōu)缺點比較

事件形式 對象形式
內存占用
實現(xiàn)難度

通過上面表格,我們清楚的了解到:

  • 事件API內存占用少于對象API胚宦,因為事件API不需要在內存中創(chuàng)建和存儲對象樹
  • 事件API實現(xiàn)難度比對象API大羔挡,因為事件API在任意時刻類中只有一個元素可使用,但是對象API能獲得整個類间唉。
    那么接下來绞灼,我們就通過比較容易實現(xiàn)的對象API入手,一起完成上面的需求呈野。
    我們Android的構建工具是Gradle低矮,因此我們結合transform和Gradle插件方式來完成該需求,接下來我們來看看gradle官方提供的3種插件形式
    6 Gradle插件的3種形式
插件形式 說明
Build script 直接在build script中寫插件代碼被冒,不可復用
buildSrc 獨立項目結構军掂,只能在本構建體系中復用,無法提供給其他項目
Standalone 獨立項目結構昨悼,發(fā)布到倉庫蝗锥,可以復用

由于我們是demo,并不需要共享給其他項目率触,因此采用buildSrc方式即可终议,但是正常項目中都采用Standalone形式。

5 插樁實踐

目標 : 刪除所有以test開頭的方法

接下來我們來完成一個非常小的需求葱蝗,刪除所有以test開頭的方法穴张。為什么說這是一個小需求,因為這并不涉及指令的操作两曼,所有操作通過方法名完成即可皂甘。通過完成這個demo,只是拋磚引玉悼凑。如若后期需要偿枕,可以逐步深入到指令級別替換璧瞬。
接下來的步驟就是創(chuàng)建demo的過程

  • 1 新建buildSrc目錄,用來存放源代碼位置渐夸。針對不同語言可以新建不同目錄彪蓬。


    圖2-項目整體結構

    如上圖所示的是buildSrc的結構。

  • 2 在buildSrc的gradle文件中我們需要配置如下代碼
apply plugin: 'groovy'
dependencies {
   compile gradleApi()//在使用自定義插件時候捺萌,一定要引用org.gradle.api.Plugin
   compile 'com.android.tools.build:gradle:3.3.2'//使用自定義transform時候,需要引用com.android.build.api.transform.Transform
   compile 'org.ow2.asm:asm:6.0'
   compile 'commons-io:commons-io:2.6'
}
repositories {
   mavenCentral()
   jcenter()
   google()
}
  • 3 重寫Transform API
    在groovy目錄下新建一個groovy類并繼承Transform膘茎,注意導包com.android.build.api.transform桃纯,并實現(xiàn)抽象方法和transform方法,如下
class MyTransform extends Transform {
   Project project
   MyTransform(Project project) {
       this.project = project
   }
   @Override
   String getName() {
       return "MyTransform"
   }
   //設置輸入類型披坏,我們是針對class文件處理
   @Override
   Set<QualifiedContent.ContentType> getInputTypes() {
       return TransformManager.CONTENT_CLASS
   }
   //設置輸入范圍态坦,我們選擇整個項目
   @Override
   Set<? super QualifiedContent.Scope> getScopes() {
       return TransformManager.SCOPE_FULL_PROJECT
   }
   @Override
   boolean isIncremental() {
       return true
   }
   //重點就是該方法,我們需要將修改字節(jié)碼的邏輯就從這里開始
   @Override
   void transform(Context context, Collection<TransformInput> inputs, Collection<TransformInput> referencedInputs, TransformOutputProvider outputProvider, boolean isIncremental) throws IOException, TransformException, InterruptedException {
       inputs.each {
           TransformInput input ->
               input.getJarInputs().each {
               //處理jar文件棒拂,代碼太多伞梯,這里暫時不貼
               }
               input.getDirectoryInputs().each {
               //處理目錄文件,這里的ASMHelper.transformClass()是修改字節(jié)碼邏輯
                   def destDir = transformInvocation.outputProvider.getContentLocation(
                           "${dir.name}_transformed",
                           dir.contentTypes,
                           dir.scopes,
                           Format.DIRECTORY)
                   if (dir.file) {
                       def modifiedRecord = [:]
                       dir.file.traverse(type: FileType.FILES, nameFilter: ~/.*\.class/) {
                           File classFile ->
                               def className = classFile.absolutePath.replace(dir.getFile().getAbsolutePath(), "")
                               if (!ASMHelper.filter(className)) {
                                   def transformedClass = ASMHelper.transformClass(classFile, dir.file, transformInvocation.context.temporaryDir)
                                   modifiedRecord[(className)] = transformedClass
                               }
                       }
                       FileUtils.copyDirectory(dir.file, destDir)
                       modifiedRecord.each { name, file ->
                           def targetFile = new File(destDir.absolutePath, name)
                           if (targetFile.exists()) {
                               targetFile.delete()
                           }
                           FileUtils.copyFile(file, targetFile)
                       }
                       modifiedRecord.clear()
               }
       }
   }
}
  • 4 實現(xiàn)字節(jié)碼修改邏輯
    Transform我們已經(jīng)定義完成帚屉,接下來就要針對讀入的字節(jié)碼進行修改谜诫。我們采用對象API進行解析class文件。一共就是3個步驟:
    1 將輸入流轉化為ClassNode
    2 處理ClassNode攻旦,這里就是我們的業(yè)務邏輯所在
    3 將ClassNode轉為字節(jié)數(shù)組輸出
    當然還有其他文件的IO操作喻旷,這里因為篇幅限制未貼出,如若需要demo牢屋,可以私信且预。
static byte[] modifyClass(InputStream inputStream) {
       ClassNode classNode = new ClassNode(Opcodes.ASM5)
       ClassReader classReader = new ClassReader(inputStream)
       //1 將讀入的字節(jié)轉為classNode
       classReader.accept(classNode, 0)
       //2 對classNode的處理邏輯
       Iterator<MethodNode> iterator = classNode.methods.iterator();
       while (iterator.hasNext()) {
           MethodNode node = iterator.next()
           if (node.name.startsWith("test")) {
               iterator.remove()
           }
       }
       ClassWriter classWriter = new ClassWriter(0)
       //3  將classNode轉為字節(jié)數(shù)組
       classNode.accept(classWriter)
       return classWriter.toByteArray()
   }
  • 5 插件化
    上面我們完成了字節(jié)碼修改邏輯以及定義Transform,但是并沒有完成插件的定義烙无。結合Transform API我們了解到锋谐,需要將我們自定義的Transform注冊到插件中,如下
class MyPlugin implements Plugin<Project> {
    @Override
    void apply(Project project) {
        project.android.registerTransform(new MyTransform(project))
    }
}
  • 6 提供可對外使用的插件
    插件完成了截酷,但是怎么才能對外使用呢涮拗?上面我們說到,我們采取3種插件形式之一的buildSrc迂苛。我們上文中創(chuàng)建了plugin.properties文件多搀。只需要在該文件中編輯實現(xiàn)類即可
implementation-class=MyPlugin
  • 7 應用方應用插件
    在應用方的gradle文件中做如下配置
apply plugin: 'plugin'

上面代碼我們注意到,plugin這個插件和plugin.properties的文件名是一樣的灾部。是的康铭,應用方應用的插件名和我們定義的properties文件名保持一致。

  • 8 結果展示
    源代碼如下赌髓,經(jīng)過我們插件處理之后从藤,編譯后的字節(jié)碼應該沒有了testDemo方法催跪。
public class MainActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(android.R.layout.activity_list_item);
    }
    public void testDemo() {
        System.out.println("demo test");
    }
}

那么,處理后的字節(jié)碼在哪呢夷野?在$project/build/intermediates/transforms/MyTransform/...
MyTransform是我自定義Transform的類名懊蒸,下面有debug和release包。繼續(xù)下去大家應該能找到對應的類悯搔。

圖3-結果展示.png

上圖我們看到骑丸,已經(jīng)沒有的testDemo方法。成功妒貌!

6 結束語

通過上面實戰(zhàn)練習通危,相信你已經(jīng)初步掌握了插樁的基本技術,但是這還遠遠不夠灌曙;在項目中會遇到各式各樣的問題菊碟,現(xiàn)實情況可能沒有demo這么簡單;不過沒關系在刺,如果在插樁過程中遇到任何問題逆害,都可以私信給我,我將盡我所能的給你提供最優(yōu)質的免費咨詢服務蚣驼。同時魄幕,我也非常歡迎大家互相交流技術,共同成長颖杏。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
禁止轉載梅垄,如需轉載請通過簡信或評論聯(lián)系作者。
  • 序言:七十年代末输玷,一起剝皮案震驚了整個濱河市队丝,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌欲鹏,老刑警劉巖机久,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異赔嚎,居然都是意外死亡膘盖,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進店門尤误,熙熙樓的掌柜王于貴愁眉苦臉地迎上來侠畔,“玉大人,你說我怎么就攤上這事损晤∪砉祝” “怎么了?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵尤勋,是天一觀的道長喘落。 經(jīng)常有香客問我茵宪,道長,這世上最難降的妖魔是什么瘦棋? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任稀火,我火速辦了婚禮,結果婚禮上赌朋,老公的妹妹穿的比我還像新娘凰狞。我一直安慰自己,他們只是感情好沛慢,可當我...
    茶點故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布赡若。 她就那樣靜靜地躺著,像睡著了一般颠焦。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上往枣,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天伐庭,我揣著相機與錄音,去河邊找鬼分冈。 笑死圾另,一個胖子當著我的面吹牛,可吹牛的內容都是我干的雕沉。 我是一名探鬼主播集乔,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼坡椒!你這毒婦竟也來了扰路?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤倔叼,失蹤者是張志新(化名)和其女友劉穎汗唱,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體丈攒,經(jīng)...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡哩罪,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了巡验。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片际插。...
    茶點故事閱讀 38,137評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖显设,靈堂內的尸體忽然破棺而出框弛,到底是詐尸還是另有隱情,我是刑警寧澤捕捂,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布功咒,位于F島的核電站愉阎,受9級特大地震影響,放射性物質發(fā)生泄漏力奋。R本人自食惡果不足惜榜旦,卻給世界環(huán)境...
    茶點故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望景殷。 院中可真熱鬧溅呢,春花似錦、人聲如沸猿挚。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽绩蜻。三九已至铣墨,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間办绝,已是汗流浹背伊约。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留孕蝉,地道東北人屡律。 一個月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像降淮,于是被迫代替她去往敵國和親超埋。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,901評論 2 345

推薦閱讀更多精彩內容