1. 程式人生 > >Android熱修復原理簡述

Android熱修復原理簡述

本文為《2018夯實基礎》系列之熱修復原理簡述
作者:Bob

一、背景

① 為什麼會出現熱修復技術?

大家都是開發,所以應該都知道有一個東西我們永遠也避免不了。不錯,**Bug!**我們在開發階段碰到bug那還好,直接解決就是了,大不了讓測試多測一輪。可是,如果我們發出去的版本出現線上Bug,那可怎麼辦?大多數小的公司可能會選擇,重新發新的版本去覆蓋安裝。這種方式成本較高,而且使用者一般都比較討厭經常需要更新版本的APP。對於小公司來說,出現線上問題影響範圍還是比較小的,畢竟使用者量較少。可是對於一些巨頭公司,有些問題不能及時修復那是致命的。所以呢,有這麼一些技術儲備和能力比較高的公司為我們開了先河。

② 致敬先輩!

對於像手機QQ空間支付寶360這樣的巨頭公司當然無法忍受出現線上Bug要等發版解決。所以他們搞出了熱修復的技術方案。Android的熱修復技術大多都源於此。

③ 原理簡述

說了那麼多,那到底熱修復是怎樣的一個技術呢?
在說熱修復之前,我們得先知道我們手機中執行的APP實際是我們寫的Java程式碼編譯成class檔案,然後打包成dex檔案執行在手機中的。也就是說,我們手機裡面實際執行的是dex檔案。所以,如果我們的APP出現線上問題,90%的可能性是我們寫的程式碼導致的Bug,也就是執行在手機裡面的dex檔案出現了Bug。所以熱修復的思想就是靜默的下載服務端的新的dex檔案

替換出現問題的dex檔案

二、熱修復的實現原理

替換dex檔案說起來比較簡單,但是真實現起來,還是需要我們考慮更多的細節。下面我們圍繞這個替換dex檔案詳細的分析去實現的步驟:

① Dex分包

我們知道在最開始的時候(ART還沒有推出),安卓是使用Dalvik虛擬機器來執行我們的應用程式的,安卓專案在打包APK的時候,會將所有編譯生成的class檔案打包成dex檔案。並且Dalvik虛擬機器在我們安裝應用的時候通過DexOpt工具對dex檔案進行優化,DexOpt有個缺點,就是在執行的時候會將dex中的類中的所有方法ID檢索出來存在一個連結串列中,而連結串列的長度定義的型別為short

型別,這就導致dex檔案中的方法總數不能超過short的範圍,也就是不能超過65536個。顯然當我們的專案比較大的時候,這個方法數是不夠的。所以後面Android就推出了ART這種本身就支援多dex的APK。

講了這麼多,dex分包和我們這裡講的熱修復又有什麼關係呢?前面我們已經講了替換dex的思路,如果只有一個dex,不去拆分,顯然我們是沒辦法替換的。要想替換掉發生Bug的dex,我們首先得有能正常執行的dex程式碼去做替換這件事吧?所以,我們會拆出一個比較穩定的主dex,作為去實現從服務端下載和替換動作的程式碼。當其他模組出現Bug時,在去更新對應模組的dex檔案。那麼一般我們如何去拆分dex呢?

在Android5.0之前呢,我們需要引入谷歌提供的multidex.jar支援multidex。所以需要我們首先在專案的build.gradle中加入如下配置

android {
    defaultConfig {
        multiDexEnabled true
    }
}

dependencies {
    compile ‘com.android.support:multidex:1.0.0}

然後我們藉助一個好用的配置分包的第三方庫:DexKnife來配置如何分包。這裡就不對DexKnife詳細介紹了,讀者可以去Dexknife的使用文件上檢視詳細的使用方法。

② 替換dex檔案

替換dex檔案的邏輯一般是我們進入主頁面以後,請求一個介面查詢服務端當前是否有新的dex需要替換,如果有就在後臺默默下載後去替換。當然這裡麵包含一些版本、dex名稱等引數去區分。如何去下載檔案不在本文的介紹範圍,相信讀者都做過了。這裡主要講解當我們在服務端下載好了一個需要替換的dex包以後,如何將它替換進去。

在講替換之前,我們首先得了解一下我們的打包的dex檔案是如何被載入到虛擬機器執行的。Android裡面是使用的BaseDexClassLoader去載入dex中的類,所以接下來我們分析下BaseDexClassLoader的原始碼。

private final DexPathList pathList;

