騰訊-熱修復連環炮(熱修復是什麼 有接觸過tinker嗎,tinker原理是什麼)

熱修復是什麼

答:

熱修復無疑是這2年較火的新技術,是作爲安卓工程師必學的技能之一。在熱修復出現之前,一個已經上線的app中如果出現了bug,即使是一個非常小的bug,不及時更新的話有可能存在風險,若要及時更新就得將app重新打包發佈到應用市場後,讓用戶再一次下載,這樣就大大降低了用戶體驗,當熱修復出現之後,這樣的問題就不再是問題了。

目前較火的熱修復方案大致分爲兩派,分別是:

  1. 阿里系:spohix、andfix:從底層二進制入手(c語言)。
  2. 騰訊系:tinker:從java加載機制入手。

有接觸過tinker嗎

答: 有接觸過Tinker的 Tinker是一個比較優異修復架構

修復的原理是什麼

答: 關於bug的概念自己百度百科吧,我認爲的bug一般有2種(可能不太準確):

  • 代碼功能不符合項目預期,即代碼邏輯有問題。
  • 程序代碼不夠健壯導致App運行時崩潰。

這兩種情況一般是一個或多個class出現了問題,在一個理想的狀態下,我們只需將修復好的這些個class更新到用戶手機上的app中就可以修復這些bug了。但說着簡單,要怎麼才能動態更新這些class呢?其實,不管是哪種熱修復方案,肯定是如下幾個步驟:

  1. 下發補丁(內含修復好的class)到用戶手機,即讓app從服務器上下載(網絡傳輸)
  2. app通過**"某種方式"**,使補丁中的class被app調用(本地更新)

這裏的**"某種方式"**,對本篇而言,就是使用Android的類加載器,通過類加載器加載這些修復好的class,覆蓋對應有問題的class,理論上就能修復bug了。所以,下面就先來了解和分析Android中的類加載器吧。

Tinker源碼分析

Tinker工程結構

直接從github上clone Tinker的源碼進行食用如下:

接入流程

 

  1. gradle相關配置主項目中build.gradle加入
buildscript {
    dependencies {
        classpath ('com.tencent.tinker:tinker-patch-gradle-plugin:1.8.1')
    }
}

在app工程中build.gradle加入

dependencies {
    //可選,用於生成application類 
    provided('com.tencent.tinker:tinker-android-anno:1.8.1')
    //tinker的核心庫
    compile('com.tencent.tinker:tinker-android-lib:1.8.1') 
}
...
...
//apply tinker插件
apply plugin: 'com.tencent.tinker.patch'

這裏需要注意tinker編譯階段會判斷一個TinkerId的字段,該字段默認由git提交記錄生成HEAD(git rev-parse --short HEAD)而且是在rootproject中執行的git命令,所以個別工程可能在rootproject目錄沒有git init過,可以選擇在那初始化git或者自定義gradle修改gitSha方法。

出包還是使用正常的build過程,測試階段選擇assembleDebug,Tinker產出patch使用gradle tinkerPatchDebug同樣也支持Flavor和Variant,Tiner會在主工程build目錄下創建bakApk,下面會有一個app-yydd-hh-mm-ss的目錄裏面對應有Favor子目錄裏面包含了通過assemble出的apk包。在build目錄下的outputs中有tinkerPatch裏面同樣也區分了build variant產物。

需要注意的是在debug出包測試過程中需要修改gradle的參數

ext {
    //for some reason, you may want to ignore tinkerBuild, such as instant run debug build?
    tinkerEnabled = true

    //for normal build
    //old apk file to build patch apk
    tinkerOldApkPath = "${bakPath}/app-debug-1018-17-58-54.apk"
    //proguard mapping file to build patch apk
    tinkerApplyMappingPath = "${bakPath}/app-debug-1018-17-32-47-mapping.txt"
    //resource R.txt to build patch apk, must input if there is resource changed
    tinkerApplyResourcePath = "${bakPath}/app-debug-1018-17-32-47-R.txt"

    //使用buildvariants修改此處app信息作爲基準包
    tinkerBuildFlavorDirectory = "${bakPath}/app-1020-11-52-37"
}

release出包可以直接在gradle命令帶上後綴-POLD_APK= -PAPPLY_MAPPING= -PAPPLY_RESOURCE=

  1. Application改造

