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

性能優化系列

APP 啓動優化java

UI 繪製優化android

內存優化git

圖片壓縮github

長圖優化數組

電量優化緩存

Dex 加解密性能優化

動態替換 Application服務器

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

APP 持續運行之進程保活實現app

ProGuard 對代碼和資源壓縮

APK 極限壓縮

代碼傳送陣

完整代碼傳送陣

熱修復的背景

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

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

熱修復的效率

熱修復框架對比

框架名稱 所屬公司 是否開源 修復方式
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
    複製代碼
  • 效果

什麼是插樁?

源碼:

/**遍歷須要找到須要加載的 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. 熱修復將來發展前景是很樂觀的。

相關文章
相關標籤/搜索