Android熱修復+dex分包+相關知識總結

分類:編程 時間:2017-02-11

android熱修復+dex分包+相關知識總結

先推薦一篇文章,是根據包建強在2016GMTC全球移動開發大會上的演講整理而成,提綱挈領地講述了Android插件化技術.包括插件化的歷史,入門知識,技術流派,周邊知識,開源框架,經驗分享以及未來幾個方面講解了Android插件化技術,也包括這篇文章的主要內容Android熱修復和dex分包.從宏觀的角度給予我們一個全面的介紹.

Android插件化:從入門到放棄

一.Android熱修復解決的問題

當一個App發布之後,突然發現了一個嚴重bug需要進行緊急修復,這時候公司各方就會忙得焦頭爛額:重新打包App、測試、向各個應用市場和渠道換包、提示用戶升級、用戶下載、覆蓋安裝。有時候僅僅是為了修改了一行代碼,也要付出巨大的成本進行換包和重新發布。

這時候就提出一個問題:有沒有辦法以補丁的方式動態修復緊急Bug,不再需要重新發布App,不再需要用戶重新下載,覆蓋安裝?

熱補丁動態修復技術就是來解決這個問題的。這項技術是QQ空間團隊最早提出的.

二.技術流派

在業界內比較著名的有阿裏巴巴的AndFix、Dexposed,騰訊QQ空間的超級補丁和微信的Tinker。最近阿裏百川推出的HotFix熱修復服務就基於AndFix技術,定位於線上緊急BUG的即時修復。下面我們就分別介紹QQ空間超級熱補丁技術和微信Tinker以及阿裏百川的HotFix技術。

1.QQ空間超級補丁技術

超級補丁技術基於DEX分包方案,使用了多DEX加載的原理,大致的過程就是:把BUG方法修復以後,放到一個單獨的DEX裏,插入到dexElements數組的最前面,讓虛擬機去加載修復完後的方法。
修復的步驟為:

  1. 可以看出是通過獲取到當前應用的Classloader,即為BaseDexClassloader
  2. 通過反射獲取到他的DexPathList屬性對象pathList
  3. 通過反射調用pathList的dexElements方法把patch.dex轉化為Element[]
  4. 兩個Element[]進行合並,把patch.dex放到最前面去
  5. 加載Element[],達到修復目的

這裏寫圖片描述

2.微信Tinker

微信針對QQ空間超級補丁技術的不足提出了一個提供DEX差量包,整體替換DEX的方案。主要的原理是與QQ空間超級補丁技術基本相同,區別在於不再將patch.dex增加到elements數組中,而是差量的方式給出patch.dex,然後將patch.dex與應用的classes.dex合並,然後整體替換掉舊的DEX文件,以達到修復的目的。

這裏寫圖片描述

整體的流程如下:

這裏寫圖片描述

3.阿裏百川HotFix

阿裏百川推出的熱修復HotFix服務,相對於QQ空間超級補丁技術和微信Tinker來說,定位於緊急BUG修復的場景下,能夠最及時的修復BUG,下拉補丁立即生效無需等待。

這裏寫圖片描述

1、AndFix實現原理

AndFix不同於QQ空間超級補丁技術和微信Tinker通過增加或替換整個DEX的方案,提供了一種運行時在Native修改Filed指針的方式,實現方法的替換,達到即時生效無需重啟,對應用無性能消耗的目的。

原理圖如下:
這裏寫圖片描述

2、AndFix實現過程

對於實現方法的替換,需要在Native層操作,經過三個步驟:
這裏寫圖片描述

以上部分主要來自其他博客:
Android熱修復技術選型——三大流派解析

三.Android熱修復技術實現

1.技術選擇

本篇文章主要是講qq空間的補丁方案的實現.主要是因為現在的項目越來越大,dex分包已經是常用的技術了,而且熱修復是即時生效的,用戶體驗也很好.

2.原理分析(重要)

