熱修復框架?我們都能做出來!

本文爲《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的形式簡單實現一個熱修復的框架,讓讀者自己能夠輕鬆實現。

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

發佈了60 篇原創文章 · 獲贊 13 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章