Android?ASM插樁探索實(shí)戰(zhàn)詳情
前言
我們都知道,在Android編譯過(guò)程中,Java代碼會(huì)被編譯成Class文件,Class文件再被打進(jìn)Dex文件,虛擬機(jī)最終會(huì)加載Dex文件去執(zhí)行。

插樁,就是干涉代碼的編譯過(guò)程,在編譯期間生成新的代碼或者修改已有的代碼。
常用的EventBus、ARouter,內(nèi)部就是使用了APT(Annotation Process Tool),在編譯的最開(kāi)始解析Java文件中的注解,并生成新的Java文件。
但如果有以下兩個(gè)需求:
- 給Activity的attach()方法加日志
- 將第三方庫(kù)中所有調(diào)用getDeviceId()的地方替換為我們自己的方法,使其符合隱私規(guī)范
這兩個(gè)需求一個(gè)是需要修改Android SDK的Activity文件,一個(gè)是需要修改三方庫(kù)中的某個(gè)方法。而我們集成它們的方式是通過(guò)Jar包/AAR,本質(zhì)上也就是Class文件。這時(shí)候就需要我們能夠在編譯階段去修改Class文件,這也就是ASM發(fā)揮作用的地方。
通過(guò)本文,你可以解決如下問(wèn)題:
- ASM的作用是什么?
- 如何使用ASM?
- 如何將ASM運(yùn)用到我們的實(shí)際項(xiàng)目中來(lái)?
ASM的作用是什么?
在介紹ASM插樁之前,首先來(lái)回顧一下Java Class文件。在AS中,我們可以看到打開(kāi)一個(gè)Class文件是這樣的:

但其實(shí)這是IDE為了方便開(kāi)發(fā)者查閱,特意解析渲染了CLASS文件。如果直接拖進(jìn)編輯器查看這個(gè)文件的話,我們可以看到它其實(shí)是這樣的:

上圖是CLASS文件的16進(jìn)制代碼。一般人都看不懂這些代碼的含義...但既然AS可以將這些代碼解析成開(kāi)發(fā)者可以看懂的樣子,說(shuō)明CLASS文件肯定是遵循某個(gè)格式規(guī)范的。所以,一個(gè)熟悉CLASS文件格式規(guī)范的開(kāi)發(fā)者,是完全有能力解析所有的CLASS文件,甚至修改CLASS文件的。
ASM的開(kāi)發(fā)者就是這么做的,并且提供一套完整的API幫助我們在不需要了解CLASS文件格式規(guī)范的情況下,可以解析并修改CLASS文件。
如何使用ASM?
基本使用方式
下面我們就來(lái)使用一下ASM,看一下它能達(dá)到的效果。假設(shè)現(xiàn)在我們需要統(tǒng)計(jì)MainActivity所有方法的耗時(shí),原先的MainActivity.Class文件是這樣的:

用ASM修改過(guò)后的MainActivity.Class文件:

