Android開發:熱修復 Tinker 原理分析

熱修復

目前國內Android熱修復技術已經發展的可以說百花齊放了,從實現方式來大致分類,可以分爲:

  1. Native層實現
  2. Java層實現

之前也有人簡單分析過阿里開源的Andfix實現原理(基於Native層),這裏就不再多說了感興趣的可以搜索看下。

本文簡單分析一下Java層實現熱修復邏輯,簡單實現代碼熱修復Demo,以Tinker爲例(當然Tinker是支持代碼修復,資源修復,so修復的,感興趣的小夥伴自行移步官網~)

先梳理下思路:

Java類編譯過程

就是java類通過javac編譯成.class文件,再由dx.bat編譯成.dex文件的過程,不做贅述,簡單畫張圖~

ClassLoader簡介

Android中的java.lang.ClassLoader這個類也不同於Java中的java.lang.ClassLoader。 Android中的ClassLoader類型也可分爲系統ClassLoader和自定義ClassLoader。其中系統ClassLoader包括3種分別是:

  • BootClassLoader,Android系統啓動時會使用BootClassLoader來預加載常用類,與Java中的Bootstrap ClassLoader不同的是,它並不是由C/C++代碼實現,而是由Java實現的。BootClassLoader是ClassLoader的一個內部類。
  • PathClassLoader,全名是dalvik/system.PathClassLoader,可以加載已經安裝的Apk,也就是/data/app/package 下的apk文件,也可以加載/vendor/lib, /system/lib下的nativeLibrary。
  • DexClassLoader,全名是dalvik/system.DexClassLoader,可以加載一個未安裝的apk文件。 PathClassLoader和DexClasLoader都是繼承自 dalviksystem.BaseDexClassLoader,它們的類加載邏輯全部寫在BaseDexClassLoader中。 下圖展示了Android中的ClassLoader中的繼承體系,其中SecureClassLoader和UrlClassLoader是在Java中的類加載器,在Android中是沒法辦使用的。

.dex文件加載

通過源碼得知,.dex文件是通過BaseDexClassLoader類(ClassLoader的子類)進行加載的,這個類裏面有個成員變量DexPathList對象,而這個對象中有個有個數組存放的是DexElement對象,也就是從文件中加載的.dex文件,切入點就在這裏

對於項目來說,一般項目會分包(方法數大於64k,及大於65535個時候,google提供的分包策略),如果採用Java代碼實現熱修復,分包是肯定要做的,因爲要保證主包沒有bug,分包簡單說就是打包的apk一般有多個.dex文件

如: classes.dex,classes2.dex 等等…

那麼比如我們的classes2.dex中某個類的某個方法出現了異常,我們就可以創建一個修復包(修復後的classes2.dex文件),然後通過自定義的類加載器將修復後的classes2.dex文件copy到私有目錄,再插隊到系統ClassLoaderdexPathList對象dexElement的數組中,讓系統優先加載修復後的classes2.dex文件,以做到熱修復的目的,這種實現方式必須要執行修復邏輯後,重啓app才能實現效果~

瞭解了這些信息大致思路就有了,我們需要修復後的.dex文件加載解析,然後插隊舊的安裝包裝的.dex文件,做到插隊的操作,相當於欺騙了Android系統,大致如下:

實現原理

思路大概是,我們需要一個修復bug的.dex文件,插隊到BaseDexClassLoader類下的DexPathList對象DexElement數組中,並且排序到最前面,讓系統加載到我們修復後的.dex文件不會再加載有bug的dex文件,完成插隊(插裝),這裏會有個類加載機制的知識,本文不做詳細介紹,後面會單獨寫一篇總結~ 大致實現步驟如下:

Demo實現

1、基礎配置-主包配置

配置分包,配置分包的目的主要是打包做出來的apk會有多個.dex文件,實際項目應用中要保證主包不要有bug,demo中加載.dex文件的時候也排除了主包文件classes.dex,大致如下: 創建BaseApplication,BaseActivity,MainActivity放置在主包,其中MainActivity主要是爲了分包占位,只做了點擊跳轉分包中SecondActivity的邏輯 app目錄下的build.gradle開啓分包支持,在androiddefaultConfig下增加配置,其中multiDex-config.txt是配置保留在主包內類文件

 //開啓分包
        multiDexEnabled true
        //分包的配置,將配置文件中的放置在主包
        multiDexKeepFile file("multiDex-config.txt")

添加分包依賴:

  //multidex分包依賴
    implementation 'com.android.support:multidex:1.0.3'

Application開啓分包:

public class BaseApplication extends MultiDexApplication {
    @Override
    public void onCreate() {
        super.onCreate();
    }

    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        //安裝分包配置
        MultiDex.install(this);
    }
}

2、分包配置

分包就創建了一個SecondActivity類做模擬異常和修復異常的入口,和一個Calculate模擬異常,做了10/0的操作,修復後爲10/1

