Android線上Bug熱修復分析



針對app線上修復技術,目前有好幾種解決方案,開源界往往一個方案會有好幾種實現。重複的實現會有造輪子之嫌,但分析解決方案在技術上的探索和衍變,這輪子還是值得去推動的


關於Hot Fix技術


Hot Fix技術,簡單來說就是針對線上已發佈app出現了bug,在不推送新版本的情況下通過發佈修復補丁進行修復。通常是剛上線的app,需要快速線上修復bug,類似的技術就叫做熱修復或熱補丁。




熱修復技術能帶來什麼


  • 讓app具有了上線後被修復的可能性,增加事故風險可控性; 

  • 避免爲修復bug而快速增發新版本,讓用戶“無感”,提升體驗; 

  • 推送新版本app修復時,用戶升級覆蓋面無法保證; 

  • 避免增發修復版本的複雜流程,減少發佈新版本app成本; 


現有的技術方案


目前,從技術解決方案上來說,有以下幾種思路:


  • Dexposed

來自阿里手淘團隊,白衣(花名)基於Xposed實現了Dexposed,在此基礎上手淘團隊推出了HotPatch二方庫。


  • AndFix

出自阿里支付寶技術團隊,同樣是對方法的hook,但未基於Dexposed去實現,避免了在art上運行時存在兼容性問題。


  • 基於ClassLoader

QQ空間終端開發團隊提供了技術思路,目前基於此實現的熱門的開源項目有Nuwa,HotFix,DroidFix,這三種方案的原理卻徊然不同,各有優缺點。


技術預研


熱修復 == 動態替換 == 動態加載

得出上面的等式,是因爲熱修復一般來說就是增發patch文件,避免用戶調用錯誤代碼,並不是直接修改了原來的代碼。這相當於是對問題文件做了動態替換,而要實現動態替換就是避免默認的加載,改變成動態地加載替換文件。


動態加載的基礎是ClassLoader

Java程序在運行時加載對應的類是通過ClassLoader來實現的, Java 類可以被動態加載到 Java 虛擬機中並執行。所以ClassLoader所做的工作實質就是把類文件從硬盤讀取到內存中。




AndFix示例圖


Java中ClassLoader的基本概念:



ClassLoader


類加載器的樹狀結構:在JVM中,所有類加載器實例按樹狀結構組織,根結點爲引導類加載器。除根結點外的所有類加載器都有一個非空的父類加載器,從而構成樹狀結構;

 

雙親委託(代理)模型:當類加載器收到加載類或資源的請求時,通常都是先委託給父類加載器加載,也就是說只有當父類加載器找不到指定類或資源時,自身才會執行實際的類加載過程;


代理模式是爲了保證 Java 核心庫的類型安全。通過代理模式,對於 Java 核心庫的類的加載工作由bootClassLoader來統一完成,保證了 Java 應用所使用的都是同一個版本的 Java 核心庫的類,是互相兼容的。


類的判等:即使類完全相同(名稱相同、字節碼相同),不同類加載器實例加載的類對象也是不相等的;


這條規則是Java類加載機制中非常核心的規則,它保證了類加載機制實現“類隔離”、“保護JDK中的基礎類”等目標。


類的垃圾回收:只有當類加載器可被作爲垃圾回收的前提下,其加載的類纔有可能被回收;


Android的classLoader機制


Android的Dalvik/ART虛擬機如同標準JAVA的JVM虛擬機一樣,在運行程序時首先需要將對應的類加載到內存中。因此可以利用這一點,在程序運行時手動加載Class,從而達到代碼中動態加載可執行文件的目的。


Android的ClassLoader體系


在Android系統啓動的時候會創建一個Boot類型的ClassLoader實例,用於加載一些系統Framework層級需要的類。由於Android應用裏也需要用到一些系統的類,所以APP啓動的時候也會把這個Boot類型的ClassLoader傳進來。


此外,APP也有自己的類,這些類保存在APK的dex文件裏面,所以APP啓動的時候,也會創建一個自己的ClassLoader實例,用於加載自己dex文件中的類。


下面實際驗證看看:


@Override protected void onCreate(Bundle savedInstanceState) {      super.onCreate(savedInstanceState);      setContentView(R.layout.activity_main);
      ClassLoader classLoader = getClassLoader();      Log.i("ClassLoader" , "classLoader " + classLoader.toString());
      while (classLoader.getParent() != null) {          classLoader = classLoader.getParent();          if (classLoader != null) {              Log.i("ClassLoader", "classLoaderParent " + classLoader.toString());          }     }}


輸出結果爲:


I/ClassLoader: classLoader dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.sunteng.classloader-1/base.apk"],nativeLibraryDirectories=[/vendor/lib, /system/lib]]]I/ClassLoader: classLoaderParent java.lang.BootClassLoader@2d0a3af7

可以看見有2個Classloader實例,一個是BootClassLoader(系統啓動的時候創建的),另一個是PathClassLoader(應用啓動時創建的,用於加載當前已安裝app裏面的類)。


PathClassLoader和DexClassLoader


Android經常使用的是PathClassLoader和DexClassLoader


  • PathClassLoader


官方註釋:一個簡單的ClassLoader的實現,工作在本地文件系統中的文件和目錄的列表上,但不嘗試從網絡加載類。 Android使用這個類爲它的系統類加載器和應用類加載器。


可以看出,Android是使用這個類作爲其系統類和應用類的加載器。並且對於這個類呢,只能去加載已經安裝到Android系統中的apk文件。


  • DexClassLoader


官方註釋:一個ClassLoader的實現,從.jar和.apk文件內部加載classes.dex。這可以用於執行非安裝程序作爲已安裝應用程序的一部分的代碼。


也就是說可以加載比如sd目錄下的dex文件,獲取到不是已安裝app裏面的類。


Android中使用PathClassLoader類作爲Android的默認的類加載器,PathClassLoade本身繼承自BaseDexClassLoader,BaseDexClassLoader重寫了findClass方法,該方法是ClassLoader的核心。


#BaseDexClassLoader@Overrideprotected Class<?> findClass(String name) throws ClassNotFoundException {    List<Throwable> suppressedExceptions = new ArrayList<Throwable>();    Class c = pathList.findClass(name, suppressedExceptions);    if (c == null) {        ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on path: " + pathList);        for (Throwable t : suppressedExceptions) {            cnfe.addSuppressed(t);        }        throw cnfe;    }    return c;}


看源碼可知,BaseDexClassLoader將findClass方法委託給了pathList對象的findClass方法,pathList對象是在BaseDexClassLoader的構造函數中new出來的,它的類型是DexPathList。看下DexPathList.findClass源碼是如何做的:


#DexPathListpublic Class findClass(String name, List<Throwable> suppressed) {    for (Element element : dexElements) {        DexFile dex = element.dexFile;
        if (dex != null) {            Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);            if (clazz != null) {                return clazz;            }        }    }    if (dexElementsSuppressedExceptions != null) {        suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));    }    return null;}
#DexFile public Class loadClassBinaryName(String name, ClassLoader loader) {    return defineClass(name, loader, mCookie);}private native static Class defineClass(String name, ClassLoader loader, int cookie);


直接就是遍歷dexElements列表,然後通過調用element.dexFile對象上的loadClassBinaryName方法來加載類,如果返回值不是null,就表示加載類成功,會將這個Class對象返回。而且dexElements對象是在DexPathList類的構造函數中完成初始化的。


this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory, suppressedExceptions);


makeDexElements所做的事情就是遍歷我們傳遞來的dexPath,然後一次加載每個dex文件。可以看出,BaseDexClassLoader中有個pathList對象,pathList中包含一個DexFile的集合dexElements,而對於類加載,就是遍歷這個集合,通過DexFile去尋找。


這樣的話,我們可以在這個dexElements中去做一些事情,比如在這個數組的第一個元素放置我們的patch.jar,裏面包含修復過的類。當遍歷findClass的時候,修復的類就會被查找到,從而替代有bug的類。


一個ClassLoader可以包含多個dex文件,每個dex文件是一個Element,多個dex文件排列成一個有序的數組dexElements,當找類的時候,會按順序遍歷dex文件,然後從當前遍歷的dex文件中找類,如果找類則返回,如果找不到從下一個dex文件繼續查找


標準JVM中,ClassLoader是用defineClass加載類的,而Android中defineClass被棄用了,改用了loadClass方法,而且加載類的過程也挪到了DexFile中,在DexFile中加載類的具體方法也叫defineClass


ClassLoader特性