Tinker採用了代碼框架的方案來解決應用啓動加載默認Application導致patch無法修復它。原理就是使用一個ApplicationLike代理類來完成原Application的功能,把所有原理Application中的代碼邏輯移動到ApplicationLike中,然後刪除原來的Application類通過註解讓Tinker自動生成默認Application。

@DefaultLifeCycle(application = "com.*.Application",
        flags = ShareConstants.TINKER_ENABLE_ALL,
        loadVerifyFlag = false)
public class ApplicationLike extends DefaultApplicationLike {
    @Override
    public void onBaseContextAttached(Context base) {
        super.onBaseContextAttached(base);
        //you must install multiDex whatever tinker is installed!
        MultiDex.install(base);

        TinkerManager.setTinkerApplicationLike(this);

        TinkerManager.initFastCrashProtect();
        //should set before tinker is installed
        TinkerManager.setUpgradeRetryEnable(true);

        //installTinker after load multiDex
        //or you can put com.tencent.tinker.** to main dex
        TinkerManager.installTinker(this);
    }
    
}

TinkerManager.java

public static void installTinker(ApplicationLike appLike) {
        if (isInstalled) {
            TinkerLog.w(TAG, "install tinker, but has installed, ignore");
            return;
        }
        //or you can just use DefaultLoadReporter
        LoadReporter loadReporter = new TinkerLoadReporter(appLike.getApplication());
        //or you can just use DefaultPatchReporter
        PatchReporter patchReporter = new TinkerPatchReporter(appLike.getApplication());
        //or you can just use DefaultPatchListener
        PatchListener patchListener = new TinkerPatchListener(appLike.getApplication());
        //you can set your own upgrade patch if you need
        AbstractPatch upgradePatchProcessor = new UpgradePatch();

        TinkerInstaller.install(appLike,
                loadReporter, patchReporter, patchListener,
                TinkerResultService.class, upgradePatchProcessor);

        isInstalled = true;
    }

其中參數application代表自動生成的application包名路徑,flags代表tinker作用域包括res、so、dex,loadVerifyFlag代表是否開啓加載patch前各個文件進行md5校驗,還有一個loaderClass默認是"com.tencent.tinker.loader.TinkerLoader"表示加載Tinker的主類名。

onBaseContextAttached方法裏需要初始化一些Tinker相關回調(在installTinker方法中)PatchReporter是對patch進程中合成過程的回調接口實現,LoadReporter是對主進程加載patch dex補丁過程的回調接口實現。PatchListener可以對接收到patch補丁後做自定義的check操作比如渠道檢查和存儲空間檢查。

設置AbstractResultService的實現類TinkerResultService作爲合成補丁完成後的處理重啓邏輯的IntentService。

設置AbstractPatch的實現類UpgradePatch類作爲合成patch方法tryPatch實現類。

Tinker原理

先上github官方首頁的圖

 

BaseApk就是我們的基準包,也就是渠道上線的包。

NewApk就是我們的hotfix包,包括修復的代碼資源以及so文件。

Tinker做了對應的DexDiff、ResDiff、BsDiff來產出一個patch.apk,裏面具體內容也是由lib、res和dex文件組成,assets中還有對應的dex、res和so信息

然後Tinker通過找到基準包data/app/packagename/base.apk通過DexPatch合成新的dex,並且合成一個tinker_classN.apk(其實就是包含了所有合成dex的zip包)接着在運行時通過反射把這個合成dex文件插入到PathClassLoader中的dexElements數組的前面,保證類加載時優先加載補丁dex中的class。

接下來我們就從加載patch和合成patch來弄清Tinker的整個工作流程。

Tinker源碼分析之加載補丁Patch流程

默認情況如果使用了Tinker註解產生Application可以看到它繼承了TinkerApplication

/**
 *
 * Generated application for tinker life cycle
 *
 */
public class Application extends TinkerApplication {

    public Application() {
        super(7, "com.jiuyan.infashion.ApplicationLike", "com.tencent.tinker.loader.TinkerLoader", false);
    }

}

跟蹤到TinkerApplication在方法attachBaseContext中找到最終會調用loadTinker方法來,最後反射調用了變量loaderClassName定義類中的tryLoad方法,默認是com.tencent.tinker.loader.TinkerLoader這個類中的tryLoad方法。該方法調用tryLoadPatchFilesInternal來執行相關代碼邏輯。

