android自定義Gradle插件和asm的用法的一些知識

1 什么是插樁护赊?

聽到關(guān)于“插樁”的詞語惠遏,第一眼覺得會很高深,那到底什么是插樁呢骏啰?用通俗的話來講节吮,插樁就是將一段代碼通過某種策略插入到另一段代碼,或替換另一段代碼判耕。這里的代碼可以分為源碼和字節(jié)碼透绩,而我們所說的插樁一般指字節(jié)碼插樁。
圖1是Android開發(fā)者常見的一張圖壁熄,我們編寫的源碼(.java)通過javac編譯成字節(jié)碼(.class)帚豪,然后通過dx/d8編譯成dex文件。

image.png

我們下面要講的插樁草丧,就是在.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)缺點比較
截屏2020-07-22 下午5.25.43.png

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

  • 事件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種形式
截屏2020-07-22 下午5.25.52.png

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):

image.png
  • 描述類的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類型都用一個大寫字符來表示


    截屏2020-07-22 下午5.54.04.png

    如:
    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)建類似javapackage 實際上是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文件夾

image.png

修改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'

這里有幾點需要說明的是:

  1. 通常都是采用groovy語言來創(chuàng)建gradle plugin的石挂,groovy是兼容java的博助,你完全可以采用java來編寫插件。關(guān)于groovy語言痹愚,了解一些基礎(chǔ)語法就足夠支撐我們?nèi)ゾ帉懖寮恕?/li>
  2. 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ā)布

截屏2020-07-22 下午2.30.27.png
截屏2020-07-22 下午5.06.42.png

我們通過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兩個過濾緯度

