Tinker熱修復(fù)原理淺析

Tinker實(shí)現(xiàn)原理和源碼分析

Tinker工程結(jié)構(gòu)

直接從github上clone Tinker的源碼進(jìn)行食用如下:


image

接入流程

  1. gradle相關(guān)配置主項(xiàng)目中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編譯階段會(huì)判斷一個(gè)TinkerId的字段,該字段默認(rèn)由git提交記錄生成HEAD(git rev-parse --short HEAD)而且是在rootproject中執(zhí)行的git命令,所以個(gè)別工程可能在rootproject目錄沒有g(shù)it init過,可以選擇在那初始化git或者自定義gradle修改gitSha方法。

出包還是使用正常的build過程,測(cè)試階段選擇assembleDebug,Tinker產(chǎn)出patch使用gradle tinkerPatchDebug同樣也支持Flavor和Variant,Tiner會(huì)在主工程build目錄下創(chuàng)建bakApk,下面會(huì)有一個(gè)app-yydd-hh-mm-ss的目錄里面對(duì)應(yīng)有Favor子目錄里面包含了通過assemble出的apk包。在build目錄下的outputs中有tinkerPatch里面同樣也區(qū)分了build variant產(chǎn)物。

image

需要注意的是在debug出包測(cè)試過程中需要修改gradle的參數(shù)

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信息作為基準(zhǔn)包
    tinkerBuildFlavorDirectory = "${bakPath}/app-1020-11-52-37"
}

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

  1. Application改造

Tinker采用了代碼框架的方案來解決應(yīng)用啟動(dòng)加載默認(rèn)Application導(dǎo)致patch無法修復(fù)它。原理就是使用一個(gè)ApplicationLike代理類來完成原Application的功能,把所有原理Application中的代碼邏輯移動(dòng)到ApplicationLike中,然后刪除原來的Application類通過注解讓Tinker自動(dòng)生成默認(rèn)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;
    }

其中參數(shù)application代表自動(dòng)生成的application包名路徑,flags代表tinker作用域包括res、so、dex,loadVerifyFlag代表是否開啟加載patch前各個(gè)文件進(jìn)行md5校驗(yàn),還有一個(gè)loaderClass默認(rèn)是"com.tencent.tinker.loader.TinkerLoader"表示加載Tinker的主類名。

onBaseContextAttached方法里需要初始化一些Tinker相關(guān)回調(diào)(在installTinker方法中)PatchReporter是對(duì)patch進(jìn)程中合成過程的回調(diào)接口實(shí)現(xiàn),LoadReporter是對(duì)主進(jìn)程加載patch dex補(bǔ)丁過程的回調(diào)接口實(shí)現(xiàn)。PatchListener可以對(duì)接收到patch補(bǔ)丁后做自定義的check操作比如渠道檢查和存儲(chǔ)空間檢查。

設(shè)置AbstractResultService的實(shí)現(xiàn)類TinkerResultService作為合成補(bǔ)丁完成后的處理重啟邏輯的IntentService。

設(shè)置AbstractPatch的實(shí)現(xiàn)類UpgradePatch類作為合成patch方法tryPatch實(shí)現(xiàn)類。

Tinker原理

先上github官方首頁的圖


image

BaseApk就是我們的基準(zhǔn)包,也就是渠道上線的包。

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

Tinker做了對(duì)應(yīng)的DexDiff、ResDiff、BsDiff來產(chǎn)出一個(gè)patch.apk,里面具體內(nèi)容也是由lib、res和dex文件組成,assets中還有對(duì)應(yīng)的dex、res和so信息


image

然后Tinker通過找到基準(zhǔn)包data/app/packagename/base.apk通過DexPatch合成新的dex,并且合成一個(gè)tinker_classN.apk(其實(shí)就是包含了所有合成dex的zip包)接著在運(yùn)行時(shí)通過反射把這個(gè)合成dex文件插入到PathClassLoader中的dexElements數(shù)組的前面,保證類加載時(shí)優(yōu)先加載補(bǔ)丁dex中的class。

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

Tinker源碼分析之加載補(bǔ)丁Patch流程

默認(rèn)情況如果使用了Tinker注解產(chǎn)生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中找到最終會(huì)調(diào)用loadTinker方法來,最后反射調(diào)用了變量loaderClassName定義類中的tryLoad方法,默認(rèn)是com.tencent.tinker.loader.TinkerLoader這個(gè)類中的tryLoad方法。該方法調(diào)用tryLoadPatchFilesInternal來執(zhí)行相關(guān)代碼邏輯。

private void tryLoadPatchFilesInternal(TinkerApplication app, Intent resultIntent) {
    //..省略一大段校驗(yàn)相關(guān)邏輯代碼
    
    //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校驗(yàn),一共有包括tinker自身enable屬性以及md5和文件存在等相關(guān)檢查。

先看加載dex部分,TinkerDexLoader.loadTinkerJars傳入四個(gè)參數(shù),分別為application,patchVersionDirectory當(dāng)前patch文件目錄,oatDir當(dāng)前patch的oat文件目錄,intent,當(dāng)前patch是否需要進(jìn)行oat(由于系統(tǒng)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校驗(yàn)代碼,首先獲取到PathClassLoader并且通過判斷系統(tǒng)是否art過濾出對(duì)應(yīng)legalFiles,如果發(fā)現(xiàn)系統(tǒng)進(jìn)行過OTA升級(jí)則通過ProcessBuilder命令行執(zhí)行dex2oat進(jìn)行并行的oat優(yōu)化dex,最后調(diào)用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);
        }
    }
}

針對(duì)不同的Android版本需要對(duì)DexPathList中的dexElements生成方法makeDexElements進(jìn)行適配。

主要做的事情就是獲取當(dāng)前app運(yùn)行時(shí)PathClassLoader的父類BaseDexClassLoader中的pathList對(duì)象,通過反射它的makePathElements方法傳入對(duì)應(yīng)的path參數(shù)構(gòu)造出Element[]數(shù)組對(duì)象,然后拿到pathList中的Element[]數(shù)組對(duì)象dexElements兩者進(jìn)行合并排序,把patch的相關(guān)dex信息放在數(shù)組前端,最后合并數(shù)組結(jié)果賦值給pathList保證classloader優(yōu)先到patch中查找加載。

Tinker源碼分析之合成補(bǔ)丁Patch流程

合并代碼入口

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

傳入patch文件所在位置即可,推薦通過服務(wù)端下發(fā)下載到對(duì)應(yīng)的/data/data/應(yīng)用目錄下防止被三方軟件清理,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;
}

先進(jìn)行tinker的一些初始化配置檢查還有patch文件的md5校驗(yàn)。如果check通過returnCode為0則執(zhí)行runPatchService啟動(dòng)一個(gè)IntentService的子類TinkerPatchService來處理patch的合成。接下來看Service執(zhí)行任務(wù)代碼:

@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));

}

回調(diào)PatchReporter接口的onPatchServiceStart方法,然后取到patch文件同時(shí)調(diào)用increasingPriority啟動(dòng)一個(gè)不可見前臺(tái)Service?;?/em>這個(gè)TinkerPatchService,最后開始合成patchupgradePatchProcessor.tryPatch。同樣省略一些常規(guī)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,查看舊補(bǔ)丁是否正在執(zhí)行oat過程,后續(xù)會(huì)等待主進(jìn)程oat執(zhí)行完畢。
2.拷貝new patch到app的data目錄的tinker目錄下,防止被三方軟件刪除。
3.分別判斷執(zhí)行tryRecoverDexFiles合成dex,tryRecoverLibraryFiles合成so以及tryRecoverResourceFiles合成資源。

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

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信息轉(zhuǎn)換成String,進(jìn)入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);

}

