簡(jiǎn)介
本文主要介紹gradle打包過(guò)程中transform階段浮禾,這里大概說(shuō)下AOP(Aspect Oriented Programming)份汗,這是一種面向切面的思想,預(yù)支對(duì)應(yīng)的是OOP(Object Oriented Programming)面向?qū)ο缶幊毯兀@里不展開說(shuō)明“敬剩可以看下對(duì)AOP總結(jié)的思維導(dǎo)圖
劃重點(diǎn)
本篇文章主要介紹下面的幾點(diǎn):
- Transform可以做什么
- 簡(jiǎn)單了解App打包過(guò)程,以及介紹Transform
- 生成自己的MyConfig類文件互拾,有助我們更好理解
- 介紹ASM
Transform可以做什么
最主要的目的就是:解耦,開發(fā)人員專注于需求颜矿,其他的邊角料寄猩,交給Transform來(lái)處理骑疆。
- 權(quán)限判斷,避免代碼中相關(guān)的地方都是權(quán)限申請(qǐng)和處理的代碼
- 無(wú)痕埋點(diǎn)箍铭,簡(jiǎn)單的場(chǎng)景可以使用泊柬,但是場(chǎng)景比較復(fù)雜時(shí)坡疼,就不好處理了衣陶,目前網(wǎng)上沒有很好的解決方案
- 性能監(jiān)控柄瑰,trace+字節(jié)碼插樁剪况,完美監(jiān)控
- 事件防抖,避免短期內(nèi)多次點(diǎn)擊按鈕
- 熱修復(fù)译断,在所有方法前插入一個(gè)預(yù)留的函數(shù),可以將有bug的方法替換成下發(fā)的方法孙咪。
- 優(yōu)化代碼堪唐,例如刪除項(xiàng)目中體積很大的R文件中的字段翎蹈、優(yōu)化內(nèi)聯(lián)函數(shù)等等
- ....
還有很多功能,都值得我們?nèi)L試荤堪。
App打包過(guò)程 & Transform
首先我們回顧一下App的打包流程合陵,App打包都需要經(jīng)理哪些流程,每一個(gè)步驟都干了什么拥知?
apk打包過(guò)程
從上面的圖中大概理一下流程
- 編譯器將app的源碼編譯成DEX(Dalvik Executable)文件(其中包括運(yùn)行在Android設(shè)備上的字節(jié)碼),將所有其他的內(nèi)容轉(zhuǎn)換為已經(jīng)編譯的資源低剔。
- APK打包器將DEX文件和已編譯資源合并成單個(gè)APK。不過(guò)襟齿,必須先簽署APK镀琉,才能將應(yīng)用安裝并部署到Android設(shè)備上
- APK打包器使用調(diào)試或者發(fā)布密鑰庫(kù)簽署你的APK:
- 如果你構(gòu)建的是debug版本應(yīng)用蕊唐,打包器會(huì)使用debug密鑰庫(kù)簽署你的應(yīng)用,Android Studio自動(dòng)使用debug密鑰庫(kù)配置新項(xiàng)目
- 如果你構(gòu)建的是release版本替梨,打包器會(huì)使用release密鑰庫(kù)簽署你的應(yīng)用
- 在生成最終APK之前钓试,打包器會(huì)使用zipalign工具對(duì)應(yīng)用進(jìn)行優(yōu)化副瀑,減少其在設(shè)備上運(yùn)行時(shí)的內(nèi)存占用
然后再看一張谷歌之前的打包流程圖
這張圖相比較第一張圖而言就更加詳細(xì)了,從這張圖中糠睡,可以看到打包流程可以分為以下七步:
- aapt-打包res資源文件,生成R.java狈孔、resources.arsc和res文件(二進(jìn)制&非二進(jìn)制如res/raw和pic保持原樣)
- AIDL-Android借口定義語(yǔ)言信认,Android提供的IPC(Inter Process Communication均抽,進(jìn)程間通信)的一種獨(dú)特實(shí)現(xiàn)。這個(gè)階段處理.aidl文件油挥,生成對(duì)應(yīng)的Java接口文件。
- Java Compiler-通過(guò)Java Compiler編譯R.java深寥、Java接口文件攘乒、Java源文件惋鹅,生成.class文件。
- dex-通過(guò)dex命令负饲,將.class文件和第三方庫(kù)中的.class文件處理生成class.dex喂链。
- apkbuilder-將class.dex、resources.arsc妥泉、res文件夾(res/raw資源被原封不動(dòng)的打包進(jìn)APK之外,其他資源都會(huì)被編譯或者處理)盲链、OtherResouces(assets文件夾)蝇率、AndroidManifest.xml打包進(jìn)apk文件刽沾。
- Jarsigner-對(duì)上面的apk進(jìn)行debug或release簽名
- aipalign-將簽名后的pak進(jìn)行對(duì)其處理
最后看一張更加詳細(xì)的圖片
Transform
Transform階段就是在apk打包圖中紅圈的位置,第二張圖更加詳細(xì)的表示了Transform的過(guò)程侧漓,是在.class->.dex的過(guò)程。
Gradle Transform是Android官方提供給開發(fā)者在項(xiàng)目構(gòu)建階段由class到dex轉(zhuǎn)換期間修改class文件的一套api布蔗。目前經(jīng)典的應(yīng)用就是字節(jié)碼插樁和代碼注入技術(shù)藤违。有了這個(gè)API纵揍,我們就可以根據(jù)自己的業(yè)務(wù)需求做一些定制。
先看下transform主要有哪些方法
- getName():Transform的名稱泽谨,但是這里并不是真正的名稱,真正的名稱還需要進(jìn)行拼接
-
getInputTypes():Transform處理文件的類型
- CLASSES 表示要處理編譯后的字節(jié)碼吧雹,可能是jar包也可能是目錄
- RESOURCES表示處理標(biāo)準(zhǔn)的java資源
-
getScopes():Transform的作用域
type Des PROJECT 只處理當(dāng)前的文件 SUB_PROJECTS 只處理子項(xiàng)目 EXTERNAL_LIBRARIES 只處理外部的依賴庫(kù) TESTED_CODE 測(cè)試代碼 PROVIDED_ONLY 只處理本地或遠(yuǎn)程以provided形式引入的依賴庫(kù) PROJECT_LOCAL_DEPS (Deprecated,使用EXTERNAL_LIBRARIES) 只處理當(dāng)前項(xiàng)目的本地依賴吮炕,例如jar腊脱、aar SUB_PROJECTS_LOCAL_DEPS (Deprecated龙亲,使用EXTERNAL_LIBRARIES) 只處理子項(xiàng)目的本地依賴。 - isIncremental():是否支持增量編譯悍抑,增量編譯就是如果第二次編譯相應(yīng)的task沒有改變,那么就直接跳過(guò)搜骡,節(jié)省時(shí)間,更詳細(xì)的解釋可以看這里
-
transform():這是最主要的方法记靡,這里對(duì)文件或jar進(jìn)行處理谈竿,進(jìn)行代碼的插入。
- TransformInput:對(duì)輸入的class文件轉(zhuǎn)變成目標(biāo)字節(jié)碼文件空凸,TransformInput就是這些輸入文件的抽象嚎花。目前它包含DirectoryInput集合與JarInput集合呀洲。
- DirectoryInput:源碼方式參與項(xiàng)目編譯的所有目錄結(jié)構(gòu)及其目錄下的源文件。
- JarInput:Jar包方式參與項(xiàng)目編譯的所有本地jar或遠(yuǎn)程jar包
- TransformOutProvider:通過(guò)這個(gè)類來(lái)獲取輸出路徑道逗。
通過(guò)自定義Plugin創(chuàng)建一個(gè)類
我們都知道通過(guò)Gradle編譯后,會(huì)生成一個(gè)BuildConfig的類滓窍,其中有一些項(xiàng)目的信息卖词,例如APPLICATION_ID吏夯、DEBUG等信息,我們依照BuildConfig生成規(guī)則锦亦,也生成一個(gè)自己的MyConfig類舶替,通過(guò)這個(gè)例子我們可以了解一些gradle的語(yǔ)法和api杠园。
在自定義Plugin類的apply()函數(shù)中添加下面的代碼
class ConfigPlugin : Plugin<Project> {
override fun apply(project: Project) {
//只在'applicatoin'中使用,否則拋出異常
if (!project.plugins.hasPlugin(AppPlugin::class.java)) {
throw GradleException("this plugin is not application")
}
//獲取build.gradle中的"android"閉包
val android = project.extensions.getByType(AppExtension::class.java)
//創(chuàng)建自己的閉包
val config = project.extensions.create("config", ConfigExtension::class.java)
//遍歷"android"閉包中的"buildTypes"閉包抛蚁,一般有release和debug兩種
android.applicationVariants.all {
it as ApplicationVariantImpl
println("variant name: ${it.name}")
//創(chuàng)建自己的config task
val buildConfigTask = project.tasks.create("DemoBuildConfig${it.name.capitalize()}")
//在task最后去創(chuàng)建java文件
buildConfigTask.doLast { task ->
createJavaConfig(it, config)
}
// 找到系統(tǒng)的buildConfig Task
val generateBuildConfigTask = project.tasks.getByName(it.variantData.scope.taskContainer.generateBuildConfigTask?.name)
//自己的Config Task 依賴于系統(tǒng)的Config Task
generateBuildConfigTask.let {
buildConfigTask.dependsOn(it)
it.finalizedBy(buildConfigTask)
}
}
}
fun createJavaConfig(variant: ApplicationVariantImpl, config: ConfigExtension) {
val FileName = "MyConfig"
val constantStr = StringBuilder()
constantStr.append("\n").append("package ")
.append(config.packageName).append("; \n\n")
.append("public class $FileName {").append("\n")
config.constantMap.forEach {
constantStr.append("public static final String ${it.key} = \"${it.value}\";\n")
}
constantStr.append("} \n")
println("content: ${constantStr}")
val outputDir = variant.variantData.scope.buildConfigSourceOutputDir
val javaFile = File(outputDir, config.packageName.replace(".", "/") + "/$FileName.java")
println("javaFilePath: ${javaFile.absolutePath}")
javaFile.writeText(constantStr.toString(), Charsets.UTF_8)
}
}
就可以生成MyConfig類,這個(gè)類簡(jiǎn)單定義了一些我們可以在build.gradle中定義的變量瞧甩,可以分為下面的幾個(gè)步驟
- 判斷是否為application的module(僅在application中進(jìn)行操作)
- 遍歷buildTypes也就是release和debug
- 在對(duì)應(yīng)的buildTypes中創(chuàng)建task
- 設(shè)置自定義的task依賴于BUildConfig的Task
- 新建自定義的MyConfig.java文件
Transform的優(yōu)化:增量與并發(fā)
增量
我們想一個(gè)問(wèn)題钉跷,遍歷一遍項(xiàng)目中所有源文件和jar肚逸,時(shí)間都是很長(zhǎng)的,如果我們每次改一行編譯都需要經(jīng)過(guò)這個(gè)過(guò)程朦促,是很浪費(fèi)時(shí)間膝晾。這個(gè)時(shí)候需要實(shí)現(xiàn)增量編譯务冕,什么意思呢?增量,顧名思義臊旭,就是在已有的基礎(chǔ)上,對(duì)增加的進(jìn)行編譯离熏,這樣在編譯過(guò)一次的基礎(chǔ)上领跛,以后就會(huì)大大的縮短時(shí)間撤奸。
想要開啟增量編譯,我們需要重寫Transform的這個(gè)接口胧瓜,返回true矢棚,上面代碼中的注釋也說(shuō)明了府喳。
@Override
boolean isIncremental() {
return true
}
這里需要注意一點(diǎn):不是每次的編譯都是可以怎量編譯的,畢竟一次clean build完全沒有增量的基礎(chǔ)钝满,所以兜粘,我們需要檢查當(dāng)前的編譯是否增量編譯弯蚜。
需要做區(qū)分:
- 不是增量編譯,則清空output目錄碎捺,然后按照前面的方式,逐個(gè)class/jar處理
- 增量編譯收厨,則要檢查每個(gè)文件的Status晋柱,Status分為四種诵叁,并且對(duì)四種文件的操作不盡相同
- NOTCHANGED:當(dāng)前文件不需要處理,甚至復(fù)制操作都不用
- ADDED拧额、CHANGED:正常處理碑诉,輸出給下一個(gè)任務(wù)
- REMOVED:移除outputProvider獲取路徑對(duì)應(yīng)的文件
@Override
public void transform(TransformInvocation transformInvocation){
Collection<TransformInput> inputs = transformInvocation.getInputs();
TransformOutputProvider outputProvider = transformInvocation.getOutputProvider();
boolean isIncremental = transformInvocation.isIncremental();
//如果非增量势腮,則清空舊的輸出內(nèi)容
if(!isIncremental) {
outputProvider.deleteAll();
}
for(TransformInput input : inputs) {
for(JarInput jarInput : input.getJarInputs()) {
Status status = jarInput.getStatus();
File dest = outputProvider.getContentLocation(
jarInput.getName(),
jarInput.getContentTypes(),
jarInput.getScopes(),
Format.JAR);
if(isIncremental && !emptyRun) {
switch(status) {
case NOTCHANGED:
break;
case ADDED:
case CHANGED:
transformJar(jarInput.getFile(), dest, status);
break;
case REMOVED:
if (dest.exists()) {
FileUtils.forceDelete(dest);
}
break;
}
} else {
transformJar(jarInput.getFile(), dest, status);
}
}
for(DirectoryInput directoryInput : input.getDirectoryInputs()) {
File dest = outputProvider.getContentLocation(directoryInput.getName(),
directoryInput.getContentTypes(), directoryInput.getScopes(),
Format.DIRECTORY);
FileUtils.forceMkdir(dest);
if(isIncremental && !emptyRun) {
String srcDirPath = directoryInput.getFile().getAbsolutePath();
String destDirPath = dest.getAbsolutePath();
Map<File, Status> fileStatusMap = directoryInput.getChangedFiles();
for (Map.Entry<File, Status> changedFile : fileStatusMap.entrySet()) {
Status status = changedFile.getValue();
File inputFile = changedFile.getKey();
String destFilePath = inputFile.getAbsolutePath().replace(srcDirPath, destDirPath);
File destFile = new File(destFilePath);
switch (status) {
case NOTCHANGED:
break;
case REMOVED:
if(destFile.exists()) {
FileUtils.forceDelete(destFile);
}
break;
case ADDED:
case CHANGED:
FileUtils.touch(destFile);
transformSingleFile(inputFile, destFile, srcDirPath);
break;
}
}
} else {
transformDir(directoryInput.getFile(), dest);
}
}
}
}
這樣做真的有用嗎?讓我們用數(shù)據(jù)說(shuō)話捎拯,首先準(zhǔn)備好測(cè)試數(shù)據(jù),一個(gè)demo署照,對(duì)所有的源文件和第三方依賴庫(kù)進(jìn)行掃描,使用增量和非增量的模式進(jìn)行三次編譯建芙,然后取平均值没隘。
兩種方式計(jì)算自定義transform:
-
./gradlew assembleDebug --profile
命令禁荸,在根目錄的build/reports目錄下會(huì)生成一個(gè)文件,可以查找每一個(gè)transform所花費(fèi)的時(shí)間赶熟。 - 在自定義的transformTask之前記錄時(shí)間瑰妄,在執(zhí)行完后記錄所花費(fèi)的時(shí)間映砖,貼一下簡(jiǎn)單的代碼
Task doubleCheckTask = project.tasks["transformClassesWithDoubleCheckTransformFor${variant.name.capitalize()}"]
doubleCheckTask.configure {
def startTime
doFirst {
startTime = System.nanoTime()
}
doLast {
println()
println " --> COST: ${TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime)} ms"
println()
}
}
這里我們使用的是第二種方式,我們?cè)诿看沃桓膭?dòng)一行代碼的情況下邑退,然后讓我們看結(jié)論
試驗(yàn)次數(shù) | 非增量 | 增量 |
---|---|---|
1 | 1309ms | 151ms |
2 | 1093ms | 183ms |
3 | 1153ms | 130ms |
可以發(fā)現(xiàn),增量的速度比全量的速度快了將近10倍多地技,這是增量的文件比較少的情況蜈七,但是總的來(lái)說(shuō)增量編譯還是會(huì)大幅度增加編譯的速度莫矗。
并發(fā)編譯
并發(fā)編譯并不復(fù)雜,只需要將上面處理單個(gè)jar/class的邏輯趣苏,并發(fā)處理狡相,最后阻塞等待所有任務(wù)結(jié)束即可食磕。看下偽代碼:
WaitableExecutor waitableExecutor = WaitableExecutor.useGlobalSharedThreadPool();
//異步并發(fā)處理jar/class
waitableExecutor.execute(() -> {
//jar織入字節(jié)碼
return null;
});
waitableExecutor.execute(() -> {
//file織入字節(jié)碼
return null;
});
//等待所有任務(wù)結(jié)束
waitableExecutor.waitForTasksWithQuickFail(true);
與增量編譯一樣彬伦,做一些實(shí)驗(yàn)對(duì)比滔悉,用數(shù)據(jù)說(shuō)話单绑。
試驗(yàn)次數(shù) | 正常編譯 | 并發(fā)編譯 |
---|---|---|
1 | 1309ms | 856ms |
2 | 1093ms | 702ms |
3 | 1153ms | 790ms |
使用ASM織入代碼
從前面幾個(gè)章節(jié)中,了解了自定義Plugin搂橙、Transform歉提、Transform的優(yōu)化,最后一步苔巨,就是對(duì)目標(biāo)類進(jìn)行改造,也就是對(duì)class文件進(jìn)行代碼織入
ASM簡(jiǎn)介
ASM官網(wǎng)中這樣介紹ASM
ASM is an all purpose Java bytecode manipulation and analysis framework. It can be used to modify existing classes or to dynamically generate classes, directly in binary form. ASM provides some common bytecode transformations and analysis algorithms from which custom complex transformations and code analysis tools can be built. ASM offers similar functionality as other Java bytecode frameworks, but is focused on performance. Because it was designed and implemented to be as small and as fast as possible, it is well suited for use in dynamic systems (but can of course be used in a static way too, e.g. in compilers).
ASM是用來(lái)對(duì)Java字節(jié)碼進(jìn)行修改和分析的框架侄泽。ASM可以用來(lái)修改已經(jīng)存在的類或者動(dòng)態(tài)生成類礁芦,它是直接對(duì)二進(jìn)制文件進(jìn)行操作的悼尾。ASM提供了一些常見的字節(jié)碼轉(zhuǎn)換和分析算法,從這些轉(zhuǎn)換和分析算法中構(gòu)建定制復(fù)雜的轉(zhuǎn)換和代碼分析工具闺魏。因?yàn)樗辉O(shè)計(jì)的和實(shí)現(xiàn)的非常的小和盡可能得快未状,所以它非常和用來(lái)動(dòng)態(tài)系統(tǒng)(但是也可以用在靜態(tài)的方式舷胜,例如在編譯時(shí))
常用字節(jié)碼框架
常用的字節(jié)碼框架就三個(gè)Aspectj、Javassist烹骨、ASM翻伺,三個(gè)都有什么區(qū)別呢沮焕?
織入代碼的時(shí)期
一圖勝千言,直接看圖
效率和學(xué)習(xí)成本
- AspectJ:是一個(gè)代碼生成工具峦树,使用它定義的語(yǔ)法生成規(guī)則來(lái)編寫辣辫,基本上要掃描所有的文件魁巩,當(dāng)然AspectJx已經(jīng)實(shí)現(xiàn)的非常的好。最主要的是有個(gè)坑谷遂,在抖音目前的多module的工程上葬馋,是有很多坑的肾扰,例如,和后面的Transform過(guò)程有一些沖突集晚,導(dǎo)致代碼一直織入失敗窗悯,而且編譯的時(shí)長(zhǎng)也大大增加偷拔。
- Javasist:直接操作修改編譯后的字節(jié)碼蒋院,而且可以自定義Transform,編譯時(shí)長(zhǎng)可以做很大空間的優(yōu)化悦污,就是織入代碼的效率不如ASM铸屉。
有關(guān)javassits的使用可以看這篇文章
根據(jù)網(wǎng)上的信息切端,大神得出的數(shù)據(jù)結(jié)果,這里盜用一下踏枣,基本上有3倍的差別,文件越多钙蒙,ASM和Javasist的效率相差就越大。
ASM的用法
ASM框架中的核心類有以下幾個(gè):
- ClassReader:用來(lái)解析編譯過(guò)的class字節(jié)碼文件
- ClassWriter:用來(lái)重新構(gòu)建編譯后的類躬厌,比如修改類名马昨、屬性以及方法扛施,甚至可以生成新的類的字節(jié)碼文件
- ClassVisitor:主要負(fù)責(zé)“拜訪”類成員信息。其中包括標(biāo)記在類上的注解疙渣、類的構(gòu)造方法匙奴、類的字段妄荔、類的方法泼菌、靜態(tài)代碼塊啦租。
- AdviceAdapter:實(shí)現(xiàn)了MethodVisitor接口,主要負(fù)責(zé)“拜訪”方法的信息篷角,用來(lái)具體的方法字節(jié)碼操作焊刹。
ClassVisitor的全部方法如下内地,按一定的次序來(lái)遍歷類中的成員
讓我們簡(jiǎn)單寫個(gè)demo,這段代碼很簡(jiǎn)單阱缓,通過(guò)Visitor API讀取一個(gè)class的內(nèi)容非凌,保存到另一個(gè)文件中去
static void copy(File inputFile, File outputFile) {
def weavedBytes = inputFile.bytes
ClassReader classReader = new ClassReader(bytes)
ClassWriter classWriter = new ClassWriter(classReader,
ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS)
DoubleClickCheckModifyClassAdapter classAdapter = new DoubleClickCheckModifyClassAdapter(classWriter)
try {
classReader.accept(classAdapter, ClassReader.EXPAND_FRAMES)
weavedBytes = classWriter.toByteArray()
} catch (Exception e) {
println "Exception occurred when visit code \n " + e.printStackTrace()
}
outputFile.withOutputStream{
it.write(weavedBytes)
}
}
首先荆针,我們通過(guò)ClassReader讀取某個(gè)class文件颁糟,然后定義一個(gè)ClassWriter,這個(gè)ClassWriter其實(shí)就是一個(gè)ClassVisitor的實(shí)現(xiàn)喉悴,負(fù)責(zé)將ClassReader傳遞過(guò)來(lái)的數(shù)據(jù)寫到一個(gè)字節(jié)流中,而真正觸發(fā)這個(gè)邏輯就是通過(guò)ClassWriter的accept方式箕肃。
上面代碼DoubleClickCheckModifyClassAdapter
類婚脱,也是一個(gè)Visitor勺像,也就是我們自定義需要實(shí)現(xiàn)的功能。
最后吟宦,我們通過(guò)ClassWriter的toByteArray()篮洁,將從ClassReader傳遞到ClassWriter的字節(jié)碼導(dǎo)出殃姓,寫入新的文件即可。這樣我們就完成了字節(jié)碼的操作蜗侈,是不是感覺也不難炊邦。
ASM code
從上面的例子中焊切,可以看出來(lái),只有DoubleClickCheckModifyClassAdapter
需要自己定義艇抠,其他的都由上面的模板來(lái)寫就行枣抱,來(lái)看下這個(gè)類是怎么實(shí)現(xiàn)的佩厚。
public class DoubleClickCheckModifyClassAdapter extends ClassVisitor implements Opcodes {
public DoubleClickCheckModifyClassAdapter(ClassVisitor cv) {
super(Opcodes.ASM5, cv);
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
MethodVisitor methodVisitor = cv.visitMethod(access, name, desc, signature, exceptions);
if ((ASMUtils.isPublic(access) && !ASMUtils.isStatic(access)) &&
name.equals("onClick") &&
desc.equals("(Landroid/view/View;)V")) {
methodVisitor = new View$OnClickMethodVisitor(methodVisitor);
}
return methodVisitor;
}
}
可以從上面的visitMethod
方法中看到吆倦,我們只對(duì)View的onClick函數(shù)進(jìn)行代碼織入,然后再看下View$OnClickMethodVisitor
的實(shí)現(xiàn)
public class View$OnClickMethodVisitor extends MethodVisitor {
private boolean weaved;
public View$OnClickMethodVisitor(MethodVisitor mv) {
super(Opcodes.ASM5, mv);
}
@Override
public void visitCode() {
super.visitCode();
if (weaved) return;
AnnotationVisitor annotationVisitor =
mv.visitAnnotation("L" + DoubleCheckConfig.checkClassAnnotation + ";", false);
annotationVisitor.visitEnd();
mv.visitMethodInsn(Opcodes.INVOKESTATIC, DoubleCheckConfig.checkClassPath, "isClickable", "()Z", false);
Label l1 = new Label();
mv.visitJumpInsn(Opcodes.IFNE, l1);
mv.visitInsn(Opcodes.RETURN);
mv.visitLabel(l1);
}
@Override
public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
/*Lcom/smartdengg/clickdebounce/Debounced;*/
weaved = desc.equals("L" + DoubleCheckConfig.checkClassAnnotation + ";");
return super.visitAnnotation(desc, visible);
}
}
最主要的實(shí)現(xiàn)就是visitCode函數(shù)蚕泽,這里面,我們實(shí)現(xiàn)了代碼的織入须妻。
我們?cè)O(shè)想一下仔蝌,如果要對(duì)某個(gè)class進(jìn)行修改,那需要對(duì)字節(jié)碼具體做什么修改呢敛惊?最直觀的方法就是,先編譯生成目標(biāo)class绰更,然后看它的字節(jié)碼和原來(lái)class的字節(jié)碼有什么區(qū)別锡宋,但是這樣還不夠,其實(shí)我們最終并不是讀取字節(jié)碼特恬,而是使用ASM來(lái)修改。
問(wèn)題來(lái)了癌刽,我不懂ASM役首,對(duì)字節(jié)碼更是很陌生妒穴,怎么辦摊崭?難道我要從新學(xué)習(xí)一下字節(jié)碼讼油,才能進(jìn)行開發(fā)嗎?答案當(dāng)然不是呢簸,如果我們只是對(duì)字節(jié)碼做一些簡(jiǎn)單的操作,完全可以使用工具來(lái)幫我們完成
這里安利一個(gè)非常好用的工具根时,Intellij IDEA有個(gè)插件Asm Bytecode Outline,可以查看一個(gè)class文件的bytecode和ASM code蛤迎,同樣确虱,Android Studio同樣也有一個(gè)類似的插件ASM Bytecode Viewer實(shí)現(xiàn)了同樣的功能。
如何編寫ASM代碼
這是我們?cè)创a
public class A {
static void toast(Context context) {
Toast.makeText(context,"test",Toast.LENGTH_LONG).show();
}
}
我們想通過(guò)字節(jié)碼插裝后變?yōu)?/p>
public class A {
static void toast(Context context) {
Log.i("tag","test");
Toast.makeText(context,"test",Toast.LENGTH_LONG).show();
}
}
也就是在toast
函數(shù)的第一行插入Log代碼校辩。
- 我們將要插入的代碼先寫入源代碼中,在文件中右擊鼠標(biāo).
- 點(diǎn)擊ASM Bytecode Viewer
- 打開右側(cè)的ASM預(yù)覽界面辆童,就能看到對(duì)應(yīng)的ASM代碼
到此為止,貌似使用對(duì)比ASM code的方式把鉴,來(lái)實(shí)現(xiàn)字節(jié)碼修改也不難,但是庭砍,這種方式只是可以實(shí)現(xiàn)一些修改字節(jié)碼的基礎(chǔ)場(chǎng)景场晶,還有很多場(chǎng)景是需要對(duì)字節(jié)碼有一些基礎(chǔ)只是才能做到,而且诗轻,要閱讀懂ASM code,也是需要一定字節(jié)碼的知識(shí)凯旭。所以使套,如果要開發(fā)字節(jié)碼工程,還是需要學(xué)習(xí)一番字節(jié)碼的鞠柄。
實(shí)際應(yīng)用
Theory without practice is empty,practice without theory is blind
我們既然已經(jīng)了解了ASM的原理,那么我們應(yīng)用于實(shí)踐厌杜,我們不能為了學(xué)技術(shù)而學(xué)技術(shù)奉呛,技術(shù)最終是要服務(wù)于業(yè)務(wù)的夯尽,我們更加應(yīng)該從業(yè)務(wù)的角度出發(fā)瞧壮,來(lái)思考問(wèn)題和提升自己(題外話)匙握。
有一個(gè)場(chǎng)景,在可點(diǎn)擊的地方圈纺,經(jīng)常出現(xiàn)連擊秦忿,但是結(jié)果并不是我們想要的蛾娶,例如灯谣,跳轉(zhuǎn)到個(gè)人頁(yè)面蛔琅,我們快速點(diǎn)擊兩次,就會(huì)發(fā)現(xiàn)出現(xiàn)了兩個(gè)個(gè)人頁(yè)面罗售,需要back兩次才能回到之前的頁(yè)面辜窑,這個(gè)肯定是不符合預(yù)期的莽囤,幾乎在所有的點(diǎn)擊地方,都應(yīng)該做防抖動(dòng)的操作(抖動(dòng)朽缎,就是快速或者不小心在短時(shí)間內(nèi)多次點(diǎn)擊惨远,我也不知道為什么叫抖動(dòng),網(wǎng)上都這樣寫)北秽,現(xiàn)在有幾個(gè)思路:
- Kotlin中實(shí)現(xiàn)view的擴(kuò)展函數(shù),這樣就可以統(tǒng)一的地方做防抖動(dòng)操作最筒,但是如果大家不是理解這個(gè)點(diǎn),有可能還會(huì)調(diào)用之前的點(diǎn)擊事件床蜘,那么還有可能出現(xiàn)這個(gè)問(wèn)題辙培。
- Java中封裝一個(gè)工具類,要求每個(gè)開發(fā)人員在在OnClick中添加這個(gè)函數(shù)扬蕊,但是這個(gè)很容易被遺忘搀别,可操作性不大尾抑。
- 使用AOP(AspectJ和ASM)歇父,在編譯期間再愈,將所有的Onclick函數(shù)中做判斷,而且ASM兼容Java和Kotlin翎冲。這里選擇ASM垂睬,上一章節(jié)已經(jīng)說(shuō)明原因
從以上幾點(diǎn)中府适,可以得出使用ASM是目前最好的方案羔飞。
參考文章
ASM 操作字節(jié)碼初探
一起玩轉(zhuǎn)Android項(xiàng)目中的字節(jié)碼
一文讀懂 AOP | 你想要的最全面 AOP 方法探討