篇頭語
應(yīng)師傅指導(dǎo)隙轻,最近研究了一下從Gradle編譯入手,實(shí)現(xiàn)字節(jié)碼插樁瞳购,進(jìn)而實(shí)現(xiàn)一些功能话侄,其實(shí)網(wǎng)上相關(guān)文章也不算太少,但是就我一路研究琢磨的過程而言苛败,網(wǎng)上的文章東一塊满葛、西一塊径簿,每個文章各有其精華罢屈,但是你要是想在網(wǎng)上的某幾篇文章就搞懂怎么回事還是需要這方面功底的,所以對于小白就不是很友好篇亭,包括一會要提的ASM文檔寫的也實(shí)在”抽象“缠捌,所以我就此做一個學(xué)習(xí)總結(jié),希望小白通過我這一篇文章就可以基本上掌握脈絡(luò)译蒂,因?yàn)槲沂且粋€小白曼月,研究這個方向是一點(diǎn)一點(diǎn)的入手,所以這篇學(xué)習(xí)分享就站在小白的角度面向小白分享柔昼,所以很多基礎(chǔ)也會說哑芹,大神可以跳過。
對于沒有接觸過這方面的同學(xué)可能一下沒有理解本篇標(biāo)題的意思捕透,這很正常聪姿,最開始我也不理解,我先大概解釋一下要做什么事情乙嘀,Android 的java文件會編譯成class文件末购,然后才運(yùn)行,java是我們手寫的虎谢,class文件為只讀狀態(tài)盟榴,我們不能修改,我們接下來要做的事婴噩,簡單來說擎场,就是”暗箱操作“一下class文件羽德。使得編譯出的class文件達(dá)到我們的一些期望。
在說別的之前迅办,我們先看一下android gradle的編譯過程
我們直觀點(diǎn)先看一張圖玩般,這張圖的復(fù)雜程度我覺得剛剛好,太簡單就會忽略掉一些重要的部分礼饱,再復(fù)雜的過程我們也暫時用不到坏为,所以就先貼這一張圖。
ps:上圖中綠圈表示的工具镊绪、中間過程匀伏,不是產(chǎn)物,別看錯
從上面的流程圖蝴韭,我們可以看出apk打包流程可以分為以下七步
-
Java編譯器對工程本身的java代碼進(jìn)行編譯够颠,這些java代碼有三個來源:app的源代碼,由資源文件生成的R文件(aapt工具)榄鉴,以及有aidl文件生成的java接口文件(aidl工具)履磨。產(chǎn)出為.class文件。
這里啰嗦一點(diǎn)庆尘,因?yàn)槲以诳催@個圖的時候?qū)@里產(chǎn)生的疑問:為什么這么多種Resources剃诅?
其實(shí)細(xì)心的可以在上圖中看到兩個Resources,一個是圖中左上角的Application Resources驶忌,一個是圖中間右側(cè)的Other Resouces矛辕,這兩個Resources是有區(qū)別的,上面的Application Resources其實(shí)是指項目中我們常見的放在根目錄下的assets目錄和medule下的res目錄付魔,區(qū)別是assets中的資源會被原封不動的打包在apk內(nèi)聊品,res資源中保存的文件大多會被編譯,而且會被賦予資源id几苍,這樣就可以在程序中使用id的形式來訪問資源翻屈,res中根據(jù)類型不同分為十種子類型,包括layout妻坝、drawable伸眶、xml等,這個大家都很熟悉啦~因?yàn)槠渲械馁Y源都是需要高速響應(yīng)的惠勒,需要較高的性能要求赚抡,例如支持不同分辨率的屏幕等,所以需要快速定位資源纠屋,所以賦予ID涂臣,這些ID值以常量的形式定義在一個R.java文件中,然后會生成一個resources.arcs文件,用來描述那些具有ID值的資源的配置信息赁遗,想當(dāng)以索引表的作用署辉,在該文件中,如果某個id對應(yīng)的是string岩四,那么該文件會直接包含該值哭尝,如果id對應(yīng)的資源是某個layout或者drawable資源,那么該文件會存入對應(yīng)資源的路徑剖煌。
而asset中的文件既然沒有ID材鹦,訪問的時候就要指定文件名:AssetManager am= getAssets(); InputStream is = assset.open("filename");
如果你和我一樣此時思考到了那asset目錄中存放什么呢?我來告訴你耕姊,經(jīng)過了一堆的google桶唐、baidu和討論之后,發(fā)現(xiàn)茉兰,其實(shí)沒有什么死區(qū)別尤泽,但是你從我剛剛說的編譯方式可以看出一點(diǎn)眉頭吧,就是res中資源編譯的過程比較復(fù)雜规脸,但是調(diào)用性能更好坯约,assets資源不會編譯,所以如果你想資源被編譯然后對資源調(diào)用性能要求很高莫鸭,你就放在res目錄下吧闹丐。
另外,圖中間左側(cè)的complied Resources就是包含resources.arcs的.ap_文件和assets資源黔龟,而右側(cè)那個Resources是jni(提供api供java和其他語言通信)妇智、.so文件(share object滥玷,類似于windows中的鏈接庫氏身,如果你對這兩個文件是啥很執(zhí)著,可以自己去查)
.class文件和依賴的三方庫文件通過dex工具生成Delvik虛擬機(jī)可執(zhí)行的.dex文件惑畴,可能有一個或多個,包含了所有的class信息蛋欣,包括項目自身的class和依賴的class。產(chǎn)出為.dex文件如贷。
apkbuilder工具將.dex文件和編譯后的資源文件生成未經(jīng)簽名對齊的apk文件陷虎。這里編譯后的資源文件包括兩部分,一是由aapt編譯產(chǎn)生的編譯后的資源文件杠袱,二是依賴的三方庫里的資源文件尚猿。產(chǎn)出為未經(jīng)簽名的.apk文件。
分別由Jarsigner和zipalign對apk文件進(jìn)行簽名和對齊楣富,生成最終的apk文件凿掂。
說了這么多無非是想了解一下編譯的大概過程,但是對于此時此刻我們需要關(guān)注的就是我們要在.class 文件和第三方庫編譯成.dex文件這一步動手腳,原因是這一步可以拿到我們手寫的java編譯的.class也可以拿到我們想修改的jar包庄萎,下一步就成了apk踪少,就已經(jīng)無力回天了,所以這個契機(jī)甚好糠涛!
走到這你可能就想了援奢,既然我們要修改代碼,那為啥不直接回去改代碼不就好了忍捡?都這么大一個圈子做啥呢集漾??如果你這么想了砸脊,那咱倆真是太默契了帆竹!我覺得知道為什么這樣做很有必要,有對比才有傷害脓规!
動態(tài)修改Java代碼的原因
我貼一段網(wǎng)上很多地方都引用的例子:(我覺得你如果想了解裝飾者模式可以看看栽连,不然直接看我下面的總結(jié)也無所謂)
動態(tài)生成 Java 類與 AOP 密切相關(guān)的。AOP 的初衷在于軟件設(shè)計世界中存在這么一類代碼侨舆,零散而又耦合:零散是由于一些公有的功能(諸如著名的 log 例子)分散在所有模塊之中秒紧;同時改變 log 功能又會影響到所有的模塊。出現(xiàn)這樣的缺陷挨下,很大程度上是由于傳統(tǒng)的 面向?qū)ο缶幊套⒅匾岳^承關(guān)系為代表的“縱向”關(guān)系熔恢,而對于擁有相同功能或者說方面 (Aspect)的模塊之間的“橫向”關(guān)系不能很好地表達(dá)。例如臭笆,目前有一個既有的銀行管理系統(tǒng)叙淌,包括 Bank、Customer愁铺、Account鹰霍、Invoice 等對象,現(xiàn)在要加入一個安全檢查模塊茵乱, 對已有類的所有操作之前都必須進(jìn)行一次安全檢查茂洒。
然而 Bank、Customer瓶竭、Account督勺、Invoice 是代表不同的事務(wù),派生自不同的父類斤贰,很難在高層上加入關(guān)于 Security Checker 的共有功能智哀。對于沒有多繼承的 Java 來說,更是如此荧恍。傳統(tǒng)的解決方案是使用 Decorator 模式瓷叫,它可以在一定程度上改善耦合,而功能仍舊是分散的 —— 每個需要 Security Checker 的類都必須要派生一個 Decorator,每個需要 Security Checker 的方法都要被包裝(wrap)赞辩。下面我們以 Account類為例看一下 Decorator:
首先雌芽,我們有一個 SecurityChecker類,其靜態(tài)方法 checkSecurity執(zhí)行安全檢查功能:
public class SecurityChecker {
public static void checkSecurity() {
System.out.println("SecurityChecker.checkSecurity ...");
//TODO real security check
}
}
另一個是 Account類:
public class Account {
public void operation() {
System.out.println("operation...");
//TODO real operation
}
}
若想對 operation加入對 SecurityCheck.checkSecurity()調(diào)用辨嗽,標(biāo)準(zhǔn)的 Decorator 需要先定義一個 Account類的接口:
public interface Account {
void operation();
}
然后把原來的 Account類定義為一個實(shí)現(xiàn)類:
public class AccountImpl extends Account{
public void operation() {
System.out.println("operation...");
//TODO real operation
}
}
定義一個 Account類的 Decorator世落,并包裝 operation方法:
public class AccountWithSecurityCheck implements Account {
private Account account;
public AccountWithSecurityCheck (Account account) {
this.account = account;
}
public void operation() {
SecurityChecker.checkSecurity();
account.operation();
}
}
在這個簡單的例子里,改造一個類的一個方法還好糟需,如果是變動整個模塊屉佳,Decorator 很快就會演化成另一個噩夢。動態(tài)改變 Java 類就是要解決 AOP 的問題洲押,提供一種得到系統(tǒng)支持的可編程的方法武花,自動化地生成或者增強(qiáng) Java 代碼。這種技術(shù)已經(jīng)廣泛應(yīng)用于最新的 Java 框架內(nèi)杈帐,如 Hibernate体箕,Spring 等。
====引用完畢===
我看了一些例子或者說原因總結(jié)也就不過幾點(diǎn):
- 如果是如log一樣零散的代碼直接寫不好修改
- 如果是好多類要加一個方法則要搞共同父類或者裝飾者模式使得類特別多
- 如果是你想修改引用的包挑童,你就只能用這種方式累铅,也不能說只能,當(dāng)然你可以解壓縮jar包再用同名java類編譯成class文件替換后在壓成jar包站叼,但是這種方式明顯是不好的娃兽,而且如果你用字節(jié)碼插樁的話,即使你的一個jar包更新了尽楔,你也不用再做任何操作投储,你的插樁程序會繼續(xù)有效!
- (我自己覺得)學(xué)會了插樁阔馋,你想監(jiān)控你的項目中的某個或者某種類的行為和性能消耗簡直輕而易舉玛荞,爽呆!
現(xiàn)在我們知道了我們?yōu)槭裁匆@么做垦缅,應(yīng)該更有動力學(xué)了冲泥!
上面說了動手的時機(jī)和動手的原因,下面說要怎么擼起袖子干了壁涎,其實(shí)在很多文章中直接開始上代碼講技術(shù),不講那一步具體是做什么用的志秃,這樣的文章看起來就比較吃力怔球,而且沒有頭緒,但是我不能這么干浮还,所以為了思路清晰竟坛,我先說一下
整體上用到了哪些東西
圖畫的丑了點(diǎn),對付著看下,解釋一下:
在這些.class文件和.dex的中間過程担汤,其實(shí)是一個個Transform涎跨,每一個Transform實(shí)際上是一個gradle Task,他們想當(dāng)于加工生產(chǎn)線上的一個個環(huán)節(jié)崭歧,每一次”加工“接收上一次加工的結(jié)果作為輸入隅很,輸出送給下一個”加工“,而我們要做的事情就是創(chuàng)建一個這樣的Transform率碾,拿到上一步的輸入做一些手腳叔营,再把我們”暗箱操作“的成品傳給下一個輸入繼續(xù)編譯,通常我們自己創(chuàng)建的Transform會被加到transform隊列的第一個所宰,之后再這個transform中使用ASM來處理字節(jié)碼绒尊。而如何把Transform嫁接上去,就要使用到自定義plugin的相關(guān)內(nèi)容仔粥,所以先來解釋一下
自定義plugin的相關(guān)流程
概述
Gradle 提供了很多官方插件婴谱,用于支持Java、Groovy等工程的構(gòu)建和打包躯泰。同時也提供了自定義插件機(jī)制勘究,讓每個人都可以通過插件來實(shí)現(xiàn)特定的構(gòu)建邏輯,并可以把這些邏輯打包起來斟冕,分享給其他人口糕。插件的源碼可以使用Scale、groovy磕蛇、java編寫景描,你可以會哪個就用哪個,groovy中完美的繼承了java-
三種方式
-
你可以直接寫在build.gradle中秀撇,給個例子
/** * 分別定義Extension1 和 Extension2 類超棺,申明參數(shù)傳遞變量 */ class Extension1 { String testVariable1 = null } class Extension2 { String testVariable2 = null } /** * 插件入口類 */ class TestPlugin implements Plugin<Project> { @Override void apply(Project project) { //利用Extension創(chuàng)建e1 e2 閉包,用于接受外部傳遞的參數(shù)值 project.extensions.create('e1', Extension1) project.extensions.create('e2', Extension2) //創(chuàng)建readExtension task 執(zhí)行該task 進(jìn)行參數(shù)值的讀取以及自定義邏輯... project.task('readExtension') << { println 'e1 = ' + project['e1'].testVariable1 println 'e2 = ' + project['e2'].testVariable2 } } } /** * 依賴我們剛剛自定義的TestPlugin呵燕,注意 使用e1 {} || e2{} 一定要放 在 apply plugin:TestPlugin 后面, 因?yàn)?app plugin:TestPlugin * 會執(zhí)行 Plugin的apply 方法棠绘,進(jìn)而利用Extension 將e1 、e2 和 Extension1 Extension2 綁定再扭,編譯器才不會報錯 */ apply plugin: TestPlugin e1 { testVariable1 = 'testVariable1' } e2 { testVariable2 = 'testVariable2' }
自定義插件中官方給了很多相關(guān)的api氧苍,如果想要仔細(xì)了解可以查看官方文檔
第二種是寫在一個module中,只對一個項目可見泛范,試用于邏輯比較復(fù)雜但是對外不可見的插件让虐,這里不展開,整體寫法和第三種差不多罢荡,第三種會了第二種稍微改動就OK
-
第三種是寫成獨(dú)立項目
先在項目根目錄下建立一個module (Android Library Module),在3.4.1版本的Android studio中new plugin選擇Android library,取名為plugin赡突,這個plugin就是插件对扶,清空plugin目錄下的其他文件,只保留src/main這個目錄和build.gradle惭缰,注意src/main下的東西都清空浪南,然后注意build.gradle中內(nèi)容更改為以下,然后sync(不然下面的步驟將無法正常進(jìn)行,編譯器不會把即將新建的groovy目錄識別為groovy源代碼目錄漱受,不能在該目錄下新建包络凿,即將新建的resources也不會被編譯器識別為資源文件夾)
apply plugin: 'groovy' apply plugin: 'maven' dependencies{ // gradle sdk compile gradleApi() // groovy sdk compile localGroovy() compile 'com.android.tools.build:gradle:3.4.1' } repositories{ mavenCentral() }
注意上面的gradle版本其實(shí)是有講究的,版本最好不要太低拜效。然后在main目錄下新建一個groovy文件夾喷众,因?yàn)槲覀冮_發(fā)的插件相當(dāng)于一個Groovy項目,在groovy目錄下新建包名,com.llew.bytecode.fix.plugin ,然后在這個包名下新建BytecodeFixPlugin.groovy文件紧憾,因?yàn)橐獎?chuàng)建Gradle插件就必須要實(shí)現(xiàn)Gradle包中的org.gradle.api.Plugin接口到千,所以BytecodeFixPlugin.groovy內(nèi)容如下所示:
package com.llew.bytecode.fix.plugin; import org.gradle.api.Plugin; import org.gradle.api.Project; public class BytecodeFixPlugin implements Plugin<Project> { @Override void apply(Project project) { println "this is a gradle plugin, (*^__^*)……" } }
插件定義好之后我們要告訴Gradle哪一個類是我們定義的插件類,因此需要在main目錄下創(chuàng)建resources目錄赴穗,然后在resources目錄下創(chuàng)建META-INF目錄憔四,接著在META-INF目錄下創(chuàng)建gradle-plugins目錄,gradle-plugins目錄是自定義Gradle插件的必備目錄般眉,然后在該目錄下創(chuàng)建一個properties文件了赵,文件名為com.llew.bytecode.fix.properties,這個文件名是有技巧的甸赃,當(dāng)起完名字后如果要使用插件柿汛,就可以這樣:apply plugin 'com.llew.bytecode.fix';起完名字后還不可以使用該插件埠对,還要告訴Gradle自定義插件的具體實(shí)現(xiàn)類是哪一個络断,在com.llew.bytecode.fix.properties文件中添加如下內(nèi)容:
implementation-class=com.llew.bytecode.fix.plugin.BytecodeFixPlugin
這樣就告訴了Gradle插件的實(shí)現(xiàn)類是com.llew.bytecode.fix.plugin.BytecodeFixPlugin,定義完了以上配置后项玛,還需要把插件打包到Maven倉庫后才可以使用貌笨,為了簡單起見,我們直接把插件打包到本地Maven倉庫襟沮,在plugin的build.gradle中完整配置如下:
apply plugin: 'groovy' apply plugin: 'maven' repositories { jcenter() mavenCentral() } dependencies { compile gradleApi() compile localGroovy() compile 'com.android.tools.build:gradle:3.4.1' } group = 'com.llew.bytecode.fix' version = '1.0.0' uploadArchives { repositories { mavenDeployer { repository(url: uri("../repository")) } } }
這時候maven锥惋,plugin就配置好了,還需要提交倉庫和引用
-
建立好倉庫开伏,就可以引用了膀跌,在主項目(app)下的build.gradle中添加
apply plugin: 'com.llew.bytecode.fix'
在根目錄下的bulid.gradle的dependencies中添加這么一句
classpath 'com.llew.bytecode.fix:plugin:1.0.0'
別忘了添加本地依賴,因?yàn)閷τ趧倓偺砑拥腸lasspath硅则,任何classpath都需要到repositories所提供的地址中去查找有無淹父,然后進(jìn)行配置,添加了本地依賴就是為本地的這個插件提供來源怎虫。
buildscript {
repositories {
google()
jcenter()
maven {// 添加Maven的本地依賴
url uri('./repository')
}
}
}
注意是在根目錄的build.gradle中的buildscript中的repositories中添加了maven倉庫暑认,可能有小伙伴會問了,build.gradle buildscript 里面的repositories 和allprojects里面 repositories 的區(qū)別大审,簡單說一下:buildscript 里面的 repositories 表示只有編譯工具才會用這個倉庫蘸际,而allprojects是項目本身需要的依賴。
然后想查看效果的話可以clean一下徒扶,再make project粮彤,直接ReBuild就行,但是注意一下輸出打印的地方,AS3.0之后的Gradle Console集成在Build中
這個效果其實(shí)只是告訴我們姜骡,我們的插件在gradle編譯過程中起作用了导坟,那么可以進(jìn)行了下一個環(huán)節(jié),下一步是
在plugin中插入Transform
在剛剛的com.llew.bytecode.fix包名下建一個transform Package用來存放Transform文件圈澈,這里的transform文件可以用java寫也可以用groovy寫惫周,但是差距不大,即使你用的groovy文件康栈,也可以使用java api递递,因?yàn)間roovy更靈活,這里就用groovy文件啥么,再次提醒創(chuàng)建groovy文件的方式是新建file登舞,然后以.groovy結(jié)尾
可以看到j(luò)ava文件前面的標(biāo)記是原型的,groovy是方形的悬荣。
新建一個名字為AsmInjectTrans的transform菠秒,下面是一個transform的標(biāo)準(zhǔn)結(jié)構(gòu),無論你是建立java文件還是groovy文件
public class AsmInjectTrans extends Transform {
private static final String TAG = "BytecodeFixTransform"
@Override
String getName() {
return TAG
}
@Override
Set<QualifiedContent.ContentType> getInputTypes() {
return TransformManager.CONTENT_CLASS
}
@Override
Set<? super QualifiedContent.Scope> getScopes() {
return TransformManager.SCOPE_FULL_PROJECT
}
@Override
boolean isIncremental() {
return false
}
@Override
void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
super.transform(transformInvocation)
}
}
方法
getName()不用說了
getInputTypes()氯迂,getScopes()践叠,這個要多說幾句,在這一方面km上有一篇文章寫的很細(xì)了囚戚,對于上面的transform圖酵熙,只是展示Transform的其中一種情況。而Transform其實(shí)可以有兩種輸入驰坊,一種是消費(fèi)型的匾二,當(dāng)前Transform需要將消費(fèi)型型輸出給下一個Transform,另一種是引用型的拳芙,當(dāng)前Transform可以讀取這些輸入棺亭,而不需要輸出給下一個Transform,比如Instant Run就是通過這種方式技扼,檢查兩次編譯之間的diff的千康。至于怎么在一個Transform中聲明兩種輸入,以及怎么處理兩種輸入睹限,后面將有示例代碼譬猫。而Scope和contentType是transform輸入的兩種過濾機(jī)制
ContentType讯檐,顧名思義,就是數(shù)據(jù)類型染服,在插件開發(fā)中别洪,我們一般只能使用CLASSES和RESOURCES兩種類型,注意柳刮,其中的CLASSES已經(jīng)包含了class文件和jar文件
從圖中可以看到挖垛,除了CLASSES和RESOURCES,還有一些我們開發(fā)過程無法使用的類型秉颗,比如DEX文件痢毒,這些隱藏類型在一個獨(dú)立的枚舉類ExtendedContentType中,這些類型只能給Android編譯器使用蚕甥。另外哪替,我們一般使用 TransformManager中提供的幾個常用的ContentType集合和Scope集合,如果是要處理所有class和jar的字節(jié)碼梢灭,ContentType我們一般使用TransformManager.CONTENT_CLASS
夷家。
注意一下,這個TransformManager敏释,如果你找不到這個類库快,記得提升gradle-api版本到3.1.4以上
implementation 'com.android.tools.build:gradle-api:3.1.4'
Scope相比ContentType則是另一個維度的過濾規(guī)則,
我們可以發(fā)現(xiàn)钥顽,左邊幾個類型可供我們使用义屏,而我們一般都是組合使用這幾個類型,TransformManager有幾個常用的Scope集合方便開發(fā)者使用蜂大。
如果是要處理所有class字節(jié)碼闽铐,Scope我們一般使用TransformManager.SCOPE_FULL_PROJECT
。
isIncremental()是否增量編譯
-
transform()重點(diǎn)關(guān)注的方法奶浦,我們在這里做相關(guān)操作兄墅,
我分別貼一個java版和groovy版
//java @Override public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException { super.transform(transformInvocation); //消費(fèi)型輸入,可以從中獲取jar包和class文件夾路徑澳叉。需要輸出給下一個任務(wù) Collection<TransformInput> inputs = transformInvocation.getInputs(); //引用型輸入隙咸,無需輸出。 Collection<TransformInput> referencedInputs = transformInvocation.getReferencedInputs(); //OutputProvider管理輸出路徑成洗,如果消費(fèi)型輸入為空五督,你會發(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,就可以實(shí)現(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瓶殃,就可以實(shí)現(xiàn)編譯期間干預(yù)字節(jié)碼的目的了 FileUtils.copyDirectory(directoryInput.getFile(), dest); } } } //groovy @Override void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException { super.transform(transformInvocation) transformInvocation.inputs.each { TransformInput input -> input.directoryInputs.each { DirectoryInput directoryInput -> // 下面是一些判斷條件和處理充包,暫時可以不看 if (directoryInput.file.isDirectory()){ directoryInput.file.eachFileRecurse { File file -> def name = file.name if (name.endsWith(".class") && !name.endsWith("R.class") && !name.endsWith("BuildConfig.class") && !name.contains("R\$") ){ println("==== directoryInput file name == "+ file.getAbsolutePath()) ClassReader classReader = new ClassReader(file.bytes) ClassWriter classWriter = new ClassWriter(classReader,ClassWriter.COMPUTE_MAXS) AsmClassVisitor classVisitor = new AsmClassVisitor(Opcodes.ASM5,classWriter) classReader.accept(classVisitor,ClassReader.EXPAND_FRAMES) byte[] bytes = classWriter.toByteArray() File destFile = new File(file.parentFile.absoluteFile,name) FileOutputStream fileOutputStream = new FileOutputStream(destFile); fileOutputStream.write(bytes) fileOutputStream.close() } } } def dest = transformInvocation.outputProvider.getContentLocation(directoryInput.name,directoryInput.contentTypes,directoryInput.scopes,Format.DIRECTORY) FileUtils.copyDirectory(directoryInput.file,dest) } input.jarInputs.each {JarInput jarInput -> def jarName = jarInput.name def md5Name = DigestUtils.md5Hex(jarInput.file.getAbsolutePath()) if (jarName.endsWith(".jar")){ jarName = jarName.substring(0,jarName.length() - 4) } def dest = transformInvocation.outputProvider.getContentLocation(jarName+md5Name,jarInput.contentTypes,jarInput.scopes,Format.JAR) FileUtils.copyFile(jarInput.file,dest) } }
}
兩種寫法除了語法上略有不同以外,總體思路是一樣的遥椿,從transformInvocation獲取輸入基矮,然后從中獲取jar包和class文件淆储,一波操作之后再用outputProvider獲取文件的出口,注釋中也寫的很明白了
架子搭起來愈捅,但是此時的transform還沒有引用到plugin中遏考,下文會講解插入方式慈鸠。
這里插一嘴比較重要的蓝谨,我們編寫的transform還有即將在transform中加入的ASM代碼,還有外層的plugin中的代碼青团,甚至包括我們整個的plugin這個module譬巫,這些都屬于我們自定義插件中的內(nèi)容,如果有所修改督笆,一定要重新uploadArchives一下芦昔,也就是更新庫,不然你用的還是舊的代碼哦~ 而更新庫的時候并不會執(zhí)行transform中的內(nèi)容娃肿,注意編譯時執(zhí)行和庫發(fā)布是不一樣的
ASM
終于到了重頭戲咕缎,調(diào)整一下思路
如果做一個比喻的話我個人比較喜歡把整個流程比喻成一個水源過濾水管
- .class gradle編譯到.dex是插入時機(jī),相當(dāng)于我們要剪開過濾水管的一個特定部分
- 自定義插件(plugin)是我們打開這個過濾水管的鉗子
- transform是我們要接入這個過濾水管的一段自制水管
- 而ASM是我們自制水管的過濾網(wǎng)料扰,至于怎么過濾全看ASM的操作
這樣比喻是不是各個環(huán)節(jié)的作用就清晰很多啦~
因?yàn)锳SM是對字節(jié)碼進(jìn)行操作凭豪,所以需要掌握關(guān)于字節(jié)碼的知識,
在編譯過程中晒杈,我們的java文件會被javac編譯器編譯成.class文件嫂伞,如果你單獨(dú)打開class文件會發(fā)現(xiàn)是這樣的結(jié)構(gòu):
這一看你可能會覺得這是個什么東西?但是其實(shí)我們不用也不可能去了解數(shù)字的排列所代表的意義拯钻,我們只需要關(guān)注他的組成結(jié)構(gòu):
- Magic:該項存放了一個 Java 類文件的魔數(shù)(magic number)和版本信息帖努。一個 Java 類文件的前 4 個字節(jié)被稱為它的魔數(shù)。每個正確的 Java 類文件都是以 0xCAFEBABE 開頭的粪般,這樣保證了 Java 虛擬機(jī)能很輕松的分辨出 Java 文件和非 Java 文件拼余。
- Version:該項存放了 Java 類文件的版本信息,它對于一個 Java 文件具有重要的意義亩歹。因?yàn)?Java 技術(shù)一直在發(fā)展匙监,所以類文件的格式也處在不斷變化之中。類文件的版本信息讓虛擬機(jī)知道如何去讀取并處理該類文件捆憎。
- Constant Pool:該項存放了類中各種文字字符串舅柜、類名、方法名和接口名稱躲惰、final 變量以及對外部類的引用信息等常量致份。虛擬機(jī)必須為每一個被裝載的類維護(hù)一個常量池,常量池中存儲了相應(yīng)類型所用到的所有類型础拨、字段和方法的符號引用氮块,因此它在 Java 的動態(tài)鏈接中起到了核心的作用绍载。常量池的大小平均占到了整個類大小的 60% 左右。
- Access_flag:該項指明了該文件中定義的是類還是接口(一個 class 文件中只能有一個類或接口)滔蝉,同時還指名了類或接口的訪問標(biāo)志击儡,如 public,private, abstract 等信息蝠引。
- This Class:指向表示該類全限定名稱的字符串常量的指針阳谍。
- Super Class:指向表示父類全限定名稱的字符串常量的指針。
- Interfaces:一個指針數(shù)組螃概,存放了該類或父類實(shí)現(xiàn)的所有接口名稱的字符串常量的指針矫夯。以上三項所指向的常量,特別是前兩項吊洼,在我們用 ASM 從已有類派生新類時一般需要修改:將類名稱改為子類名稱训貌;將父類改為派生前的類名稱;如果有必要冒窍,增加新的實(shí)現(xiàn)接口递沪。
- Fields:該項對類或接口中聲明的字段進(jìn)行了細(xì)致的描述。需要注意的是综液,fields 列表中僅列出了本類或接口中的字段,并不包括從超類和父接口繼承而來的字段意乓。
- Methods:該項對類或接口中聲明的方法進(jìn)行了細(xì)致的描述。例如方法的名稱、參數(shù)和返回值類型等屋灌。需要注意的是悔捶,methods 列表里僅存放了本類或本接口中的方法袜匿,并不包括從超類和父接口繼承而來的方法。使用 ASM 進(jìn)行 AOP 編程稚疹,通常是通過調(diào)整 Method 中的指令來實(shí)現(xiàn)的居灯。
- Class attributes:該項存放了在該文件中類或接口所定義的屬性的基本信息。
篇幅原因内狗,更多相關(guān)知識詳見該字節(jié)碼博客怪嫌,總之上面的數(shù)字經(jīng)過javap可以反編譯成下面的格式
Classfile /E:/JavaCode/TestProj/out/production/TestProj/com/rhythm7/Main.class
Last modified 2018-4-7; size 362 bytes
MD5 checksum 4aed8540b098992663b7ba08c65312de
Compiled from "Main.java"
public class com.rhythm7.Main
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #4.#18 // java/lang/Object."<init>":()V
#2 = Fieldref #3.#19 // com/rhythm7/Main.m:I
#3 = Class #20 // com/rhythm7/Main
#4 = Class #21 // java/lang/Object
#5 = Utf8 m
#6 = Utf8 I
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lcom/rhythm7/Main;
#14 = Utf8 inc
#15 = Utf8 ()I
#16 = Utf8 SourceFile
#17 = Utf8 Main.java
#18 = NameAndType #7:#8 // "<init>":()V
#19 = NameAndType #5:#6 // m:I
#20 = Utf8 com/rhythm7/Main
#21 = Utf8 java/lang/Object
{
private int m;
descriptor: I
flags: ACC_PRIVATE
public com.rhythm7.Main();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/rhythm7/Main;
public int inc();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field m:I
4: iconst_1
5: iadd
6: ireturn
LineNumberTable:
line 8: 0
LocalVariableTable:
Start Length Slot Name Signature
0 7 0 this Lcom/rhythm7/Main;
}
SourceFile: "Main.java"
查看方法:
然后找一個編輯器打開就好,mac的文本編輯好像不行柳沙,你可以用sublime打開
但是如果你直接用編譯器打開會發(fā)現(xiàn)是這樣的是因?yàn)榫幾g器自動幫我們進(jìn)行了解碼
回歸正題岩灭,為什么要了解反編譯的字節(jié)碼?因?yàn)锳SM不是直接對數(shù)字字節(jié)碼進(jìn)行操作赂鲤,而是對類似于”com/rhythm7/Main.m:I“這種字節(jié)碼反編譯后的格式進(jìn)行操作噪径,之后的處理過程我們無需過問,既然是這樣的格式数初,對于開發(fā)者就友好的多找爱,我們無需關(guān)注class文件冗長的數(shù)字中方法的偏移量、編碼方式泡孩、指代含義等车摄,只需要關(guān)注字節(jié)碼指令即可。
ASM提供很多vistor接口供我們使用,在 ASM 中练般,提供了一個 ClassReader類矗漾,這個類可以直接由字節(jié)數(shù)組或由 class 文件間接的獲得字節(jié)碼數(shù)據(jù),它能正確的分析字節(jié)碼薄料,構(gòu)建出抽象的樹在內(nèi)存中表示字節(jié)碼敞贡。它會調(diào)用 accept方法,這個方法接受一個繼承了 ClassVisitor抽象類的對象實(shí)例作為參數(shù)摄职,然后依次調(diào)用 ClassVisitor接口的各個方法誊役。字節(jié)碼空間上的偏移被轉(zhuǎn)換成 visit 事件時間上調(diào)用的先后,所謂 visit 事件是指對各種不同 visit 函數(shù)的調(diào)用谷市,ClassReader知道如何調(diào)用各種 visit 函數(shù)蛔垢。在這個過程中用戶無法對操作進(jìn)行干涉,因?yàn)楸闅v的算法是框架提供的迫悠、確定的鹏漆,具體詳細(xì)的代碼可以點(diǎn)進(jìn)ClassReader類中進(jìn)行查看,但是用戶可以做的是提供不同的 Visitor 创泄,重寫Visitor中的不同的 visit方法來對字節(jié)碼樹進(jìn)行不同的修改艺玲。ClassVisitor會產(chǎn)生一些子過程,比如 visitMethod會返回一個實(shí)現(xiàn) MethordVisitor接口的實(shí)例鞠抑,visitField會返回一個實(shí)現(xiàn) FieldVisitor接口的實(shí)例饭聚,完成子過程后控制返回到父過程,繼續(xù)訪問下一節(jié)點(diǎn)搁拙。因此對于 ClassReader來說秒梳,其內(nèi)部順序訪問是有一定要求的。實(shí)際上用戶還可以不通過 ClassReader類箕速,自行手工控制這個流程酪碘,只要按照一定的順序,各個 visit 事件被先后正確的調(diào)用弧满,最后就能生成可以被正確加載的字節(jié)碼婆跑。當(dāng)然獲得更大靈活性的同時也加大了調(diào)整字節(jié)碼的復(fù)雜度。
各個 ClassVisitor通過職責(zé)鏈 (Chain-of-responsibility) 模式庭呜,可以非常簡單的封裝對字節(jié)碼的各種修改滑进,而無須關(guān)注字節(jié)碼的字節(jié)偏移,因?yàn)檫@些實(shí)現(xiàn)細(xì)節(jié)對于用戶都被隱藏了募谎,用戶要做的只是覆寫相應(yīng)的 visit 函數(shù)扶关,說了這么多可能一下子難以理解具體如何使用,那么就來實(shí)際操練一下数冬。
在使用ASM之前你需要在使用ASM的插件的gradle中配置一下
//ASM相關(guān)
dependencies {
...
implementation 'org.ow2.asm:asm:5.1'
implementation 'org.ow2.asm:asm-util:5.1'
implementation 'org.ow2.asm:asm-commons:5.1'
...
}
修改transform文件內(nèi)容變成以下
void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
super.transform(transformInvocation)
transformInvocation.inputs.each {
TransformInput input -> input.directoryInputs.each {
DirectoryInput directoryInput ->
if (directoryInput.file.isDirectory()){
directoryInput.file.eachFileRecurse { File file ->
def name = file.name
// 上面的??代碼的作用前文說過了
// 下面是判斷語句节槐,含義都看的懂搀庶,過濾一下class文件
if (name.endsWith(".class")
&& !name.endsWith("R.class")
&& !name.endsWith("BuildConfig.class")
&& !name.contains("R\$")
){
//打印log
println("==== directoryInput file name == "+ file.getAbsolutePath())
// 獲取ClassReader,參數(shù)是文件的字節(jié)數(shù)組
ClassReader classReader = new ClassReader(file.bytes)
// 獲取ClassWriter铜异,參數(shù)1是reader哥倔,參數(shù)2用于修改類的默認(rèn)行為,一般傳入ClassWriter.COMPUTE_MAXS
ClassWriter classWriter = new ClassWriter(classReader,ClassWriter.COMPUTE_MAXS)
//自定義ClassVisitor
AsmClassVisitor classVisitor = new AsmClassVisitor(Opcodes.ASM5,classWriter)
//執(zhí)行過濾操作
classReader.accept(classVisitor,ClassReader.EXPAND_FRAMES)
byte[] bytes = classWriter.toByteArray()
File destFile = new File(file.parentFile.absoluteFile,name)
FileOutputStream fileOutputStream = new FileOutputStream(destFile);
fileOutputStream.write(bytes)
fileOutputStream.close()
}
}
}
def dest = transformInvocation.outputProvider.getContentLocation(directoryInput.name,directoryInput.contentTypes,directoryInput.scopes,Format.DIRECTORY)
FileUtils.copyDirectory(directoryInput.file,dest)
}
input.jarInputs.each {JarInput jarInput ->
def jarName = jarInput.name
def md5Name = DigestUtils.md5Hex(jarInput.file.getAbsolutePath())
if (jarName.endsWith(".jar")){
jarName = jarName.substring(0,jarName.length() - 4)
}
def dest = transformInvocation.outputProvider.getContentLocation(jarName+md5Name,jarInput.contentTypes,jarInput.scopes,Format.JAR)
FileUtils.copyFile(jarInput.file,dest)
}
}}
對于上面的代碼揍庄,分成兩部分來看咆蒿,上面那部分是對于文件的處理,是這個demo的處理部分蚂子,所以重點(diǎn)看上面的部分沃测,下面的部分是對于jar包的處理,我這次沒有處理jar包食茎,但是依然需要寫出這一部分蒂破,因?yàn)槟悴豢赡茉诰幾g過程過濾的時候把jar包給丟掉了,你總要把不處理的jar傳送給下一個transform别渔,不然運(yùn)行時自然會崩潰附迷。
還有一個注意的點(diǎn),就是在你添加ClassVisitor相關(guān)類的時候需要添加相關(guān)類引用钠糊,
如果你引用了別的包中的類挟秤,整個過程不會有問題但是會導(dǎo)致編譯錯誤,一定要引用org.objectweb.asm包中的抄伍。
上面的代碼是把ASM集成到我們的自定義transform中,可以說寫法基本固定管宵,修改無非是增添更多的自定義ClassVisitor添加到責(zé)任鏈之中截珍,你完全可以增加很多的ClassVisitor用于不同方面的改寫,有利于單一職責(zé)和解耦箩朴,而且上面的代碼只是對class文件進(jìn)行了操作岗喉,jar包并沒有處理,具體對于字節(jié)碼的操作細(xì)節(jié)在我們的自定義AsmClassVisitor中炸庞,你可以在你的groovy下新建一個包用于存放你自定義的ClassVisitor文件
AsmClassVisitor
public class AsmClassVisitor extends ClassVisitor {
public AsmClassVisitor(int i) {
super(i);
}
public AsmClassVisitor(int i, ClassVisitor classVisitor) {
super(i, classVisitor);
}
@Override
public MethodVisitor visitMethod(int i, String s, String s1, String s2, String[] strings) {
MethodVisitor mv = cv.visitMethod(i,s,s1,s2,strings);
AsmMethodVisitor asmClassVisitor = new AsmMethodVisitor(Opcodes.ASM5,mv,i,s,s1);
return asmClassVisitor;
}
}
在這個類中钱床,除了構(gòu)造方法外,我只重寫了一個visitMethod方法埠居,因?yàn)槲乙P(guān)注method查牌,如果你想關(guān)注類中的變量或者注解部分,可以添加visitField和visitAnnotation方法滥壕,還有一些其他的方法纸颜,詳見ASM官網(wǎng),說到這個官方文檔是真的迷,咱一會兒再說绎橘,先回到這個方法中胁孙,可以看到重寫的visitMethod方法中返回了一個自定義的MethodVisitor
public class AsmMethodVisitor extends AdviceAdapter {
private String methodName;
private String methodDes;
protected AsmMethodVisitor(int i, MethodVisitor methodVisitor, int i1, String s, String s1) {
super(i, methodVisitor, i1, s, s1);
methodName = s;
methodDes = s1;
}
@Override
protected void onMethodEnter() {
if ("onClick".equals(methodName)&&"(Landroid/view/View;)V".equals(methodDes)){
//將引用變量推送到棧頂
mv.visitVarInsn(ALOAD,1);
//添加方法
mv.visitMethodInsn(Opcodes.INVOKESTATIC,"java/util/plugindemo2/LogUtils","system","(Landroid/view/View;)V",false);
}
}
}
分析一下這個方法
- 在AsmClassVisitor類中的visitMethod方法返回一個MethodVisitor抽象類的子類,AsmMethodVisitor繼承的是AdviceAdapter,AdviceAdapter是MethodVisitor的子類的子類涮较。稠鼻。之所以選擇繼承他是為了要使用onMethodEnter()方法,在訪問方法的開頭執(zhí)行下列語句狂票,當(dāng)然如果你可以直接繼承MethodVisitor然后重寫visitCode方法候齿,效果是一樣的,MethodVisitor的不同子類提供了大量的切入口供用戶選擇苫亦。
- 細(xì)心的同學(xué)可能會注意到毛肋,AsmClassVisitor的visitMethod方法和AsmMethodVisitor的onMethodEnter方法中分別使用了cv和mv,cv是我們在實(shí)例化AsmClassVisitor時傳入的classVisitor(classWriter)屋剑,這個classWriter中集成了帶有class文件字節(jié)碼的classReader润匙,所以要使用它來進(jìn)行操作,mv同理唉匾,是AsmMethodVisitor實(shí)例化時傳入的cv的MethodVisitor孕讳。
可以看一下整個操作流程的時序圖
- 其實(shí)也就是外層的自定義ClassVisitor用來對接類的大塊區(qū)域,如變量部分巍膘,方法部分厂财,注解部分
- 而自定義ClassVisitor中各種方法返回的類用于對區(qū)域內(nèi)更細(xì)節(jié)的部分進(jìn)行操作,例如訪問一個類的開始峡懈、寫入璃饱、結(jié)束時機(jī)。
java文件
//MainActivity.java
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button btn = findViewById(R.id.btn);
btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.d(TAG, "onClick: ");
}
});
Button btn2 = findViewById(R.id.btn2);
btn2.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.d(TAG, "onClick: ");
}
});
}
}
//LogUtils.java
package java.util.plugindemo2;
import android.util.Log;
import android.view.View;
public class LogUtils {
private static final String TAG = "LogUtils";
public static void system(View v){
Log.d(TAG, "system: "+ v.getId());
}
}
這個小小的Demo就算是完成了肪康,但是在順利執(zhí)行之前還差最后一步荚恶,就是把這個Transform整合到plugin上去,不然即使寫好了Transform磷支,沒有給他接到“水管”上去也就依然沒有用處谒撼,如何把它銜接到plugin上去就要看這個繼承了Plugin接口的類的寫法:
def android = project.extensions.getByType(AppExtension)
android.registerTransform(new AsmInjectTrans())
之后別忘記在Gradle的plugin的uploadArchive,這樣插件才會被重新安裝生效雾狈,再rebuild就可以使用了廓潜,編譯之后我們查看class文件
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
public MainActivity() {
}
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
this.setContentView(2131296284);
Button btn = (Button)this.findViewById(2131165218);
btn.setOnClickListener(new OnClickListener() {
public void onClick(View var1) {
LogUtils.system(var1);
Log.d("MainActivity", "onClick: ");
}
});
Button btn2 = (Button)this.findViewById(2131165219);
btn2.setOnClickListener(new OnClickListener() {
public void onClick(View var1) {
LogUtils.system(var1);
Log.d("MainActivity", "onClick: ");
}
});
}
}
官網(wǎng)中有太多其他的方法可以是做很多不同的事情,時間原因展示一個小小的例子善榛,持續(xù)更新