Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android熱修復分析

Android熱修復分析

編輯:關於Android編程

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

關於Hot Fix技術

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

現在有兩種熱修復解決方案:

1、JNI Hook方式解決方案 : c++動態修改方法指針,安裝補丁後立刻生效;

2、classloader類加載極致:程序啟動過程加載,安裝補丁之後需要重新啟動才能生效;

熱修復技術能帶來什麼

  • 讓app具有了上線後被修復的可能性,增加事故風險可控性;
  • 避免為修復bug而快速增發新版本,讓用戶“無感”,提升體驗;
  • 推送新版本app修復時,用戶升級覆蓋面無法保證;
  • 避免增發修復版本的復雜流程,減少發布新版本app成本;

現有的技術方案

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

*Dexposed:JNI Hook

來自阿裡手淘團隊,白衣(花名)基於Xposed實現了Dexposed,在此基礎上手淘團隊推出了HotPatch二方庫。
*AndFix:JNI Hook
出自阿裡支付寶技術團隊,同樣是對方法的hook,但未基於Dexposed去實現,避免了在art上運行時存在兼容性問題。
*基於ClassLoader
QQ空間終端開發團隊提供了技術思路,目前基於此實現的熱門的開源項目有Nuwa,HotFix,DroidFix,這三種方案的原理卻徊然不同,各有優缺點。

技術調研

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

得出上面的等式,是因為熱修復一般來說就是增發patch文件,避免用戶調用錯誤代碼,並不是直接修改了原來的代碼。這相當於是對問題文件做了動態替換,而要實現動態替換就是避免默認的加載,改變成動態地加載替換文件。
動態加載的基礎是ClassLoader,Java程序在運行時加載對應的類是通過ClassLoader來實現的, Java 類可以被動態加載到 Java 虛擬機中並執行。所以ClassLoader所做的工作實質就是把類文件從硬盤讀取到內存中。
 

Java中ClassLoader的基本概念

*類加載器的樹狀結構:在JVM中,所有類加載器實例按樹狀結構組織,根結點為引導類加載器。除根結點外的所有類加載器都有一個非空的父類加載器,從而構成樹狀結構;
*雙親委托(代理)模型:當類加載器收到加載類或資源的請求時,通常都是先委托給父類加載器加載,也就是說只有當父類加載器找不到指定類或資源時,自身才會執行實際的類加載過程;

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

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

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

  • 類的垃圾回收:只有當類加載器可被作為垃圾回收的前提下,其加載的類才有可能被回收

Android的classLoader機制

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

在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裡面的類)。

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
@Override
protected Class findClass(String name) throws ClassNotFoundException {
    List suppressedExceptions = new ArrayList();
    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源碼是如何做的:
#DexPathList
public Class findClass(String name, List 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程序比起一般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本身也有很多自身的特性。

接下來就是考慮實際編碼實現了。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved