Tinker接入及原始碼分析(三)

yangxi_001發表於2017-02-10

該系列文章分析基於 Tinker1.7.6 版本

Tinker專案地址:https://github.com/Tencent/tinker

Tinker接入及原始碼分析(一):簡單介紹以及如何接入

Tinker接入及原始碼分析(二):載入補丁原始碼分析

Tinker接入及原始碼分析(三):合成補丁原始碼分析

上篇文章分析了載入補丁的原始碼,本篇文章會繼續分析tinker初始化過程以及合成補丁的過程。

之前也說過,使用Tinker之前必須通過如下程式碼初始化Tinker:

TinkerInstaller.install(applicationLike);

這是最簡單的初始化方法,也支援很多自定義引數,等我們分析完預設的情況,自定義引數也就好理解了。

先看一下這個方法的實現:

/**
     * install tinker with default config, you must install tinker before you use their api
     * or you can just use {@link TinkerApplicationHelper}'s api
     *
     * @param applicationLike
     */
    public static Tinker install(ApplicationLike applicationLike) {
        Tinker tinker = new Tinker.Builder(applicationLike.getApplication()).build();
        Tinker.create(tinker);
        tinker.install(applicationLike.getTinkerResultIntent());
        return tinker;
    }

Tinker自定義引數很多,所以這裡使用了Builder模式初始化Tinker,這裡主要看一下Builder類裡面的預設實現,後面分析會用到這些預設引數:

public static class Builder {
        private final Context context;
        private final boolean mainProcess;
        private final boolean patchProcess;

        private int status = -1;
        private LoadReporter  loadReporter;
        private PatchReporter patchReporter;
        private PatchListener listener;
        private File          patchDirectory;
        private File          patchInfoFile;
        private File          patchInfoLockFile;
        private Boolean       tinkerLoadVerifyFlag;

        /**
         * Start building a new {@link Tinker} instance.
         */
        public Builder(Context context) {
            if (context == null) {
                throw new TinkerRuntimeException("Context must not be null.");
            }
            this.context = context;
            this.mainProcess = TinkerServiceInternals.isInMainProcess(context);
            this.patchProcess = TinkerServiceInternals.isInTinkerPatchServiceProcess(context);
            this.patchDirectory = SharePatchFileUtil.getPatchDirectory(context);
            if (this.patchDirectory == null) {
                TinkerLog.e(TAG, "patchDirectory is null!");
                return;
            }
            this.patchInfoFile = SharePatchFileUtil.getPatchInfoFile(patchDirectory.getAbsolutePath());
            this.patchInfoLockFile = SharePatchFileUtil.getPatchInfoLockFile(patchDirectory.getAbsolutePath());
            TinkerLog.w(TAG, "tinker patch directory: %s", patchDirectory);
        }

        //省略了set方法

        public Tinker build() {
            if (status == -1) {
                status = ShareConstants.TINKER_ENABLE_ALL;
            }
            if (loadReporter == null) {
                loadReporter = new DefaultLoadReporter(context);
            }
            if (patchReporter == null) {
                patchReporter = new DefaultPatchReporter(context);
            }
            if (listener == null) {
                listener = new DefaultPatchListener(context);
            }
            if (tinkerLoadVerifyFlag == null) {
                tinkerLoadVerifyFlag = false;
            }
            return new Tinker(context, status, loadReporter, patchReporter, listener, patchDirectory,
                patchInfoFile, patchInfoLockFile, mainProcess, patchProcess, tinkerLoadVerifyFlag);
        }
    }

上面程式碼省略了set方法,我們只關注預設設定。其中mainProcess,patchProcess判斷當前是否是應用程式和補丁合成程式。loadReporter,patchReporter 顧名思義是一些過程的回撥。PatchListener 是我們關注的重點,也是補丁合成的入口,它的預設實現是DefaultPatchListener,下面分析會用到。

patchDirectory,patchInfoFile,patchInfoLockFile分別是:

  • /data/data/package_name/tinker/
  •  /data/data/package_name/tinker/patch.info
  • /data/data/package_name/tinker/info.lock

tinkerLoadVerifyFlag是新建Application時傳進去的引數,用於判斷是否每次載入都做md5校驗。