首先執(zhí)行方法extractDexDiffInternals傳入了合成后dex路徑,前面讀取的dex_meta信息,patch文件以及type類型dex。為了節(jié)約篇幅只提取了主要的代碼,詳細(xì)代碼參考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內(nèi)容

image

對(duì)應(yīng)的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.循環(huán)遍歷獲取到patch中各個(gè)classes.dex的crc和md5信息以及一大片校驗(yàn)代碼,調(diào)用patchDexFile方法對(duì)base.apk和patch中的dex做合并生成新的dex。

3.把合成的dex壓縮為一個(gè)tinker_classN.apk

接下來看patchDexFile方法,同樣只提取了關(guān)鍵代碼。

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官網(wǎng)上對(duì)raw和jar區(qū)別的解釋

Tinker中的dex配置'raw'與'jar'模式應(yīng)該如何選擇?
它們應(yīng)該說各有優(yōu)劣勢(shì),大概應(yīng)該有以下幾條原則:
如果你的minSdkVersion小于14, 那你務(wù)必要選擇'jar'模式;
以一個(gè)10M的dex為例,它壓縮成jar大約為4M,即'jar'模式能節(jié)省6M的ROM空間。
對(duì)于'jar'模式,我們需要驗(yàn)證壓縮包流中dex的md5,這會(huì)更耗時(shí),在小米2S上數(shù)據(jù)大約為'raw'模式126ms, 'jar'模式為246ms。
因?yàn)樵诤铣蛇^程中我們已經(jīng)校驗(yàn)了各個(gè)文件的Md5,并將它們存放在/data/data/..目錄中。默認(rèn)每次加載時(shí)我們并不會(huì)去校驗(yàn)tinker文件的Md5,但是你也可通過開啟loadVerifyFlag強(qiáng)制每次加載時(shí)校驗(yàn),但是這會(huì)帶來一定的時(shí)間損耗。
簡(jiǎn)單來說,'jar'模式更省空間,但是運(yùn)行時(shí)校驗(yàn)的耗時(shí)大約為'raw'模式的兩倍。如果你沒有打開運(yùn)行時(shí)校驗(yàn),推薦使用'jar'模式。

最后通過ZipFile拿到base.apk和patch中對(duì)應(yīng)dex文件進(jìn)行合成為patchedDexFile。核心部分是如何把差分的dex和基準(zhǔn)dex做合成處理產(chǎn)生新的dex,這部分涉及到了dex文件結(jié)構(gòu)、DexDiff和DexPatch算法,官方wiki里提供了一篇分析文章

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Tinker熱修復(fù)原理分析 熱補(bǔ)丁技術(shù)是在用戶不需要重新安裝應(yīng)用的情況下實(shí)現(xiàn)應(yīng)用更新,可快速解決一些線上問題。熱補(bǔ)...
    嘎啦果安卓獸閱讀 11,632評(píng)論 2 22
  • 前言 在 Tinker學(xué)習(xí)計(jì)劃(1)-Tinker的集成 這邊文章中我們首先學(xué)習(xí)了如何去集成Tinker熱更新框架...
    徐正峰閱讀 2,367評(píng)論 0 3
  • Tinker使用 前言 寫在前面的話,在上家公司一直在主導(dǎo)組件框架的開發(fā),所以對(duì)Android領(lǐng)域組件化,熱更新的...
    徐正峰閱讀 2,066評(píng)論 6 6
  • 一生只愛自廝殺 心字成灰最恰 拆不掉,解不開,亂窠臼,一堆麻 也說離愁,沒有離愁 也愛凡塵,貪戀溫柔 終究得撒手,...
    蛇神閱讀 114評(píng)論 0 0
  • 有無數(shù)次,想好好寫點(diǎn)東西,腦袋中總是一片空白。寫點(diǎn)什么東西,大家愛看呢?我自己對(duì)哪塊事物有比較深的感觸,能寫出一...
    小蟻人閱讀 222評(píng)論 1 1

友情鏈接更多精彩內(nèi)容