private void tryLoadPatchFilesInternal(TinkerApplication app, Intent resultIntent) {
    //..省略一大段校驗相關邏輯代碼
    
    //now we can load patch jar
    if (isEnabledForDex) {
        boolean loadTinkerJars = TinkerDexLoader.loadTinkerJars(app, patchVersionDirectory, oatDex, resultIntent, isSystemOTA);
        if (isSystemOTA) {
            // update fingerprint after load success
            patchInfo.fingerPrint = Build.FINGERPRINT;
            patchInfo.oatDir = loadTinkerJars ? ShareConstants.INTERPRET_DEX_OPTIMIZE_PATH : ShareConstants.DEFAULT_DEX_OPTIMIZE_PATH;
            // reset to false
            oatModeChanged = false;

            if (!SharePatchInfo.rewritePatchInfoFileWithLock(patchInfoFile, patchInfo, patchInfoLockFile)) {
                ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_REWRITE_PATCH_INFO_FAIL);
                Log.w(TAG, "tryLoadPatchFiles:onReWritePatchInfoCorrupted");
                    return;
                }
                // update oat dir
                resultIntent.putExtra(ShareIntentUtil.INTENT_PATCH_OAT_DIR, patchInfo.oatDir);
            }
            if (!loadTinkerJars) {
                Log.w(TAG, "tryLoadPatchFiles:onPatchLoadDexesFail");
                return;
            }
    }

    //now we can load patch resource
    if (isEnabledForResource) {
        boolean loadTinkerResources = TinkerResourceLoader.loadTinkerResources(app, patchVersionDirectory, resultIntent);
            if (!loadTinkerResources) {
                Log.w(TAG, "tryLoadPatchFiles:onPatchLoadResourcesFail");
                return;
            }
        }
        // kill all other process if oat mode change
        if (oatModeChanged) {
            ShareTinkerInternals.killAllOtherProcess(app);
            Log.i(TAG, "tryLoadPatchFiles:oatModeChanged, try to kill all other process");
    }
    //all is ok!
    ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_OK);
    Log.i(TAG, "tryLoadPatchFiles: load end, ok!");
    return;
}

這裏省略了非常多的Tinker校驗,一共有包括tinker自身enable屬性以及md5和文件存在等相關檢查。

先看加載dex部分,TinkerDexLoader.loadTinkerJars傳入四個參數,分別爲application,patchVersionDirectory當前patch文件目錄,oatDir當前patch的oat文件目錄,intent,當前patch是否需要進行oat(由於系統OTA更新需要dex oat重新生成緩存)。

/**
 * Load tinker JARs and add them to
 * the Application ClassLoader.
 *
 * @param application The application.
 */
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
public static boolean loadTinkerJars(final TinkerApplication application, String directory, String oatDir, Intent intentResult, boolean isSystemOTA) {
    if (loadDexList.isEmpty() && classNDexInfo.isEmpty()) {
            Log.w(TAG, "there is no dex to load");
            return true;
    }

    PathClassLoader classLoader = (PathClassLoader) TinkerDexLoader.class.getClassLoader();
    if (classLoader != null) {
            Log.i(TAG, "classloader: " + classLoader.toString());
    } else {
            Log.e(TAG, "classloader is null");
            ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_VERSION_DEX_CLASSLOADER_NULL);
            return false;
    }
    String dexPath = directory + "/" + DEX_PATH + "/";

    ArrayList<File> legalFiles = new ArrayList<>();

    for (ShareDexDiffPatchInfo info : loadDexList) {
        //for dalvik, ignore art support dex
        if (isJustArtSupportDex(info)) {
            continue;
        }

        String path = dexPath + info.realName;
        File file = new File(path);

        //...check md5
        legalFiles.add(file);
    }
    //... verify merge classN.apk
    
    File optimizeDir = new File(directory + "/" + oatDir);

    if (isSystemOTA) {
        final boolean[] parallelOTAResult = {true};
        final Throwable[] parallelOTAThrowable = new Throwable[1];
        String targetISA;
        try {
            targetISA = ShareTinkerInternals.getCurrentInstructionSet();
        } catch (Throwable throwable) {
            Log.i(TAG, "getCurrentInstructionSet fail:" + throwable);
//                try {
//                    targetISA = ShareOatUtil.getOatFileInstructionSet(testOptDexFile);
//                } catch (Throwable throwable) {
                // don't ota on the front
            deleteOutOfDateOATFile(directory);

            intentResult.putExtra(ShareIntentUtil.INTENT_PATCH_INTERPRET_EXCEPTION, throwable);
            ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_GET_OTA_INSTRUCTION_SET_EXCEPTION);
            return false;
//               }
        }

        deleteOutOfDateOATFile(directory);

        Log.w(TAG, "systemOTA, try parallel oat dexes, targetISA:" + targetISA);
        // change dir
        optimizeDir = new File(directory + "/" + INTERPRET_DEX_OPTIMIZE_PATH);

        TinkerDexOptimizer.optimizeAll(
            legalFiles, optimizeDir, true, targetISA,
            new TinkerDexOptimizer.ResultCallback() {
                //... callback
            }
        );


        if (!parallelOTAResult[0]) {
            Log.e(TAG, "parallel oat dexes failed");
            intentResult.putExtra(ShareIntentUtil.INTENT_PATCH_INTERPRET_EXCEPTION, parallelOTAThrowable[0]);
            ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_OTA_INTERPRET_ONLY_EXCEPTION);
            return false;
        }
    }
    try {
        SystemClassLoaderAdder.installDexes(application, classLoader, optimizeDir, legalFiles);
    } catch (Throwable e) {
        Log.e(TAG, "install dexes failed");
//            e.printStackTrace();
        intentResult.putExtra(ShareIntentUtil.INTENT_PATCH_EXCEPTION, e);
        ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_VERSION_DEX_LOAD_EXCEPTION);
        return false;
    }

    return true;
}

省略了幾處md5校驗代碼,首先獲取到PathClassLoader並且通過判斷系統是否art過濾出對應legalFiles,如果發現系統進行過OTA升級則通過ProcessBuilder命令行執行dex2oat進行並行的oat優化dex,最後調用installDexes來安裝dex。

 @SuppressLint("NewApi")
public static void installDexes(Application application, PathClassLoader loader, File dexOptDir, List<File> files)
        throws Throwable {
    Log.i(TAG, "installDexes dexOptDir: " + dexOptDir.getAbsolutePath() + ", dex size:" + files.size());

    if (!files.isEmpty()) {
        files = createSortedAdditionalPathEntries(files);
        ClassLoader classLoader = loader;
        if (Build.VERSION.SDK_INT >= 24 && !checkIsProtectedApp(files)) {
            classLoader = AndroidNClassLoader.inject(loader, application);
        }
        //because in dalvik, if inner class is not the same classloader with it wrapper class.
        //it won't fail at dex2opt
        if (Build.VERSION.SDK_INT >= 23) {
            V23.install(classLoader, files, dexOptDir);
        } else if (Build.VERSION.SDK_INT >= 19) {
            V19.install(classLoader, files, dexOptDir);
        } else if (Build.VERSION.SDK_INT >= 14) {
            V14.install(classLoader, files, dexOptDir);
        } else {
            V4.install(classLoader, files, dexOptDir);
        }
        //install done
        sPatchDexCount = files.size();
        Log.i(TAG, "after loaded classloader: " + classLoader + ", dex size:" + sPatchDexCount);

        if (!checkDexInstall(classLoader)) {
            //reset patch dex
            SystemClassLoaderAdder.uninstallPatchDex(classLoader);
            throw new TinkerRuntimeException(ShareConstants.CHECK_DEX_INSTALL_FAIL);
        }
    }
}

針對不同的Android版本需要對DexPathList中的dexElements生成方法makeDexElements進行適配。

主要做的事情就是獲取當前app運行時PathClassLoader的父類BaseDexClassLoader中的pathList對象,通過反射它的makePathElements方法傳入對應的path參數構造出Element[]數組對象,然後拿到pathList中的Element[]數組對象dexElements兩者進行合併排序,把patch的相關dex信息放在數組前端,最後合併數組結果賦值給pathList保證classloader優先到patch中查找加載。

Tinker源碼分析之合成補丁Patch流程

合併代碼入口