使用ClassLoader的一個特點就是,當ClassLoader在成功加載某個類之後,會把得到類的實例緩存起來。下次再請求加載該類的時候,ClassLoader會直接使用緩存的類的實例,而不會嘗試再次加載。也就是說,如果程序不重新啓動,加載過一次的類就無法重新加載。


如果使用ClassLoader來動態升級APP或者動態修復BUG,都需要重新啓動APP才能生效。


除了使用ClassLoader外,還可以使用jni hook的方式修改程序的執行代碼。後者做的已經是Native層級的工作了,直接修改應用運行時的內存地址,所以使用jni hook的方式時,不用重新應用就能生效。


而阿里的dexposed和AndFix採用了jni hook方案


Android使用動態加載

Android程序比起一般Java程序在使用動態加載時麻煩在哪裏


使用ClassLoader動態加載一個外部的類是非常容易的事情,所以很容易就能實現動態加載新的可執行代碼的功能,但是比起一般的Java程序,在Android程序中使用動態加載主要有兩個麻煩的問題:


  • Android中許多組件類(如Activity、Service等)是需要在Manifest文件裏面註冊後才能工作的(系統會檢查該組件有沒有註冊),所以即使動態加載了一個新的組件類進來,沒有註冊的話還是無法工作; 

  • Res資源是Android開發中經常用到的,而Android是把這些資源用對應的R.id註冊好,運行時通過這些ID從Resource實例中獲取對應的資源。如果是運行時動態加載進來的新類,那類裏面用到R.id的地方將會拋出找不到資源或者用錯資源的異常,因爲新類的資源ID根本和現有的Resource實例中保存的資源ID對不上; 


說到底,一個Android程序和標準的Java程序最大的區別就在於他們的上下文環境(Context)不同。


Android中context可以給程序提供組件需要用到的功能,也可以提供一些主題、Res等資源,而現在的各種Android動態加載框架中,核心要解決的東西也正是如何給外部的新類提供上下文環境的問題。


希望最終的效果


能夠簡單地集成熱修復sdk,開發者修改代碼後能輕鬆地完成向用戶發Patch操作,在用戶無感知的情況下修復bug。


技術選型


  • 對開發者友好,使用熱修復要簡單直接,能儘快解決問題; 

  • 對用戶友好,儘量減少用戶感知; 

  • 減小bug的影響,儘量擴大修復時覆蓋的用戶範圍。 

  • 就一個理念:只有適合當前情況的纔是最好的。


插件化和熱修復


前面關於Android中ClassLoader的介紹,Android使用PathClassLoader作爲其類加載器,DexClassLoader可以從.jar和.apk類型的文件內部加載classes.dex文件。


如果大家對於插件化有所瞭解,其實Android應用的插件化,就可以利用DexClassLoader來動態加載非安裝應用的類來實現,當然也就可以做到只有單用戶點擊相應插件模塊,纔會從網絡獲取相應插件文件,再通過DexClassLoader實現類加載。


而熱修復可以利用BaseDexClassLoader中的pathList對象,pathList中包含一個DexFile的集合dexElements,我們可以在這個dexElements中去做一些事情,比如在這個數組的第一個元素放置我們的patch.jar,裏面包含修復過的類。


這樣的話,當遍歷findClass的時候,我們修復的類就會被查找到,從而替代有bug的類。不過這樣處理還存在一個CLASS_ISPREVERIFIED的問題


熱修復具體實施


上面分析了Android中的類的加載的流程,可以看出:


  • DexPathList對象中的dexElements列表是類加載的一個核心,一個類如果能被成功加載,那麼它的dex一定會出現在dexElements所對應的dex文件中。 

  • exElements中出現的順序也很重要,在dexElements前面出現的dex會被優先加載,一旦Class被加載成功,就會立即返回。 

  • 我們的如果想做hot fix,一定要保證我們的pacth dex文件出現在dexElements列表的前面。


要實現熱修復,就需要我們在運行時去更改PathClassLoader.pathList.dexElements,由於這些屬性都是private的,因此需要通過反射來修改。


另外,構造我們自己的dex文件所對應的dexElements數組的時候,我們也可以採取一個比較取巧的方式:


  • 通過構造一個DexClassLoader對象來加載我們的dex文件 

  • 調用一次dexClassLoader.loadClass(dummyClassName)方法 

  • 這樣dexClassLoader.pathList.dexElements中就會包含我們的dex



