<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
當我們寫了一個方法,那麼這個方法是如何被執行的呢?
public int add(){ int a = 10; int b = 20; return a + b; }
其實方法的本質就是arm指令,在Android當中,dalvik或者art虛擬機器器的執行引擎會執行arm指令
add方法是java程式碼,java程式碼編譯成class檔案,還需要一步轉換為dex檔案,才能被Android虛擬機器器執行,dex檔案包含了app的所有程式碼,因此方法也是存在dex檔案中,那麼通過dx命令,可以檢視方法被編譯成的位元組碼指令
dx --dex --verbose --dump-to=dex_method.txt --dump-method=Method.add --verbose-dump Method.class
Android中可以通過dx命令將class檔案轉換為dex檔案,dx.bat位於Android SDK中的build-tools資料夾下,那麼可以通過dx命令將class檔案翻譯成arm指令集
可以看一下,列印輸出的arm指令集,ART執行某個方法的時候,執行的就是這個指令集,當apk安裝的時候,dex檔案會被dex2oat工具翻譯成本地機器碼(arm指令集)儲存在oat檔案中,當apk執行的時候oat會被載入到記憶體中,存在虛擬機器器的方法區中
執行的時候,會構建一個棧幀壓入虛擬機器器棧中,然後每一個方法在ART中都對應一個ArtMethod(這個後邊會說),ArtMethod中的invoke函數會找到當前方法對應的本地機器碼執行,執行完成之後,棧幀出棧
關注點回到指令集上,在每一行指令前有一個數位,代表程式計數器記錄的行號,精簡之後的指令集(只保留每個行號的最後一個)
Method.add:()I: regs: 0002; ins: 0001; outs: 0000 0000: const/16 v0, #int 30 // #001e 0002: return v0 0003: code-address debug info line_start: 4 parameters_size: 0000 0000: prologue end 0000: line 4 0000: line 6 end sequence source file: "Method.java"
另外還有一種方式獲取位元組碼,是通過javap獲取,這種跟arm指令有啥區別呢?其實都是位元組碼,但是javap獲取的位元組碼是JVM執行的位元組碼,Android虛擬機器器是Dalvik或者Art虛擬機器器,執行的是arm指令集
public int add(); descriptor: ()I flags: ACC_PUBLIC Code: stack=2, locals=3, args_size=1 0: bipush 10 2: istore_1 3: bipush 20 5: istore_2 6: iload_2 7: iload_1 8: iadd 9: ireturn LineNumberTable: line 4: 0 line 5: 3 line 6: 6
這兩者有什麼區別呢?我們看同是執行 10 + 20 ,JVM是先建立一個10變數,然後再建立20 ,最後將兩個相加然後返回;但是從ART機器指令中可以看到是直接計算好了,然後建立v0 = 30,直接返回,所以:Android編譯器在編譯的過程中會做優化,提高執行的效率(這個可以自己去試一下,javac並沒有做優化處理)
當一個class類載入進來之後,class類中有方法、成員變數等,這些類的資訊載入的時候是放在方法區,當Java層呼叫某個方法時,ART虛擬機器器找到該方法對應的本地機器碼指令,在虛擬機器器棧中,該方法棧幀入棧,CPU去讀取每行指令,程式計數器+1,等到方法執行完畢,棧幀出棧。
之前我們介紹過阿里的AndFix或者Sophix是通過hook native層替換已經載入的類的方法,接下來我們著重看一下,AndFix熱修復是怎麼實現的
Method.add:()I: regs: 0002; ins: 0001; outs: 0000 0000: const/16 v0, #int 30 // #001e 0002: return v0 0003: code-address debug info line_start: 4 parameters_size: 0000 0000: prologue end 0000: line 4 0000: line 6 end sequence source file: "Method.java"
public class Method { public int add(){ int a = 10; int b = 20; return a + b; } } //呼叫 Method method = new Method(); method.add();
我們看下這個方法,通過Method物件去呼叫,method是在堆記憶體中,通過物件可以拿到類資訊在方法區中。
當執行這個方法時,ART執行引擎從方法區中找到方法的本地機器指令,通過CPU執行得到結果,如果add方法中丟擲異常導致app崩潰,那麼如何修復?
既然要做到方法替換,首先必須要了解方法在虛擬機器器中的形態;其實前面有提到,方法在虛擬機器器中對應的結構體就是ArtMethod,每個方法在ART中對應一個ArtMethod。
# Android 10.0/art/runtime/art_method.h protected: GcRoot<mirror::Class> declaring_class_; std::atomic<std::uint32_t> access_flags_; uint32_t dex_code_item_offset_; uint32_t dex_method_index_; uint16_t method_index_; union { uint16_t hotness_count_; uint16_t imt_index_; }; // Fake padding field gets inserted here. // Must be the last fields in the method. struct PtrSizedFields { // Depending on the method type, the data is // - native method: pointer to the JNI function registered to this method // or a function to resolve the JNI function, // - conflict method: ImtConflictTable, // - abstract/interface method: the single-implementation if any, // - proxy method: the original interface method or constructor, // - other methods: the profiling data. void* data_; // Method dispatch from quick compiled code invokes this pointer which may cause bridging into // the interpreter. void* entry_point_from_quick_compiled_code_; } ptr_sized_fields_;
在ArtMethod中,有一個結構體PtrSizedFields,其中一個成員變數為entry_point_from_quick_compiled_code_,這個指標指向的就是在方法區中該方法本地機器碼的記憶體地址,也就是說,如果想要實現熱修復,那麼就將entry_point_from_quick_compiled_code_指向正確的方法機器碼指令地址即可。
除此之外,看下其他成員變數的含義:
對於hotness_count_,這裡需要說一下ART的編譯模式,Dalvik的就先不介紹了
在Android 5.0之後,Android編譯器由ART代替了Dalvik,採用了全新的編譯模式AOT,代替JIT;
什麼是AOT?就是全量編譯,在APK安裝的時候,會將所有的dex檔案編譯成本地機器碼,然後在執行方法時會直接拿到相應的機器碼執行,速度非常快,但是這也帶來一些問題:
(1)安裝時間長
因為在安裝的過程中做全量的編譯,耗時非常嚴重;早先的Android手機我們在安裝的時候,進度條一直在轉但就是裝不上,這種是非常差的使用者體驗
(2)儲存空間
因為全量編譯的時候,dex被編譯成機器碼之後,儲存在.oat檔案中,10M的dex翻譯成的機器碼記憶體激增4-5倍,大量的檔案儲存在手機中會佔據記憶體空間
所以在Android N之後,採用了混合編譯模式,AOP + 解釋 + JIT
全新的混編模式不再在APK安裝的時候進行全量編譯,而是會解釋位元組碼,因此安裝的速度很快;此外新增了一個JIT編譯器,會在App執行的時候分析程式碼,把結果儲存在Profile中,並且在空閒時間分析並編譯這些程式碼;
接著上面的hotness_count_,其實用來記錄這個方法被呼叫的次數,當超過某個閾值之後,這個方法會被標記為熱程式碼,這些熱方法在裝置空閒的時候做編譯,並儲存在名為app_image的base.art檔案中,這個art檔案會在類載入之前載入到記憶體中,意味著當呼叫這個方法的時候,不再需要編譯為機器碼,而是直接執行拿到結果。
首先建立一個C++的模組,然後C++版本可選擇個人熟悉的,我對C++ 11的一些特性比較熟悉
其實AndFix實現的關鍵,就是找到ArtMethod,在JNI層是能夠實現的,通過JNIEnv的FromReflectedMethod函數
public class AndFixManager { //native熱修復方法 public static native void fix(Method wrong, Method right); }
//fix對應的JNI介面 extern "C" JNIEXPORT void JNICALL Java_com_tal_andfix_AndFixManager_fix( JNIEnv *env, jclass clazz, jobject wrong, jobject right) { //獲取ArtMethod env->FromReflectedMethod(wrong); }
其實在Java層呼叫的時候,是需要反射獲取某個方法,也就是說,在Java層反射拿到的方法其實就是ArtMethod,只不過再底層的我們看不到,那現在就能看到了!
try { Class<?> clazz = Class.forName("com.tal.demo02.FixDemo"); Method run = clazz.getDeclaredMethod("run"); AndFixManager.fix(run,run); } catch (Exception e) { e.printStackTrace(); }
之前我們看原始碼的時候,可以看到ArtMethod.h中存在很多系統的標頭檔案,全部匯入工程中不現實
因為我們需要的是ArtMethod的一個結構體的成員變數,所以我們只需要針對性地匯入即可,art_method.h如下;
#ifndef DEMO02_ART_METHOD_H #define DEMO02_ART_METHOD_H #endif //DEMO02_ART_METHOD_H #include "stdint.h" namespace art{ namespace mirror{ class ArtMethod final { public: uint32_t declaring_class_; std::atomic<std::uint32_t> access_flags_; uint32_t dex_code_item_offset_; uint32_t dex_method_index_; uint16_t method_index_; union { uint16_t hotness_count_; uint16_t imt_index_; }; struct PtrSizedFields { void* data_; void* entry_point_from_quick_compiled_code_; } ptr_sized_fields_; }; } }
最終在Java層呼叫JNI方法,執行到JNI層,獲取到ArtMethod
extern "C" JNIEXPORT void JNICALL Java_com_tal_andfix_AndFixManager_fix( JNIEnv *env, jclass clazz, jobject wrong, jobject right) { //獲取ArtMethod ArtMethod *artMethod = reinterpret_cast<ArtMethod *>(env->FromReflectedMethod(wrong)); }
這裡通過斷點可以看到,ArtMethod已經拿到了,而且關鍵資訊entry_point_from_quick_compiled_code_,也就是arm指令集的記憶體地址拿到了!
public class FixDemo { public void run(){ throw new IllegalArgumentException(); } }
public class FixDemo { public void run(){ Log.e("TAG","已經被修復了"); } }
現在有一個場景就是,當執行FixDemo的run方法時丟擲異常導致崩潰,這種場景下,使用熱修復技術怎麼修復呢,就是方法替換,arm指令集替換
public class AndFixManager { public static void bugFix(){ try { Class clazz = Class.forName("com.take.andfix.FixDemo"); Method wrong = clazz.getDeclaredMethod("run"); //正確的方法 Class clazz1 = Class.forName("com.take.andfix.fox.FixDemo"); Method right = clazz1.getDeclaredMethod("run"); AndFixManager.fix(wrong, right); }catch (Exception e){ } } public static native void fix(Method wrong, Method right); }
丟擲異常的類是andfix包下的,當線上需要修復時,下發patch包,然後載入fox包下的方法,呼叫native fix方法
extern "C" JNIEXPORT void JNICALL Java_com_tal_andfix_AndFixManager_fix(JNIEnv *env, jclass clazz, jobject wrong, jobject right) { //獲取ArtMethod ArtMethod *wrongMethod = reinterpret_cast<ArtMethod *>(env->FromReflectedMethod(wrong)); ArtMethod *rightMethod = reinterpret_cast<ArtMethod *>(env->FromReflectedMethod(right)); //方法替換 wrongMethod->declaring_class_ = rightMethod->declaring_class_; wrongMethod->access_flags_ = rightMethod->access_flags_; wrongMethod->dex_code_item_offset_ = rightMethod->dex_code_item_offset_; wrongMethod->dex_method_index_ = rightMethod->dex_method_index_; wrongMethod->method_index_ = rightMethod->method_index_; wrongMethod->ptr_sized_fields_.data_ = rightMethod->ptr_sized_fields_.data_; wrongMethod->ptr_sized_fields_.entry_point_from_quick_compiled_code_ = rightMethod->ptr_sized_fields_.entry_point_from_quick_compiled_code_; }
然後再次執行run方法
binding.sampleText.setOnClickListener { AndFixManager.bugFix() val fixDemo = FixDemo() fixDemo.run() } 列印出的結果:E/TAG: 已經被修復了
其實現在阿里的AndFix和Sophix已經不維護了,但是這種熱修復的思想我們是需要了解的,尤其是通過hook native底層替換方法,能夠幫助我們更好地瞭解JVM虛擬機器器和Android虛擬機器器。
在上面簡單的demo中,我們是知道那個類的哪個方法發生異常,在程式碼中寫死的,但真正的線上環境中,其實是不知道哪個類會報錯,一般我們都會使用bugly,像crash跟anr都能夠實時監控到
當app某個方法拋異常之後,通過bugly上報到後臺,比如com.take.andfix.FixDemo這個類中的run方法丟擲了異常,那麼我們需要針對這個類的方法做修復,如果做到動態化,需要使用註解修飾這個修復類
/** * 修復類需要使用這個註解 */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface andfix { String clazz(); String method(); }
public class FixDemo { @andfix(clazz = "com.tal.andfix.FixDemo",method = "run") public void run(){ Log.e("TAG","已經被修復了"); } }
這樣在熱修復時,能夠知道這個修復類要修復線上環境中那個類的哪個方法
在打包dex的時候,需要把整個包名路徑下的class檔案一起打包,通過命令列完成dex打包
dx --dex --output fix.dex /xxxx/Desktop/dx
將打包成功的dex修復包,放到sd卡中 :
dex檔案的載入,通過DexFile實現,如果不熟悉可以看下原始碼,art虛擬機器器會將dex轉換為odex,因此載入dex檔案的時候,需要傳入一個odex檔案的快取路徑。
將dex檔案載入到記憶體之後,可以獲取到dex檔案中全部的類,通過DexFile.loadClass就可以將這個類通過類載入器載入。
/** * dex檔案載入,將dex檔案載入到記憶體 * @param context * @param dexFile */ private static void loadFixDex(Context context, File dexFile) { try { DexFile odex = DexFile.loadDex( dexFile.getAbsolutePath(), new File(context.getCacheDir(), "odex").getAbsolutePath(), Context.MODE_PRIVATE ); Enumeration<String> entries = odex.entries(); while (entries.hasMoreElements()){ //全類名 String clazzName = entries.nextElement(); //載入類 Class aClass = odex.loadClass(clazzName, context.getClassLoader()); //處理類 if(aClass != null){ processClass(aClass); } } } catch (Exception e) { e.printStackTrace(); } }
這裡會有一個問題,就是既然拿到了全類名,為什麼不能通過方式1獲取,而是需要通過方式2獲取?原因就是,Class.forName是從當前apk中查詢這個類,但是這個類是在dex檔案中,是從伺服器端下發的,並沒有放在apk中,因此通過Class.forName是找不到的,通過DexFile.loadClass才是真正載入類到了記憶體中
//方式1 Class.forName("xxxxxxxxxx") //方式2 odex.loadClass(clazzName, context.getClassLoader())
拿類之後,通過反射能夠拿到修復類中的方法,當然不是每個方法都是需要被修復的,我們需要判斷的是,上面是否有我們自定義的註解,如果有,那麼就能夠通過反射,拿到丟擲異常的這個方法,因為註解上有我們傳入的類名和方法名,最終呼叫JNI的介面實現動態替換方法
private static void processClass(Class aClass) { //獲取方法上的註解 Method[] methods = aClass.getMethods(); for (Method method:methods){ andfix annotation = method.getAnnotation(andfix.class); if(annotation != null){ //如果存在這個註解,那麼就執行方法替換 String clazz = annotation.clazz(); String method1 = annotation.method(); //獲取wrong方法 try { Class<?> wrongMethodClass = Class.forName(clazz); //這裡注意,修復類的方法,要和被修復的方法,引數一致!!!!! Method wrongMethod = wrongMethodClass.getDeclaredMethod(method1,method.getParameterTypes()); //動態方法替換 fix(wrongMethod,method); } catch (Exception e) { e.printStackTrace(); } } } }
一切準備就緒之後,可以通過載入dex修補程式包來修復
binding.sampleText.setOnClickListener { // AndFixManager.bugFix() AndFixManager.loadFixDex( this, File(System.getenv("EXTERNAL_STORAGE"), "fix.dex") ) val fixDemo = FixDemo() fixDemo.run() }
這裡可能會碰到一些載入SD卡中檔案報錯的問題,比如:
No original dex files found for dex location /sdcard/fix.dex
這裡需要新增檔案的讀寫許可權,才能夠保證有效的熱修復,除此之外,在Android 10以上的版本,需要在清單檔案中新增android:requestLegacyExternalStorage屬性
android:requestLegacyExternalStorage="true"
通過這種hook native底層的方式,最大的優勢在於能夠真正實現熱修復,不需要重新啟動app就能夠修復,但是存在的弊端也是比較明顯的,就是相容性問題,每個Android的版本,native層都會有變化,比如art_method.h,其實每個版本都是不一樣的,我這次使用的就是Android 10中的art_method標頭檔案,有興趣的可以看看之前Android版本的標頭檔案,其實還是有差別的,所以在做相容性問題的時候,需要根據版本來適配不同的標頭檔案
到此這篇關於Android AndFix熱修復原理詳情的文章就介紹到這了,更多相關Android AndFix 內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45