Tinker.with(context).getPatchListener().onPatchReceived(patchLocation);

傳入patch文件所在位置即可,推薦通過服務端下發下載到對應的/data/data/應用目錄下防止被三方軟件清理,onPatchReceived方法在DefaultPatchListener.java中。

@Override
public int onPatchReceived(String path) {
    File patchFile = new File(path);

    int returnCode = patchCheck(path, SharePatchFileUtil.getMD5(patchFile));

    if (returnCode == ShareConstants.ERROR_PATCH_OK) {
        TinkerPatchService.runPatchService(context, path);
    } else {
        Tinker.with(context).getLoadReporter().onLoadPatchListenerReceiveFail(new File(path), returnCode);
    }
    return returnCode;
}

先進行tinker的一些初始化配置檢查還有patch文件的md5校驗。如果check通過returnCode爲0則執行runPatchService啓動一個IntentService的子類TinkerPatchService來處理patch的合成。接下來看Service執行任務代碼:

@Override
protected void onHandleIntent(Intent intent) {
    final Context context = getApplicationContext();
    Tinker tinker = Tinker.with(context);
    tinker.getPatchReporter().onPatchServiceStart(intent);

    if (intent == null) {
        TinkerLog.e(TAG, "TinkerPatchService received a null intent, ignoring.");
        return;
    }
    String path = getPatchPathExtra(intent);
    if (path == null) {
        TinkerLog.e(TAG, "TinkerPatchService can't get the path extra, ignoring.");
        return;
    }
    File patchFile = new File(path);

    long begin = SystemClock.elapsedRealtime();
    boolean result;
    long cost;
    Throwable e = null;

    increasingPriority();
    PatchResult patchResult = new PatchResult();
    try {
        if (upgradePatchProcessor == null) {
            throw new TinkerRuntimeException("upgradePatchProcessor is null.");
        }
        result = upgradePatchProcessor.tryPatch(context, path, patchResult);
    } catch (Throwable throwable) {
        e = throwable;
        result = false;
        tinker.getPatchReporter().onPatchException(patchFile, e);
    }

    cost = SystemClock.elapsedRealtime() - begin;
    tinker.getPatchReporter().
    onPatchResult(patchFile, result, cost);

    patchResult.isSuccess = result;
    patchResult.rawPatchFilePath = path;
    patchResult.costTime = cost;
    patchResult.e = e;

    AbstractResultService.runResultService(context, patchResult, getPatchResultExtra(intent));

}

回調PatchReporter接口的onPatchServiceStart方法,然後取到patch文件同時調用increasingPriority啓動一個不可見前臺Service保活這個TinkerPatchService,最後開始合成patchupgradePatchProcessor.tryPatch。同樣省略一些常規check代碼:

@Override
public boolean tryPatch(Context context, String tempPatchPath, PatchResult patchResult) {
    Tinker manager = Tinker.with(context);
    final File patchFile = new File(tempPatchPath);
    //...省略
    
    //check ok, we can real recover a new patch
    final String patchDirectory = manager.getPatchDirectory().getAbsolutePath();

    File patchInfoLockFile = SharePatchFileUtil.getPatchInfoLockFile(patchDirectory);
    File patchInfoFile = SharePatchFileUtil.getPatchInfoFile(patchDirectory);

    SharePatchInfo oldInfo = SharePatchInfo.readAndCheckPropertyWithLock(patchInfoFile, patchInfoLockFile);

    //it is a new patch, so we should not find a exist
    SharePatchInfo newInfo;

    //already have patch
    if (oldInfo != null) {
        if (oldInfo.oldVersion == null || oldInfo.newVersion == null || oldInfo.oatDir == null) {
            TinkerLog.e(TAG, "UpgradePatch tryPatch:onPatchInfoCorrupted");
            manager.getPatchReporter().onPatchInfoCorrupted(patchFile, oldInfo.oldVersion, oldInfo.newVersion);
            return false;
        }

        if (!SharePatchFileUtil.checkIfMd5Valid(patchMd5)) {
            TinkerLog.e(TAG, "UpgradePatch tryPatch:onPatchVersionCheckFail md5 %s is valid", patchMd5);
            manager.getPatchReporter().onPatchVersionCheckFail(patchFile, oldInfo, patchMd5);
            return false;
        }
        // if it is interpret now, use changing flag to wait main process
        final String finalOatDir = oldInfo.oatDir.equals(ShareConstants.INTERPRET_DEX_OPTIMIZE_PATH)
            ? ShareConstants.CHANING_DEX_OPTIMIZE_PATH : oldInfo.oatDir;
        newInfo = new SharePatchInfo(oldInfo.oldVersion, patchMd5, Build.FINGERPRINT, finalOatDir);
    } else {
        newInfo = new SharePatchInfo("", patchMd5, Build.FINGERPRINT, ShareConstants.DEFAULT_DEX_OPTIMIZE_PATH);
    }
    
    //it is a new patch, we first delete if there is any files
    //don't delete dir for faster retry
//        SharePatchFileUtil.deleteDir(patchVersionDirectory);
    final String patchName = SharePatchFileUtil.getPatchVersionDirectory(patchMd5);

    final String patchVersionDirectory = patchDirectory + "/" + patchName;

    TinkerLog.i(TAG, "UpgradePatch tryPatch:patchVersionDirectory:%s", patchVersionDirectory);

    //copy file
    File destPatchFile = new File(patchVersionDirectory + "/" + SharePatchFileUtil.getPatchVersionFile(patchMd5));

    //...省略
    
    if (!DexDiffPatchInternal.tryRecoverDexFiles(manager, signatureCheck, context, patchVersionDirectory, destPatchFile)) {
        TinkerLog.e(TAG, "UpgradePatch tryPatch:new patch recover, try patch dex failed");
        return false;
    }

    if (!BsDiffPatchInternal.tryRecoverLibraryFiles(manager, signatureCheck, context, patchVersionDirectory, destPatchFile)) {
        TinkerLog.e(TAG, "UpgradePatch tryPatch:new patch recover, try patch library failed");
        return false;
    }

    if (!ResDiffPatchInternal.tryRecoverResourceFiles(manager, signatureCheck, context, patchVersionDirectory, destPatchFile)) {
        TinkerLog.e(TAG, "UpgradePatch tryPatch:new patch recover, try patch resource failed");
        return false;
    }
    
    //...省略
}

1.檢查是否有之前的patch信息oldInfo,查看舊補丁是否正在執行oat過程,後續會等待主進程oat執行完畢。 2.拷貝new patch到app的data目錄的tinker目錄下,防止被三方軟件刪除。 3.分別判斷執行tryRecoverDexFiles合成dex,tryRecoverLibraryFiles合成so以及tryRecoverResourceFiles合成資源。

主要看下dex合成過程,這也是我們最關心的地方。

protected static boolean tryRecoverDexFiles(Tinker manager, ShareSecurityCheck checker, Context context,
                                                String patchVersionDirectory, File patchFile) {
    if (!manager.isEnabledForDex()) {
        TinkerLog.w(TAG, "patch recover, dex is not enabled");
            return true;
    }
    String dexMeta = checker.getMetaContentMap().get(DEX_META_FILE);

    if (dexMeta == null) {
        TinkerLog.w(TAG, "patch recover, dex is not contained");
        return true;
    }

    long begin = SystemClock.elapsedRealtime();
    boolean result = patchDexExtractViaDexDiff(context, patchVersionDirectory, dexMeta, patchFile);
    long cost = SystemClock.elapsedRealtime() - begin;
    TinkerLog.i(TAG, "recover dex result:%b, cost:%d", result, cost);
    return result;
}

讀取patch包assets/dex_meta.txt信息轉換成String,進入patchDexExtractViaDexDiff方法。

private static boolean patchDexExtractViaDexDiff(Context context, String patchVersionDirectory, String meta, final File patchFile) {
        String dir = patchVersionDirectory + "/" + DEX_PATH + "/";

    if (!extractDexDiffInternals(context, dir, meta, patchFile, TYPE_DEX)) {
        TinkerLog.w(TAG, "patch recover, extractDiffInternals fail");
        return false;
    }

    File dexFiles = new File(dir);
    File[] files = dexFiles.listFiles();
    List<File> dexList = files != null ? Arrays.asList(files) : null;

    final String optimizeDexDirectory = patchVersionDirectory + "/" + DEX_OPTIMIZE_PATH + "/";
    return dexOptimizeDexFiles(context, dexList, optimizeDexDirectory, patchFile);

}