前面已將說過,現在遇到的問題是,已經發布的項目中有bug,我們不想發布新的版本,需要神不知鬼不覺的修改我們之前的代碼,用新的代碼將舊的代碼替換,就解決了問題.

那麽如何將舊代碼替換呢?

這就考察我們的基礎知識了.Java文件編譯成.class文件,JVM通過解碼class文件中的內容來運行.而在android的Dalvik虛擬機(5.0以下)或者ART(5.0及以上)中都運行的是Dalvik字節碼,Dalvik字節碼是哪來的呢,是通過Android SDK中的dx工具,將.class文件中的java字節碼轉化為Dalvik字節碼,存放在DEX(Dalvik Executable)可執行文件中.

JVM:  .Java -> .class
Dalvik(ART): .Java -> .class -> .dex
Dex文件其實是所有class文件的集合,並做了很多優化.
實際上Dalvik在加載dex文件時,還是用了DexOpt優化了dex文件,生成了odex文件(有興趣的同學可以自己去查).

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

由此我們可以想到,根據ClassLoader的加載方式,如果我們將修改過的dex文件放到有bug的dex文件前面,不就實現了熱修復的功能嗎.

那我們怎麽才能加載dex文件呢?

app啟動的時候會找到默認的classes.dex文件並加載,android系統其實支持多個dex文件,在默認的dex文件中找不到所需要的類時,系統會加載其他的dex文件.我們只要將我們的實現的功能放到一個新的dex文件中,在系統沒使用到有bug的dex文件的時候將新生成的dex文件加載進ClassLoader就可以實現熱修復.

這裏就涉及到dex分包技術了.
稍後再說dex分包,我們再把思路走捋一遍:
將我們自己的代碼在打包的時候放的第二個dex文件中(後面會講如何實現)->用戶使用,發現有bug->我們修改好代碼,重新編譯,生成正確的dex文件->用戶打開app是檢測到有熱修復文件->下載,動態加載新的dex文件放在原有的dex文件前面->沒有bug了

3.dex分包介紹

dex分包技術最早應該是解決65536的問題.

當一個app的功能越來越復雜,代碼量越來越多,也許有一天便會突然遇到下列現象:
1. 生成的apk在2.3以前的機器無法安裝,提示INSTALL_FAILED_DEXOPT
2. 方法數量過多,編譯時出錯,提示:
Conversion to Dalvik format failed:Unable to execute dex: method ID not in [0, 0xffff]: 65536

出現這種問題的原因是:
1. Android2.3及以前版本用來執行dexopt(用於優化dex文件)的內存只分配了5M
無法安裝(Android 2.3 INSTALL_FAILED_DEXOPT)問題,是由dexopt的LinearAlloc限制引起的,在Android版本不同分別經歷了4M/5M/8M/16M限制,目前主流4.2.x系統上可能都已到16M, 在Gingerbread或者以下系統LinearAllocHdr分配空間只有5M大小的, 高於Gingerbread的系統提升到了8M。Dalvik linearAlloc是一個固定大小的緩沖區。在應用的安裝過程中,系統會運行一個名為dexopt的程序為該應用在當前機型中運行做準備。dexopt使用LinearAlloc來存儲應用的方法信息。Android 2.2和2.3的緩沖區只有5MB,Android 4.x提高到了8MB或16MB。當方法數量過多導致超出緩沖區大小時,會造成dexopt崩潰。.
2. 一個dex文件最多只支持65536個方法。
超過最大方法數限制的問題,是由於DEX文件格式限制,一個DEX文件中method個數采用使用原生類型short來索引文件中的方法,也就是4個字節共計最多表達65536個method,field/class的個數也均有此限制。對於DEX文件,則是將工程所需全部class文件合並且壓縮到一個DEX文件期間,也就是Android打包的DEX過程中, 單個DEX文件可被引用的方法總數(自己開發的代碼以及所引用的Android框架、類庫的代碼)被限制為65536。