初始化好Tinker之後再呼叫Tinker.create(tinker);

/**
     * create custom tinker by {@link Tinker.Builder}
     * please do it when very first your app start.
     *
     * @param tinker
     */
    public static void create(Tinker tinker) {
        if (sInstance != null) {
            throw new TinkerRuntimeException("Tinker instance is already set.");
        }
        sInstance = tinker;
    }

sInstance是靜態變數,保證Tinker是單例的,並且只初始化一次。

最後呼叫tinker.install(applicationLike.getTinkerResultIntent());

public void install(Intent intentResult) {
        install(intentResult, DefaultTinkerResultService.class, new UpgradePatch());
}

    /**
     * you must install tinker first!!
     *
     * @param intentResult
     * @param serviceClass
     * @param upgradePatch
     */
    public void install(Intent intentResult, Class<? extends AbstractResultService> serviceClass,
                        AbstractPatch upgradePatch) {
        sInstalled = true;
        TinkerPatchService.setPatchProcessor(upgradePatch, serviceClass);

        if (!isTinkerEnabled()) {
            TinkerLog.e(TAG, "tinker is disabled");
            return;
        }
        if (intentResult == null) {
            throw new TinkerRuntimeException("intentResult must not be null.");
        }
        tinkerLoadResult = new TinkerLoadResult();
        tinkerLoadResult.parseTinkerResult(getContext(), intentResult);
        //after load code set
        loadReporter.onLoadResult(patchDirectory, tinkerLoadResult.loadCode, tinkerLoadResult.costTime);

        if (!loaded) {
            TinkerLog.w(TAG, "tinker load fail!");
        }
}

這裡值得注意的是install方法的後面兩個引數,serviceClass 是用於補丁合成成功後啟動的Service來處理合成結果,upgradePatch 是真正合成補丁的類,分別提供了預設實現DefaultTinkerResultService和UpgradePatch,這兩個引數也支援自定義。在install方法中會呼叫TinkerPatchService.setPatchProcessor(upgradePatch, serviceClass); 將這兩個引數通過靜態方法設定給TinkerPatchService類,TinkerPatchService類是合成補丁的Service,並且執行在新的程式中。

這樣就完成了Tinker的初始化。

第一篇文章介紹過使用以下方法來載入補丁:

TinkerInstaller.onReceiveUpgradePatch(context, patchLocation)

看一下具體實現:

/**
     * new patch file to install, try install them with :patch process
     *
     * @param context
     * @param patchLocation
     */
    public static void onReceiveUpgradePatch(Context context, String patchLocation) {
        Tinker.with(context).getPatchListener().onPatchReceived(patchLocation);
    }

這裡會呼叫PatchListener,還記得之前這個引數的預設實現嗎?

我們來看一下DefaultPatchListener的onPatchReceived方法:

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

patchCheck(patch)方法會判斷是否開啟了Tinker,以及補丁檔案是否存在。然後會啟動TinkerPatchService:TinkerPatchService.runPatchService(context, path);

TinkerPatchService是繼承於IntentService,IntentService與普通Service的區別這裡就不說了,看它的onHandleIntent方法,繼承IntentService必須實現該方法,並且可以進行耗時操作:

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

    }

先進行了引數校驗,increasingPriority(),這個方法用於提高程式優先順序,防止被回收:

private void increasingPriority() {
//        if (Build.VERSION.SDK_INT > 24) {
//            TinkerLog.i(TAG, "for Android 7.1, we just ignore increasingPriority job");
//            return;
//        }
        TinkerLog.i(TAG, "try to increase patch process priority");
        try {
            Notification notification = new Notification();
            if (Build.VERSION.SDK_INT < 18) {
                startForeground(notificationId, notification);
            } else {
                startForeground(notificationId, notification);
                // start InnerService
                startService(new Intent(this, InnerService.class));
            }
        } catch (Throwable e) {
            TinkerLog.i(TAG, "try to increase patch process priority error:" + e);
        }
    }

然後呼叫result = upgradePatchProcessor.tryPatch(context, path, patchResult);進行合成補丁,返回一個結果碼,這裡下面再詳細說,先繼續往下看, 最後會啟動另一個Service:

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

這個Service就是之前傳進來的DefaultTinkerResultService,並且將合成結果帶給它回撥onPatchResult方法:

public class DefaultTinkerResultService extends AbstractResultService {
    private static final String TAG = "Tinker.DefaultTinkerResultService";

    /**
     * we may want to use the new patch just now!!
     *
     * @param result
     */
    @Override
    public void onPatchResult(PatchResult result) {
        if (result == null) {
            TinkerLog.e(TAG, "DefaultTinkerResultService received null result!!!!");
            return;
        }
        TinkerLog.i(TAG, "DefaultTinkerResultService received a result:%s ", result.toString());

        //first, we want to kill the recover process
        TinkerServiceInternals.killTinkerPatchServiceProcess(getApplicationContext());

        // if success and newPatch, it is nice to delete the raw file, and restart at once
        // only main process can load an upgrade patch!
        if (result.isSuccess) {
            File rawFile = new File(result.rawPatchFilePath);
            if (rawFile.exists()) {
                TinkerLog.i(TAG, "save delete raw patch file");
                SharePatchFileUtil.safeDeleteFile(rawFile);
            }
            if (checkIfNeedKill(result)) {
                android.os.Process.killProcess(android.os.Process.myPid());
            } else {
                TinkerLog.i(TAG, "I have already install the newly patch version!");
            }
        }
    }

    public boolean checkIfNeedKill(PatchResult result) {
        Tinker tinker = Tinker.with(getApplicationContext());
        if (tinker.isTinkerLoaded()) {
            TinkerLoadResult tinkerLoadResult = tinker.getTinkerLoadResultIfPresent();
            if (tinkerLoadResult != null) {
                String currentVersion = tinkerLoadResult.currentVersion;
                if (result.patchVersion != null && result.patchVersion.equals(currentVersion)) {
                    return false;
                }
            }
        }
        return true;
    }
}

在onPatchResult方法中會殺死補丁合成的程式,如果補丁合成成功,會將原始資料刪掉,並且殺死當前程式。當然使用者也可以自定義這個類,實現更好的邏輯,比如不直接殺死當前程式,而是當使用者退出應用,切到後臺,或者關閉螢幕的時候殺死應用,達到重啟的目的,具體實現可以參考Simple中的實現。這樣整個補丁的合成過程就結束了。目前為止大致Tinker初始化以及補丁合成流程已經講完了,有興趣的繼續往下看真正合成補丁的呼叫

result = upgradePatchProcessor.tryPatch(context, path, patchResult);

還記得之前初始化的方法嗎:

public void install(Intent intentResult) {
        install(intentResult, DefaultTinkerResultService.class, new UpgradePatch());
}

