涉及知識點:APM, java Agent, plugin, bytecode, asm, InvocationHandler, smail
一. 背景介紹
APM : 應用程序性能管理坤溃。 2011年時國外的APM行業(yè) NewRelic 和 APPDynamics 已經在該領域拔得頭籌裆装,國內近些年來也出現一些APM廠商首有,如: 聽云绝骚, OneAPM, 博睿(bonree) 云智慧,阿里百川碼力舌界。 (據分析,國內android端方案都是抄襲NewRelic公司的泰演,由于該公司的sdk未混淆呻拌,業(yè)界良心)
能做什么: crash監(jiān)控,卡頓監(jiān)控睦焕,內存監(jiān)控柏锄,增加trace,網絡性能監(jiān)控复亏,app頁面自動埋點,等缭嫡。
二. 方案介紹
性能監(jiān)控其實就是hook 代碼到項目代碼中缔御,從而做到各種監(jiān)控。常規(guī)手段都是在項目中增加代碼妇蛀,但如何做到非侵入式的耕突,即一個sdk即可。
1. 如何hook
切面編程-- AOP评架。
我們的方案是AOP的一種眷茁,通過修改app class字節(jié)碼的形式將我們項目的class文件進行修改,從而做到嵌入我們的監(jiān)控代碼纵诞。
通過查看Adnroid編譯流程圖上祈,可以知道編譯器會將所有class文件打包稱dex文件,最終打包成apk浙芙。那么我們就需要在class編譯成dex文件的時候進行代碼注入登刺。比如我想統(tǒng)計某個方法的執(zhí)行時間,那我只需要在每個調用了這個方法的代碼前后都加一個時間統(tǒng)計就可以了嗡呼。關鍵點就在于編譯dex文件時候注入代碼纸俭,這個編譯過程是由dx執(zhí)行,具體類和方法為com.android.dx.command.dexer.Main#processClass
南窗。此方法的第二個參數就是class的byte數組揍很,于是我們只需要在進入processClass方法的時候用ASM工具對class進行改造并替換掉第二個參數,最后生成的apk就是我們改造過后的了万伤。
類:com.android.dx.command.dexer.Main
新的難點: 要讓jvm在執(zhí)行processClass之前先執(zhí)行我們的代碼窒悔,必須要對com.android.dx.command.dexer.Main(以下簡稱為dexer.Main)進行改造。如何才能達到這個目的壕翩?這時Instrumentation和VirtualMachine就登場了蛉迹,參考第三節(jié)。
2. hook 到哪里
一期主要是網絡性能監(jiān)控放妈。如何能截獲到網絡數據
通過調研發(fā)現目前有下面集中方案:
- root手機北救,通過adb 命令進行截獲荐操。
- 建立vpn,將所有網絡請求進行截獲珍策。
- 參考聽云托启,newrelic等產品,針對特定庫進行代理截獲攘宙。
也許還有其他的方式屯耸,需要繼續(xù)調研。
目前我們參考newrelic等公司產品蹭劈,針對特定網絡請求庫進行代理的的方式進行網絡數據截獲疗绣。比如okhtt3, httpclient铺韧, 等網絡庫多矮。
三. Java Agent
In general, a javaagent is a JVM “plugin”, a specially crafted .jar file, that utilizes the Instrumentation API that the JVM provides.
http://www.infoq.com/cn/articles/javaagent-illustrated/
由于我們要修改Dexer 的Main類, 而該類是在編譯時期由java虛擬機啟動的哈打, 所以我們需要通過agent來修改dexer Main類塔逃。
javaagent的主要功能如下:
- 可以在加載class文件之前作攔截,對字節(jié)碼做修改
- 可以在運行期對已加載類的字節(jié)碼做變化
JVMTI:JVM Tool Interface料仗,是JVM暴露出來的一些供用戶擴展的接口集合湾盗。JVMTI是基于事件驅動的,JVM每執(zhí)行到一定的邏輯就會調用一些事件的回調接口(如果有的話)立轧,這些接口可以供開發(fā)者擴展自己的邏輯格粪。
instrument agent: javaagent功能就是它來實現的,另外instrument agent還有個別名叫JPLISAgent(Java Programming Language Instrumentation Services Agent)肺孵,這個名字也完全體現了其最本質的功能:就是專門為Java語言編寫的插樁服務提供支持的匀借。
兩種加載agent的方式:
- 在啟動時加載, 啟動JVM時指定agent類。這種方式平窘,Instrumentation的實例通過agent class的premain方法被傳入吓肋。
- 在運行時加載,JVM提供一種當JVM啟動完成后開啟agent機制。這種情況下瑰艘,Instrumention實例通過agent代碼中的的agentmain傳入是鬼。
參考例子instrumentation 功能介紹(javaagent)
有了javaagent, 我們就可以在編譯app時重新修改dex 的Main類紫新,對應修改processClass方法均蜜。
4. Java Bytecode
如何修改class文件? 我們需要了解java字節(jié)碼芒率,然后需要了解ASM開發(fā)囤耳。通過ASM編程來修改字節(jié)碼,從而修改class文件。(也可以使用javaassist來進行修改)
在介紹字節(jié)代碼指令之前,有必要先來介紹 Java 虛擬機執(zhí)行模型充择。我們知道,Java 代碼是 在線程內部執(zhí)行的德玫。每個線程都有自己的執(zhí)行棧,棧由幀組成。每個幀表示一個方法調用:每次 調用一個方法時,會將一個新幀壓入當前線程的執(zhí)行棧椎麦。當方法返回時,或者是正常返回,或者 是因為異常返回,會將這個幀從執(zhí)行棧中彈出,執(zhí)行過程在發(fā)出調用的方法中繼續(xù)進行(這個方 法的幀現在位于棧的頂端)宰僧。
每一幀包括兩部分:一個局部變量部分和一個操作數棧部分。局部變量部分包含可根據索引 以隨機順序訪問的變量观挎。由名字可以看出,操作數棧部分是一個棧,其中包含了供字節(jié)代碼指令 用作操作數的值琴儿。
字節(jié)代碼指令
字節(jié)代碼指令由一個標識該指令的操作碼和固定數目的參數組成:
- 操作碼是一個無符號字節(jié)值——即字節(jié)代碼名
- 參數是靜態(tài)值,確定了精確的指令行為。它們緊跟在操作碼之后給出.比如GOTO標記 指令(其操作碼的值為 167)以一個指明下一條待執(zhí)行指令的標記作為參數標記嘁捷。不要 將指令參數與指令操作數相混淆:參數值是靜態(tài)已知的,存儲在編譯后的代碼中,而 操作數值來自操作數棧,只有到運行時才能知道造成。
參考: https://en.wikipedia.org/wiki/Java_bytecode_instruction_listings
常見指令:
- const 將什么數據類型壓入操作數棧。
- push 表示將單字節(jié)或短整型的常量壓入操作數棧雄嚣。
- ldc 表示將什么類型的數據從常量池中壓入操作數棧谜疤。
- load 將某類型的局部變量數據壓入操作數棧頂。
- store 將操作數棧頂的數據存入指定的局部變量中现诀。
- pop 從操作數棧頂彈出數據
- dup 復制棧頂的數據并將復制的值也壓入棧頂。
- swap 互換棧頂的數據
- invokeVirtual 調用實例方法
- invokeSepcial 調用超類構造方法履肃,實例初始化仔沿,私有方法等。
- invokeStatic 調用靜態(tài)方法
- invokeInterface 調用接口
- getStatic
- getField
- putStatic
- putField
- New
查看demo:
Java源代碼
public static void print(String param) {
System.out.println("hello " + param);
new TestMain().sayHello();
}
public void sayHello() {
System.out.println("hello agent");
}
字節(jié)碼
// access flags 0x9
public static print(Ljava/lang/String;)V
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
NEW java/lang/StringBuilder
DUP
INVOKESPECIAL java/lang/StringBuilder.<init> ()V
LDC "hello "
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
ALOAD 0
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
NEW com/paic/agent/test/TestMain
DUP
INVOKESPECIAL com/paic/agent/test/TestMain.<init> ()V
INVOKEVIRTUAL com/paic/agent/test/TestMain.sayHello ()V
RETURN
public sayHello()V
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
LDC "hello agent"
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
RETURN
5. ASM 開發(fā)
由于程序分析尺棋、生成和轉換技術的用途眾多,所以人們針對許多語言實現了許多用于分析封锉、 生成和轉換程序的工具,這些語言中就包括 Java 在內。ASM 就是為 Java 語言設計的工具之一, 用于進行運行時(也是脫機的)類生成與轉換膘螟。于是,人們設計了 ASM1庫,用于處理經過編譯 的 Java 類成福。
ASM 并不是惟一可生成和轉換已編譯 Java 類的工具,但它是最新、最高效的工具之一,可 從 http://asm.objectweb.org 下載荆残。其主要優(yōu)點如下:
- 有一個簡單的模塊API,設計完善奴艾、使用方便。
- 文檔齊全,擁有一個相關的Eclipse插件内斯。
- 支持最新的 Java 版本——Java 7蕴潦。
- 小而快、非撤常可靠潭苞。
- 擁有龐大的用戶社區(qū),可以為新用戶??供支持。
- 源許可開放,幾乎允許任意使用真朗。
核心類: ClassReader, ClassWriter, ClassVisitor
參考demo:
{
// print 方法的ASM代碼
mv = cw.visitMethod(ACC_PUBLIC + ACC_STATIC, "print", "(Ljava/lang/String;)V", null, null);
mv.visitCode();
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitTypeInsn(NEW, "java/lang/StringBuilder");
mv.visitInsn(DUP);
mv.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
mv.visitLdcInsn("hello ");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
mv.visitVarInsn(ALOAD, 0);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
mv.visitTypeInsn(NEW, "com/paic/agent/test/TestMain");
mv.visitInsn(DUP);
mv.visitMethodInsn(INVOKESPECIAL, "com/paic/agent/test/TestMain", "<init>", "()V", false);
mv.visitMethodInsn(INVOKEVIRTUAL, "com/paic/agent/test/TestMain", "sayHello", "()V", false);
mv.visitInsn(RETURN);
mv.visitEnd();
}
{
//sayHello 的ASM代碼
mv = cw.visitMethod(ACC_PUBLIC, "sayHello", "()V", null, null);
mv.visitCode();
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("hello agent");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
mv.visitInsn(RETURN);
mv.visitEnd();
}
6. 實現原理
1. Instrumentation和VirtualMachine
VirtualMachine有個loadAgent方法此疹,它指定的agent會在main方法前啟動,并調用agent的agentMain方法,agentMain的第二個參數是Instrumentation蝗碎,這樣我們就能夠給Instrumentation設置ClassFileTransformer來實現對dexer.Main的改造湖笨,同樣也可以用ASM來實現。一般來說衍菱,APM工具包括三個部分赶么,plugin、agent和具體的業(yè)務jar包脊串。這個agent就是我們說的由VirtualMachine啟動的代理辫呻。而plugin要做的事情就是調用loadAgent方法。對于Android Studio而言琼锋,plugin就是一個Gradle插件放闺。 實現gradle插件可以用intellij創(chuàng)建一個gradle工程并實現Plugin< Project >接口,然后把tools.jar(在jdk的lib目錄下)和agent.jar加入到Libraries中缕坎。在META-INF/gradle-plugins目錄下創(chuàng)建一個properties文件怖侦,并在文件中加入一行內容“implementation-class=插件類的全限定名“。artifacs配置把源碼和META-INF加上谜叹,但不能加tools.jar和agent.jar匾寝。(tools.jar 在 jdk中, 不過一般需要自己拷貝到工程目錄中的荷腊, agent.jar開發(fā)完成后放到plugin工程中用于獲取jar包路徑)艳悔。
2. ClassFileTransformer
agent的實現相對plugin則復雜很多,首先需要提供agentmain(String args, Instrumentation inst)方法女仰,并給Instrumentation設置ClassFileTransformer猜年,然后在transformer里改造dexer.Main。當jvm成功執(zhí)行到我們設置的transformer時疾忍,就會發(fā)現傳進來的class根本就沒有dexer.Main乔外。坑爹呢這是一罩。杨幼。。前面提到了聂渊,執(zhí)行dexer.Main的是dx.bat推汽,也就是說,它和plugin根本不在一個進程里歧沪。
3. ProcessBuilder
dx.bat其實是由ProcessBuilder的start方法啟動的歹撒,ProcessBuilder有一個command成員,保存的是啟動目標進程攜帶的參數诊胞,只要我們給dx.bat帶上-javaagent參數就能給dx.bat所在進程指定我們的agent了暖夭。于是我們可以在執(zhí)行start方法前锹杈,調用command方法獲取command,并往其中插入-javaagent參數迈着。參數的值是agent.jar所在的路徑竭望,可以使用agent.jar其中一個class類實例的getProtectionDomain().getCodeSource().getLocation().toURI().getPath()獲得≡2ぃ可是到了這里我們的程序可能還是無法正確改造class咬清。如果我們把改造類的代碼單獨放到一個類中,然后用ASM生成字節(jié)碼調用這個類的方法來對command參數進行修改奴潘,就會發(fā)現拋出了ClassDefNotFoundError錯誤旧烧。這里涉及到了ClassLoader的知識。
4. ClassLoader和InvocationHandler
關于ClassLoader的介紹很多画髓,這里不再贅述掘剪。ProcessBuilder類是由Bootstrap ClassLoader加載的,而我們自定義的類則是由AppClassLoader加載的奈虾。Bootstrap ClassLoader處于AppClassLoader的上層夺谁,我們知道,上層類加載器所加載的類是無法直接引用下層類加載器所加載的類的肉微。但如果下層類加載器加載的類實現或繼承了上層類加載器加載的類或接口匾鸥,上層類加載器加載的類獲取到下層類加載的類的實例就可以將其強制轉型為父類,并調用父類的方法碉纳。這個上層類加載器加載的接口扫腺,部分APM使用InvocationHandler。還有一個問題村象,ProcessBuilder怎么才能獲取到InvocationHandler子類的實例呢?有一個比較巧妙的做法攒至,在agent啟動的時候厚者,創(chuàng)建InvocationHandler實例,并把它賦值給Logger的treeLock成員迫吐。treeLock是一個Object對象库菲,并且只是用來加鎖的,沒有別的用途志膀。但treeLock是一個final成員熙宇,所以記得要修改其修飾,去掉final溉浙。Logger同樣也是由Bootstrap ClassLoader加載烫止,這樣ProcessBuilder就能通過反射的方式來獲取InvocationHandler實例了。(詳見:核心代碼例子)
上層類加載器所加載的類是無法直接引用下層類加載器所加載的類的
層次 | 加載器 | 類 |
---|---|---|
上層 | BootStrapClassLoader | ProcessBuilder |
下層 | AppClassLoader | ProcessBuilderMethodVisitor操作的自定義類 |
這一句話的理解: 我們的目的是通過ProcessBuilderMethodVisitor將我們的代碼(自定義修改類)寫入ProcessBuilder.class中去讓BootStrapClassLoader類加載器進行加載戳稽,而此時馆蠕, BootStrapClassLoader是無法引用到我們自定義的類的,因為我們自定義的類是AppClassLoader加載的。
但如果下層類加載器加載的類實現或繼承了上層類加載器加載的類或接口互躬,上層類加載器加載的類獲取到下層類加載的類的實例就可以將其強制轉型為父類播赁,并調用父類的方法。
層次 | 加載器 | 類 |
---|---|---|
上層 | BootStrapClassLoader | Looger |
下層 | AppClassLoader | InvocationDispatcher |
這句話的理解: 這里我們可以看到自定義類InvocationDispatcher是由AppClassLoader加載的吼渡, 我們在運行RewriterAgent(AppClassLoader加載)類時容为,通過反射的方式將InvocationDispatcher對象放入Looger(由于引用了Looger.class,所以此時logger已經被BootStrapClassLoader加載)類的treelock對象中,即下層類加載器加載的類實現了上層類加載器加載的類寺酪;當我們通過ProcessBuilderMethodVisitor類處理ProcessBuilder.class文件時坎背,可以通過Logger提取成員變量,插入對應的調用邏輯房维。當運行到ProcessBuilder時沼瘫,再通過這段代碼動態(tài)代理的方式調用對應的業(yè)務。可以將其強制轉型為父類咙俩,并調用父類的方法 ,請參考http://stackoverflow.com/questions/1504633/what-is-the-point-of-invokeinterface耿戚, 這里詳細介紹了invokeInterface 和 invokeVirtual 的區(qū)別。
5. CallSiteReplace 和 WrapReturn
實現上我們目前主要做這兩種阿趁, 一種是代碼調用替換膜蛔, 另一種是代碼包裹返回。主要是提前寫好對應規(guī)則的替換代碼脖阵, 生成配置文件表皂股, 在agent中visit每一個class代碼, 遇到對應匹配調用時將進行代碼替換命黔。
7. 核心代碼
ProcessBuilderMethodVisitor
DexClassTransformer#createDexerMainClassAdapter
InvocationDispatcher
BytecodeBuilder
public BytecodeBuilder loadInvocationDispatcher() {
this.adapter.visitLdcInsn(Type.getType(TransformConstant.INVOCATION_DISPATCHER_CLASS));
this.adapter.visitLdcInsn(TransformConstant.INVOCATION_DISPATCHER_FILED_NAME);
this.adapter.invokeVirtual(Type.getType(Class.class), new Method("getDeclaredField", "(Ljava/lang/String;)Ljava/lang/reflect/Field;"));
this.adapter.dup();
this.adapter.visitInsn(Opcodes.ICONST_1);
this.adapter.invokeVirtual(Type.getType(Field.class), new Method("setAccessible", "(Z)V"));
this.adapter.visitInsn(Opcodes.ACONST_NULL);
this.adapter.invokeVirtual(Type.getType(Field.class), new Method("get", "(Ljava/lang/Object;)Ljava/lang/Object;"));
return this;
}
解析:
順序 | 棧 | 指令 | 描述 |
---|---|---|---|
8 | InvocationDispatcher object | invokeVirtual | 調用get方法返回具體實例對象 |
7 | null | ACONST_NULL | null 入棧 |
6 | Field object | invokeVirtual | 調用setAccessible,改為可訪問的呜呐,目前棧中只剩一個對象 |
5 | true | ICONST_1 | 1 即為true,入棧 |
4 | Field object | dup | 拷貝一份悍募,目前棧中只剩兩個對象 |
3 | Field object | invokeVirtual | 調用getDeclaredField 獲取treeLock存儲的Field |
2 | treelock | ldc | treelock 入棧 |
1 | Logger.class Type | ldc | Logger.class type 入棧 |
WrapMethodClassVisitor#MethodWrapMethodVisitor
private boolean tryReplaceCallSite(int opcode, String owner, String name, String desc, boolean itf) {
Collection<ClassMethod> replacementMethods = this.context.getCallSiteReplacements(owner, name, desc);
if (replacementMethods.isEmpty()) {
return false;
}
ClassMethod method = new ClassMethod(owner, name, desc);
Iterator<ClassMethod> it = replacementMethods.iterator();
if (it.hasNext()) {
ClassMethod replacementMethod = it.next();
boolean isSuperCallInOverride = (opcode == Opcodes.INVOKESPECIAL) && !owner.equals(this.context.getClassName())
&& this.name.equals(name) && this.desc.equals(desc);
//override 方法
if (isSuperCallInOverride) {
this.log.info(MessageFormat.format("[{0}] skipping call site replacement for super call in overriden method : {1}:{2}",
this.context.getFriendlyClassName(), this.name, this.desc));
return false;
}
Method originMethod = new Method(name, desc);
//處理init方法蘑辑, 構造對象, 調用替換的靜態(tài)方法來替換init坠宴。
if (opcode == Opcodes.INVOKESPECIAL && name.equals("<init>")) {
//調用父類構造方法
if (this.context.getSuperClassName() != null && this.context.getSuperClassName().equals(owner)) {
this.log.info(MessageFormat.format("[{0}] skipping call site replacement for class extending {1}",
this.context.getFriendlyClassName(), this.context.getFriendlySuperClassName()));
return false;
}
this.log.info(MessageFormat.format("[{0}] tracing constructor call to {1} - {2}", this.context.getFriendlyClassName(), method.toString(), owner));
//開始處理創(chuàng)建對象的邏輯
//保存參數到本地
int[] arguments = new int[originMethod.getArgumentTypes().length];
for (int i = arguments.length -1 ; i >= 0; i--) {
arguments[i] = this.newLocal(originMethod.getArgumentTypes()[i]);
this.storeLocal(arguments[i]);
}
//由于init 之前會有一次dup,及創(chuàng)建一次洋魂, dup一次, 此時如果執(zhí)行了new 和 dup 操作樹棧中會有兩個對象喜鼓。
this.visitInsn(Opcodes.POP);
if (this.newInstructionFound && this.dupInstructionFound) {
this.visitInsn(Opcodes.POP);
}
//載入參數到操作數棧
for (int arg : arguments) {
this.loadLocal(arg);
}
//使用要替換的方法副砍,執(zhí)行靜態(tài)方法進行對象創(chuàng)建
super.visitMethodInsn(Opcodes.INVOKESTATIC, replacementMethod.getClassName(), replacementMethod.getMethodName(), replacementMethod.getMethodDesc(), false);
//如果此時才調用了dup,也需要pop庄岖, (這一部分的場景暫時還沒有構造出來, 上面的邏輯為通用的)
if (this.newInstructionFound && !this.dupInstructionFound) {
this.visitInsn(Opcodes.POP);
}
} else if (opcode == Opcodes.INVOKESTATIC) {
//替換靜態(tài)方法
this.log.info(MessageFormat.format("[{0}] replacing call to {1} with {2}", this.context.getFriendlyClassName(), method.toString(), replacementMethod.toString()));
super.visitMethodInsn(Opcodes.INVOKESTATIC, replacementMethod.getClassName(), replacementMethod.getMethodName(), replacementMethod.getMethodDesc(), false);
} else {
// 其他方法調用, 使用新方法替換舊方法的調用豁翎。 先判斷創(chuàng)建的對象是否為null,
Method newMethod = new Method(replacementMethod.getMethodName(), replacementMethod.getMethodDesc());
this.log.info(MessageFormat.format("[{0}] replacing call to {1} with {2}", this.context.getFriendlyClassName(), method.toString(), replacementMethod.toString()));
//從操作數棧上取原始參數類型到本地變量中
int[] originArgs = new int[originMethod.getArgumentTypes().length];
for (int i = originArgs.length -1 ; i >= 0; i--) {
originArgs[i] = this.newLocal(originMethod.getArgumentTypes()[i]);
this.storeLocal(originArgs[i]);
}
//操作數棧中只剩操作對象了隅忿, 需要dup谨垃, 拷貝一份作為檢查新method的第一個參數启搂。
this.dup();
//檢查操作數棧頂對象類型是否和新method的第一個參數一致。
this.instanceOf(newMethod.getArgumentTypes()[0]);
Label isInstanceOfLabel = new Label();
//instanceof 結果不等于0 則跳轉到 isInstanceofLabel刘陶,執(zhí)行替換調用
this.visitJumpInsn(Opcodes.IFNE, isInstanceOfLabel);
//否則執(zhí)行原始調用
for (int arg : originArgs) {
this.loadLocal(arg);
}
super.visitMethodInsn(opcode, owner, name, desc, itf);
Label endLabel = new Label();
//跳轉到結束label
this.visitJumpInsn(Opcodes.GOTO, endLabel);
this.visitLabel(isInstanceOfLabel);
//處理替換的邏輯
//load 參數胳赌, 第一個為 obj, 后面的為原始參數
this.checkCast(newMethod.getArgumentTypes()[0]);
for (int arg: originArgs) {
this.loadLocal(arg);
}
super.visitMethodInsn(Opcodes.INVOKESTATIC, replacementMethod.getClassName(), replacementMethod.getMethodName(), replacementMethod.getMethodDesc(), false);
//結束
this.visitLabel(endLabel);
}
this.context.markModified();
return true;
}
return false;
}
解析
詳細見tryReplaceCallSite
注釋即可匙隔。
8. 驗證
將生成的apk反編譯疑苫,查看class 字節(jié)碼。我們一般會通過JD-GUI來查看纷责。我們來查看一下sample生成的結果:
private void testOkhttpCall()
{
OkHttpClient localOkHttpClient = new OkHttpClient.Builder().build();
Object localObject = new Request.Builder().url("https://test3-fbtoam.pingan.com.cn:15443/btoa/portal/common/getPublicKey");
if (!(localObject instanceof Request.Builder))
{
localObject = ((Request.Builder)localObject).build();
if ((localOkHttpClient instanceof OkHttpClient)) {
break label75;
}
}
label75:
for (localObject = localOkHttpClient.newCall((Request)localObject);; localObject = OkHttp3Instrumentation.newCall((OkHttpClient)localOkHttpClient, (Request)localObject))
{
((Call)localObject).enqueue(new Callback()
{
public void onFailure(Call paramAnonymousCall, IOException paramAnonymousIOException)
{
}
public void onResponse(Call paramAnonymousCall, Response paramAnonymousResponse)
throws IOException
{
}
});
return;
localObject = OkHttp3Instrumentation.build((Request.Builder)localObject);
break;
}
}
上面的代碼估計沒有幾個人能夠看懂捍掺, 尤其for循環(huán)里面的邏輯。其實是由于不同的反編譯工具造成的解析問題導致的再膳,所以看起來邏輯混亂挺勿,無法符合預期。
想用查看真實的結果喂柒, 我們來看下反編譯后的smail不瓶。
詳細smail指令參考http://pallergabor.uw.hu/androidblog/dalvik_opcodes.html
.method private testOkhttpCall()V
.locals 6
.prologue
.line 35
const-string v3, "https://test3-fbtoam.pingan.com.cn:15443/btoa/portal/common/getPublicKey"
.line 36
.local v3, "url":Ljava/lang/String;
new-instance v4, Lokhttp3/OkHttpClient$Builder;
invoke-direct {v4}, Lokhttp3/OkHttpClient$Builder;-><init>()V
invoke-virtual {v4}, Lokhttp3/OkHttpClient$Builder;->build()Lokhttp3/OkHttpClient;
move-result-object v1
//new OkHttpClient.Builder().build(); 即為okhttpclient,放到 v1 中
.line 37
.local v1, "okHttpClient":Lokhttp3/OkHttpClient;
new-instance v4, Lokhttp3/Request$Builder;
invoke-direct {v4}, Lokhttp3/Request$Builder;-><init>()V
invoke-virtual {v4, v3}, Lokhttp3/Request$Builder;->url(Ljava/lang/String;)Lokhttp3/Request$Builder;
move-result-object v4
//new Request.Builder().url(url)執(zhí)行了這一段語句,將結果放到了v4中灾杰。
instance-of v5, v4, Lokhttp3/Request$Builder;
if-nez v5, :cond_0
invoke-virtual {v4}, Lokhttp3/Request$Builder;->build()Lokhttp3/Request;
move-result-object v2
.line 38
.local v2, "request":Lokhttp3/Request;
//判斷v4中存儲的是否為Request.Builder類型蚊丐,如果是則跳轉到cond_0, 否則執(zhí)行Request.Builder.build()方法,將結果放到v2中.
:goto_0
instance-of v4, v1, Lokhttp3/OkHttpClient;
if-nez v4, :cond_1
invoke-virtual {v1, v2}, Lokhttp3/OkHttpClient;->newCall(Lokhttp3/Request;)Lokhttp3/Call;
move-result-object v0
.line 39
.end local v1 # "okHttpClient":Lokhttp3/OkHttpClient;
.local v0, "call":Lokhttp3/Call;
//goto_0 標簽:判斷v1 中的值是否為 OKHttpclient 類型艳吠, 如果是跳轉為cond_1 麦备, 否則調用OKHttpclient.newCall, 并將結果放到v0 中。
:goto_1
new-instance v4, Lcom/paic/apm/sample/MainActivity$1;
invoke-direct {v4, p0}, Lcom/paic/apm/sample/MainActivity$1;-><init>(Lcom/paic/apm/sample/MainActivity;)V
invoke-interface {v0, v4}, Lokhttp3/Call;->enqueue(Lokhttp3/Callback;)V
.line 51
return-void
//goto_1 標簽: 執(zhí)行 v0.enqueue(new Callback());并return;
.line 37
.end local v0 # "call":Lokhttp3/Call;
.end local v2 # "request":Lokhttp3/Request;
.restart local v1 # "okHttpClient":Lokhttp3/OkHttpClient;
:cond_0
check-cast v4, Lokhttp3/Request$Builder;
invoke-static {v4}, Lcom/paic/agent/android/instrumentation/okhttp3/OkHttp3Instrumentation;->build(Lokhttp3/Request$Builder;)Lokhttp3/Request;
move-result-object v2
goto :goto_0
//cond_0:標簽: 執(zhí)行com.paic.agent.android.instrumentation.okhttp3.OkHttp3Instrumentation.build(v4)昭娩, 并將結果放到v2中凛篙,并goto 到 goto_0
.line 38
.restart local v2 # "request":Lokhttp3/Request;
:cond_1
check-cast v1, Lokhttp3/OkHttpClient;
.end local v1 # "okHttpClient":Lokhttp3/OkHttpClient;
invoke-static {v1, v2}, Lcom/paic/agent/android/instrumentation/okhttp3/OkHttp3Instrumentation;->newCall(Lokhttp3/OkHttpClient;Lokhttp3/Request;)Lokhttp3/Call;
move-result-object v0
goto :goto_1
//cond_1 標簽: 執(zhí)行com.paic.agent.android.instrumentation.okhttp3.OkHttp3Instrumentation.newCall(v1, v2), 并將結果放到v0中栏渺, goto 到goto_1
.end method
解析后的偽代碼
String v3 = "https://test3-fbtoam.pingan.com.cn:15443/btoa/portal/common/getPublicKey";
object v1 = new OkhttpClient.Builder().build();
object v4 = new Reqeust.Builder().url(v3);
object v2 ;
object v0 ;
if (v4 instanceof Request.Builder) {
cond_0:
v2 = com.paic.agent.android.instrumentation.okhttp3.OkHttp3Instrumentation.build(v4);
} else {
v2 = (Request.Builder)v4.build();
}
goto_0:
if (v1 instanceof OkHttpClient) {
cond_1:
v0 = com.paic.agent.android.instrumentation.okhttp3.OkHttp3Instrumentation.newCall(v1, v2);
} else {
v0 = v1.newCall(v2); // v0 is Call
}
goto_1:
v4 = new Callback();
v0.enqueue(v4);
return;
查看偽代碼呛梆, 符合預期結果。驗證完畢迈嘹。