通過把dexClassLoader.pathList.dexElements插入到系統默認的classLoader.pathList.dexElements列表前面,就可以讓系統優先加載我們的dex中的類,從而可以實現熱修復了。


思考


通過分析三者的差異化對比,以及思考到底什麼纔是合適的,通過hook方法的方式實現起來確實最直接,但是問題卻也很明顯,首先成功覆蓋率和穩定性是個問題,而且操作起來複雜性比較高。


而通過classloader考慮的是從系統動態加載的特性入手,所以理所當然以侷限於系統的特性,比如由於對於已經加載的類,類加載器不會再調用loadClass方法,所以想要修復要等到下次啓動程序纔行。


動態加載技術


Android項目中,動態加載技術按照加載的可執行文件的不同大致可以分爲兩種:


1.動態加載so庫;

2.動態加載dex/jar/apk文件(通常都是這種)


所以理解起來就是:


1.動態調用外部的Dex文件則是完全沒有問題的。

2.在APK文件中往往有一個或者多個Dex文件,我們寫的每一句代碼都會被編譯到這些文件裏面。

3.Android應用運行的時候就是通過執行這些Dex文件完成應用的功能的。

4.雖然一個APK一旦構建出來,我們是無法更換裏面的Dex文件,但是我們可以通過加載外部的Dex文件來實現。


外部文件可以放在外部存儲,或者從網絡下載。


因此最極端的情況就是,直接把APK自身帶有的Dex文件當做空殼,只是作爲一個程序的入口,所有的功能都通過從服務器下載最新的Dex文件完成。


當然,一般來說只要利用Android動態加載技術,通過動態加載新的dex的方式,完成對有bug類的“替換”,來達到避免調用存在bug的代碼,這也就是所謂的Hot Fix。


總體的思路就是這樣,至於具體的實現,就有很多環節需要細化的,因爲Android本身也有很多自身的特性。




本文是由 石先 源碼地址


https://github.com/baishixian


有興趣的同學可查看Github 源碼歡迎 star & fork,如果您覺得不錯,可以分享給小夥伴哦,支持小編也可以在下方+1,投稿及有疑問或者問題的小夥伴可以在下方留言,小編會第一時間與您聯繫!

針對app線上修復技術,目前有好幾種解決方案,開源界往往一個方案會有好幾種實現。重複的實現會有造輪子之嫌,但分析解決方案在技術上的探索和衍變,這輪子還是值得去推動的


關於Hot Fix技術


Hot Fix技術,簡單來說就是針對線上已發佈app出現了bug,在不推送新版本的情況下通過發佈修復補丁進行修復。通常是剛上線的app,需要快速線上修復bug,類似的技術就叫做熱修復或熱補丁。




熱修復技術能帶來什麼


  • 讓app具有了上線後被修復的可能性,增加事故風險可控性; 

  • 避免爲修復bug而快速增發新版本,讓用戶“無感”,提升體驗; 

  • 推送新版本app修復時,用戶升級覆蓋面無法保證; 

  • 避免增發修復版本的複雜流程,減少發佈新版本app成本; 


現有的技術方案


目前,從技術解決方案上來說,有以下幾種思路:


  • Dexposed

來自阿里手淘團隊,白衣(花名)基於Xposed實現了Dexposed,在此基礎上手淘團隊推出了HotPatch二方庫。


  • AndFix

出自阿里支付寶技術團隊,同樣是對方法的hook,但未基於Dexposed去實現,避免了在art上運行時存在兼容性問題。


  • 基於ClassLoader

QQ空間終端開發團隊提供了技術思路,目前基於此實現的熱門的開源項目有Nuwa,HotFix,DroidFix,這三種方案的原理卻徊然不同,各有優缺點。


技術預研


熱修復 == 動態替換 == 動態加載

得出上面的等式,是因爲熱修復一般來說就是增發patch文件,避免用戶調用錯誤代碼,並不是直接修改了原來的代碼。這相當於是對問題文件做了動態替換,而要實現動態替換就是避免默認的加載,改變成動態地加載替換文件。


動態加載的基礎是ClassLoader

Java程序在運行時加載對應的類是通過ClassLoader來實現的, Java 類可以被動態加載到 Java 虛擬機中並執行。所以ClassLoader所做的工作實質就是把類文件從硬盤讀取到內存中。




AndFix示例圖


Java中ClassLoader的基本概念:



ClassLoader


類加載器的樹狀結構:在JVM中,所有類加載器實例按樹狀結構組織,根結點爲引導類加載器。除根結點外的所有類加載器都有一個非空的父類加載器,從而構成樹狀結構;

 

雙親委託(代理)模型:當類加載器收到加載類或資源的請求時,通常都是先委託給父類加載器加載,也就是說只有當父類加載器找不到指定類或資源時,自身才會執行實際的類加載過程;


代理模式是爲了保證 Java 核心庫的類型安全。通過代理模式,對於 Java 核心庫的類的加載工作由bootClassLoader來統一完成,保證了 Java 應用所使用的都是同一個版本的 Java 核心庫的類,是互相兼容的。


類的判等:即使類完全相同(名稱相同、字節碼相同),不同類加載器實例加載的類對象也是不相等的;


這條規則是Java類加載機制中非常核心的規則,它保證了類加載機制實現“類隔離”、“保護JDK中的基礎類”等目標。


類的垃圾回收:只有當類加載器可被作爲垃圾回收的前提下,其加載的類纔有可能被回收;


Android的classLoader機制


Android的Dalvik/ART虛擬機如同標準JAVA的JVM虛擬機一樣,在運行程序時首先需要將對應的類加載到內存中。因此可以利用這一點,在程序運行時手動加載Class,從而達到代碼中動態加載可執行文件的目的。


Android的ClassLoader體系


在Android系統啓動的時候會創建一個Boot類型的ClassLoader實例,用於加載一些系統Framework層級需要的類。由於Android應用裏也需要用到一些系統的類,所以APP啓動的時候也會把這個Boot類型的ClassLoader傳進來。


此外,APP也有自己的類,這些類保存在APK的dex文件裏面,所以APP啓動的時候,也會創建一個自己的ClassLoader實例,用於加載自己dex文件中的類。


下面實際驗證看看:


@Override protected void onCreate(Bundle savedInstanceState) {      super.onCreate(savedInstanceState);      setContentView(R.layout.activity_main);
      ClassLoader classLoader = getClassLoader();      Log.i("ClassLoader" , "classLoader " + classLoader.toString());
      while (classLoader.getParent() != null) {          classLoader = classLoader.getParent();          if (classLoader != null) {              Log.i("ClassLoader", "classLoaderParent " + classLoader.toString());          }     }}


輸出結果爲:


I/ClassLoader: classLoader dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.sunteng.classloader-1/base.apk"],nativeLibraryDirectories=[/vendor/lib, /system/lib]]]I/ClassLoader: classLoaderParent java.lang.BootClassLoader@2d0a3af7

可以看見有2個Classloader實例,一個是BootClassLoader(系統啓動的時候創建的),另一個是PathClassLoader(應用啓動時創建的,用於加載當前已安裝app裏面的類)。


PathClassLoader和DexClassLoader


Android經常使用的是PathClassLoader和DexClassLoader


  • PathClassLoader


官方註釋:一個簡單的ClassLoader的實現,工作在本地文件系統中的文件和目錄的列表上,但不嘗試從網絡加載類。 Android使用這個類爲它的系統類加載器和應用類加載器。


可以看出,Android是使用這個類作爲其系統類和應用類的加載器。並且對於這個類呢,只能去加載已經安裝到Android系統中的apk文件。


  • DexClassLoader


官方註釋:一個ClassLoader的實現,從.jar和.apk文件內部加載classes.dex。這可以用於執行非安裝程序作爲已安裝應用程序的一部分的代碼。


也就是說可以加載比如sd目錄下的dex文件,獲取到不是已安裝app裏面的類。


Android中使用PathClassLoader類作爲Android的默認的類加載器,PathClassLoade本身繼承自BaseDexClassLoader,BaseDexClassLoader重寫了findClass方法,該方法是ClassLoader的核心。


#BaseDexClassLoader@Overrideprotected Class<?> findClass(String name) throws ClassNotFoundException {    List<Throwable> suppressedExceptions = new ArrayList<Throwable>();    Class c = pathList.findClass(name, suppressedExceptions);    if (c == null) {        ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on path: " + pathList);        for (Throwable t : suppressedExceptions) {            cnfe.addSuppressed(t);        }        throw cnfe;    }    return c;}