這裡的UpgradePatch物件便會賦值給upgradePatchProcessor,合成補丁的時候呼叫它的tryPatch方法:

    @Override
    public boolean tryPatch(Context context, String tempPatchPath, PatchResult patchResult) {
        Tinker manager = Tinker.with(context);

        final File patchFile = new File(tempPatchPath);
        //check the signature, we should create a new checker
        ShareSecurityCheck signatureCheck = new ShareSecurityCheck(context);

        int returnCode = ShareTinkerInternals.checkTinkerPackage(context, manager.getTinkerFlags(), patchFile, signatureCheck);

        //it is a new patch, so we should not find a exist
        SharePatchInfo oldInfo = manager.getTinkerLoadResultIfPresent().patchInfo;
        String patchMd5 = SharePatchFileUtil.getMD5(patchFile);

        //use md5 as version
        patchResult.patchVersion = patchMd5;

        SharePatchInfo newInfo;

        //already have patch
        if (oldInfo != null) {
            newInfo = new SharePatchInfo(oldInfo.oldVersion, patchMd5, Build.FINGERPRINT);
        } else {
            newInfo = new SharePatchInfo("", patchMd5, Build.FINGERPRINT);
        }

        //check ok, we can real recover a new patch
        final String patchDirectory = manager.getPatchDirectory().getAbsolutePath();

        final String patchName = SharePatchFileUtil.getPatchVersionDirectory(patchMd5);

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

        //it is a new patch, we first delete if there is any files
        //don't delete dir for faster retry
//        SharePatchFileUtil.deleteDir(patchVersionDirectory);

        //copy file
        File destPatchFile = new File(patchVersionDirectory + "/" + SharePatchFileUtil.getPatchVersionFile(patchMd5));
        try {
            SharePatchFileUtil.copyFileUsingStream(patchFile, destPatchFile);
            TinkerLog.w(TAG, "UpgradePatch after %s size:%d, %s size:%d", patchFile.getAbsolutePath(), patchFile.length(),
                destPatchFile.getAbsolutePath(), destPatchFile.length());
        } catch (IOException e) {
//            e.printStackTrace();
            TinkerLog.e(TAG, "UpgradePatch tryPatch:copy patch file fail from %s to %s", patchFile.getPath(), destPatchFile.getPath());
            manager.getPatchReporter().onPatchTypeExtractFail(patchFile, destPatchFile, patchFile.getName(), ShareConstants.TYPE_PATCH_FILE);
            return false;
        }

        //we use destPatchFile instead of patchFile, because patchFile may be deleted during the patch process
        if (!DexDiffPatchInternal.tryRecoverDexFiles(manager, signatureCheck, context, patchVersionDirectory, destPatchFile)) {
            TinkerLog.e(TAG, "UpgradePatch tryPatch:new patch recover, try patch dex failed");
            return false;
        }

        final File patchInfoFile = manager.getPatchInfoFile();

        if (!SharePatchInfo.rewritePatchInfoFileWithLock(patchInfoFile, newInfo, SharePatchFileUtil.getPatchInfoLockFile(patchDirectory))) {
            TinkerLog.e(TAG, "UpgradePatch tryPatch:new patch recover, rewrite patch info failed");
            manager.getPatchReporter().onPatchInfoCorrupted(patchFile, newInfo.oldVersion, newInfo.newVersion);
            return false;
        }

        TinkerLog.w(TAG, "UpgradePatch tryPatch: done, it is ok");
        return true;
    }

這個方法比較長,刪除了一些校驗的程式碼以及合成資原始檔等方法,主要看dex檔案的合成過程。開始是初始化一些目錄,再將補丁檔案拷貝到目標目錄中:

SharePatchFileUtil.copyFileUsingStream(patchFile, destPatchFile);

再呼叫以下方法:

DexDiffPatchInternal.tryRecoverDexFiles(manager, signatureCheck, context, patchVersionDirectory, destPatchFile)

看具體實現:

    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;
    }

