1. 程式人生 > >Android線上bug熱修復分析

Android線上bug熱修復分析

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

關於Hot Fix技術

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

fix it

熱修復技術能帶來什麼

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

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

  • 類的判等:即使類完全相同(名稱相同、位元組碼相同),不同類載入器例項載入的類物件也是不相等的;

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

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

Android的classLoader機制

Android的Dalvik/ART虛擬機器如同標準JAVA的JVM虛擬機器一樣,在執行程式時首先需要將對應的類載入到記憶體中。因此可以利用這一點,在程式執行時手動載入Class,從而達到程式碼中動態載入可執行檔案的目的。

Android的ClassLoader體系.png

在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 [email protected]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<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原始碼是如何做的:
#DexPathList
public 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程式比起一般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的問題安卓App熱補丁動態修復技術介紹

熱修復具體實施

上面分析了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本身也有很多自身的特性。

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

相關推薦

Android線上bug修復分析

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

Android之移動修復

com don load query 監聽 基線 mis tps 事件 阿裏雲最近推出了移動熱修復服務,聽說這個服務傻瓜式接入,性能相對較好,對新技術比較好奇的我決定嘗試一下。 移動熱修復.png 首先,需要開通這個服務,創建應用 創建應用.png 然後,在

介紹自己的一個Android插樁修復框架項目QuickPatch

pid android版本 通過 fly 特性 put javassist 執行 自動生成 QuickPatch項目地址:https://gitee.com/egg90/QuickPatch 和 https://github.com/eggfly/QuickPatch 同步

Android 代碼修復詳解

value file final ext.get vat 根據 tde arch x文件 java:類加載原理:當類加載器收到加載類或資源的請求時,通常都是先委托給父類加載器加載,也就是說只有當父類加載器找不到指定類或資源時,自身才會執行實際的類加載過程,具體的加載過程如下

Android Dex 分包+修復(QQ空間技術方案)

import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException;

Android打補丁 修復(HotFix)小結

需求場景:    當我們的app釋出以後,發現有bug,比如維護資料錯誤,應用邏輯錯誤,嚴重的可能引發應用崩潰。這時修改應用可能只需要修改幾行程式碼,或者某個方法就可以搞定。以前為了解決這樣的

關於Android APP線上修復bug方案的調研(一)(AndFix)

調研背景:      App釋出出去後,如果發現有緊急或重要bug如何進行修復呢?      重新發布一版APK?但這樣代價太大....       那麼有沒有一種方案能夠不用更新整個APK,而只把伺服器上的很小的補丁檔案下載下來進行修復bug呢?       本文的調研也

android原生修復流程和原理分析實現

首先apk就是一個壓縮檔案,解壓apk檔案的內容如下圖: 安卓原生熱修復主要原理圖和流程圖如下,我花了好長時間才繪好,中間改了好幾次,應該來說是很直觀明白的,其中有截取了BaseDexClassLoader的關鍵原始碼,還有DexPathList的原始碼 a.現將打

Android修復 Dex注入實現靜默消滅bug

      當app上線後發現緊急bug,如果重新發布版本週期比較長,並且對使用者體驗不好,此時熱修復就派上用場了。熱修復就是為緊急bug而生,能夠快速修復bug,並且使用者無感知。針對熱修復,阿里系先後推出AndFix、HotFix、SophFix,騰訊系也推出QQ空間超級補丁

Android修復技術原理分析

2015年以來,Android開發領域裡對熱修復技術的討論和分享越來越多,同時也出現了一些不同的解決方案,如QQ空間補丁方案、阿里AndFix以 及微信Tinker,它們在原理各有不同,適用場景各異,到底採用哪種方案,是開發者比較頭疼的問題。本文希望通過介紹QQ空間補丁、Tinker以及基於AndF

Android 修復方案分析

絕大部分的APP專案其實都需要一個動態化方案,來應對線上緊急bug修復發新版本的高成本.之前有利用加殼,分拆兩個dex結合DexClassLoader實現了一套全量更新的熱更方案.實現原理在Android 基於Proxy/Delegate 實現bug熱修復這篇部

線上修復技術

images sse idt classes lib 反射 png 兩個 logs 沒學會、沒接觸的時候感覺很難,學會了也就沒那麽難 1.前言 2.相關技術 阿裏巴巴 AndFix、Dexposed QQ空間 超級補丁 微信 Tinker (一)

Android修復技術

android新技術更新版本一直以來是移動端的一大痛點,各大公司也推出了相應的解決方案。1)AndFix(阿裏巴巴):兼容性不太好,親試過,上線反饋崩潰問題特別嚴重。2)Tinker(微信):集成起來是相當的麻煩 看完http://blog.csdn.net/u010983881/article/detai

全面了解Android修復技術

服務 補丁 按順序 體積 及其 試用 sta x文件 犯錯 WeTest 導讀 本文探討了Android熱修復技術的發展脈絡,現狀及其未來。 熱修復技術概述 熱修復技術在近年來飛速發展,尤其是在InstantRun方案推出之後,各種熱修復技術競相湧現。國內大部分成熟的主流

Android 修復的相關總結(主要是阿裏百川的)

else if aes stringbu tag initial att ide 新手 append 1.主流的熱修復是 QQ 、微信和阿裏百川 2.我建議使用阿裏百川的原因第一:團隊在釘釘有專門的客服 二、對於新手來說非常方便 3.操作步驟:阿裏百川的api文檔很詳細

Android修復框架匯總整理(Hotfix)

支付 業務開發 桌面 lib 業務 exce 修復技術 同進程 熱更新 ??Android平臺出現了一些優秀的熱更新方案,主要可以分為兩類:一類是基於multidex的熱更新框架,包括Nuwa、Tinker等;另一類就是native hook方案,如阿裏開源的Andfix和

android--------阿裏 Sophix移動修復

後臺 val too otf 應用程序 pac pub exc get 移動熱修復(Mobile Hotfix)是阿裏雲提供的全平臺App熱修復服務方案。產品基於阿裏巴巴首創hotpatch技術,提供最細粒度熱修復能力,讓您無需等待實時修復應用線上問題。 移動熱修復提供的熱

Android修復技術原理詳解(最新最全版本)

總結 核心 桌面圖標 實時 開源 穩定性 安卓 定義 check 本文框架 什麽是熱修復? 熱修復框架分類 技術原理及特點 Tinker框架解析 各框架對比圖 總結 ??通過閱讀本文,你會對熱修復技術有更深的認知,本文會列出各類框架的優缺點以及技術原理,文章末尾簡單描述

android--------阿裏 AndFix 修復

void xtend width directory adb src 進入 情況 style AndFix,全稱是Android hot-fix。是阿裏開源的一個熱補丁框架,允許APP在不重新發布版本的情況下修復線上的bug。 支持Android 2.3 到 6.0,並且支

Android 修復 Tinker接入及源碼淺析

uil obj 安全 Language num sse b2c rom 其中 一、概述 放了一個大長假,happy,先祝大家2017年笑口常開。 假期中一行代碼沒寫,但是想著馬上要上班了,趕緊寫篇博客回顧下技能,於是便有了本文。 熱修復這項技術,基本上已經成為項目比較