注:獲取修復後的classes2.dex文件可以通過直接buildapk直接解壓獲取,或者用build-tools下的dx.bat執行命令獲取

簡單貼下SecondActivity,點擊fix按鈕後的代碼:

  private void update() {
        //將下載的修復包,複製到私有目錄,解壓從.dex文件中取到對應的.class文件
        //從sd卡取修復包
        File sourceFile = new File(Environment.getExternalStorageDirectory(), Constants.DEX_NAME);
        //目標文件
        File targetFile = new File(getDir(Constants.DEX_DIR, Context.MODE_PRIVATE).getAbsolutePath() + File.separator + Constants.DEX_NAME);
        if (targetFile.exists()) {
            targetFile.delete();
            Log.e("update","刪除原有dex文件(已使用的)");
        }
        //將SD卡中的修復包copy到私有目錄
        FileUtils.copyFile(sourceFile,targetFile);
        Log.e("update","copy完成");
        FixDexUtils.loadDexFile(this);
    }

3、FixModule

新建一個Module處理熱修復的相關邏輯

只有五個文件,核心文件代碼在FixDexUtils,其它是工具類,還有個定義了幾個常量 看下FixDexUtils中的代碼

public class FixDexUtils {

    //修復文件可能有多個
    private static HashSet<File> loadedDex = new HashSet<>();

    //不建議這麼寫,demo演示用
    static {
        loadedDex.clear();
    }

    public static void loadDexFile(Context context) {
        //獲取私有目錄
        File fileDir = context.getDir(Constants.DEX_DIR, Context.MODE_PRIVATE);
        //遍歷篩選私有目錄中的.dex文件
        File[] listFiles = fileDir.listFiles();

        for (int i = 0; i < listFiles.length; i++) {
            //文件名以.dex結尾,且不是主包.dex文件
            if (listFiles[i].getName().endsWith(Constants.DEX_SUFFIX) && !"classes.dex".equalsIgnoreCase(listFiles[i].getName())) {
                loadedDex.add(listFiles[i]);
            }
        }
        //創建自定義的類加載器
        createDexClassLoader(context ,fileDir);
    }

    /**
     * @param context
     * @param fileDir
     * 創建自己的類加載器,加載私有目錄的.dex文件,上面已經將修復包中的dex文件copy到私有目錄
     */
    private static void createDexClassLoader(Context context, File fileDir) {
        //解壓目錄
       String optimizedDir = fileDir.getAbsolutePath()+File.separator+"opt_dex";
        File fileOpt = new File(optimizedDir);
        if (!fileOpt.exists()) {
            fileOpt.mkdirs();
        }

        for (File dex : loadedDex) {
            //創建自己的類加載器,臨時的
            DexClassLoader classLoader = new DexClassLoader(dex.getAbsolutePath(), optimizedDir, null, context.getClassLoader());
            //有一個修復文件,就插裝一次
            hotFix(classLoader,context);
        }
    }

    private static void hotFix(DexClassLoader classLoader, Context context) {
        try {
            //獲取系統的PathClassLoader類加載器
            PathClassLoader pathClassLoader = (PathClassLoader)context.getClassLoader();
            //獲取自己的dexElements數組

            Object myElements = ReflectUtils.getDexElements(ReflectUtils.getPathList(classLoader));
            //獲取系統的dexElements數組
            Object systemElements = ReflectUtils.getDexElements(ReflectUtils.getPathList(pathClassLoader));
            //合併數組,並排序,生成一個新的數組
            Object dexElements=ArrayUtils.combineArray(myElements,systemElements);
            //通過反射獲取系統的pathList屬性
            Object systemPathList = ReflectUtils.getPathList(pathClassLoader);
            //通過反射,將合併後新的dexElements賦值給系統的pathList
            ReflectUtils.setFieldValue(systemPathList,"dexElements",dexElements);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

主要做的工作就是:就是上面那張流程圖,就是先通過遍歷,解壓等操作,獲取到需要執行熱修復的.dex文件集合,遍歷該集合,對應每次創建一個臨時的DexClassLoader,然後執行修復步驟,細分就是六步:

最終實現效果如圖(Demo使用手機是華爲8.0的手機):

注:這裏爲了效果圖更直觀,已經重啓過一次app
注: 這種方式實現的熱修復必須要重啓App纔可以實現修復,這一點也是類加載機制決定的,如下圖中修復之後再次打開加載執行修復後的classes.dex文件是在BaseApplication中調用了修復方法

最後

在這裏我也分享一份由幾位大佬一起收錄整理的Android學習PDF+架構視頻+面試文檔+源碼筆記高級架構技術進階腦圖、Android開發面試專題資料,高級進階架構資料

如果你有需要的話,可以 點這領取

喜歡本文的話,不妨順手給我點個小贊、評論區留言或者轉發支持一下唄~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章