image.png

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)``` 這個方法中如果啥都不寫會報這個錯誤
截屏2020-07-22 下午3.38.19.png

就是因為在這個方法中必須要實現(xiàn)如下內(nèi)容


截屏2020-07-22 下午3.45.29.png

這個方法對應(yīng)的就是下面這個目錄文件需了,/intermediates/transforms/LifeCycleTransform/debug 這個目錄
如果沒有寫會是這個樣子的

截屏2020-07-22 下午3.52.21.png

這樣就會導(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 這個目錄下涮俄,如下

image.png

在app->build->intermediates->transforms中蛉拙,可以看到所有的Transform,包括我們剛才自定義的Transform禽拔。從上圖中可以看到刘离,這里的0.jar、1.jar睹栖、2.jar等等硫惕,都是通過outputProvider.getContentLocation()方法來生成的,這個Transform目錄下的class文件野来、jar包等恼除,會當(dāng)做下一個Transform的inputs傳遞過去。

asm的原理:

image.png

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é)碼娩梨。我這里推薦一個比較好用的方法:

  1. 將要注入的java源碼先寫出來;
  2. 通過javac編譯出class文件览徒;
  3. 通過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();
}

注意:

  1. 每個module的build的目錄纽什,每一次build的時候,都會生成一個build的目錄

  2. transform 每一次的調(diào)試躲叼,用println來調(diào)試芦缰,會發(fā)現(xiàn)運行了一次transform,第二次在運行程序枫慷,就會發(fā)現(xiàn)看不到Log了让蕾,如果你要看Log,就要把依賴插件的Module的build的目錄刪除掉或听,才可以看到Log 或者clean project

  3. TransformInput:所謂Transform就是對輸入的class文件轉(zhuǎn)變成目標字節(jié)碼文件探孝,TransformInput就是這些輸入文件的抽象。目前它包括兩部分:DirectoryInput集合與JarInput集合誉裆。

  4. DirectoryInput:它代表著以源碼方式參與項目編譯的所有目錄結(jié)構(gòu)及其目錄下的源碼文件顿颅,可以借助于它來修改輸出文件的目錄結(jié)構(gòu)、已經(jīng)目標字節(jié)碼文件

  5. JarInput:它代表著以jar包方式參與項目編譯的所有本地jar包或遠程jar包足丢,可以借助于它來動態(tài)添加jar包粱腻。

  6. TransformOutputProvider:它代表的是Transform的輸出,例如可以通過它來獲取輸出路徑

  7. getName:用于指明本Transform的名字斩跌,也是代表該Transform的task的名字

  8. ContentType绍些,數(shù)據(jù)類型,
    有CLASSES和RESOURCES兩種滔驶。
    其中的CLASSES包含了源項目中的.class文件和第三方庫中的.class文件遇革。
    RESOURCES僅包含源項目中的.class文件

  9. getInputTypes:用于指明Transform的輸入類型,可以作為輸入過濾的手段揭糕。在TransformManager定義了如下的幾種類型:

dir.traverse(type: FileType.FILES,nameFilter:~/.*\.class/) { file ->
  System.out.println("find class:" + file.name)
 }

這個是遍歷目錄 File.traverse File里邊的方法
~/.*\.class/這個是正則表達式

  1. 遍歷文件夾
def dir = new File("/")
//eachFile()方法返回該目錄下的所有文件和子目錄萝快,不遞歸
dir.eachFile { file ->
    println file.name
}
dir.eachFileMatch(~/.*\.txt/) {file ->
    println file.name
}
  1. 遞歸遍歷文件夾
def dir = new File("/")
//dir.eachFileRecurse()方法會遞歸顯示該目錄下所有的文件和目錄
dir.eachFileRecurse { file ->
    println file.name
}
dir.eachFileRecurse(FileType.FILES) { file ->
    println file.name
}
  1. 一些更復(fù)雜的遍歷方法你可以使用traverse方法,但需要你設(shè)置一個特殊的標志指示如何遍歷:
dir.traverse { file ->
    //如果當(dāng)前文件是一個目錄且名字是bin著角,則停止遍歷
    if (file.directory && file.name=='bin') {
        FileVisitResult.TERMINATE
    //否則打印文件名字并繼續(xù)
    } else {
        println file.name
        FileVisitResult.CONTINUE
   }
}
  1. 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模式包

  1. android 的文件夾開頭為小寫字母
    包名都是小寫字母的
    如果文件夾開頭是大寫的話脏里,會找不到dataBinding

androidStudio 調(diào)試插件開發(fā)

1、創(chuàng)建remote調(diào)試任務(wù):
選擇 Eidt Configurations


image.png

點左上角的 + 號虹曙,選擇 remote迫横。Name可以隨意命名,其他配置可以不用動根吁,端口就5005员淫,點ok關(guān)閉


image.png

配置完以后


image.png

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)試的過程

image.png

run是插入的一些知識)

image.png

這邊run的按鈕哮针,運行的assembleDebug/assembleRelease
執(zhí)行的是 assembleRelease 還是 assembleDebug 實際是由 build variants 設(shè)置的類型決定的关面。

image.png
image.png

運行是沒有調(diào)試功能的,直接運行轉(zhuǎn)手機

debug:直接段點調(diào)試

attach debug
想象一下下面的場景:你的APK如果已經(jīng)運行在普通模式(非Debug)的情況下十厢,你突然想Debug等太,而又不想重新運行浪費時間,該怎么辦呢蛮放?

這邊的普通模式缩抡,就是點擊了run 按鈕

普通模式下想設(shè)置斷點進行調(diào)試可不可以呢?

當(dāng)然是可以的包颁,不僅可以瞻想,這種方式已經(jīng)漸漸替代了原先的方案伐脖,畢竟很方便煞茫,不是嗎?那具體要怎么做呢


image.png

二窄驹、點擊Attach調(diào)試

image.png
image.png

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)安裝過了柴信。


image.png

點擊 Install安裝宽气,安裝后點擊 Restart IDE 重啟 IDEA


image.png

這邊注意,用這個插件看字節(jié)碼萄涯,要是java文件或者groovy文件绪氛,kotlin文件看不到涝影,kotlin文件需要dex反編譯成class文件燃逻,用jd-gui.jar查看

2.3.2使用
使用時直接選擇 View --> Show Bytecode With jclasslib


image.png

注意:如果是自己項目的源碼需要先編譯


image.png

jclasslib窗口(查看字節(jié)碼)


image.png

可以查看基本信息伯襟、常量池姆怪、接口、屬性俺附、函數(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文件同在一個目錄中)


image.png

打開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
image.png

3逆瑞、jd-gui(這個工具是把class文件翻譯成基本的代碼)
雙擊運行 jd-gui-1.4.0.jar 文件祈远,
將.jar文件拖到工作區(qū)即可打開车份。


image.png
截屏2020-07-23 下午3.54.26.png

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
image.png
kotlin 用自帶AS 導(dǎo)航欄-Tools-kotlin-show kotlin bytecode
image.png

javap指令查看字節(jié)碼(和jclasslib效果一樣)

javap -v xx.class

借鑒連接

三種方式插樁

Android中Gradle插件和Transform

組件化自定義Gradle插件

字節(jié)碼鏈接

查看asmbyteCode的連接

Android Gradle Transform 詳解

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末栋艳,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子晴叨,更是在濱河造成了極大的恐慌兼蕊,老刑警劉巖件蚕,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件牵啦,死亡現(xiàn)場離奇詭異妄痪,居然都是意外死亡,警方通過查閱死者的電腦和手機土浸,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進店門栅迄,熙熙樓的掌柜王于貴愁眉苦臉地迎上來毅舆,“玉大人愈腾,你說我怎么就攤上這事虱黄〕髀遥” “怎么了?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵作瞄,是天一觀的道長宗挥。 經(jīng)常有香客問我契耿,道長螃征,這世上最難降的妖魔是什么会傲? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任淌山,我火速辦了婚禮泼疑,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘移稳。我一直安慰自己个粱,他們只是感情好都许,可當(dāng)我...
    茶點故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布胶征。 她就那樣靜靜地躺著睛低,像睡著了一般。 火紅的嫁衣襯著肌膚如雪骂铁。 梳的紋絲不亂的頭發(fā)上从铲,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天,我揣著相機與錄音泣懊,去河邊找鬼麻惶。 笑死窃蹋,一個胖子當(dāng)著我的面吹牛警没,可吹牛的內(nèi)容都是我干的杀迹。 我是一名探鬼主播,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼大州,長吁一口氣:“原來是場噩夢啊……” “哼厦画!你這毒婦竟也來了滥朱?” 一聲冷哼從身側(cè)響起焚虱,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤鹃栽,失蹤者是張志新(化名)和其女友劉穎民鼓,沒想到半個月后丰嘉,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年荐开,在試婚紗的時候發(fā)現(xiàn)自己被綠了晃听。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片能扒。...
    茶點故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡初斑,死狀恐怖越平,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情晦溪,我是刑警寧澤三圆,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布舟肉,位于F島的核電站路媚,受9級特大地震影響樊销,放射性物質(zhì)發(fā)生泄漏围苫。R本人自食惡果不足惜剂府,卻給世界環(huán)境...
    茶點故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一腺占、第九天 我趴在偏房一處隱蔽的房頂上張望衰伯。 院中可真熱鬧嚎研,春花似錦库倘、人聲如沸教翩。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽蚂且。三九已至杏死,卻和暖如春捆交,著一層夾襖步出監(jiān)牢的瞬間品追,已是汗流浹背肉瓦。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工风宁, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留戒财,地道東北人饮寞。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓幽崩,卻偏偏與公主長得像慌申,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子咨油,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,916評論 2 344