首先執行方法extractDexDiffInternals傳入了合成後dex路徑,前面讀取的dex_meta信息,patch文件以及type類型dex。爲了節約篇幅只提取了主要的代碼,詳細代碼參考github。

private static boolean extractDexDiffInternals(Context context, String dir, String meta, File patchFile, int type) {
    //parse
    patchList.clear();
    ShareDexDiffPatchInfo.parseDexDiffPatchInfo(meta, patchList);    
    //獲取base.apk
    String apkPath = applicationInfo.sourceDir;
    apk = new ZipFile(apkPath);
    patch = new ZipFile(patchFile);
    for (ShareDexDiffPatchInfo info : patchList) {
        String patchRealPath;
        if (infoPath.equals("")) {
            patchRealPath = info.rawName;
        } else {
            patchRealPath = info.path + "/" + info.rawName;
        }
        File extractedFile = new File(dir + info.realName);
        //..省略
        
        ZipEntry patchFileEntry = patch.getEntry(patchRealPath);
        ZipEntry rawApkFileEntry = apk.getEntry(patchRealPath);
        
        patchDexFile(apk, patch, rawApkFileEntry, patchFileEntry, info, extractedFile);
    }
    
    if (!mergeClassNDexFiles(context, patchFile, dir)) {
        return false;
    }
}

1.解析dex_meta內容

對應的

ShareDexDiffPatchInfo

信息

final String name = kv[0].trim();
final String path = kv[1].trim();
final String destMd5InDvm = kv[2].trim();
final String destMd5InArt = kv[3].trim();
final String dexDiffMd5 = kv[4].trim();
final String oldDexCrc = kv[5].trim();
final String newDexCrc = kv[6].trim();
final String dexMode = kv[7].trim();

2.循環遍歷獲取到patch中各個classes.dex的crc和md5信息以及一大片校驗代碼,調用patchDexFile方法對base.apk和patch中的dex做合併生成新的dex。

3.把合成的dex壓縮爲一個tinker_classN.apk

接下來看patchDexFile方法,同樣只提取了關鍵代碼。

private static void patchDexFile(
        ZipFile baseApk, ZipFile patchPkg, ZipEntry oldDexEntry, ZipEntry patchFileEntry,
        ShareDexDiffPatchInfo patchInfo, File patchedDexFile) throws IOException {
    InputStream oldDexStream = null;
    InputStream patchFileStream = null;

    oldDexStream = new BufferedInputStream(baseApk.getInputStream(oldDexEntry));
    patchFileStream = (patchFileEntry != null ? new BufferedInputStream(patchPkg.getInputStream(patchFileEntry)) : null);
    
    //...省略判斷dex是否是jar類型或者是raw類型,做不同處理

    new DexPatchApplier(oldDexStream, patchFileStream).executeAndSaveTo(patchedDexFile);    
}

下面是github官網上對raw和jar區別的解釋

Tinker中的dex配置'raw'與'jar'模式應該如何選擇? 它們應該說各有優劣勢,大概應該有以下幾條原則: 如果你的minSdkVersion小於14, 那你務必要選擇'jar'模式; 以一個10M的dex爲例,它壓縮成jar大約爲4M,即'jar'模式能節省6M的ROM空間。 對於'jar'模式,我們需要驗證壓縮包流中dex的md5,這會更耗時,在小米2S上數據大約爲'raw'模式126ms, 'jar'模式爲246ms。 因爲在合成過程中我們已經校驗了各個文件的Md5,並將它們存放在/data/data/..目錄中。默認每次加載時我們並不會去校驗tinker文件的Md5,但是你也可通過開啓loadVerifyFlag強制每次加載時校驗,但是這會帶來一定的時間損耗。 簡單來說,'jar'模式更省空間,但是運行時校驗的耗時大約爲'raw'模式的兩倍。如果你沒有打開運行時校驗,推薦使用'jar'模式。

最後通過ZipFile拿到base.apk和patch中對應dex文件進行合成爲patchedDexFile。核心部分是如何把差分的dex和基準dex做合成處理產生新的dex,這部分涉及到了dex文件結構、DexDiff和DexPatch算法

扣扣掃碼加入粉絲羣,領取福利

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