具體的實(shí)現(xiàn)代碼:
// 讀取Class文件 String clazzFilePath = "/Users/xiaozhi/AndroidStudioProjects/ASMTest/app/build/intermediates/javac/debug/classes/com/xiaozhi/asmtest/MainActivity.class"; ClassReader classReader = new ClassReader(new FileInputStream(clazzFilePath)); ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS); MethodTimeConsumeClassVisitor methodTimeConsumeClassVisitor = new MethodTimeConsumeClassVisitor(Opcodes.ASM5, classWriter); classReader.accept(methodTimeConsumeClassVisitor, ClassReader.SKIP_FRAMES); // 寫入Class文件 byte[] bytes = classWriter.toByteArray(); FileOutputStream fos = new FileOutputStream(clazzFilePath); fos.write(bytes); fos.flush(); fos.close();
首先在第6行,通過(guò)ClassReader.accept(classVisitor, parsingOptions)讀取Class文件。然后將修改完的字節(jié)碼用FileOutputStream寫回原文件,原先的Class代碼也就被修改了。但這里我們看不到是怎么修改的,因?yàn)?strong>修改其實(shí)就發(fā)生在讀取階段,ClassReader負(fù)責(zé)讀取解析Class文件,遇到相應(yīng)節(jié)點(diǎn)后,調(diào)用ClassVisitor中的方法去修改相應(yīng)的節(jié)點(diǎn)代碼(4、5行)。
這里涉及到兩個(gè)類,ClassWriter與MethodTimeConsumeClassVisitor,這兩個(gè)類都繼承于ClassVisitor。結(jié)合第9行我們可以猜測(cè),ClassWriter肯定可以記錄我們修改后的字節(jié)碼。既然ClassWriter是用來(lái)記錄的,而第6行ClassReader.accept(classVisitor, parsingOptions)讀取Class文件又只能接收一個(gè)classVisitor,那我們?cè)趺从昧硪粋€(gè)ClassVisitor去修改Class文件呢?
我們可以看到ClassVisitor有這么一個(gè)構(gòu)造函數(shù):
public ClassVisitor(final int api, final ClassVisitor classVisitor)
所以我們第5行的代碼,實(shí)際上是用自定義的ClassVisitor-MethodTimeConsumeClassVisitor,代理了ClassWriter,在需要修改的Class節(jié)點(diǎn)復(fù)寫方法進(jìn)行修改就可以了。
另外我們額外了解一下構(gòu)造函數(shù)中的幾個(gè)參數(shù)。
// 接收Flag參數(shù),用于設(shè)置方法的操作數(shù)棧的深度。COMPUTE_MAXS可以自動(dòng)幫我們計(jì)算stackSize。 ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS); // 接收api與ClassVisitor。 Opcodes.ASM4~Opcodes.ASM9標(biāo)識(shí)了ASM的版本信息 MethodTimeConsumeClassVisitor methodTimeConsumeClassVisitor = new MethodTimeConsumeClassVisitor(Opcodes.ASM5, classWriter); // 接收ClassVisitor與parsingOptions參數(shù)。 parsingOptions用來(lái)決定解析Class的方式,SKIP_FRAMES代表跳過(guò)MethodVisitor.visitFrame方法 classReader.accept(methodTimeConsumeClassVisitor, ClassReader.SKIP_FRAMES);
自定義ClassVisitor
下面我們具體看一下怎么通過(guò)自定義ClassVisitor修改Class文件。
public class MethodTimeConsumeClassVisitor extends ClassVisitor {
private String mOwner;
public MethodTimeConsumeClassVisitor(int api, ClassVisitor classVisitor) {
super(api, classVisitor);
}
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
super.visit(version, access, name, signature, superName, interfaces);
mOwner = name;
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
MethodVisitor methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions);
return new TimeConsumeMethodVisitor(mOwner, api, methodVisitor, access, name, descriptor);
}
}我們可以看到第9行與第15行,分別是visit方法與visitMethod方法,對(duì)應(yīng)的是訪問(wèn)Class文件頭部與Class文件方法這兩個(gè)節(jié)點(diǎn)。
類似的還有很多節(jié)點(diǎn):
visit visitSource? visitOuterClass? ( visitAnnotation | visitAttribute )* ( visitInnerClass | visitField | visitMethod )* visitEnd
我們要統(tǒng)計(jì)MainActivity所有方法的耗時(shí),就需要重寫visitMethod方法。第15行的visitMethod返回了一個(gè)MethodVisitor,顧名思義就是用來(lái)遍歷修改Method,跟ClassVisitor是一個(gè)道理只不過(guò)維度不同罷了。類似的還有AnnotationVisitor與FiledVisitor,它們分別在visitAnnotation和visitField方法中返回,用來(lái)訪問(wèn)修改注解與字段。
然后我們來(lái)看這個(gè)MethodVisitor是怎么修改方法的:
static class TimeConsumeMethodVisitor extends AdviceAdapter {
private final String methodName;
private final int access;
private final String descriptor;
private final String owner;
private static final String METHOD_TIME_CONSUME_LOG_TAG = "METHOD_TIME_CONSUME_ASM_HOOK";
private static final String METHOD_TIME_CONSUME_LOG = "method time consume:";
protected TimeConsumeMethodVisitor(String owner, int api, MethodVisitor methodVisitor, int access, String name, String descriptor) {
super(api, methodVisitor, access, name, descriptor);
this.owner = owner;
this.methodName = name;
this.access = access;
this.descriptor = descriptor;
}
@Override
protected void onMethodEnter() {
System.out.println("TimeConsumeMethodVisitor onMethodEnter. clazzName:" + owner + ", methodName:" + methodName + ", access:" + access + ", descriptor:" + descriptor);
visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
visitVarInsn(LSTORE, 1);
super.onMethodEnter();
}
@Override
protected void onMethodExit(int opcode) {
System.out.println("TimeConsumeMethodVisitor onMethodExit. clazzName:" + owner + ", methodName:" + methodName + ", access:" + access + ", descriptor:" + descriptor);
visitLdcInsn(METHOD_TIME_CONSUME_LOG_TAG);
visitTypeInsn(NEW, "java/lang/StringBuilder");
visitInsn(DUP);
visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
visitLdcInsn(METHOD_TIME_CONSUME_LOG);
visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
visitVarInsn(LLOAD, 1);
visitInsn(LSUB);
visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(J)Ljava/lang/StringBuilder;", false);
visitLdcInsn("ms" + ", clazz:" + owner + ", method:" + methodName);
visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
visitMethodInsn(INVOKESTATIC, "com/taobao/yyds/common/utils/Logger", "d", "(Ljava/lang/String;Ljava/lang/String;)V", false);
super.onMethodExit(opcode);
}
}在第19行onMethodEnter()方法中,我們實(shí)現(xiàn)了
long startTime = System.currentTimeMillis();
在第29行onMethodExit()方法中,我們實(shí)現(xiàn)了
Logger.d("METHOD_TIME_CONSUME_ASM_HOOK", "method time consume:" + (System.currentTimeMillis() - var1) + "ms, clazz:com/xiaozhi/asmtest/MainActivity, method:");至于其中所用的API,我們可以通過(guò)函數(shù)名大致推斷出什么意思,感興趣的話可以去學(xué)習(xí)這些API的使用。但因?yàn)閯側(cè)腴T,我這里使用了一個(gè)偷懶的方式:ASM ByteCode Viewer。
ASM ByteCode Viewer
ASM ByteCode Viewer是專門用于ASM插樁的AS插件。
安裝該插件后,我們可以很方便地查看一個(gè)Class文件怎么用ASM的API去寫出來(lái):