關於dex分包方案有很多:
1.使用dx命令,Google推薦的方法
具體方法可參考: Android關於Dex拆分(MultiDex)技術詳解
2.Eclipse中的方法,使用ant腳本
具體方法可參考: Android APK DEX分包總結
3.使用第三方的插件或工具
具體方法可參考: Android Studio中Dex分包方案

4.dex分包的實現

本文使用的上述第三種方法
使用一個第三方的包dexnkife來完成分包的配置:
https://github.com/ceabie/DexKnifePlugin

具體方法上面推薦的文章裏面都介紹了,這裏就稍微講幾句.
project的build.gradle加入

buildscript {
    dependencies {
        classpath 'com.ceabie.dextools:gradle-dexknife-plugin:1.5.6'
    }
}

在app的build.gradle加入

apply plugin: 'com.ceabie.dexnkife'

在project的新建dexknife.txt文件,在其中添加dex的分包配置。
這裏寫圖片描述

主要是在dexknife.txt添加這句:

# 這條配置可以指定這個包下類在第二dex中.
# 包名.文件名.**
com.demo.android.mymultidextest.NewPro.**  

將某一文件夾下的類都添加到第二個dex文件中
其他默認的類都添加到主dex文件中.

接下來就只需要編譯就行了。系統會自動生成maindexlist.txt也就是主包中的class列表。

運行之後生成了2個dex文件.
這裏寫圖片描述

那麽如何查看dex文件的內容呢?
Android APK反編譯就這麽簡單 詳解(附圖)
常用的反編譯工具 Android反編譯工具包(最新)

裏面有3個工具:
apktool
作用:資源文件獲取,可以提取出圖片文件和布局文件進行使用查看
dex2jar
作用:將apk反編譯成Java源碼(classes.dex轉化成jar文件)
jd-gui
作用:查看APK中classes.dex轉化成出的jar文件,即源碼文件

我們用到dex2jar,將兩個dex文件轉化成jar文件
這裏寫圖片描述

使用jd-gui,打開dex文件,可查看裏面的內容.
這裏寫圖片描述

如上圖, classes2.dex文件中,只有一個文件夾,就是我們之前寫在dexknife.txt文件中的

此時,dex分包基本完成.註意:
1.每次發布新版本的時候,將新的文件夾或類文件添加到dexknife.txt文件中,保證該類在第二個dex文件中,如果第一個dex文件中出現bug,是不能通過今天介紹的方法解決的.

5.熱修復的實現

此時,情況是這樣的:

我們發布的app有bug,我們修改好代碼後,生成了新的dex文件,用戶打開app時,將新的dex文件下載到手機了,那麽此時應該加載新的dex到內存.

Android 中有三個 ClassLoader, 分別為 URLClassLoader 、 PathClassLoader 、 DexClassLoader 。其中

URLClassLoader 只能用於加載jar文件,但是由於 dalvik 不能直接識別jar,所以在 Android 中無法使用這個加載器。
PathClassLoader 它只能加載已經安裝的apk。因為 PathClassLoader 只會去讀取 /data/dalvik-cache 目錄下的 dex 文件。例如我們安裝一個包名為 com.hujiang.xxx 的 apk,那麽當 apk 安裝過程中,就會在 /data/dalvik-cache 目錄下生產一個名為 data@[email protected]@classes.dex 的 ODEX 文件。在使用 PathClassLoader 加載 apk 時,它就會去這個文件夾中找相應的 ODEX 文件,如果 apk 沒有安裝,自然會報 ClassNotFoundException 。
DexClassLoader 是最理想的加載器。它的構造函數包含四個參數,分別為:
1.dexPath,指目標類所在的APK或jar文件的路徑.類裝載器將從該路徑中尋找指定的目標類,該類必須是APK或jar的全路徑.如果要包含多個路徑,路徑之間必須使用特定的分割符分隔,特定的分割符可以使用system.getProperty(“path.separtor”)獲得.
2.dexOutputDir,由於dex文件被包含在APK或者Jar文件中,因此在裝載目標類之前需要先從APK或Jar文件中解壓出dex文件,該參數就是制定解壓出的dex 文件存放的路徑.在Android系統中,一個應用程序一般對應一個linux用戶id,應用程序僅對屬於自己的數據目錄路徑有寫的權限,因此,該參數可以使用該程序的數據路徑.
3.libPath,指目標類中所使用的C/C++庫存放的路徑
4.classload,是指該裝載器的父裝載器,一般為當前執行類的裝載器