看源碼可知,BaseDexClassLoader將findClass方法委託給了pathList對象的findClass方法,pathList對象是在BaseDexClassLoader的構造函數中new出來的,它的類型是DexPathList。看下DexPathList.findClass源碼是如何做的:


#DexPathListpublic Class findClass(String name, List<Throwable> suppressed) {    for (Element element : dexElements) {        DexFile dex = element.dexFile;
        if (dex != null) {            Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);            if (clazz != null) {                return clazz;            }        }    }    if (dexElementsSuppressedExceptions != null) {        suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));    }    return null;}
#DexFile public Class loadClassBinaryName(String name, ClassLoader loader) {    return defineClass(name, loader, mCookie);}private native static Class defineClass(String name, ClassLoader loader, int cookie);


直接就是遍歷dexElements列表,然後通過調用element.dexFile對象上的loadClassBinaryName方法來加載類,如果返回值不是null,就表示加載類成功,會將這個Class對象返回。而且dexElements對象是在DexPathList類的構造函數中完成初始化的。


this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory, suppressedExceptions);


makeDexElements所做的事情就是遍歷我們傳遞來的dexPath,然後一次加載每個dex文件。可以看出,BaseDexClassLoader中有個pathList對象,pathList中包含一個DexFile的集合dexElements,而對於類加載,就是遍歷這個集合,通過DexFile去尋找。


這樣的話,我們可以在這個dexElements中去做一些事情,比如在這個數組的第一個元素放置我們的patch.jar,裏面包含修復過的類。當遍歷findClass的時候,修復的類就會被查找到,從而替代有bug的類。


一個ClassLoader可以包含多個dex文件,每個dex文件是一個Element,多個dex文件排列成一個有序的數組dexElements,當找類的時候,會按順序遍歷dex文件,然後從當前遍歷的dex文件中找類,如果找類則返回,如果找不到從下一個dex文件繼續查找


標準JVM中,ClassLoader是用defineClass加載類的,而Android中defineClass被棄用了,改用了loadClass方法,而且加載類的過程也挪到了DexFile中,在DexFile中加載類的具體方法也叫defineClass


ClassLoader特性


使用ClassLoader的一個特點就是,當ClassLoader在成功加載某個類之後,會把得到類的實例緩存起來。下次再請求加載該類的時候,ClassLoader會直接使用緩存的類的實例,而不會嘗試再次加載。也就是說,如果程序不重新啓動,加載過一次的類就無法重新加載。


如果使用ClassLoader來動態升級APP或者動態修復BUG,都需要重新啓動APP才能生效。


除了使用ClassLoader外,還可以使用jni hook的方式修改程序的執行代碼。後者做的已經是Native層級的工作了,直接修改應用運行時的內存地址,所以使用jni hook的方式時,不用重新應用就能生效。


而阿里的dexposed和AndFix採用了jni hook方案


Android使用動態加載

Android程序比起一般Java程序在使用動態加載時麻煩在哪裏


使用ClassLoader動態加載一個外部的類是非常容易的事情,所以很容易就能實現動態加載新的可執行代碼的功能,但是比起一般的Java程序,在Android程序中使用動態加載主要有兩個麻煩的問題:


  • Android中許多組件類(如Activity、Service等)是需要在Manifest文件裏面註冊後才能工作的(系統會檢查該組件有沒有註冊),所以即使動態加載了一個新的組件類進來,沒有註冊的話還是無法工作; 

  • Res資源是Android開發中經常用到的,而Android是把這些資源用對應的R.id註冊好,運行時通過這些ID從Resource實例中獲取對應的資源。如果是運行時動態加載進來的新類,那類裏面用到R.id的地方將會拋出找不到資源或者用錯資源的異常,因爲新類的資源ID根本和現有的Resource實例中保存的資源ID對不上; 


說到底,一個Android程序和標準的Java程序最大的區別就在於他們的上下文環境(Context)不同。


Android中context可以給程序提供組件需要用到的功能,也可以提供一些主題、Res等資源,而現在的各種Android動態加載框架中,核心要解決的東西也正是如何給外部的新類提供上下文環境的問題。


希望最終的效果


能夠簡單地集成熱修復sdk,開發者修改代碼後能輕鬆地完成向用戶發Patch操作,在用戶無感知的情況下修復bug。