如果我想很快速地知道如何通過(guò)ASM API去給方法加耗時(shí)日志,只需要先在本地Java文件中寫好這一段邏輯,然后通過(guò)插件查看對(duì)應(yīng)的API是怎么樣的就可以了。但建議還是要多了解下這些API,因?yàn)槲覀儗懙腏ava代碼可能不是通用的,在其它Java文件中不一定就能順利地編譯成功,因此往往會(huì)有需要進(jìn)行適配的地方,排查的過(guò)程中就需要我們了解API才行了。
到這里,ASM的基本使用就已經(jīng)講好了。如果感興趣可以參考官方文檔asm.ow2.io/asm4-guide.…去實(shí)踐。
如何將ASM運(yùn)用都我們的實(shí)際項(xiàng)目中來(lái)?
上一節(jié)我們已經(jīng)知道如何用ASM對(duì)一個(gè)Class文件進(jìn)行修改,那么怎么運(yùn)用到我們的項(xiàng)目中來(lái)呢?Android打包過(guò)程中會(huì)將Class文件打包成Dex文件,在這個(gè)階段我們可以借助AGP(Android Gradle Plugin)與Android Transform來(lái)遍歷訪問(wèn)到所有需要的Class文件,再通過(guò)ASM去修改。
引入工程
Android Gradle Plugin
自定義插件一共分為5個(gè)步驟:
- 創(chuàng)建插件項(xiàng)目
- 配置插件
- 實(shí)現(xiàn)插件
- 發(fā)布插件
- 應(yīng)用插件
創(chuàng)建插件項(xiàng)目
跟其它子模塊一樣,我們需要?jiǎng)?chuàng)建一個(gè)插件模塊,然后在根目錄的settings.gradle中引入該模塊。
配置插件
首先,插件模塊的文件目錄需要嚴(yán)格遵守以下目錄結(jié)構(gòu)(因?yàn)槲覀冞x擇用groovy實(shí)現(xiàn)插件,所以要用groovy文件夾):
main
├── groovy
├── resources
├── META-INF
├── gradle-plugins
├── *.properties
上圖中的配置文件需要特別注意,該配置文件代表著插件id->插件實(shí)現(xiàn)類的映射。