以上一段摘自:Android 熱修復,沒你想的那麽難

首先將新的dex文件下載到手機,我這裏放到了根目錄下
在項目中創建Application文件,在onCreate()方法中調用以下代碼

為啥必須在application中呢? 因為在application中,先拿到加載程序本身的PathClassloader,,我們先稱它為originalClassloader,因為在你的originalClassloader裏,已經保存有對主程序中dex文件的鏈接,所以,能找到你的主程序的class。

獲得dex文件路徑

String libPath= Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "classes2.dex";

獲取PathClassLoader

 PathClassLoader pathClassLoader = (PathClassLoader)getClassLoader();

創建DexClassLoader

 DexClassLoader dexClassLoader = new DexClassLoader(libPath, getDir("dex", 0).getAbsolutePath(), libPath, getClassLoader());

需要把我們的dex加載到系統中,這裏主要就是使用反射技術合並dexList即可

try{
                Object dexElements = combineArray(getDexElements(getPathList(pathClassLoader)), getDexElements(getPathList(dexClassLoader)));
                Object pathList = getPathList(pathClassLoader);
                setField(pathList, pathList.getClass(), "dexElements", dexElements);
                return "SUCCESS";
            } catch (Throwable e) {
                e.printStackTrace();
                return android.util.Log.getStackTraceString(e);
            }
public static Object combineArray(Object arrayLhs, Object arrayRhs) {
        Class<?> localClass = arrayLhs.getClass().getComponentType();
        int i = Array.getLength(arrayLhs);
        int j = i + Array.getLength(arrayRhs);
        Object result = Array.newInstance(localClass, j);
        for (int k = 0; k < j; ++k) {
            if (k < i) {
                Array.set(result, k, Array.get(arrayLhs, k));
            } else {
                Array.set(result, k, Array.get(arrayRhs, k - i));
            }
        }
        return result;
    }
public Object getDexElements(Object paramObject)
            throws IllegalArgumentException, NoSuchFieldException, IllegalAccessException {
        return getField(paramObject, paramObject.getClass(), "dexElements");
    }
public Object getPathList(Object baseDexClassLoader)
        throws IllegalArgumentException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
            return getField(baseDexClassLoader, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");
    }
public Object getField(Object obj, Class<?> cl, String field)
            throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException {
        Field localField = cl.getDeclaredField(field);
        localField.setAccessible(true);
        return localField.get(obj);
    }
 public static void setField(Object obj, Class<?> cl, String field, Object value)
            throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException {

        Field localField = cl.getDeclaredField(field);
        localField.setAccessible(true);
        localField.set(obj, value);
    }

以上方法執行完畢後就將dex文件加載完畢.

四.總結

回顧一下,本問介紹的熱修復方法.

當我們新發布的app出現bug需要緊急處理時,我們修改好代碼,編譯得到新的dex文件,用戶下載後需要重新啟動app,在Application中回去加載下載的dex文件,達到修復的目的.

這種方法的用戶體驗還是稍微差一點,不過我的主要目的是介紹其相關的知識,一些android系統的運行過程,希望對大家都有所幫助.

如有錯誤,歡迎指出!

五.相關文章

Android 熱修復,沒你想的那麽難
安卓App熱補丁動態修復技術介紹
Android熱修復技術選型——三大流派解析
Android 熱補丁動態修復框架小結
Android 熱修復其實很簡單

Android 熱修復,插件式開發—基本知識

Android dex分包方案
Android Dex 分包指南
【轉】Android插件化從入門到放棄-最強合集


Tags:

文章來源:


ads
ads

相關文章
ads

相關文章

ad