0. 前言
我的所有原創(chuàng)Android知識(shí)體系,已打包整理到GitHub.努力打造一系列適合初中高級(jí)工程師能夠看得懂的優(yōu)質(zhì)文章,歡迎star~
建議閱讀本篇文章之前掌握以下相關(guān)知識(shí)點(diǎn): Android打包流程+Gradle插件+Java字節(jié)碼
在Android Gradle Plugin中,有一個(gè)叫Transform API(從1.5.0版本才有的)的東西.利用這個(gè)Transform API咱可以在.class文件轉(zhuǎn)換成dex文件之前,對(duì).class文件進(jìn)行處理.比如監(jiān)控,埋點(diǎn)之類的.
而對(duì).class文件進(jìn)行處理這個(gè)操作,咱們這里使用ASM.ASM是一個(gè)通用的Java字節(jié)碼操作和分析框架淮蜈。它可以直接以二進(jìn)制形式用于修改現(xiàn)有類或動(dòng)態(tài)生成類.咱們在打包的時(shí)候,直接操作字節(jié)碼修改class,對(duì)運(yùn)行時(shí)性能是沒有任何影響的,所以它的效率是相當(dāng)高的.
本篇文章給大家簡單介紹一下Transform和ASM的使用,最后再結(jié)合一個(gè)小栗子練習(xí)一下.文中demo源碼地址
1. 使用Transform API
1.1 注冊一個(gè)自定義的Transform
首先寫一個(gè)Plugin,然后通過registerTransform方法進(jìn)行注冊自定義的Transform.
class MethodTimeTransformPlugin implements Plugin<Project> {
@Override
void apply(Project project) {
//注冊方式1
AppExtension appExtension = project.extensions.getByType(AppExtension)
appExtension.registerTransform(new MethodTimeTransform())
//注冊方式2
//project.android.registerTransform(new MethodTimeTransform())
}
}
通過獲取module的Project的AppExtension,通過它的registerTransform方法注冊的Transform.
這里注冊之后,會(huì)在編譯過程中的TransformManager#addTransform中生成一個(gè)task,然后在執(zhí)行這個(gè)task的時(shí)候會(huì)執(zhí)行到我們自定義的Transform的transform方法.這個(gè)task的執(zhí)行時(shí)機(jī)其實(shí)就是.class
文件轉(zhuǎn)換成.dex
文件的時(shí)候,轉(zhuǎn)換的邏輯是定義在transform方法中的.
1.2 自定義一個(gè)Transform
先讓大家看一下比較標(biāo)準(zhǔn)的Transform模板代碼:
class MethodTimeTransform extends Transform {
@Override
String getName() {
return "MethodTimeTransform"
}
@Override
Set<QualifiedContent.ContentType> getInputTypes() {
//需要處理的數(shù)據(jù)類型,這里表示class文件
return TransformManager.CONTENT_CLASS
}
@Override
Set<? super QualifiedContent.Scope> getScopes() {
//作用范圍
return TransformManager.SCOPE_FULL_PROJECT
}
@Override
boolean isIncremental() {
//是否支持增量編譯
return true
}
@Override
void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
super.transform(transformInvocation)
//TransformOutputProvider管理輸出路徑,如果消費(fèi)型輸入為空,則outputProvider也為空
TransformOutputProvider outputProvider = transformInvocation.outputProvider
//transformInvocation.inputs的類型是Collection<TransformInput>,可以從中獲取jar包和class文件夾路徑驴剔。需要輸出給下一個(gè)任務(wù)
transformInvocation.inputs.each { input -> //這里的input是TransformInput
input.jarInputs.each { jarInput ->
//處理jar
processJarInput(jarInput, outputProvider)
}
input.directoryInputs.each { directoryInput ->
//處理源碼文件
processDirectoryInput(directoryInput, outputProvider)
}
}
}
void processJarInput(JarInput jarInput, TransformOutputProvider outputProvider) {
File dest = outputProvider.getContentLocation(jarInput.file.absolutePath, jarInput.contentTypes, jarInput.scopes, Format.JAR)
//將修改過的字節(jié)碼copy到dest,就可以實(shí)現(xiàn)編譯期間干預(yù)字節(jié)碼的目的
println("拷貝文件 $dest -----")
FileUtils.copyFile(jarInput.file, dest)
}
void processDirectoryInput(DirectoryInput directoryInput, TransformOutputProvider outputProvider) {
File dest = outputProvider.getContentLocation(directoryInput.name, directoryInput.contentTypes, directoryInput.scopes, Format
.DIRECTORY)
//將修改過的字節(jié)碼copy到dest,就可以實(shí)現(xiàn)編譯期間干預(yù)字節(jié)碼的目的
println("拷貝文件夾 $dest -----")
FileUtils.copyDirectory(directoryInput.file, dest)
}
}
-
getName()
: 表示當(dāng)前Transform名稱,這個(gè)名稱會(huì)被用來創(chuàng)建目錄,它會(huì)出現(xiàn)在app/build/intermediates/transforms目錄下面. -
getInputTypes()
: 需要處理的數(shù)據(jù)類型,用于確定我們需要對(duì)哪些類型的結(jié)果進(jìn)行轉(zhuǎn)換,比如class,資源文件等:-
CONTENT_CLASS
:表示需要處理java的class文件 -
CONTENT_JARS
:表示需要處理java的class與資源文件 -
CONTENT_RESOURCES
:表示需要處理java的資源文件 -
CONTENT_NATIVE_LIBS
:表示需要處理native庫的代碼 -
CONTENT_DEX
:表示需要處理DEX文件 -
CONTENT_DEX_WITH_RESOURCES
:表示需要處理DEX與java的資源文件
-
-
getScopes()
: 表示Transform要操作的內(nèi)容范圍(上面demo里面使用的SCOPE_FULL_PROJECT
是Scope的集合,包含了Scope.PROJECT
,Scope.SUB_PROJECTS
,Scope.EXTERNAL_LIBRARIES
這幾個(gè)東西.當(dāng)然,TransformManager里面還有一些其他集合,這里不做舉例).- PROJECT: 只有項(xiàng)目內(nèi)容
- SUB_PROJECTS: 只有子項(xiàng)目
- EXTERNAL_LIBRARIES: 只有外部庫
- TESTED_CODE: 測試代碼
- PROVIDED_ONLY: 只提供本地或遠(yuǎn)程依賴項(xiàng)
-
isIncremental()
: 是否支持增量更新- 如果返回true,則TransformInput會(huì)包含一份修改的文件列表
- 如果是false,則進(jìn)行全量編譯,刪除上一次輸出內(nèi)容
-
transform()
: 進(jìn)行具體轉(zhuǎn)換邏輯.- 消費(fèi)型Transform: 在transform方法中,我們需要將每個(gè)jar包和class文件復(fù)制到dest路徑,這個(gè)dest路徑就是下一個(gè)Transform的輸入數(shù)據(jù).在復(fù)制的時(shí)候,我們可以將jar和class文件的字節(jié)碼做一些修改,再進(jìn)行復(fù)制. 可以看出,如果我們注冊了Transform,但是又不將內(nèi)容復(fù)制到下一個(gè)Transform需要的輸入路徑的話,就會(huì)出問題,比如少了一些class之類的.上面的demo中僅僅是將所有的輸入文件拷貝到目標(biāo)目錄下,并沒有對(duì)字節(jié)碼文件進(jìn)行任何處理.
- 引用型Transform: 當(dāng)前Transform可以讀取這些輸入,而不需要輸出給下一個(gè)Transform.
可以看出,最關(guān)鍵的核心代碼就是transform()方法里面,我們需要做一些class文件字節(jié)碼的修改,才能讓Transform發(fā)揮其效果.
道理是這個(gè)道理,但是字節(jié)碼那玩意兒想改就能改么? 忘記字節(jié)碼是什么的小伙伴可以看我之前發(fā)的文章 Java字節(jié)碼解讀 復(fù)習(xí)一下. 字節(jié)碼比較復(fù)雜,連"讀懂"都非常非常困難,還讓我去改它,那更是難上加難.
不過,幸好咱們可以借助后面介紹的ASM工具進(jìn)行方便的修改字節(jié)碼工作.
1.3 增量編譯
就是Transform中的isIncremental()
方法返回值,如果是false的話,則表示不開啟增量編譯,每次都得處理每個(gè)文件,非常非常拖慢編譯時(shí)間. 我們可以借助該方法,返回值改成true,開啟增量編譯.當(dāng)然,開啟了增量編譯之后需要檢查每個(gè)文件的Status,然后根據(jù)這個(gè)文件的Status進(jìn)行不同的操作.
具體的Status如下:
- NOTCHANGED: 當(dāng)前文件不需要處理,連復(fù)制操作也不用
- ADDED: 正常處理,輸出給下一個(gè)任務(wù)
- CHANGED: 正常處理,輸出給下一個(gè)任務(wù)
- REMOVED: 移除outputProvider獲取路徑對(duì)應(yīng)的文件
來看一下代碼如何實(shí)現(xiàn),咱將上面的dmeo代碼簡單改改:
@Override
void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
super.transform(transformInvocation)
printCopyRight()
//TransformOutputProvider管理輸出路徑,如果消費(fèi)型輸入為空,則outputProvider也為空
TransformOutputProvider outputProvider = transformInvocation.outputProvider
//當(dāng)前是否是增量編譯,由isIncremental方法決定的
// 當(dāng)上面的isIncremental()寫的返回true,這里得到的值不一定是true,還得看當(dāng)時(shí)環(huán)境.比如clean之后第一次運(yùn)行肯定就不是增量編譯嘛.
boolean isIncremental = transformInvocation.isIncremental()
if (!isIncremental) {
//不是增量編譯則刪除之前的所有文件
outputProvider.deleteAll()
}
//transformInvocation.inputs的類型是Collection<TransformInput>,可以從中獲取jar包和class文件夾路徑揭北。需要輸出給下一個(gè)任務(wù)
transformInvocation.inputs.each { input -> //這里的input是TransformInput
input.jarInputs.each { jarInput ->
//處理jar
processJarInput(jarInput, outputProvider, isIncremental)
}
input.directoryInputs.each { directoryInput ->
//處理源碼文件
processDirectoryInput(directoryInput, outputProvider, isIncremental)
}
}
}
/**
* 處理jar
* 將修改過的字節(jié)碼copy到dest,就可以實(shí)現(xiàn)編譯期間干預(yù)字節(jié)碼的目的
*/
void processJarInput(JarInput jarInput, TransformOutputProvider outputProvider, boolean isIncremental) {
def status = jarInput.status
File dest = outputProvider.getContentLocation(jarInput.file.absolutePath, jarInput.contentTypes, jarInput.scopes, Format.JAR)
if (isIncremental) {
switch (status) {
case Status.NOTCHANGED:
break
case Status.ADDED:
case Status.CHANGED:
transformJar(jarInput.file, dest)
break
case Status.REMOVED:
if (dest.exists()) {
FileUtils.forceDelete(dest)
}
break
}
} else {
transformJar(jarInput.file, dest)
}
}
void transformJar(File jarInputFile, File dest) {
//println("拷貝文件 $dest -----")
FileUtils.copyFile(jarInputFile, dest)
}
/**
* 處理源碼文件
* 將修改過的字節(jié)碼copy到dest,就可以實(shí)現(xiàn)編譯期間干預(yù)字節(jié)碼的目的
*/
void processDirectoryInput(DirectoryInput directoryInput, TransformOutputProvider outputProvider, boolean isIncremental) {
File dest = outputProvider.getContentLocation(directoryInput.name, directoryInput.contentTypes, directoryInput.scopes, Format
.DIRECTORY)
FileUtils.forceMkdir(dest)
println("isIncremental = $isIncremental")
if (isIncremental) {
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 Status.NOTCHANGED:
break
case Status.ADDED:
case Status.CHANGED:
FileUtils.touch(destFile)
transformSingleFile(inputFile, destFile)
break
case Status.REMOVED:
if (destFile.exists()) {
FileUtils.forceDelete(destFile)
}
break
}
}
} else {
transformDirectory(directoryInput.file, dest)
}
}
void transformSingleFile(File inputFile, File destFile) {
println("拷貝單個(gè)文件")
FileUtils.copyFile(inputFile, destFile)
}
void transformDirectory(File directoryInputFile, File dest) {
println("拷貝文件夾 $dest -----")
FileUtils.copyDirectory(directoryInputFile, dest)
}
根據(jù)是否為增量更新,如果不是,則刪除之前的所有文件.然后對(duì)每個(gè)文件進(jìn)行狀態(tài)判斷,根據(jù)其狀態(tài)來決定到底是該刪除,或者復(fù)制.開啟增量編譯之后,速度會(huì)有特別大的提升.
1.4 并發(fā)編譯
畢竟是在電腦上進(jìn)行編譯,盡管壓榨電腦性能,我們把并發(fā)編譯給搞起.說來也輕巧,就下面幾行代碼就行
private WaitableExecutor mWaitableExecutor = WaitableExecutor.useGlobalSharedThreadPool()
transformInvocation.inputs.each { input -> //這里的input是TransformInput
input.jarInputs.each { jarInput ->
//處理jar
mWaitableExecutor.execute(new Callable<Object>() {
@Override
Object call() throws Exception {
//多線程
processJarInput(jarInput, outputProvider, isIncremental)
return null
}
})
}
//處理源碼文件
input.directoryInputs.each { directoryInput ->
//多線程
mWaitableExecutor.execute(new Callable<Object>() {
@Override
Object call() throws Exception {
processDirectoryInput(directoryInput, outputProvider, isIncremental)
return null
}
})
}
}
//等待所有任務(wù)結(jié)束
mWaitableExecutor.waitForTasksWithQuickFail(true)
增加的代碼不多,其他都是之前的.就是讓處理邏輯的地方放線程里面去執(zhí)行,然后得等這些線程都處理完成才結(jié)束任務(wù).
到這里Transform基本的API也將介紹完了,原理(系統(tǒng)有一些列Transform用于在class轉(zhuǎn)dex的過程中的處理邏輯,我們也可以自定義Transform參與其中,這個(gè)Transform最終其實(shí)是在一個(gè)Task里面執(zhí)行的.)的話也知曉了個(gè)大概,接下來我們看看如何利用ASM修改字節(jié)碼實(shí)現(xiàn)炫酷的功能吧.
2. ASM
2.1 介紹
官網(wǎng)上是這樣介紹ASM的: ASM是一個(gè)通用的Java字節(jié)碼操作和分析框架十酣。它可以直接以二進(jìn)制形式用于修改現(xiàn)有類或動(dòng)態(tài)生成類髓堪。ASM提供了一些常見的字節(jié)碼轉(zhuǎn)換和分析算法宾符,可從中構(gòu)建定制的復(fù)雜轉(zhuǎn)換和代碼分析工具踢星。ASM提供了與其他Java字節(jié)碼框架類似的功能统刮,但是側(cè)重于 性能故源。因?yàn)樗脑O(shè)計(jì)和實(shí)現(xiàn)是盡可能的小和盡可能快污抬,所以它非常適合在動(dòng)態(tài)系統(tǒng)中使用(但當(dāng)然也可以以靜態(tài)方式使用,例如在編譯器中)绳军。(可能翻譯得不是很準(zhǔn)確,英文好的同學(xué)可以去官網(wǎng)看原話)
2.2 引入ASM
下面是我的demo中的buildSrc里面build.gradle配置.它包含了Plugin+Transform+ASM的所有依賴,放心拿去用.
dependencies {
implementation gradleApi()
implementation localGroovy()
//常用io操作
implementation "commons-io:commons-io:2.6"
// Android DSL Android編譯的大部分gradle源碼
implementation 'com.android.tools.build:gradle:3.6.2'
implementation 'com.android.tools.build:gradle-api:3.6.2'
//ASM
implementation 'org.ow2.asm:asm:7.1'
implementation 'org.ow2.asm:asm-util:7.1'
implementation 'org.ow2.asm:asm-commons:7.1'
}
2.3 ASM基本使用
在使用之前我們先來看一些常用的對(duì)象
- ClassReader : 按照J(rèn)ava虛擬機(jī)規(guī)范中定義的方式來解析class文件中的內(nèi)容,在遇到合適的字段時(shí)調(diào)用ClassVisitor中相應(yīng)的方法
- ClassVisitor : Java中類的訪問者,提供一系列方法由ClassReader調(diào)用.它是一個(gè)抽象類,在使用時(shí)需要繼承此類.
- ClassWriter : 它是一個(gè)繼承了ClassVisitor的類,主要負(fù)責(zé)將ClassReader傳遞過來的數(shù)據(jù)寫到一個(gè)字節(jié)流中.在傳遞數(shù)據(jù)完成之后,可以通過它的toByteArray方法獲得完整的字節(jié)流.
- ModuleVisitor : Java中模塊的訪問者,作為ClassVisitor.visitModule方法的返回值,要是不關(guān)心模塊的使用情況,可以返回一個(gè)null.
- AnnotationVisitor : Java中注解的訪問者,作為ClassVisitor.visitTypeAnnotation的返回值,不關(guān)心注解使用情況也是可以返回null.
- FieldVisitor : Java中字段的訪問者,作為ClassVisitor.visitField的返回值,不關(guān)心字段使用情況也是可以返回null.
- MethodVisitor:Java中方法的訪問者,作為ClassVisitor.visitMethod的返回值,不關(guān)心方法使用情況也是可以返回null.
上面這些對(duì)象先簡單過一下,眼熟就行,待會(huì)兒會(huì)使用到這些對(duì)象.
大體工作流程: 通過ClassReader讀取class字節(jié)碼文件,然后ClassReader將讀取到的數(shù)據(jù)通過一個(gè)ClassVisitor(上面的ClassWriter其實(shí)就是一個(gè)ClassVisitor)將數(shù)據(jù)表現(xiàn)出來.表現(xiàn)形式: 將字節(jié)碼的每個(gè)細(xì)節(jié)按順序通過接口的方式傳遞給ClassVisitor.就比如說,訪問到了class文件的xx方法,就會(huì)回調(diào)ClassVisitor的visitMethod方法;訪問到了class文件的屬性,就會(huì)回調(diào)ClassVisitor的visitField方法.
ClassWriter是一個(gè)繼承了ClassVisitor的類,它保存了這些由ClassReader讀取出來的字節(jié)流數(shù)據(jù),最后通過它的toByteArray方法獲得完整的字節(jié)流.
上面的概念比較生硬,咱們先來寫一個(gè)簡單的復(fù)制class文件的方法:
private void copyFile(File inputFile, File outputFile) {
FileInputStream inputStream = new FileInputStream(inputFile)
FileOutputStream outputStream = new FileOutputStream(outputFile)
//1. 構(gòu)建ClassReader對(duì)象
ClassReader classReader = new ClassReader(inputStream)
//2. 構(gòu)建ClassVisitor的實(shí)現(xiàn)類ClassWriter
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS)
//3. 將ClassReader讀取到的內(nèi)容回調(diào)給ClassVisitor接口
classReader.accept(classWriter, ClassReader.EXPAND_FRAMES)
//4. 通過classWriter對(duì)象的toByteArray方法拿到完整的字節(jié)流
outputStream.write(classWriter.toByteArray())
inputStream.close()
outputStream.close()
}
看到這里,可能有的同學(xué)已經(jīng)有點(diǎn)感覺了.ClassReader對(duì)象就是專門負(fù)責(zé)讀取字節(jié)碼文件的,而ClassWriter就是一個(gè)繼承了ClassVisitor的類,當(dāng)ClassReader讀取字節(jié)碼文件的時(shí)候,數(shù)據(jù)會(huì)通過ClassVisitor回調(diào)回來.咱們可以自定義一個(gè)ClassWriter用來接收讀取到的字節(jié)數(shù)據(jù),接收數(shù)據(jù)的同時(shí),咱們再插入一點(diǎn)東西到這些數(shù)據(jù)的前面或者后面,最后通過ClassWriter的toByteArray方法將這些字節(jié)碼數(shù)據(jù)導(dǎo)出,寫入新的文件,這就是我們所說的插樁了.
現(xiàn)在咱們舉個(gè)栗子,到底插樁能有啥用?就實(shí)現(xiàn)一個(gè)簡單的需求吧,在每個(gè)方法的最前面插入一句打印Hello World!
的代碼.
修改前的代碼如下所示:
private void test() {
System.out.println("test");
}
預(yù)期修改后的代碼:
private void test() {
System.out.println("Hello World!");
System.out.println("test");
}
將上面的復(fù)制文件的代碼簡單改改
void traceFile(File inputFile, File outputFile) {
FileInputStream inputStream = new FileInputStream(inputFile)
FileOutputStream outputStream = new FileOutputStream(outputFile)
ClassReader classReader = new ClassReader(inputStream)
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS)
classReader.accept(new HelloClassVisitor(classWriter)), ClassReader.EXPAND_FRAMES)
outputStream.write(classWriter.toByteArray())
inputStream.close()
outputStream.close()
}
唯一有變化的地方就是classReader的accept方法傳入的ClassVisitor對(duì)象變了,咱自定義了一個(gè)HelloClassVisitor.
class HelloClassVisitor extends ClassVisitor {
HelloClassVisitor(ClassVisitor cv) {
//這里需要指定一下版本Opcodes.ASM7
super(Opcodes.ASM7, cv)
}
@Override
MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
def methodVisitor = cv.visitMethod(access, name, descriptor, signature, exceptions)
return new HelloMethodVisitor(api, methodVisitor, access, name, descriptor)
}
}
我們自定義了一個(gè)ClassVisitor,它將ClassWriter傳入其中.在ClassVisitor的實(shí)現(xiàn)中,只要傳入了classVisitor對(duì)象,那么就會(huì)將功能委托給這個(gè)classVisitor對(duì)象.相當(dāng)于我傳入的這個(gè)ClassWriter就讀取到了字節(jié)碼,最后toByteArray就是所有的字節(jié)碼.多說無益,看看代碼:
public abstract class ClassVisitor {
/** The class visitor to which this visitor must delegate method calls. May be null. */
protected ClassVisitor cv;
public ClassVisitor(final int api, final ClassVisitor classVisitor) {
if (api != Opcodes.ASM7 && api != Opcodes.ASM6 && api != Opcodes.ASM5 && api != Opcodes.ASM4) {
throw new IllegalArgumentException("Unsupported api " + api);
}
this.api = api;
this.cv = classVisitor;
}
public AnnotationVisitor visitAnnotation(final String descriptor, final boolean visible) {
if (cv != null) {
return cv.visitAnnotation(descriptor, visible);
}
return null;
}
public MethodVisitor visitMethod(
final int access,
final String name,
final String descriptor,
final String signature,
final String[] exceptions) {
if (cv != null) {
return cv.visitMethod(access, name, descriptor, signature, exceptions);
}
return null;
}
...
}
有了我們傳入的ClassWriter,咱們在自定義ClassVisitor的時(shí)候,只需要關(guān)注需要修改的地方即可.咱們是想對(duì)方法進(jìn)行插樁,自然就得關(guān)心visitMethod方法,該方法會(huì)在ClassReader閱讀class文件里面的方法時(shí)會(huì)回調(diào).這里我們首先是在HelloClassVisitor的visitMethod中調(diào)用了ClassVisitor的visitMethod方法,拿到MethodVisitor對(duì)象.
而MethodVisitor是和ClassVisitor是類似的,在ClassReader閱讀方法的時(shí)候會(huì)回調(diào)這個(gè)類里面的visitParameter(訪問方法參數(shù)),visitAnnotationDefault(訪問注解的默認(rèn)值),visitAnnotation(訪問注解)等等.
所以為了能夠?qū)Ψ椒ú鍢?咱們需要再包一層,自己實(shí)現(xiàn)一下MethodVisitor,我們將ClassWriter.visitMethod返回的MethodVisitor傳入自定義的MethodVisitor,并在方法剛開始的地方進(jìn)行插樁.AdviceAdapter是一個(gè)繼承自MethodVisitor的類,它能夠方便的回調(diào)方法進(jìn)入(onMethodEnter)和方法退出(onMethodExit). 我們只需要在方法進(jìn)入,也就是onMethodEnter方法里面進(jìn)行插樁即可.
class HelloMethodVisitor extends AdviceAdapter {
HelloMethodVisitor(int api, MethodVisitor methodVisitor, int access, String name, String descriptor) {
super(api, methodVisitor, access, name, descriptor)
}
//方法進(jìn)入
@Override
protected void onMethodEnter() {
super.onMethodEnter()
//這里的mv是MethodVisitor
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("Hello World!");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
}
}
插樁的核心代碼,需要一些字節(jié)碼的核心知識(shí),這里不展開介紹,推薦大家閱讀《深入理解Java虛擬機(jī)》關(guān)于字節(jié)碼的章節(jié).
當(dāng)然,要想快速地寫出這些代碼也是有捷徑的,安裝一個(gè)ASM Bytecode Outline
插件,然后隨便寫一個(gè)Test類,然后隨便寫一個(gè)方法
public class Test {
public void hello() {
System.out.println("Hello World!");
}
}
然后選中該Test.java文件,右鍵菜單,點(diǎn)擊Show ByteCode outline
在右側(cè)窗口內(nèi)選擇ASMified,即可得到如下代碼:
mv = cw.visitMethod(ACC_PUBLIC, "hello", "()V", null, null);
mv.visitCode();
Label l0 = new Label();
mv.visitLabel(l0);
mv.visitLineNumber(42, l0);
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("Hello World!");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
Label l1 = new Label();
mv.visitLabel(l1);
mv.visitLineNumber(43, l1);
mv.visitInsn(RETURN);
Label l2 = new Label();
mv.visitLabel(l2);
mv.visitLocalVariable("this", "Lcom/xfhy/gradledemo/Test;", null, l0, l2, 0);
mv.visitMaxs(2, 1);
mv.visitEnd();
其中關(guān)于Label的咱不需要,所以只剩下核心代碼
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("Hello World!");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
到這里,ASM的基本使用已經(jīng)告一段落.ASM可操作性非常強(qiáng),人有多大膽,地有多大產(chǎn).只要你想實(shí)現(xiàn)的,基本都能實(shí)現(xiàn).關(guān)鍵在于你的想法.但是有個(gè)小問題,上面的插件只能生成一些簡單的代碼,如果需要寫一些復(fù)雜的邏輯,就必須深入Java字節(jié)碼,才能自己寫出來或者是看懂ASM的插樁代碼.
3. ASM 實(shí)戰(zhàn) 防快速點(diǎn)擊(抖動(dòng))
上面那個(gè)小demo在每個(gè)方法里面打印一句"Hello World!"好像沒什么實(shí)際意義..咱決定做個(gè)有實(shí)際意義的東西,一般情況下,我們在做開發(fā)的會(huì)去防止用戶快速點(diǎn)擊某個(gè)View.這是為了追求更好的用戶體驗(yàn),如果不處理的話,在快速點(diǎn)擊Button的時(shí)候可能會(huì)連續(xù)打開2個(gè)相同的界面,在用戶看來確實(shí)有點(diǎn)奇怪,影響體驗(yàn).所以,一般情況下,我們會(huì)去做一下限制.
處理的時(shí)候,其實(shí)也很簡單,我們只需要取快速點(diǎn)擊事件中的其中一次點(diǎn)擊事件就行了.有哪些方案進(jìn)行處理呢?下面是我想到的幾種
- 在BaseActivity的dispatchTouchEvent里判斷一下,如果
ACTION_DOWN
&&快速點(diǎn)擊則返回true就行. - 寫一個(gè)工具類,記錄上一次點(diǎn)擊的時(shí)間,每次在onClick里面判斷一下,是否為快速點(diǎn)擊,如果是,則不響應(yīng)事件.
- 可以在方案2的基礎(chǔ)上,記錄每個(gè)View上一次的點(diǎn)擊時(shí)間,控制更為精準(zhǔn).
下面是我簡單實(shí)現(xiàn)的一個(gè)工具類FastClickUtil.java
public class FastClickUtil {
private static final int FAST_CLICK_TIME_DISTANCE = 300;
private static long sLastClickTime = 0;
public static boolean isFastDoubleClick() {
long time = System.currentTimeMillis();
long timeDistance = time - sLastClickTime;
if (0 < timeDistance && timeDistance < FAST_CLICK_TIME_DISTANCE) {
return true;
}
sLastClickTime = time;
return false;
}
}
有了這個(gè)工具類,那咱們就可以在每個(gè)onClick方法的最前面插入isFastDoubleClick()
判斷語句,簡單判斷一下即可實(shí)現(xiàn)防抖.就像下面這樣:
public void onClick(View view) {
if (!FastClickUtil.isFastDoubleClick()) {
......
}
}
為了實(shí)現(xiàn)上面這個(gè)最終效果,我們其實(shí)只需要這樣做:
- 找到onClick方法
- 進(jìn)行插樁
除了自定義ClassVisitor,其他代碼是和上面的demo差不多的,咱直接看自定義ClassVisitor.
class FastClickClassVisitor extends ClassVisitor {
FastClickClassVisitor(ClassVisitor classVisitor) {
super(Opcodes.ASM7, classVisitor)
}
@Override
MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
def methodVisitor = cv.visitMethod(access, name, descriptor, signature, exceptions)
if (name == "onClick" && descriptor == "(Landroid/view/View;)V") {
return new FastMethodVisitor(api, methodVisitor, access, name, descriptor)
} else {
return methodVisitor
}
}
}
在ClassVisitor里面的visitMethod里面,只需要找到onClick方法,然后自定義自己的MethodVisitor.
class FastMethodVisitor extends AdviceAdapter {
FastMethodVisitor(int api, MethodVisitor methodVisitor, int access, String name, String descriptor) {
super(api, methodVisitor, access, name, descriptor)
}
//方法進(jìn)入
@Override
protected void onMethodEnter() {
super.onMethodEnter()
mv.visitMethodInsn(INVOKESTATIC, "com/xfhy/gradledemo/FastClickUtil", "isFastDoubleClick", "()Z", false)
Label label = new Label()
mv.visitJumpInsn(IFEQ, label)
mv.visitInsn(RETURN)
mv.visitLabel(label)
}
}
在方法進(jìn)入(onMethodEnter()
)里面調(diào)用FastClickUtil的靜態(tài)方法isFastDoubleClick()判斷一下即可.到此,我們的小案例計(jì)算全部完成了.可以看到,利用ASM輕輕松松就能實(shí)現(xiàn)我們之前看起來比較麻煩的功能,而且低侵入性,不用改動(dòng)之前的所有代碼.
插樁之后可以將編譯完成的apk直接拖入jadx里面看一下最終源碼驗(yàn)證,也可以直接將apk安裝到手機(jī)上進(jìn)行驗(yàn)證.
當(dāng)然了,上面的這種實(shí)現(xiàn)有些不太人性化的地方.比如某些View的點(diǎn)擊事件,不需要防抖.怎么辦?用上面這種方式不太合適,咱可以自定義一個(gè)注解,在不需要處理防抖的onClick方法上標(biāo)注一下這個(gè)注解.然后在ASM這邊判斷一下,如果某onClick方法上有這個(gè)注解就不進(jìn)行插樁.事情完美解決.這里就不帶著大家實(shí)現(xiàn)了,留給大家課后實(shí)踐.