效能優化 (九) APP 穩定性之熱修復原理探索

DevYK發表於2019-06-09

效能優化系列

APP 啟動優化

UI 繪製優化

記憶體優化

圖片壓縮

長圖優化

電量優化

Dex 加解密

動態替換 Application

APP 穩定性之熱修復原理探索

APP 持續執行之程式保活實現

ProGuard 對程式碼和資源壓縮

APK 極限壓縮

程式碼傳送陣

完整程式碼傳送陣

效能優化 (九) APP 穩定性之熱修復原理探索

熱修復的背景

  • 剛釋出的版本出現了嚴重的 bug ,需要開發者去解決 bug,然後在測試打包重新發布,這會耗費大量的人力,物力,代價比較大。
  • 如果當前的 bug 不影響使用者使用也不會崩潰,但是了下個版本是大版本,那麼兩個版本之間間隔時間會很長,這樣要等到下個大版本釋出在修復 bug , 而之前版本的 bug 還存在,雖說不影響使用,但是是一個潛在的 bug。
  • 版本升級率不高,並且需要長時間來完成版本迭代,前版本的 bug 就會一直影響不升級的使用者。
  • 有一些小但是很重要的功能需要在短時間內完成版本迭代,比如假日活動。

..等等, 這裡只是拿幾個常見的舉例說明。

熱修復的效率

效能優化 (九) APP 穩定性之熱修復原理探索

熱修復框架對比

框架名稱 所屬公司 是否開源 修復方式
Dexposed alibaba 開源 實時修復
Andfix alibaba 開源 實時修復
Hotfix alibaba 暫未開源 實時修復
Qzone 超級補丁 QQ 空間 暫未開源 冷啟動修復
QFix 手 Q 團隊 開源 冷啟動修復
Robust 美團 開源 實時修復
Nuwa 大眾點評 開源 冷啟動修復
RocooFix 百度金融 開源 冷啟動修復
Aceso 美麗說蘑菇街 開源 實時修復
Amigo 餓了麼 開源 冷啟動修復
Tinker 微信 開源 冷啟動修復
Sophix alibaba 未開源 實時修復 + 冷啟動修復

程式碼修復(今日主題 - 類載入方式)

底層替換方式

  • 在已載入的類中直接替換原有方法,是在原有類的基礎上進行修改,無法實現對原有類進行方法和欄位的增減,這樣會破壞原有類的結構。
  • 不穩定。直接修改 JVM 方法實體的具體欄位來實現的。Android 是開源的,不同的手機廠商開源對程式碼進行修改,所以像 Andfix 就會出現在部分機型上的修復失敗的現象。

ClassLoader 類載入方式

  • APP 重新啟動後,讓 ClassLoader 去載入新的類。

  • class 暫未被載入到系統中,收到推送利用插樁原理讓 ClassLoader 優先載入修復好的 dex 。

實現自己的熱修復框架

Dex 分包

65536 限制

com.android.dex.DexIndexOverflowException: method ID not in [0, 0xffff]: 65536
複製程式碼

當應用程式報 65536 錯誤的根本原因是,應用的方法數量超過了最大數 65536 個,因為 DVM Bytecode 的限制, DVM 指令集的方法呼叫指令 invoke-kind 索引為 16 bits, 最多能引用 65535 個方法

LinearAlloc 限制

INSTALL_FAILED_DEXOPT
複製程式碼

在安裝應用時可能會提示 上面的錯誤,產生的原因是 LinearAlloc 限制。 DVM 中的 LinearAlloc 是一個固定的快取區,當方法數超出快取區的大小時會報錯。

解決

為了解決 65536 限制和 LinearAlloc 限制,從而產生了 Dex 分包機制。 Dex 分包方案主要做的時在打包時將應用程式碼分成多個 Dex,將應用啟動時必須用到的類和這些類的直接引用類放到主 Dex 中,其它程式碼放到次 Dex 中。當應用啟動時先載入主 Dex,等到應用啟動後再動態地載入次Dex,從而緩解了主 Dex 的 65536 限制和 LinearAlloc 限制

  • gradle 配置

    android {
        compileSdkVersion 26
        defaultConfig {
            applicationId "com.ykun.hotfix"
            minSdkVersion 15
            targetSdkVersion 26
            versionCode 1
            versionName "1.0"
            testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    
            // 開啟分包
            multiDexEnabled true
            // 設定分包配置檔案
            multiDexKeepFile file('multidex.keep')
        }
        dexOptions {
            javaMaxHeapSize "4g"
            preDexLibraries = false
            additionalParameters = [ // 配置multidex引數
                                     '--multi-dex', // 多dex分包
                                     '--set-max-idx-number=50000', // 每個包內方法數上限
                                     '--main-dex-list=' + '/multidex.keep', // 打包到主classes.dex的檔案列表
                                     '--minimal-main-dex'
            ]
        }
    
    }
    複製程式碼
  • 配置 multidex.keep 將指定的 class 放入 class.dex 中

    格式:

    //參考
    com/ykun/hotfix/BaseActivity.class
    com/ykun/hotfix/BaseApplication.class
    com/ykun/hotfix/MainActivity.class
    複製程式碼
  • 效果

    效能優化 (九) APP 穩定性之熱修復原理探索

