編輯:Android資訊
涉及知識點:APM, Java Agent, plugin, bytecode, asm, InvocationHandler, smail
APM : 應用程序性能管理。 2011年時國外的APM行業 NewRelic 和 APPDynamics 已經在該領域拔得頭籌,國內近些年來也出現一些APM廠商,如: 聽雲, OneAPM, 博睿(bonree) 雲智慧,阿裡百川碼力。 (據分析,國內android端方案都是抄襲NewRelic公司的,由於該公司的sdk未混淆,業界良心)
能做什麼: crash監控,卡頓監控,內存監控,增加trace,網絡性能監控,app頁面自動埋點,等。
性能監控其實就是hook 代碼到項目代碼中,從而做到各種監控。常規手段都是在項目中增加代碼,但如何做到非侵入式的,即一個sdk即可。
切面編程-- AOP。
我們的方案是AOP的一種,通過修改app class字節碼的形式將我們項目的class文件進行修改,從而做到嵌入我們的監控代碼。
通過查看Adnroid編譯流程圖,可以知道編譯器會將所有class文件打包稱dex文件,最終打包成apk。那麼我們就需要在class編譯成dex文件的時候進行代碼注入。比如我想統計某個方法的執行時間,那我只需要在每個調用了這個方法的代碼前後都加一個時間統計就可以了。關鍵點就在於編譯dex文件時候注入代碼,這個編譯過程是由dx執行,具體類和方法為com.android.dx.command.dexer.Main#processClass
。此方法的第二個參數就是class的byte數組,於是我們只需要在進入processClass方法的時候用ASM工具對class進行改造並替換掉第二個參數,最後生成的apk就是我們改造過後的了。
類:com.android.dx.command.dexer.Main
新的難點: 要讓jvm在執行processClass之前先執行我們的代碼,必須要對com.android.dx.command.dexer.Main(以下簡稱為dexer.Main)進行改造。如何才能達到這個目的?這時Instrumentation和VirtualMachine就登場了,參考第三節。
一期主要是網絡性能監控。如何能截獲到網絡數據
通過調研發現目前有下面集中方案:
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的主要功能如下:
JVMTI:JVM Tool Interface,是JVM暴露出來的一些供用戶擴展的接口集合。JVMTI是基於事件驅動的,JVM每執行到一定的邏輯就會調用一些事件的回調接口(如果有的話),這些接口可以供開發者擴展自己的邏輯。
instrument agent: javaagent功能就是它來實現的,另外instrument agent還有個別名叫JPLISAgent(Java Programming Language Instrumentation Services Agent),這個名字也完全體現了其最本質的功能:就是專門為Java語言編寫的插樁服務提供支持的。
兩種加載agent的方式:
參考例子instrumentation 功能介紹(javaagent)
有了javaagent, 我們就可以在編譯app時重新修改dex 的Main類,對應修改processClass方法。
如何修改class文件? 我們需要了解java字節碼,然後需要了解ASM開發。通過ASM編程來修改字節碼,從而修改class文件。(也可以使用javaassist來進行修改)
在介紹字節代碼指令之前,有必要先來介紹 Java 虛擬機執行模型。我們知道,Java 代碼是 在線程內部執行的。每個線程都有自己的執行棧,棧由幀組成。每個幀表示一個方法調用:每次 調用一個方法時,會將一個新幀壓入當前線程的執行棧。當方法返回時,或者是正常返回,或者 是因為異常返回,會將這個幀從執行棧中彈出,執行過程在發出調用的方法中繼續進行(這個方 法的幀現在位於棧的頂端)。
每一幀包括兩部分:一個局部變量部分和一個操作數棧部分。局部變量部分包含可根據索引 以隨機順序訪問的變量。由名字可以看出,操作數棧部分是一個棧,其中包含了供字節代碼指令 用作操作數的值。
字節代碼指令由一個標識該指令的操作碼和固定數目的參數組成:
參考: https://en.wikipedia.org/wiki/Java_bytecode_instruction_listings
常見指令:
Java源代碼
public static void print(String param) { System.out.println("hello " + param); new TestMain().sayHello(); } public void sayHello() { System.out.println("hello agent"); }
字節碼
// 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
由於程序分析、生成和轉換技術的用途眾多,所以人們針對許多語言實現了許多用於分析、 生成和轉換程序的工具,這些語言中就包括 Java 在內。ASM 就是為 Java 語言設計的工具之一, 用於進行運行時(也是脫機的)類生成與轉換。於是,人們設計了 ASM1庫,用於處理經過編譯 的 Java 類。
ASM 並不是惟一可生成和轉換已編譯 Java 類的工具,但它是最新、最高效的工具之一,可 從 http://asm.objectweb.org 下載。其主要優點如下:
核心類: 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(); }
VirtualMachine有個loadAgent方法,它指定的agent會在main方法前啟動,並調用agent的agentMain方法,agentMain的第二個參數是Instrumentation,這樣我們就能夠給Instrumentation設置ClassFileTransformer來實現對dexer.Main的改造,同樣也可以用ASM來實現。一般來說,APM工具包括三個部分,plugin、agent和具體的業務jar包。這個agent就是我們說的由VirtualMachine啟動的代理。而plugin要做的事情就是調用loadAgent方法。對於Android Studio而言,plugin就是一個Gradle插件。 實現gradle插件可以用intellij創建一個gradle工程並實現Plugin< Project >接口,然後把tools.jar(在jdk的lib目錄下)和agent.jar加入到Libraries中。在META-INF/gradle-plugins目錄下創建一個properties文件,並在文件中加入一行內容“implementation-class=插件類的全限定名“。artifacs配置把源碼和META-INF加上,但不能加tools.jar和agent.jar。(tools.jar 在 jdk中, 不過一般需要自己拷貝到工程目錄中的, agent.jar開發完成後放到plugin工程中用於獲取jar包路徑)。
agent的實現相對plugin則復雜很多,首先需要提供agentmain(String args, Instrumentation inst)方法,並給Instrumentation設置ClassFileTransformer,然後在transformer裡改造dexer.Main。當jvm成功執行到我們設置的transformer時,就會發現傳進來的class根本就沒有dexer.Main。坑爹呢這是。。。前面提到了,執行dexer.Main的是dx.bat,也就是說,它和plugin根本不在一個進程裡。
dx.bat其實是由ProcessBuilder的start方法啟動的,ProcessBuilder有一個command成員,保存的是啟動目標進程攜帶的參數,只要我們給dx.bat帶上-javaagent參數就能給dx.bat所在進程指定我們的agent了。於是我們可以在執行start方法前,調用command方法獲取command,並往其中插入-javaagent參數。參數的值是agent.jar所在的路徑,可以使用agent.jar其中一個class類實例的getProtectionDomain().getCodeSource().getLocation().toURI().getPath()獲得。可是到了這裡我們的程序可能還是無法正確改造class。如果我們把改造類的代碼單獨放到一個類中,然後用ASM生成字節碼調用這個類的方法來對command參數進行修改,就會發現拋出了ClassDefNotFoundError錯誤。這裡涉及到了ClassLoader的知識。
關於ClassLoader的介紹很多,這裡不再贅述。ProcessBuilder類是由Bootstrap ClassLoader加載的,而我們自定義的類則是由AppClassLoader加載的。Bootstrap ClassLoader處於AppClassLoader的上層,我們知道,上層類加載器所加載的類是無法直接引用下層類加載器所加載的類的。但如果下層類加載器加載的類實現或繼承了上層類加載器加載的類或接口,上層類加載器加載的類獲取到下層類加載的類的實例就可以將其強制轉型為父類,並調用父類的方法。這個上層類加載器加載的接口,部分APM使用InvocationHandler。還有一個問題,ProcessBuilder怎麼才能獲取到InvocationHandler子類的實例呢?有一個比較巧妙的做法,在agent啟動的時候,創建InvocationHandler實例,並把它賦值給Logger的treeLock成員。treeLock是一個Object對象,並且只是用來加鎖的,沒有別的用途。但treeLock是一個final成員,所以記得要修改其修飾,去掉final。Logger同樣也是由Bootstrap ClassLoader加載,這樣ProcessBuilder就能通過反射的方式來獲取InvocationHandler實例了。(詳見:核心代碼例子)
上層類加載器所加載的類是無法直接引用下層類加載器所加載的類的
這一句話的理解: 我們的目的是通過ProcessBuilderMethodVisitor將我們的代碼(自定義修改類)寫入ProcessBuilder.class中去讓BootStrapClassLoader類加載器進行加載,而此時, BootStrapClassLoader是無法引用到我們自定義的類的,因為我們自定義的類是AppClassLoader加載的。
但如果下層類加載器加載的類實現或繼承了上層類加載器加載的類或接口,上層類加載器加載的類獲取到下層類加載的類的實例就可以將其強制轉型為父類,並調用父類的方法。
這句話的理解: 這裡我們可以看到自定義類InvocationDispatcher是由AppClassLoader加載的, 我們在運行RewriterAgent(AppClassLoader加載)類時,通過反射的方式將InvocationDispatcher對象放入Looger(由於引用了Looger.class,所以此時logger已經被BootStrapClassLoader加載)類的treelock對象中,即下層類加載器加載的類實現了上層類加載器加載的類;當我們通過ProcessBuilderMethodVisitor類處理ProcessBuilder.class文件時,可以通過Logger提取成員變量,插入對應的調用邏輯。當運行到ProcessBuilder時,再通過這段代碼動態代理的方式調用對應的業務。可以將其強制轉型為父類,並調用父類的方法 ,請參考http://stackoverflow.com/questions/1504633/what-is-the-point-of-invokeinterface, 這裡詳細介紹了invokeInterface 和 invokeVirtual 的區別。
實現上我們目前主要做這兩種, 一種是代碼調用替換, 另一種是代碼包裹返回。主要是提前寫好對應規則的替換代碼, 生成配置文件表, 在agent中visit每一個class代碼, 遇到對應匹配調用時將進行代碼替換。
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; }
解析:
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方法, 構造對象, 調用替換的靜態方法來替換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)); //開始處理創建對象的邏輯 //保存參數到本地 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,及創建一次, dup一次, 此時如果執行了new 和 dup 操作樹棧中會有兩個對象。 this.visitInsn(Opcodes.POP); if (this.newInstructionFound && this.dupInstructionFound) { this.visitInsn(Opcodes.POP); } //載入參數到操作數棧 for (int arg : arguments) { this.loadLocal(arg); } //使用要替換的方法,執行靜態方法進行對象創建 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) { //替換靜態方法 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 { // 其他方法調用, 使用新方法替換舊方法的調用。 先判斷創建的對象是否為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,執行替換調用 this.visitJumpInsn(Opcodes.IFNE, isInstanceOfLabel); //否則執行原始調用 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
注釋即可。
將生成的apk反編譯,查看class 字節碼。我們一般會通過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循環裡面的邏輯。其實是由於不同的反編譯工具造成的解析問題導致的,所以看起來邏輯混亂,無法符合預期。
想用查看真實的結果, 我們來看下反編譯後的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)執行了這一段語句,將結果放到了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, 否則執行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 標簽: 執行 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:標簽: 執行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 標簽: 執行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;
查看偽代碼, 符合預期結果。驗證完畢。
在Android 系統中,所有安裝 到 系統的應用程序都必有一個數字證書,此數字證書用於標識應用程序的作者和在應用程序之間建立信任關系,如果一個 permissi
什麼是Context? 一個Context意味著一個場景,一個場景就是我們和軟件進行交互的一個過程。比如當你使用微信的時候,場景包括聊天界面、通訊錄、朋友圈,以及
Microsoft 本周發布了 Visual Studio 2015 預覽版, 裡面包含 Android 開發工具. 安裝的時候, 如果選 Android 開發,
前言 在Android開發中,ImageLoader應該算得上是最重要的開源庫之一,由於項目原因(不能使用開源庫),前段時間自己也是需要實現一個簡單的ImageL