其它項(xiàng)目應(yīng)用插件時(shí)所用的插件id,就是配置文件的文件前綴com.yyds.asm.plugin。而實(shí)際的實(shí)現(xiàn)類就是com.xiaozhi.plugin.ASMPlugin。
另外,我們需要在build.gradle中配置如下內(nèi)容:
plugins {
id 'groovy'
}
dependencies {
implementation gradleApi()
implementation localGroovy()
}
sourceSets {
main {
groovy {
srcDir 'src/main/groovy'
}
resources {
srcDir 'src/main/resources'
}
}
}實(shí)現(xiàn)插件
接下來(lái)就是實(shí)現(xiàn)我們自定義的插件了,我們可以在ASMPlugin中寫我們需要執(zhí)行的邏輯:
class ASMPlugin implements Plugin<Project> {
@Override
void apply(Project project) {
println("ASMPlugin apply")
}
}發(fā)布插件
插件項(xiàng)目寫完后,我們需要將其發(fā)布到maven倉(cāng)庫(kù)中去(這里可以選擇先將其發(fā)布到本地maven倉(cāng)庫(kù)),從而讓其它模塊可以方便地進(jìn)行依賴。
我們需要在build.gradle中添加以下代碼:
uploadArchives {
repositories {
mavenDeployer {
//設(shè)置插件的GAV參數(shù)
pom.groupId = 'com.xiaozhi.plugin.asm'
pom.artifactId = 'asmArt'
pom.version = '1.0.1'
//文件發(fā)布到下面目錄
repository(url: uri('../maven_repo'))
}
}
}sync后,我們就可以在gradle tasks中看到上傳插件的task:

