Android?ASM插樁探索實(shí)戰(zhàn)詳情
前言
我們都知道,在Android編譯過程中,Java代碼會(huì)被編譯成Class文件,Class文件再被打進(jìn)Dex文件,虛擬機(jī)最終會(huì)加載Dex文件去執(zhí)行。
插樁,就是干涉代碼的編譯過程,在編譯期間生成新的代碼或者修改已有的代碼。
常用的EventBus、ARouter,內(nèi)部就是使用了APT(Annotation Process Tool),在編譯的最開始解析Java文件中的注解,并生成新的Java文件。
但如果有以下兩個(gè)需求:
- 給Activity的attach()方法加日志
- 將第三方庫中所有調(diào)用getDeviceId()的地方替換為我們自己的方法,使其符合隱私規(guī)范
這兩個(gè)需求一個(gè)是需要修改Android SDK的Activity文件,一個(gè)是需要修改三方庫中的某個(gè)方法。而我們集成它們的方式是通過Jar包/AAR,本質(zhì)上也就是Class文件。這時(shí)候就需要我們能夠在編譯階段去修改Class文件,這也就是ASM發(fā)揮作用的地方。
通過本文,你可以解決如下問題:
- ASM的作用是什么?
- 如何使用ASM?
- 如何將ASM運(yùn)用到我們的實(shí)際項(xiàng)目中來?
ASM的作用是什么?
在介紹ASM插樁之前,首先來回顧一下Java Class文件。在AS中,我們可以看到打開一個(gè)Class文件是這樣的:
但其實(shí)這是IDE為了方便開發(fā)者查閱,特意解析渲染了CLASS文件。如果直接拖進(jìn)編輯器查看這個(gè)文件的話,我們可以看到它其實(shí)是這樣的:
上圖是CLASS文件的16進(jìn)制代碼。一般人都看不懂這些代碼的含義...但既然AS可以將這些代碼解析成開發(fā)者可以看懂的樣子,說明CLASS文件肯定是遵循某個(gè)格式規(guī)范的。所以,一個(gè)熟悉CLASS文件格式規(guī)范的開發(fā)者,是完全有能力解析所有的CLASS文件,甚至修改CLASS文件的。
ASM的開發(fā)者就是這么做的,并且提供一套完整的API幫助我們在不需要了解CLASS文件格式規(guī)范的情況下,可以解析并修改CLASS文件。
如何使用ASM?
基本使用方式
下面我們就來使用一下ASM,看一下它能達(dá)到的效果。假設(shè)現(xiàn)在我們需要統(tǒng)計(jì)MainActivity所有方法的耗時(shí),原先的MainActivity.Class文件是這樣的:
用ASM修改過后的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行,通過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行我們可以猜測,ClassWriter肯定可以記錄我們修改后的字節(jié)碼。既然ClassWriter是用來記錄的,而第6行ClassReader.accept(classVisitor, parsingOptions)讀取Class文件又只能接收一個(gè)classVisitor,那我們怎么用另一個(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)識了ASM的版本信息 MethodTimeConsumeClassVisitor methodTimeConsumeClassVisitor = new MethodTimeConsumeClassVisitor(Opcodes.ASM5, classWriter); // 接收ClassVisitor與parsingOptions參數(shù)。 parsingOptions用來決定解析Class的方式,SKIP_FRAMES代表跳過MethodVisitor.visitFrame方法 classReader.accept(methodTimeConsumeClassVisitor, ClassReader.SKIP_FRAMES);
自定義ClassVisitor
下面我們具體看一下怎么通過自定義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方法,對應(yīng)的是訪問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,顧名思義就是用來遍歷修改Method,跟ClassVisitor是一個(gè)道理只不過維度不同罷了。類似的還有AnnotationVisitor與FiledVisitor,它們分別在visitAnnotation和visitField方法中返回,用來訪問修改注解與字段。
然后我們來看這個(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,我們可以通過函數(shù)名大致推斷出什么意思,感興趣的話可以去學(xué)習(xí)這些API的使用。但因?yàn)閯側(cè)腴T,我這里使用了一個(gè)偷懶的方式:ASM ByteCode Viewer。
ASM ByteCode Viewer
ASM ByteCode Viewer是專門用于ASM插樁的AS插件。
安裝該插件后,我們可以很方便地查看一個(gè)Class文件怎么用ASM的API去寫出來:
如果我想很快速地知道如何通過ASM API去給方法加耗時(shí)日志,只需要先在本地Java文件中寫好這一段邏輯,然后通過插件查看對應(yīng)的API是怎么樣的就可以了。但建議還是要多了解下這些API,因?yàn)槲覀儗懙腏ava代碼可能不是通用的,在其它Java文件中不一定就能順利地編譯成功,因此往往會(huì)有需要進(jìn)行適配的地方,排查的過程中就需要我們了解API才行了。
到這里,ASM的基本使用就已經(jīng)講好了。如果感興趣可以參考官方文檔asm.ow2.io/asm4-guide.…去實(shí)踐。
如何將ASM運(yùn)用都我們的實(shí)際項(xiàng)目中來?
上一節(jié)我們已經(jīng)知道如何用ASM對一個(gè)Class文件進(jìn)行修改,那么怎么運(yùn)用到我們的項(xiàng)目中來呢?Android打包過程中會(huì)將Class文件打包成Dex文件,在這個(gè)階段我們可以借助AGP(Android Gradle Plugin)與Android Transform來遍歷訪問到所有需要的Class文件,再通過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)插件
接下來就是實(shí)現(xiàn)我們自定義的插件了,我們可以在ASMPlugin中寫我們需要執(zhí)行的邏輯:
class ASMPlugin implements Plugin<Project> { @Override void apply(Project project) { println("ASMPlugin apply") } }
發(fā)布插件
插件項(xiàng)目寫完后,我們需要將其發(fā)布到maven倉庫中去(這里可以選擇先將其發(fā)布到本地maven倉庫),從而讓其它模塊可以方便地進(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倉庫了,我們可以在本地的maven_repo文件夾中找到。
應(yīng)用插件
現(xiàn)在,我們的項(xiàng)目就可以很方便地依賴這個(gè)插件了。
只用做兩個(gè)步驟:
- 在工程根目錄build.gralde添加maven倉庫與插件依賴:
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就是對應(yīng)的插件項(xiàng)目中配置文件的前綴名。
Android Transform
現(xiàn)在假設(shè)app模塊已經(jīng)應(yīng)用了我們的ASM插件,那么還需要使用Transform才能訪問到app模塊在編譯過程中產(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()方法,在這里我們就能訪問到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中可以訪問到所有的DirectoryInput和JarInput,分別代表著我們項(xiàng)目中的Class文件與依賴的JAR包/AAR包中的Class文件。DirectoryInput和JarInput都繼承于QualifiedContent,調(diào)用getFile()方法就可以拿到Class文件的所有信息了。
- 處理輸入源
獲取Class文件后,其實(shí)我們就可以用ASM去修改它了。相當(dāng)于把我們之前ASM修改Class文件的代碼復(fù)制過來就可以了,這一部分留到下一節(jié)中講。
- 將輸入源文件拷貝到目標(biāo)文件中
處理完之后,我們還要記得把輸入源文件拷貝到輸出路徑中去,否則下一個(gè)transform可能就要失敗了,因?yàn)樗也坏捷斎朐戳?。?7行transformInvocation.getOutputProvider().getContentLocation()可以確保我們獲取到最終的輸出路徑。
現(xiàn)在Transform寫好了,但我們還沒有應(yīng)用。應(yīng)用很簡單,只需要在插件中注冊一下就好了:
class ASMPlugin implements Plugin<Project> { @Override void apply(Project project) { def android = project.getExtensions().findByType(AppExtension) android.registerTransform(new ASMTransform()) } }
然后,重新發(fā)布插件到maven倉庫,sync一下,我們就可以在app模塊的gradle tasks中看到我們剛寫好的transform了:
至此,所有鏈路都已經(jīng)走通了,我們知道ASM如何修改Class文件,并可以利用AGP與Transfrom應(yīng)用到我們的工程中來。下面我們就用這條鏈路來實(shí)現(xiàn)一下方法節(jié)流。
方法節(jié)流
Android中最常見的方法節(jié)流就是防重復(fù)點(diǎn)擊。假設(shè)當(dāng)用戶在首頁2s內(nèi)頻繁點(diǎn)擊了商品或者誤觸了商品時(shí),我們期望只打開一次商詳頁面,這時(shí)候就需要對點(diǎn)擊事件做節(jié)流。
首先我們需要定義一個(gè)注解,并聲明到點(diǎn)擊事件上,同時(shí)支持設(shè)置節(jié)流時(shí)長duration:
private final View.OnClickListener mOnClickListener = new View.OnClickListener() { @Override @MethodThrottle(duration = 3000) public void onClick(View view) { } }
接下來就是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在訪問每個(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é)流代碼 } }
具體的代碼就不放上去了,通過ASM ByteCode Viewer可以很方便地生成。這樣我們在平時(shí)代碼中要做方法節(jié)流時(shí),只需要給方法聲明一個(gè)注解就可以了。
方法耗時(shí)日志
類似的,我們可以用這個(gè)鏈路實(shí)現(xiàn)很多AOP邏輯。給方法加耗時(shí)日志這一點(diǎn),在上面的ASM基本使用那一節(jié)中其實(shí)已經(jīng)講過了。
運(yùn)用到工程中來,其實(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è)人覺得還是挺重要的,那就是Gradle插件該如何調(diào)試。即使有ASM ByteCode Viewer插件的幫助,我們在插件中寫的ASM代碼也不可能一鍵完成,很大概率會(huì)碰到各種各樣的編譯錯(cuò)誤問題。本地打日志又比較麻煩,所以調(diào)試手段是必不可少的。篇幅受限,這里就不額外寫了,可以參考Android Studio調(diào)試Gradle插件詳情
發(fā)布線上的額外工作
雖然我們已經(jīng)集成了ASM插件模塊,但并不意味著這樣就能上線了。至少還需要完成以下的工作才行。
插件項(xiàng)目的maven倉庫
因?yàn)樵谡{(diào)試階段,插件的maven倉庫是用的本地的maven倉庫。但如果要集成進(jìn)CI,肯定是需要線上的maven倉庫的,所以到時(shí)候需要申請上傳到某個(gè)maven倉中才行。
編譯影響評估
第一點(diǎn)是必須保證CI打包時(shí)沒問題。第二點(diǎn)就是看這樣做是否會(huì)影響到編譯時(shí)長,畢竟Transform是在編譯階段加了一個(gè)Task。如果對編譯時(shí)長有比較大的影響,還需要額外做一些編譯優(yōu)化的工作。可以用./gradlew --profile --rerun-tasks assembleDebug
命令查看各環(huán)節(jié)的編譯耗時(shí)。
Tips
在做這個(gè)Demo的過程中,因?yàn)樽约阂彩堑谝淮谓佑|,遇到了不少坑,拿幾點(diǎn)貼一下:
- 每次plugin改動(dòng)都要重新發(fā)布一下,否則plugin中transform、asm的邏輯都不會(huì)更新,因?yàn)槟玫氖莔aven倉中的。
- transform每次debug前都要clean一下,否則debug不進(jìn)去。
- 即使transform沒修改任何東西,也需要將源文件jar directory拷貝到目標(biāo)文件。否則最后編譯出的build/transforms文件夾中會(huì)少很多jar包,造成啟動(dòng)時(shí)找不到各種Class而崩潰。
- classpath引入后,造成啟動(dòng)后某so崩潰。一直以為是插件寫的有什么問題,排查很久后也沒找到原因。最后拿崩潰棧去找so庫接口人,升級so版本后問題就解決了。
總結(jié)
通過本篇文章,我們了解到ASM的作用,學(xué)會(huì)ASM基本API的使用,進(jìn)而利用AGP與Transform將ASM運(yùn)用到實(shí)際項(xiàng)目中來。實(shí)戰(zhàn)中,ASM可以實(shí)現(xiàn)常見的AOP邏輯,如方法節(jié)流與方法耗時(shí)日志。不僅如此,當(dāng)我們以后為需要修改三方庫代碼而發(fā)愁時(shí),或許可以想想,ASM能幫助我們搞定嗎?
到此這篇關(guān)于Android ASM插樁探索實(shí)戰(zhàn)詳情的文章就介紹到這了,更多相關(guān)Android ASM插樁探內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
開源電商app常用標(biāo)簽"hot"之第三方開源LabelView
這篇文章主要介紹了開源電商app常用標(biāo)簽"hot"之第三方開源LabelView,對開源電商app相關(guān)資料感興趣的朋友一起學(xué)習(xí)吧2015-12-12Kotlin語言中CompileSdkVersion與targetSdkVersion的區(qū)別淺析
這篇文章主要介紹了Kotlin語言中CompileSdkVersion和targetSdkVersion有什么區(qū)別,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)吧2023-02-02解決Android調(diào)用系統(tǒng)分享給微信,出現(xiàn)分享失敗,分享多文件必須為圖片格式的問題
這篇文章主要介紹了解決Android調(diào)用系統(tǒng)分享給微信,出現(xiàn)分享失敗,分享多文件必須為圖片格式的問題,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-09-09分享Android中ExpandableListView控件使用教程
這篇文章主要介紹了Android中ExpandableListView控件使用教程,可以實(shí)現(xiàn)二級列表展示效果,需要的朋友可以參考下2015-12-12Android Studio打包APK文件具體實(shí)現(xiàn)步驟解析
這篇文章主要介紹了Android Studio打包APK文件具體實(shí)現(xiàn)步驟解析,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-11-11Android采取BroadcastReceiver方式自動(dòng)獲取驗(yàn)證碼
這篇文章主要介紹了Android采取BroadcastReceiver方式自動(dòng)獲取驗(yàn)證碼,感興趣的小伙伴們可以參考一下2016-08-08