在這個類中,我們首先看到有一個包裝類DexPathList是用來儲存需要去載入的dex檔案列表,我們繼續觀察DexPathList的原始碼,發現:

    /**
     * List of dex/resource (class path) elements.
     * Should be called pathElements, but the Facebook app uses reflection
     * to modify 'dexElements' (http://b/7726934).
     */
    private Element[] dexElements;

用dexElements陣列儲存dex檔案的路徑,看註釋可以知道當時的谷歌的工程師蠻有意思,將Facebook會使用反射呼叫都寫進去了,哈哈,調皮。然後我看看他是如何去給將dex檔案目錄放到dexElements陣列中的呢?

    public DexPathList(ClassLoader definingContext, ByteBuffer[] dexFiles) {
    	...省略
        // TODO 這裡呼叫makePathElements()方法
        this.nativeLibraryPathElements = makePathElements(this.systemNativeLibraryDirectories);
		...省略
       
    }
    
    // 我們繼續看makePathElements幹了什麼?
    @SuppressWarnings("unused")
    private static Element[] makePathElements(List<File> files, File optimizedDirectory,
            List<IOException> suppressedExceptions) {
        // 這裡直接呼叫了makeDexElements方法
        return makeDexElements(files, optimizedDirectory, suppressedExceptions, null);
    }
    
    // 繼續追蹤makeDexElements方法
     private static Element[] makeDexElements(List<File> files, File optimizedDirectory,
            List<IOException> suppressedExceptions, ClassLoader loader) {
      Element[] elements = new Element[files.size()];
      int elementsPos = 0;
      /*
       * Open all files and load the (direct or contained) dex files up front.
       */
      for (File file : files) {
          if (file.isDirectory()) {
              // We support directories for looking up resources. Looking up resources in
              // directories is useful for running libcore tests.
              elements[elementsPos++] = new Element(file);
          } else if (file.isFile()) {
              String name = file.getName();

              if (name.endsWith(DEX_SUFFIX)) {
                  // Raw dex file (not inside a zip/jar).
                  try {
                      DexFile dex = loadDexFile(file, optimizedDirectory, loader, elements);
                      if (dex != null) {
                          elements[elementsPos++] = new Element(dex, null);
                      }
                  } catch (IOException suppressed) {
                      System.logE("Unable to load dex file: " + file, suppressed);
                      suppressedExceptions.add(suppressed);
                  }
              } else {
                  DexFile dex = null;
                  try {
                      dex = loadDexFile(file, optimizedDirectory, loader, elements);
                  } catch (IOException suppressed) {
                      /*
                       * IOException might get thrown "legitimately" by the DexFile constructor if
                       * the zip file turns out to be resource-only (that is, no classes.dex file
                       * in it).
                       * Let dex == null and hang on to the exception to add to the tea-leaves for
                       * when findClass returns null.
                       */
                      suppressedExceptions.add(suppressed);
                  }

                  if (dex == null) {
                      elements[elementsPos++] = new Element(file);
                  } else {
                      elements[elementsPos++] = new Element(dex, file);
                  }
              }
          } else {
              System.logW("ClassLoader referenced unknown path: " + file);
          }
      }
      if (elementsPos != elements.length) {
          elements = Arrays.copyOf(elements, elementsPos);
      }
      return elements;
    }

我們可以看到,原始碼中是將dex檔案封裝成Element物件存到陣列中的。所以,像Facebook或者阿里巴巴、騰訊這樣的巨頭公司呢看到了其中的本質,也就是Android在載入類的時候,就是在dexElements陣列中去遍歷dex檔案,如果在某一個dex檔案中找到了該類就載入。所以,我們的思路是將我們新的修復過Bug的dex檔案如果能放到dexElements中的最前面,那麼當系統去載入我們出錯的類的時候,會優先載入到我們修復過的類了,從而起到修復Bug的作用。

那麼,一般是怎麼做的呢?首先,我們例項一個BaseDexClassLoader類去載入我們從服務端下載下來的dex檔案到記憶體中,當然這一切需要用到反射去拿到DexPathList類中的dexElements陣列,然後將我們的dex檔案載入進去成為一個Element物件;然後,我們通過反射拿到我們APP本身的dexElements陣列去將我們新的Element放入到最前面。這樣就能夠讓新的dex比有Bug的Dex優先被載入了。

三、小結

本節主要是對使用熱修復的來龍去脈,以及我們通過原始碼的分析找到為什麼能夠去實現熱修復的原因。我們也給大家介紹了使用熱修復需要知道的哪些技術點以及業內比較成功的公司。下一節我們將圍繞本節的原理以及分析以demo的形式簡單實現一個熱修復的框架,讓讀者自己能夠輕鬆實現。

歡迎大家關注轉發,共同提高哦~

微信公眾號:南京Android部落