主要就是計算耗時,最終方法是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;
        }

        final Tinker manager = Tinker.with(context);

        File dexFiles = new File(dir);
        File[] files = dexFiles.listFiles();

        if (files != null) {
            final String optimizeDexDirectory = patchVersionDirectory + "/" + DEX_OPTIMIZE_PATH + "/";
            File optimizeDexDirectoryFile = new File(optimizeDexDirectory);

            if (!optimizeDexDirectoryFile.exists() && !optimizeDexDirectoryFile.mkdirs()) {
                TinkerLog.w(TAG, "patch recover, make optimizeDexDirectoryFile fail");
                return false;
            }

            TinkerLog.w(TAG, "patch recover, try to optimize dex file count:%d", files.length);

            boolean isSuccess = TinkerParallelDexOptimizer.optimizeAll(
                    files, optimizeDexDirectoryFile,
                    new TinkerParallelDexOptimizer.ResultCallback() {
                        long startTime;
                        @Override
                        public void onStart(File dexFile, File optimizedDir) {
                            startTime = System.currentTimeMillis();
                            TinkerLog.i(TAG, "start to optimize dex %s", dexFile.getPath());
                        }

                        @Override
                        public void onSuccess(File dexFile, File optimizedDir) {
                            // Do nothing.
                            TinkerLog.i(TAG, "success to optimize dex %s use time %d",
                                dexFile.getPath(), (System.currentTimeMillis() - startTime));
                        }

                        @Override
                        public void onFailed(File dexFile, File optimizedDir, Throwable thr) {
                            TinkerLog.i(TAG, "fail to optimize dex %s use time %d",
                                dexFile.getPath(), (System.currentTimeMillis() - startTime));
                            SharePatchFileUtil.safeDeleteFile(dexFile);
                            manager.getPatchReporter().onPatchDexOptFail(patchFile, dexFile, optimizeDexDirectory, dexFile.getName(), thr);
                        }
                    }
            );
            //list again
            if (isSuccess) {
                for (File file : files) {
                    try {
                        if (!SharePatchFileUtil.isLegalFile(file)) {
                            TinkerLog.e(TAG, "single dex optimizer file %s is not exist, just return false", file);
                            return false;
                        }
                        String outputPathName = SharePatchFileUtil.optimizedPathFor(file, optimizeDexDirectoryFile);
                        File outputFile = new File(outputPathName);
                        if (!SharePatchFileUtil.isLegalFile(outputFile)) {
                            TinkerLog.e(TAG, "parallel dex optimizer file %s fail, optimize again", outputPathName);
                            long start = System.currentTimeMillis();
                            DexFile.loadDex(file.getAbsolutePath(), outputPathName, 0);
                            TinkerLog.i(TAG, "success single dex optimize file, path: %s, use time: %d", file.getPath(), (System.currentTimeMillis() - start));
                            if (!SharePatchFileUtil.isLegalFile(outputFile)) {
                                manager.getPatchReporter()
                                    .onPatchDexOptFail(patchFile, file, optimizeDexDirectory,
                                        file.getName(), new TinkerRuntimeException("dexOpt file:" + outputPathName + " is not exist"));
                                return false;
                            }
                        }
                    } catch (Throwable e) {
                        TinkerLog.e(TAG, "dex optimize or load failed, path:" + file.getPath());
                        //delete file
                        SharePatchFileUtil.safeDeleteFile(file);
                        manager.getPatchReporter().onPatchDexOptFail(patchFile, file, optimizeDexDirectory, file.getName(), e);
                        return false;
                    }
                }
            }
            return isSuccess;
        }
        return true;
    }

首先extractDexDiffInternals(context, dir, meta, patchFile, TYPE_DEX)是合成全量補丁,後面是通過DexFile.loadDex生成優化後的dex檔案,這個過程貌似做了兩遍。主要看extractDexDiffInternals,哎,不貼程式碼了,程式碼好多,自己看吧。這個方法中會拿到兩個檔案,一個原始包檔案,一個是補丁檔案:

apk = new ZipFile(apkPath);
patch = new ZipFile(patchFile);

安全性校驗完了之後會分別呼叫extractDexFile(zipFile, entryFile,extractTo, dexInfo) 或者 patchDexFile(baseApk, patchPkg, oldDexEntry, patchFileEntry, patchInfo, patchedDexFile);這裡分了三種情況,

第一種情況是直接將補丁包中的dex檔案拷貝到了目標資料夾下,這種情況應該是下發的補丁包就是全量包;

第二種情況是直接拷貝原apk包的dex檔案,有這麼一段註釋:

// Small patched dex generating strategy was disabled, we copy full original dex directly now.

為什麼要把原始Apk包裡的dex檔案複製過去呢?我也想不明白,問了一下張紹文老大,他的回答是:

因為內聯以及地址錯亂的問題

對,就是這個原因(因回答過於簡潔,還是不太明白)。有知道的小夥伴歡迎留言告知,說的稍微詳細一點。

這兩種情況呼叫的是extractDexFile,不同的是傳進去的包不一樣,一個是補丁包,一個是原始包。

第三種情況是將原始dex與補丁dex合成全量dex,呼叫patchDexFile,最終呼叫如下方法合成補丁:

new DexPatchApplier(zis, (int) entry.getSize(), patchFileStream).executeAndSaveTo(zos);

繼續往下看DexPatchApplier類,這個可是合成dex檔案的核心所在

額。。。不看了,看不下去了,有點想吐,暈程式碼。。。

等不暈的時候再來看吧,分析Tinker原始碼的文章就暫時告一段落了,對這系列文章有疑問的,或者發現寫的有錯誤的歡迎在下方留言;如果接入遇到問題的也可以留言。

相關文章