技術選型


  • 對開發者友好,使用熱修復要簡單直接,能儘快解決問題; 

  • 對用戶友好,儘量減少用戶感知; 

  • 減小bug的影響,儘量擴大修復時覆蓋的用戶範圍。 

  • 就一個理念:只有適合當前情況的纔是最好的。


插件化和熱修復


前面關於Android中ClassLoader的介紹,Android使用PathClassLoader作爲其類加載器,DexClassLoader可以從.jar和.apk類型的文件內部加載classes.dex文件。


如果大家對於插件化有所瞭解,其實Android應用的插件化,就可以利用DexClassLoader來動態加載非安裝應用的類來實現,當然也就可以做到只有單用戶點擊相應插件模塊,纔會從網絡獲取相應插件文件,再通過DexClassLoader實現類加載。


而熱修復可以利用BaseDexClassLoader中的pathList對象,pathList中包含一個DexFile的集合dexElements,我們可以在這個dexElements中去做一些事情,比如在這個數組的第一個元素放置我們的patch.jar,裏面包含修復過的類。


這樣的話,當遍歷findClass的時候,我們修復的類就會被查找到,從而替代有bug的類。不過這樣處理還存在一個CLASS_ISPREVERIFIED的問題


熱修復具體實施


上面分析了Android中的類的加載的流程,可以看出:


  • DexPathList對象中的dexElements列表是類加載的一個核心,一個類如果能被成功加載,那麼它的dex一定會出現在dexElements所對應的dex文件中。 

  • exElements中出現的順序也很重要,在dexElements前面出現的dex會被優先加載,一旦Class被加載成功,就會立即返回。 

  • 我們的如果想做hot fix,一定要保證我們的pacth dex文件出現在dexElements列表的前面。


要實現熱修復,就需要我們在運行時去更改PathClassLoader.pathList.dexElements,由於這些屬性都是private的,因此需要通過反射來修改。


另外,構造我們自己的dex文件所對應的dexElements數組的時候,我們也可以採取一個比較取巧的方式:


  • 通過構造一個DexClassLoader對象來加載我們的dex文件 

  • 調用一次dexClassLoader.loadClass(dummyClassName)方法 

  • 這樣dexClassLoader.pathList.dexElements中就會包含我們的dex



通過把dexClassLoader.pathList.dexElements插入到系統默認的classLoader.pathList.dexElements列表前面,就可以讓系統優先加載我們的dex中的類,從而可以實現熱修復了。


思考


通過分析三者的差異化對比,以及思考到底什麼纔是合適的,通過hook方法的方式實現起來確實最直接,但是問題卻也很明顯,首先成功覆蓋率和穩定性是個問題,而且操作起來複雜性比較高。


而通過classloader考慮的是從系統動態加載的特性入手,所以理所當然以侷限於系統的特性,比如由於對於已經加載的類,類加載器不會再調用loadClass方法,所以想要修復要等到下次啓動程序纔行。


動態加載技術


Android項目中,動態加載技術按照加載的可執行文件的不同大致可以分爲兩種:


1.動態加載so庫;

2.動態加載dex/jar/apk文件(通常都是這種)


所以理解起來就是:


1.動態調用外部的Dex文件則是完全沒有問題的。

2.在APK文件中往往有一個或者多個Dex文件,我們寫的每一句代碼都會被編譯到這些文件裏面。

3.Android應用運行的時候就是通過執行這些Dex文件完成應用的功能的。

4.雖然一個APK一旦構建出來,我們是無法更換裏面的Dex文件,但是我們可以通過加載外部的Dex文件來實現。


外部文件可以放在外部存儲,或者從網絡下載。


因此最極端的情況就是,直接把APK自身帶有的Dex文件當做空殼,只是作爲一個程序的入口,所有的功能都通過從服務器下載最新的Dex文件完成。


當然,一般來說只要利用Android動態加載技術,通過動態加載新的dex的方式,完成對有bug類的“替換”,來達到避免調用存在bug的代碼,這也就是所謂的Hot Fix。


總體的思路就是這樣,至於具體的實現,就有很多環節需要細化的,因爲Android本身也有很多自身的特性。




本文是由 石先 源碼地址


https://github.com/baishixian


有興趣的同學可查看Github 源碼歡迎 star & fork,如果您覺得不錯,可以分享給小夥伴哦,支持小編也可以在下方+1,投稿及有疑問或者問題的小夥伴可以在下方留言,小編會第一時間與您聯繫!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章