什麼是插樁?

效能優化 (九) APP 穩定性之熱修復原理探索

原始碼:

/**遍歷需要找到需要載入的 class */ 
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;
    }
複製程式碼

插樁原理:

通過原始碼得知 findClass 是通過遍歷 dexElements 來找到 class, 如果我們反射得到 DexPathList 的私有陣列 dexElements,我們外部改變這個陣列內部順序索引,將修復好的 dex 放入 [0] 的位置,那麼是不是能夠優先使用修復好的 dex 勒? 很明顯,是成立的。下面開始擼程式碼吧。

程式碼實現

  1. 接收來至伺服器發來的補丁包,如果修復包已經存在則刪除,copy 到私有目錄防止使用者不小心刪除。

    /**這裡模擬已經下載好的 dex 補丁包*/    
    private void downloadPatch() {
            //1 從伺服器下載dex檔案 比如v1.1修復包檔案(classes2.dex)
            File sourceFile = new File(Environment.getExternalStorageDirectory(), "classes2.dex");
            // 目標路徑:私有目錄
            //getDir("odex", Context.MODE_PRIVATE) data/user/0/包名/app_odex
            File targetFile = new File(getDir("hotfix",
                    Context.MODE_PRIVATE).getAbsolutePath() + File.separator + "classes2.dex");
            if (targetFile.exists()) {
                targetFile.delete();
            }
            try {
                // 複製dex到私有目錄
                FileUtils.copyFile(sourceFile, targetFile);
                Toast.makeText(this, "Bug 修復成功!", Toast.LENGTH_SHORT).show();
                FixDexUtils.loadFixedDex(this);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    複製程式碼
  2. 建立修復包的類載入器 DexClassLoader (通過原始碼得知是繼承的 BaseDexClassLoader)

        /**
         * 建立類載入器
         *
         * @param context
         * @param fileDir
         */
        private static void createDexClassLoader(Context context, File fileDir) {
            String optimizedDirectory = fileDir.getAbsolutePath() + File.separator + "opt_dex";
            File fOpt = new File(optimizedDirectory);
            if (!fOpt.exists()) {
                fOpt.mkdirs();
            }
            DexClassLoader classLoader;
            for (File dex : loadedDex) {
                //初始化類載入器
                classLoader = new DexClassLoader(dex.getAbsolutePath(), optimizedDirectory, null,
                        context.getClassLoader());
                //熱修復
                hotFix(classLoader, context);
            }
        }
    複製程式碼
  3. 獲取系統的 PathClassLoader

    PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();
    複製程式碼
  4. 獲取修復包的 dexElements

    Object pathList = ReflectUtils.reflect(myClassLoader).field("pathList").get();
    Object myDexElements = ReflectUtils.reflect(pathList).field("dexElements").get();
    複製程式碼
  5. 獲取系統的 dexElements

    Object sysPathList = ReflectUtils.reflect(pathClassLoader).field("pathList").get();
    Object sysDexElements = ReflectUtils.reflect(sysPathList).field("dexElements").get();
    複製程式碼
  6. 將系統的 dexElements 和 修復包的 dexElements merge 成新的 dexElements

    // 合併,這裡利用插樁原理進行合併陣列,將修復好的 class2.dex 放入第一位,優先加入就行了
    Object dexElements = ArrayUtils.combineArray(myDexElements, sysDexElements);
    複製程式碼
  7. 重新賦值給 DexPathList 的 dexElements 屬性

    //重新賦值
    ReflectUtils.reflect(sysPathList).field("dexElements", dexElements);
    複製程式碼

熱修復未來發展

  1. 熱修復 = “黑科技”?

    • 熱修復不同於國內 APP 程式保活這種 “黑科技”,讓 app 常駐後臺,既耗電又佔用記憶體,浪費很多手機資源。還有 APP 的推送服務,無節操地對使用者進行資訊轟炸。還有更無節操的全家桶 app。導致 Android手機卡頓不堪,這些所謂的 “黑科技” 都是為了手機廠商的利益而損害使用者的體驗。

    • 而熱修復是能夠讓開發者和使用者雙贏的。不僅廠商能快速迭代更新 app,使功能儘快上線,而且熱更新過程使用者無感知,節省大量更新時間,提高使用者體驗。更重要的能保證 app 的功能穩定,bug 能及時修復。

  2. IOS 封殺了熱修復功能,Android 的熱修復也會被 pass 掉嗎?

    • google 和 apple 公司在中國的 diwei 不一樣

    • Android 和 IOS 的開放性不同

  3. 熱修復未來發展前景是很樂觀的。

相關文章