背景
最近在調(diào)研在Android中運(yùn)用AOP,發(fā)現(xiàn)主要有這幾種技術(shù)方案:

- APT:可以在編譯期幫我們生成Java文件(需要手動(dòng)拼接代碼,或使用Javapoet),但無法修改已有Java文件,應(yīng)用案例:ButterKnife、Dragger2、EventBus3、DataBinding、AndroidAnnotation,主要就是一些DI框架
- AspectJ:可以修改Java文件,功能強(qiáng)大,最常見的AOP庫,但需要學(xué)習(xí)AspectJ語法,在Android端集成稍復(fù)雜,應(yīng)用案例:Hugo、AspectJx
- Javassist:可以修改Class字節(jié)碼,通過反射在編譯時(shí)加入邏輯,性能較低,應(yīng)用案例:HotFix
- ASM:可以修改Class字節(jié)碼,在編譯時(shí)插入邏輯,性能好,有ASM Btyecode Outline插件支持生成ASM代碼,應(yīng)用案例:各種JVM上的語言,比如Kotlin
- ASMDEX、DexMaker:也是靜態(tài)織入代碼,學(xué)習(xí)成本太高
- cglib:運(yùn)行時(shí)織入代碼,作用于class字節(jié)碼,常用的動(dòng)態(tài)代理庫,比JVM自帶的動(dòng)態(tài)代理更靈活,但不適用于Android,因?yàn)锳ndroid運(yùn)行時(shí)是dex文件,不是class文件
- xposed、dexposed、epic:運(yùn)行時(shí)hook,有兼容性問題,只適合調(diào)試時(shí)玩玩,不適合生產(chǎn)環(huán)境
也有一些基于上面這些技術(shù)方案的工具庫,比如Hunter、lancet、X-AOP,但集成到自己的項(xiàng)目中,還是有各種問題。
最開始考慮使用滬江的AspectJx,但是運(yùn)行時(shí)一直報(bào)找不到Application的錯(cuò)誤,無法運(yùn)行起來,只好放棄,其實(shí)這應(yīng)該是Android端最簡單的AOP方案了,雖然需要學(xué)一點(diǎn)AspectJ的語法,但是并不算太難,無奈集成失敗,只好放棄。
目前考慮利用ASM+自定義gradle插件,在編譯時(shí),通過gradle的Transformer修改Class文件,織入AOP的代碼,使用起來會(huì)比用AspectJ麻煩一點(diǎn),但好在有插件寫ASM Code,比起用Javassist手動(dòng)拼接Java代碼,還是好一點(diǎn)。
開始寫一個(gè)ASM的Demo
創(chuàng)建的類的原型
package com.ezbuy.asmdemo;
/**
* author : yutianran
* time : 2019/01/15
* desc :
* version: 1.0
*/
public class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public void say(String desc) {
System.out.println(String.format("Hello,%s", desc));
}
public String getInfo() {
return "name=" + name + ",age=" + age;
}
}
根據(jù)原型,利用插件自動(dòng)生成的ASM代碼
先安裝ASM Btyecode Outline插件,重啟Android Studio后,右鍵原型文件,點(diǎn)擊show Bytecode outline,

然后等它編譯完成,就會(huì)出現(xiàn)我們想要的ASM的代碼

