在上篇文章中,咱們以AspectJ爲引子介紹了AOP及其設計思想,傳送門Android AspectJ詳解,咱們用AspectJ能夠方便的實現一些簡單的代碼織入,而不須要關心底層字節碼的實現,而ASM則偏向底層一些,ASM提供的API徹底是面向Java字節碼編程,若是你對Java字節碼的結構和原理不甚瞭解,很難直接上手。html
但正是由於ASM的原理是直接操做字節碼,那麼理論上對字節碼的任意修改,均可以用ASM實現。由於不管是哪一種AOP技術,最終跑在JVM上的都是class字節碼。java
而AspectJ所處的位置更偏向應用層,它將操做字節碼這件事封裝到內部,給外部提供的就是一些篩選切面的註解,並在這個切面下編寫java代碼,最終是經過AspectJ的ajc編譯器實現代碼的織入。android
文中的ASM項目示例戳這裏。git
ASM是一個字節碼操做框架,可用來動態生成字節碼或者對現有的類進行加強。ASM能夠直接生成二進制的class字節碼,也能夠在class被加載進虛擬機前動態改變其行爲,好比方法執行先後插入代碼,添加成員變量,修改父類,添加接口等等。github
ASM官方網站編程
ASM經過訪問者模式依次遍歷class字節碼中的各個部分,並不斷的經過回調的方式通知上層(這有點像SAX解析xml的過程),上層可在業務關心的某個訪問點,修改原有邏輯。segmentfault
之因此能夠這麼作,是由於java字節碼是按照嚴格的JVM規範生成二進制字節流,ASM只是按照這個規範對java字節碼的一次解釋,將晦澀難懂的字節碼背後對應的JVM指令一條條的轉換成ASM API。數組
好比,一句簡單的日誌打印bash
Log.d("tag", " onCreate");
複製代碼
轉換成ASM API將會是下面這樣:app
mv.visitLdcInsn("tag");
mv.visitLdcInsn("onCreate");
mv.visitMethodInsn(INVOKESTATIC, "android/util/Log", "d", "(Ljava/lang/String;Ljava/lang/String;)I", false);
mv.visitInsn(POP);
複製代碼
若是你稍懂JVM彙編指令的話,能夠看出大體意思。
而後咱們經過javap指令查看一下這行代碼對應的JVM彙編指令,以下圖:
這樣是否是就很清楚了?就是這四條指令,ASM作的就是按照JVM的規範,生成代碼對應的JVM指令並寫入字節碼文件。
上面的例子,用到了javap指令,所以咱們首先須要對java字節碼結構作一個大體的介紹,這樣整個ASM流程最底層的原理就算清楚了。
咱們經過javac指令將一個java源文件編譯成.class的字節碼文件,這個文件直接經過文本編輯器打開將會看到全是16進制的字節碼。
class Demo {
int i = 0;
public void test() {
i += 1;
}
}
複製代碼
class字節碼結構組成結構如圖。
各個部分佔用字節大小:
其中u一、u二、u四、u8分別表明1個字節、2個字節、4個字節、8個字節的無符號數。無符號數用於描述數字、索引引用、數量值、字符串值。
cp_info、field_info這些以info結尾的是表,一個表由一個或多個元素組成,這裏元素能夠是常量、字段、方法等等。
好比按咱們Demo.class字節碼的信息,cafe babe是魔數,按表順序後面跟的四個字節0000 0034是分別是次版本和主版本,轉換成10進制是52.0,查看java虛擬機版本映射關係表,52表示JDK 1.8,也就是該類是用JDK 1.8進行編譯的。
以後的兩個字節0012表示常量池大小,爲十進制的18,因爲常量池常量下標從1開始,也就是有17個常量。
0a00後面的內容就是第一個具體的常量信息。
常量分爲兩類字面量和符號引用
0a對應十進制的10,10表示MethodRef,即方法引用。
字節碼結構的後續內容較多,並非本文重點,再也不展開,除此以外還須要掌握JVM常見的指令,好比aload、invokespecial、ldc等等,感興趣的小夥伴可參考認識 .class 文件的字節碼結構,補充學習。
但在目前,即便咱們不懂這些也不妨礙咱們開發,由於ASM提供了相應工具幫助咱們編寫ASM API代碼,莫慌~~
字節碼嚴格遵照着JVM規範,直接讀字節碼文件是瘋狂的事情,咱們可經過javap指令能夠將字節碼反編譯成易懂的彙編指令。
javap -v Demo.class
-v 表示verbose,將會打印 行號+本地變量表信息+反編譯彙編代碼+常量池等所有信息。
在Android Studio中,可經過jclasslib插件查看更清晰。
ASM經過訪問者模式,將類文件的內容從頭至尾掃描一遍,每次掃描到相應內容時,會回調ClassVisitor內部相應的方法。
常見的visitor以下表。
類型 | visitor |
---|---|
Class | ClassVisitor |
Field | FieldVisitor |
Method | MethodVisitor |
Annotation | AnnotationVisitor |
ClassVisitor的調用順序爲:
visit
visitSource?
visitOuterClass?
( visitAnnotation | visitAttribute )*
( visitInnerClass | visitField | visitMethod )*
visitEnd
複製代碼
MethodVisitor的調用順序爲:
visitAnnotationDefault?
( visitAnnotation | visitParameterAnnotation | visitAttribute )*
( visitCode
( visitTryCatchBlock | visitLabel | visitFrame | visitXxxInsn |
visitLocalVariable | visitLineNumber )*
visitMaxs )?
visitEnd
複製代碼
完整的訪問順序咱們能夠經過時序圖瞭解:
ClassReader能夠方便地讓咱們對class文件進行讀取與解析,解析到某一個結構就會通知到ClassVisitor的相應方法,好比解析到類方法時,就會回調ClassVisitor.visitMethod方法。
咱們能夠經過更改ClassVisitor中相應結構方法返回值,實現對類的代碼切入,好比更改ClassVisitor.visitMethod()方法的返回值MethodVisitor實例。
經過ClassWriter的toByteArray()方法,獲得class文件的字節碼內容,最後經過文件流寫入方式覆蓋掉原先的內容,實現class文件的改寫。
咱們舉個例子,咱們想爲FragmentActivity這個類的onCreate方法中添加一段日誌打印,能夠按下面的步驟。
//建立ClassReader,傳入class字節碼的輸入流
ClassReader classReader = new ClassReader(IOUtils.toByteArray(inputStream))
//建立ClassWriter,綁定classReader
ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
//建立自定義的LifecycleClassVisitor,並綁定classWriter
ClassVisitor cv = new LifecycleClassVisitor(classWriter)
//接受一個實現了 ClassVisitor接口的對象實例做爲參數,而後依次調用 ClassVisitor接口的各個方法
classReader.accept(cv, EXPAND_FRAMES)
//toByteArray方法會將最終修改的字節碼以 byte 數組形式返回。
byte[] code = classWriter.toByteArray()
複製代碼
最終code就是修改後的字節碼數組。
咱們能夠將它寫入文件輸出到本地。
File file = new File("Test.class");
FileOutputStream fos = new FileOutputStream(file);
fos.write(classFile);
fos.close();
複製代碼
在Android體系下咱們經過Gradle Transform工具,在java代碼編譯成.class文件以後,.class優化爲.dex文件前將代碼織入。 使用Transform須要開發一個自定義的gradle plugin,plugin的開發不是本文的核心,咱們暫且跳過。
咱們只須要知道在一次transform過程當中,Gradle會將本地工程中編譯的代碼、jar包 / aar包 / 依賴的三方庫中的代碼,做爲輸入源交由咱們的插件處理,這也就是說ASM一樣能夠對工程外部的類進行修改或織入。
若是咱們須要在指定的類,指定的方法中織入代碼,須要編寫相應的過濾條件,這也是相比於AspectJ而言不太方便的地方,AspectJ可經過聲明切面註解完成精準的織入。
下面舉個例子,假設咱們想在FragmentActivity的onCreate方法執行前打印一行日誌,能夠這麼作。
建立LifecycleClassVisitor類繼承於ClassVisitor,複寫visitMethod方法。
public class LifecycleClassVisitor extends ClassVisitor implements Opcodes {
...
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
//匹配FragmentActivity
if ("android/support/v4/app/FragmentActivity".equals(this.mClassName)) {
if ("onCreate".equals(name) ) {
//處理onCreate
return new LifecycleOnCreateMethodVisitor(mv);
}
}
return mv;
}
}
複製代碼
訪問到onCreate這個方法時,咱們須要繼續自定義一個MethodVisitor,告訴ASM你想如何處理這個方法。
根據上述的訪問時序圖咱們知道,在方法訪問開始時會回調MethodVisitor的visitCode方法,所以咱們複寫此方法後將會在onCreate方法開頭織入代碼。
public class LifecycleOnCreateMethodVisitor extends MethodVisitor {
...
@Override
public void visitCode() {
super.visitCode();
//方法執行前插入
mv.visitLdcInsn("tag");
mv.visitLdcInsn("onCreate start");
mv.visitMethodInsn(INVOKESTATIC, "android/util/Log", "d", "(Ljava/lang/String;Ljava/lang/String;)I", false);
mv.visitInsn(POP);
}
@Override
public void visitInsn(int opcode) {
//方法執行後插入
if (opcode == Opcodes.RETURN) {
mv.visitLdcInsn("tag");
mv.visitLdcInsn("onCreate end");
mv.visitMethodInsn(INVOKESTATIC, "android/util/Log", "d", "(Ljava/lang/String;Ljava/lang/String;)I", false);
mv.visitInsn(POP);
}
super.visitInsn(opcode);
}
@Override
public void visitEnd() {
super.visitEnd();
//warn 若想在方法最後織入代碼,寫在這裏是無效的
}
}
複製代碼
這裏值得注意的是若想在方法最後織入代碼,寫在visitEnd方法內是無效的,回調它的時候類已經訪問結束了。 咱們只能迂迴解決,咱們知道方法執行結束前都會有一個return指令,若是你的方法返回值爲void,那編譯成字節碼時會默認補上一個return指令。 return指令根據返回對象的類型不一樣,會有不一樣的指令,好比:
因爲咱們知道onCreate方法的返回值就是空,因此咱們只須要捕獲這個return指令就能夠了。 這裏的指令範圍很是廣,好比加減乘除、條件判斷、aload等等,這些指令常量被封裝到Opcodes類中。
訪問者模式爲指令提供的回調就是visitInsn方法,所以就有了上面visitInsn方法的代碼。
因爲在方法先後插入代碼這種需求很常見,而上述模板代碼寫起來又太難看,所以ASM還提供了一個AdviceAdapter類,對一些常見的切面作了二次封裝。
若是咱們用AdviceAdapter編寫上述代碼會變得更直觀清爽。
public class OnCreateMethodAdapter extends AdviceAdapter {
...
@Override
protected void onMethodEnter() {
super.onMethodEnter();
//方法開頭織入代碼
mv.visitLdcInsn("tag");
mv.visitLdcInsn("onCreate start");
mv.visitMethodInsn(INVOKESTATIC, "android/util/Log", "d", "(Ljava/lang/String;Ljava/lang/String;)I", false);
mv.visitInsn(POP);
}
@Override
protected void onMethodExit(int opcode) {
//方法末尾織入代碼
}
}
複製代碼
ok,到這裏咱們以在某個方法先後織入一段代碼的例子講完了,ASM能實現關於字節碼的任何修改,其中涉及的API能夠十分複雜,對於好比修改類名、添加方法等,最好經過查閱ASM官方文檔完成開發。
考慮到直接使用ASM API編寫JVM指令比較困難,所以官方提供了一個插件幫助咱們完成API的編寫。
咱們只須要先在任意位置編寫須要織入的java代碼,而後即可經過這個插件生成對應的ASM代碼,愛了愛了...
雖然ASM很強大,但若是你使用了AspectJ以後再開看ASM,就會發現有一些新的問題。
不過,ASM優勢更加明顯:
AOP相關參考舊文:談談Android AOP技術方案。