執(zhí)行task,插件就會(huì)發(fā)布到本地的maven倉(cāng)庫(kù)了,我們可以在本地的maven_repo文件夾中找到。
應(yīng)用插件
現(xiàn)在,我們的項(xiàng)目就可以很方便地依賴這個(gè)插件了。
只用做兩個(gè)步驟:
- 在工程根目錄build.gralde添加maven倉(cāng)庫(kù)與插件依賴:
buildscript {
repositories {
···
maven { url uri('./maven_repo') }
···
}
dependencies {
···
classpath "com.xiaozhi.plugin.asm:asmArt:1.0.1"
···
}
}- 在想要依賴插件的項(xiàng)目的build.gradle中應(yīng)用插件:
plugins {
id 'com.xiaozhi.plugin'
}可以看到,這個(gè)id就是對(duì)應(yīng)的插件項(xiàng)目中配置文件的前綴名。
Android Transform
現(xiàn)在假設(shè)app模塊已經(jīng)應(yīng)用了我們的ASM插件,那么還需要使用Transform才能訪問(wèn)到app模塊在編譯過(guò)程中產(chǎn)生/依賴的所有Class文件。
自定義一個(gè)Transform:
public class ASMTransform extends Transform {
// transfrom名稱
@Override
public String getName() {
return this.getClass().getSimpleName();
}
// 輸入源,class文件
@Override
public Set<QualifiedContent.ContentType> getInputTypes() {
return TransformManager.CONTENT_CLASS;
}
// 文件范圍,整個(gè)工程
@Override
public Set<? super QualifiedContent.Scope> getScopes() {
return TransformManager.SCOPE_FULL_PROJECT;
}
// 是否增量編譯,可用于編譯優(yōu)化
@Override
public boolean isIncremental() {
return false;
}
// 核心方法
@Override
public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
}
}我們主要看第29行transform()方法,在這里我們就能訪問(wèn)到app模塊的所有Class文件。
實(shí)現(xiàn)如下:
@Override
public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
super.transform(transformInvocation);
if (!transformInvocation.isIncremental()) {
//不是增量編譯刪除所有的outputProvider
transformInvocation.getOutputProvider().deleteAll();
}
// 獲取輸入源
Collection<TransformInput> inputs = transformInvocation.getInputs();
inputs.forEach(transformInput -> {
Collection<DirectoryInput> directoryInputs = transformInput.getDirectoryInputs();
Collection<JarInput> jarInputs = transformInput.getJarInputs();
directoryInputs.forEach(new Consumer<DirectoryInput>() {
@Override
public void accept(DirectoryInput directoryInput) {
try {
// 處理輸入源
handleDirectoryInput(directoryInput);
} catch (IOException e) {
System.out.println("handleDirectoryInput error:" + e.toString());
}
}
});
for (DirectoryInput directoryInput : directoryInputs) {
// 獲取output目錄
File dest = transformInvocation.getOutputProvider().getContentLocation(
directoryInput.getName(),
directoryInput.getContentTypes(),
directoryInput.getScopes(),
Format.DIRECTORY);
//這里執(zhí)行字節(jié)碼的注入,不操作字節(jié)碼的話也要將輸入路徑拷貝到輸出路徑
try {
FileUtils.copyDirectory(directoryInput.getFile(), dest);
} catch (IOException e) {
System.out.println("output copy error:" + e.toString());
}
}
for (JarInput jarInput : jarInputs) {
// 獲取output目錄
File dest = transformInvocation.getOutputProvider().getContentLocation(
jarInput.getName(),
jarInput.getContentTypes(),
jarInput.getScopes(),
Format.JAR);
//這里執(zhí)行字節(jié)碼的注入,不操作字節(jié)碼的話也要將輸入路徑拷貝到輸出路徑
try {
FileUtils.copyFile(jarInput.getFile(), dest);
} catch (IOException e) {
System.out.println("output copy error:" + e.toString());
}
}
});
}這里的邏輯主要是三端:
- 獲取輸入源
第9行獲取到的TransformInput中可以訪問(wèn)到所有的DirectoryInput和JarInput,分別代表著我們項(xiàng)目中的Class文件與依賴的JAR包/AAR包中的Class文件。DirectoryInput和JarInput都繼承于QualifiedContent,調(diào)用getFile()方法就可以拿到Class文件的所有信息了。
- 處理輸入源
獲取Class文件后,其實(shí)我們就可以用ASM去修改它了。相當(dāng)于把我們之前ASM修改Class文件的代碼復(fù)制過(guò)來(lái)就可以了,這一部分留到下一節(jié)中講。
- 將輸入源文件拷貝到目標(biāo)文件中
處理完之后,我們還要記得把輸入源文件拷貝到輸出路徑中去,否則下一個(gè)transform可能就要失敗了,因?yàn)樗也坏捷斎朐戳?。?7行transformInvocation.getOutputProvider().getContentLocation()可以確保我們獲取到最終的輸出路徑。
現(xiàn)在Transform寫好了,但我們還沒(méi)有應(yīng)用。應(yīng)用很簡(jiǎn)單,只需要在插件中注冊(cè)一下就好了:
class ASMPlugin implements Plugin<Project> {
@Override
void apply(Project project) {
def android = project.getExtensions().findByType(AppExtension)
android.registerTransform(new ASMTransform())
}
}然后,重新發(fā)布插件到maven倉(cāng)庫(kù),sync一下,我們就可以在app模塊的gradle tasks中看到我們剛寫好的transform了:

至此,所有鏈路都已經(jīng)走通了,我們知道ASM如何修改Class文件,并可以利用AGP與Transfrom應(yīng)用到我們的工程中來(lái)。下面我們就用這條鏈路來(lái)實(shí)現(xiàn)一下方法節(jié)流。
方法節(jié)流
Android中最常見(jiàn)的方法節(jié)流就是防重復(fù)點(diǎn)擊。假設(shè)當(dāng)用戶在首頁(yè)2s內(nèi)頻繁點(diǎn)擊了商品或者誤觸了商品時(shí),我們期望只打開(kāi)一次商詳頁(yè)面,這時(shí)候就需要對(duì)點(diǎn)擊事件做節(jié)流。
首先我們需要定義一個(gè)注解,并聲明到點(diǎn)擊事件上,同時(shí)支持設(shè)置節(jié)流時(shí)長(zhǎng)duration:
private final View.OnClickListener mOnClickListener = new View.OnClickListener() {
@Override
@MethodThrottle(duration = 3000)
public void onClick(View view) {
}
}接下來(lái)就是ASM的舞臺(tái)了?;仡櫸覀兩弦还?jié)中處理輸入源相關(guān)的代碼
Collection<DirectoryInput> directoryInputs = transformInput.getDirectoryInputs();
directoryInputs.forEach(new Consumer<DirectoryInput>() {
@Override
public void accept(DirectoryInput directoryInput) {
try {
// 處理輸入源
handleDirectoryInput(directoryInput);
} catch (IOException e) {
System.out.println("handleDirectoryInput error:" + e.toString());
}
}
});在第7行handleDirectoryInput()方法中,我們利用ASM修改Class文件:
/**
* 處理文件目錄下的class文件
*/
private static void handleDirectoryInput(DirectoryInput directoryInput) throws IOException {
List<File> files = new ArrayList<>();
//列出目錄所有文件(包含子文件夾,子文件夾內(nèi)文件)
listFiles(files, directoryInput.getFile());
for (File file: files) {
ClassReader classReader = new ClassReader(new FileInputStream(file.getAbsolutePath()));
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
MethodThrottleClassVisitor methodThrottleClassVisitor = new MethodThrottleClassVisitor(Opcodes.ASM5, classWriter);
classReader.accept(methodThrottleClassVisitor, ClassReader.SKIP_FRAMES);
byte[] code = classWriter.toByteArray();
FileOutputStream fos = new FileOutputStream(file.getAbsolutePath());
fos.write(code);
fos.close();
}
}關(guān)鍵是MethodThrottleClassVisitor類,我們看它主要是怎么實(shí)現(xiàn)的:
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
MethodVisitor methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions);
throttleMethodVisitor = new ThrottleMethodVisitor(mOwner, api, methodVisitor, access, name, descriptor);
return throttleMethodVisitor;
}visitMethod()方法返回一個(gè)自定義的MethodVisitor。ThrottleMethodVisitor在訪問(wèn)每個(gè)方法時(shí),若發(fā)現(xiàn)方法聲明了@MethodThrottle注解,就會(huì)插入節(jié)流代碼:
@Override
public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
System.out.println("ThrottleMethodVisitor visitAnnotation. clazzName:" + owner + ", methodName:" + methodName + ", access:" + access + ", descriptor" + descriptor + ", annotationDesc:" + desc);
throttle = "Lcom/taobao/yyds/common/annotation/MethodThrottle;".equals(desc);
···
}
@Override
protected void onMethodEnter() {
System.out.println("ThrottleMethodVisitor onMethodEnter. clazzName:" + owner + ", methodName:" + methodName + ", access:" + access + ", descriptor" + descriptor + ", throttle:" + throttle);
if (throttle) {
// 插入方法節(jié)流代碼
}
}具體的代碼就不放上去了,通過(guò)ASM ByteCode Viewer可以很方便地生成。這樣我們?cè)谄綍r(shí)代碼中要做方法節(jié)流時(shí),只需要給方法聲明一個(gè)注解就可以了。
方法耗時(shí)日志
類似的,我們可以用這個(gè)鏈路實(shí)現(xiàn)很多AOP邏輯。給方法加耗時(shí)日志這一點(diǎn),在上面的ASM基本使用那一節(jié)中其實(shí)已經(jīng)講過(guò)了。
運(yùn)用到工程中來(lái),其實(shí)就跟方法節(jié)流一樣,自定義一個(gè)注解,然后在Transform中加一點(diǎn)處理輸入流的邏輯就好了:
/**
* 處理文件目錄下的class文件
*/
private static void handleDirectoryInput(DirectoryInput directoryInput) throws IOException {
List<File> files = new ArrayList<>();
//列出目錄所有文件(包含子文件夾,子文件夾內(nèi)文件)
listFiles(files, directoryInput.getFile());
for (File file: files) {
// 方法節(jié)流
methodThrottleASM(file);
// 方法耗時(shí)
methodTimeConsumeASM(file);
}
}如何調(diào)試
另外有一點(diǎn)我個(gè)人覺(jué)得還是挺重要的,那就是Gradle插件該如何調(diào)試。即使有ASM ByteCode Viewer插件的幫助,我們?cè)诓寮袑懙腁SM代碼也不可能一鍵完成,很大概率會(huì)碰到各種各樣的編譯錯(cuò)誤問(wèn)題。本地打日志又比較麻煩,所以調(diào)試手段是必不可少的。篇幅受限,這里就不額外寫了,可以參考Android Studio調(diào)試Gradle插件詳情
發(fā)布線上的額外工作
雖然我們已經(jīng)集成了ASM插件模塊,但并不意味著這樣就能上線了。至少還需要完成以下的工作才行。
插件項(xiàng)目的maven倉(cāng)庫(kù)
因?yàn)樵谡{(diào)試階段,插件的maven倉(cāng)庫(kù)是用的本地的maven倉(cāng)庫(kù)。但如果要集成進(jìn)CI,肯定是需要線上的maven倉(cāng)庫(kù)的,所以到時(shí)候需要申請(qǐng)上傳到某個(gè)maven倉(cāng)中才行。
編譯影響評(píng)估
第一點(diǎn)是必須保證CI打包時(shí)沒(méi)問(wèn)題。第二點(diǎn)就是看這樣做是否會(huì)影響到編譯時(shí)長(zhǎng),畢竟Transform是在編譯階段加了一個(gè)Task。如果對(duì)編譯時(shí)長(zhǎng)有比較大的影響,還需要額外做一些編譯優(yōu)化的工作??梢杂?code>./gradlew --profile --rerun-tasks assembleDebug命令查看各環(huán)節(jié)的編譯耗時(shí)。
Tips
在做這個(gè)Demo的過(guò)程中,因?yàn)樽约阂彩堑谝淮谓佑|,遇到了不少坑,拿幾點(diǎn)貼一下:
- 每次plugin改動(dòng)都要重新發(fā)布一下,否則plugin中transform、asm的邏輯都不會(huì)更新,因?yàn)槟玫氖莔aven倉(cāng)中的。
- transform每次debug前都要clean一下,否則debug不進(jìn)去。
- 即使transform沒(méi)修改任何東西,也需要將源文件jar directory拷貝到目標(biāo)文件。否則最后編譯出的build/transforms文件夾中會(huì)少很多jar包,造成啟動(dòng)時(shí)找不到各種Class而崩潰。
- classpath引入后,造成啟動(dòng)后某so崩潰。一直以為是插件寫的有什么問(wèn)題,排查很久后也沒(méi)找到原因。最后拿崩潰棧去找so庫(kù)接口人,升級(jí)so版本后問(wèn)題就解決了。
總結(jié)
通過(guò)本篇文章,我們了解到ASM的作用,學(xué)會(huì)ASM基本API的使用,進(jìn)而利用AGP與Transform將ASM運(yùn)用到實(shí)際項(xiàng)目中來(lái)。實(shí)戰(zhàn)中,ASM可以實(shí)現(xiàn)常見(jiàn)的AOP邏輯,如方法節(jié)流與方法耗時(shí)日志。不僅如此,當(dāng)我們以后為需要修改三方庫(kù)代碼而發(fā)愁時(shí),或許可以想想,ASM能幫助我們搞定嗎?
到此這篇關(guān)于Android ASM插樁探索實(shí)戰(zhàn)詳情的文章就介紹到這了,更多相關(guān)Android ASM插樁探內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
開(kāi)源電商app常用標(biāo)簽"hot"之第三方開(kāi)源LabelView
這篇文章主要介紹了開(kāi)源電商app常用標(biāo)簽"hot"之第三方開(kāi)源LabelView,對(duì)開(kāi)源電商app相關(guān)資料感興趣的朋友一起學(xué)習(xí)吧2015-12-12
Kotlin語(yǔ)言中CompileSdkVersion與targetSdkVersion的區(qū)別淺析
這篇文章主要介紹了Kotlin語(yǔ)言中CompileSdkVersion和targetSdkVersion有什么區(qū)別,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)吧2023-02-02
解決Android調(diào)用系統(tǒng)分享給微信,出現(xiàn)分享失敗,分享多文件必須為圖片格式的問(wèn)題
這篇文章主要介紹了解決Android調(diào)用系統(tǒng)分享給微信,出現(xiàn)分享失敗,分享多文件必須為圖片格式的問(wèn)題,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-09-09
分享Android中ExpandableListView控件使用教程
這篇文章主要介紹了Android中ExpandableListView控件使用教程,可以實(shí)現(xiàn)二級(jí)列表展示效果,需要的朋友可以參考下2015-12-12
Android Studio打包APK文件具體實(shí)現(xiàn)步驟解析
這篇文章主要介紹了Android Studio打包APK文件具體實(shí)現(xiàn)步驟解析,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-11-11
Android采取BroadcastReceiver方式自動(dòng)獲取驗(yàn)證碼
這篇文章主要介紹了Android采取BroadcastReceiver方式自動(dòng)獲取驗(yàn)證碼,感興趣的小伙伴們可以參考一下2016-08-08