代碼如下:
package com.ezbuy.asmdemo;
import java.util.*;
import org.objectweb.asm.*;
public class PersonDump implements Opcodes {
public static byte[] dump() throws Exception {
ClassWriter cw = new ClassWriter(0);
FieldVisitor fv;
MethodVisitor mv;
AnnotationVisitor av0;
cw.visit(V1_7, ACC_PUBLIC + ACC_SUPER, "com/ezbuy/asmdemo/Person", null, "java/lang/Object", null);
cw.visitSource("Person.java", null);
{
fv = cw.visitField(ACC_PRIVATE, "name", "Ljava/lang/String;", null, null);
fv.visitEnd();
}
{
fv = cw.visitField(ACC_PRIVATE, "age", "I", null, null);
fv.visitEnd();
}
{
mv = cw.visitMethod(ACC_PUBLIC, "<init>", "(Ljava/lang/String;I)V", null, null);
mv.visitCode();
Label l0 = new Label();
mv.visitLabel(l0);
mv.visitLineNumber(14, l0);
mv.visitVarInsn(ALOAD, 0);
mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
Label l1 = new Label();
mv.visitLabel(l1);
mv.visitLineNumber(15, l1);
mv.visitVarInsn(ALOAD, 0);
mv.visitVarInsn(ALOAD, 1);
mv.visitFieldInsn(PUTFIELD, "com/ezbuy/asmdemo/Person", "name", "Ljava/lang/String;");
Label l2 = new Label();
mv.visitLabel(l2);
mv.visitLineNumber(16, l2);
mv.visitVarInsn(ALOAD, 0);
mv.visitVarInsn(ILOAD, 2);
mv.visitFieldInsn(PUTFIELD, "com/ezbuy/asmdemo/Person", "age", "I");
Label l3 = new Label();
mv.visitLabel(l3);
mv.visitLineNumber(17, l3);
mv.visitInsn(RETURN);
Label l4 = new Label();
mv.visitLabel(l4);
mv.visitLocalVariable("this", "Lcom/ezbuy/asmdemo/Person;", null, l0, l4, 0);
mv.visitLocalVariable("name", "Ljava/lang/String;", null, l0, l4, 1);
mv.visitLocalVariable("age", "I", null, l0, l4, 2);
mv.visitMaxs(2, 3);
mv.visitEnd();
}
{
mv = cw.visitMethod(ACC_PUBLIC, "say", "(Ljava/lang/String;)V", null, null);
mv.visitCode();
Label l0 = new Label();
mv.visitLabel(l0);
mv.visitLineNumber(20, l0);
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("Hello,%s");
mv.visitInsn(ICONST_1);
mv.visitTypeInsn(ANEWARRAY, "java/lang/Object");
mv.visitInsn(DUP);
mv.visitInsn(ICONST_0);
mv.visitVarInsn(ALOAD, 1);
mv.visitInsn(AASTORE);
mv.visitMethodInsn(INVOKESTATIC, "java/lang/String", "format", "(Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;", false);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
Label l1 = new Label();
mv.visitLabel(l1);
mv.visitLineNumber(21, l1);
mv.visitInsn(RETURN);
Label l2 = new Label();
mv.visitLabel(l2);
mv.visitLocalVariable("this", "Lcom/ezbuy/asmdemo/Person;", null, l0, l2, 0);
mv.visitLocalVariable("desc", "Ljava/lang/String;", null, l0, l2, 1);
mv.visitMaxs(6, 2);
mv.visitEnd();
}
{
mv = cw.visitMethod(ACC_PUBLIC, "getInfo", "()Ljava/lang/String;", null, null);
mv.visitCode();
Label l0 = new Label();
mv.visitLabel(l0);
mv.visitLineNumber(24, l0);
mv.visitTypeInsn(NEW, "java/lang/StringBuilder");
mv.visitInsn(DUP);
mv.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
mv.visitLdcInsn("name=");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
mv.visitVarInsn(ALOAD, 0);
mv.visitFieldInsn(GETFIELD, "com/ezbuy/asmdemo/Person", "name", "Ljava/lang/String;");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
mv.visitLdcInsn(",age=");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
mv.visitVarInsn(ALOAD, 0);
mv.visitFieldInsn(GETFIELD, "com/ezbuy/asmdemo/Person", "age", "I");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(I)Ljava/lang/StringBuilder;", false);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
mv.visitInsn(ARETURN);
Label l1 = new Label();
mv.visitLabel(l1);
mv.visitLocalVariable("this", "Lcom/ezbuy/asmdemo/Person;", null, l0, l1, 0);
mv.visitMaxs(2, 1);
mv.visitEnd();
}
cw.visitEnd();
return cw.toByteArray();
}
}
測(cè)試類,比較兩種方式
好了,原型類和Class生成類我們都有了,現(xiàn)在我們來比較下兩種方式創(chuàng)建對(duì)象和調(diào)用方法的區(qū)別
這里為了防止Class命名沖突,我將原型文件重命名為Person2了
package com.ezbuy.asmdemo;
import com.shanhy.demo.asm.hello.MyClassLoader;
import org.junit.Test;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
public class TestClient {
@Test
public void testFile() throws Exception {
Person2 libai = new Person2("libai", 24);
String info = libai.getInfo();
libai.say(info);
}
@Test
public void testGen() throws Exception {
//ASM創(chuàng)建Class
byte[] data = PersonDump.dump();
MyClassLoader myClassLoader = new MyClassLoader();
Class<?> personClass = myClassLoader.defineClass("com.ezbuy.asmdemo.Person", data);
//反射調(diào)用構(gòu)造方法
Constructor<?> constructor = personClass.getConstructor(String.class, int.class);
Object libai = constructor.newInstance("libai", 24);
//反射調(diào)用普通方法
Method getInfo = personClass.getMethod("getInfo", null);
Object info = getInfo.invoke(libai, null);
Method say = personClass.getMethod("say", String.class);
say.invoke(libai, info);
}
}
跑一下test方法,發(fā)現(xiàn)兩個(gè)方法輸出結(jié)果是一致的

最后,放一下我這個(gè)demo用的依賴
implementation 'junit:junit:4.12'
//ASM相關(guān)
implementation 'org.ow2.asm:asm:5.1'
implementation 'org.ow2.asm:asm-util:5.1'
implementation 'org.ow2.asm:asm-commons:5.1'
代碼已上傳到碼云:ASMDemo
總結(jié)
可能你會(huì)說,我明明可以自己手寫一個(gè)Java文件啊,干嘛還得這么麻煩去ASM去動(dòng)態(tài)創(chuàng)建Class呢。但是,你想一下,如果你能操控Class文件,可以動(dòng)態(tài)的添加Class、方法、字段,那你可以做多少黑科技的事情啊。在編譯的時(shí)候,通過gradle提供的Transformer,在這里做做手腳,給原有的方法加加料,比如加上日志統(tǒng)計(jì)、性能監(jiān)控、埋點(diǎn)、權(quán)限控制、事務(wù)控制、防抖,不是很容易么。只要你定義好在什么時(shí)候,加上什么代碼,就可以實(shí)現(xiàn)批量修改多個(gè)Class,而且不侵入原有的Java文件,業(yè)務(wù)邏輯和通用的切面邏輯解耦,多爽。假如現(xiàn)在要你在你的包下面的每個(gè)方法里面都加一個(gè)統(tǒng)計(jì)該方法的耗時(shí),你原來的方式得修改你的每個(gè)方法,但現(xiàn)在,你只需要在編譯時(shí)加一點(diǎn)料就可以了。