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的核心庫(kù)
    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目錄沒(méi)有g(shù)it init過(guò)靴庆,可以選擇在那初始化git或者自定義gradle修改gitSha方法。

出包還是使用正常的build過(guò)程,測(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子目錄里面包含了通過(guò)assemble出的apk包沽翔。在build目錄下的outputs中有tinkerPatch里面同樣也區(qū)分了build variant產(chǎn)物。

image

需要注意的是在debug出包測(cè)試過(guò)程中需要修改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采用了代碼框架的方案來(lái)解決應(yīng)用啟動(dòng)加載默認(rèn)Application導(dǎo)致patch無(wú)法修復(fù)它窿凤。原理就是使用一個(gè)ApplicationLike代理類來(lái)完成原Application的功能仅偎,把所有原理Application中的代碼邏輯移動(dòng)到ApplicationLike中,然后刪除原來(lái)的Application類通過(guò)注解讓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代表是否開(kāi)啟加載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)程中合成過(guò)程的回調(diào)接口實(shí)現(xiàn)寝并,LoadReporter是對(duì)主進(jìn)程加載patch dex補(bǔ)丁過(guò)程的回調(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官方首頁(yè)的圖


image

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

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

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


image

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

接下來(lái)我們就從加載patch和合成patch來(lái)弄清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方法來(lái),最后反射調(diào)用了變量loaderClassName定義類中的tryLoad方法,默認(rèn)是com.tencent.tinker.loader.TinkerLoader這個(gè)類中的tryLoad方法裸扶。該方法調(diào)用tryLoadPatchFilesInternal來(lái)執(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并且通過(guò)判斷系統(tǒng)是否art過(guò)濾出對(duì)應(yīng)legalFiles戒傻,如果發(fā)現(xiàn)系統(tǒng)進(jìn)行過(guò)OTA升級(jí)則通過(guò)ProcessBuilder命令行執(zhí)行dex2oat進(jìn)行并行的oat優(yōu)化dex,最后調(diào)用installDexes來(lái)安裝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ì)象,通過(guò)反射它的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文件所在位置即可口蝠,推薦通過(guò)服務(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通過(guò)returnCode為0則執(zhí)行runPatchService啟動(dòng)一個(gè)IntentService的子類TinkerPatchService來(lái)處理patch的合成。接下來(lái)看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è)不可見(jiàn)前臺(tái)Service泵挤矗活這個(gè)TinkerPatchService,最后開(kāi)始合成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過(guò)程,后續(xù)會(huì)等待主進(jìn)程oat執(zhí)行完畢寸五。
2.拷貝new patch到app的data目錄的tinker目錄下,防止被三方軟件刪除孵睬。
3.分別判斷執(zhí)行tryRecoverDexFiles合成dex播歼,tryRecoverLibraryFiles合成so以及tryRecoverResourceFiles合成資源。

主要看下dex合成過(guò)程,這也是我們最關(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

接下來(lái)看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)該說(shuō)各有優(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)樵诤铣蛇^(guò)程中我們已經(jīng)校驗(yàn)了各個(gè)文件的Md5,并將它們存放在/data/data/..目錄中闲擦。默認(rèn)每次加載時(shí)我們并不會(huì)去校驗(yàn)tinker文件的Md5,但是你也可通過(guò)開(kāi)啟loadVerifyFlag強(qiáng)制每次加載時(shí)校驗(yàn)慢味,但是這會(huì)帶來(lái)一定的時(shí)間損耗。
簡(jiǎn)單來(lái)說(shuō)墅冷,'jar'模式更省空間贮缕,但是運(yùn)行時(shí)校驗(yàn)的耗時(shí)大約為'raw'模式的兩倍。如果你沒(méi)有打開(kāi)運(yùn)行時(shí)校驗(yàn)俺榆,推薦使用'jar'模式感昼。

最后通過(guò)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)系作者
  • 序言:七十年代末宵溅,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子上炎,更是在濱河造成了極大的恐慌恃逻,老刑警劉巖雏搂,帶你破解...
    沈念sama閱讀 221,888評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異寇损,居然都是意外死亡凸郑,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,677評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門矛市,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)芙沥,“玉大人,你說(shuō)我怎么就攤上這事浊吏《颍” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 168,386評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵找田,是天一觀的道長(zhǎng)歌憨。 經(jīng)常有香客問(wèn)我,道長(zhǎng)墩衙,這世上最難降的妖魔是什么躺孝? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,726評(píng)論 1 297
  • 正文 為了忘掉前任,我火速辦了婚禮底桂,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘惧眠。我一直安慰自己籽懦,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,729評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布氛魁。 她就那樣靜靜地躺著暮顺,像睡著了一般。 火紅的嫁衣襯著肌膚如雪秀存。 梳的紋絲不亂的頭發(fā)上捶码,一...
    開(kāi)封第一講書(shū)人閱讀 52,337評(píng)論 1 310
  • 那天,我揣著相機(jī)與錄音或链,去河邊找鬼惫恼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛澳盐,可吹牛的內(nèi)容都是我干的祈纯。 我是一名探鬼主播,決...
    沈念sama閱讀 40,902評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼叼耙,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼腕窥!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起筛婉,我...
    開(kāi)封第一講書(shū)人閱讀 39,807評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤簇爆,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體入蛆,經(jīng)...
    沈念sama閱讀 46,349評(píng)論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡响蓉,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,439評(píng)論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了安寺。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片厕妖。...
    茶點(diǎn)故事閱讀 40,567評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖挑庶,靈堂內(nèi)的尸體忽然破棺而出言秸,到底是詐尸還是另有隱情,我是刑警寧澤迎捺,帶...
    沈念sama閱讀 36,242評(píng)論 5 350
  • 正文 年R本政府宣布举畸,位于F島的核電站,受9級(jí)特大地震影響凳枝,放射性物質(zhì)發(fā)生泄漏抄沮。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,933評(píng)論 3 334
  • 文/蒙蒙 一岖瑰、第九天 我趴在偏房一處隱蔽的房頂上張望叛买。 院中可真熱鬧,春花似錦蹋订、人聲如沸率挣。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,420評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)椒功。三九已至,卻和暖如春智什,著一層夾襖步出監(jiān)牢的瞬間动漾,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,531評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工荠锭, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留旱眯,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,995評(píng)論 3 377
  • 正文 我出身青樓证九,卻偏偏與公主長(zhǎng)得像键思,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子甫贯,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,585評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容

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