tinker熱修復——補丁載入合成

鋸齒流沙發表於2017-12-26

《Android tinker熱修復——實戰接入專案》看了該文章的讀者應該懂得如何使用tinker接入自己的專案了,但是作為開發人員,會使用人家的框架還遠遠不夠,我們需要學習別人的設計思想和原理,來提高自己。

在會使用了tinker的基礎上,接下來我們深入學習一下tinker到底是如何工作,如何載入合成補丁的,如何修復bug的。由於tinker熱修復框架比較強大,而且原理和思想比較深,所以一篇文章去探索是遠遠不夠的,我們需要一個步驟一個步驟的解析,而該片文章主要是探析tinker的補丁載入合成。

我們看下tinker的工作流程:

tinker.png

接下來我們分析一下原始碼的實現過程,這裡我們挑了合成patch的流程。

開始補丁合成

下載完成補丁之後,將要呼叫TinkerInstaller的onReceiveUpgradePatch方法。

tinker.png

安裝合成新的補丁包,並且啟動補丁程式。

Tinker.with(context):

tinker.png

這裡主要是初始化tinker的基本配置,並且使用同步機制來初始化配置。

new Builder(context).build():

tinker.png

tinker.png

初始化的配置,我都已經在程式碼中註釋好了。在build()方法的最後,new了Tinker物件,而在Tinker構造方法裡面主要是將剛剛初始化的預設配置,快取到Tinker裡面。

我們看回onReceiveUpgradePatch方法裡面呼叫的getPatchListener,而該方法主要是返回剛剛在build裡面初始化的DefaultPatchListener物件。

tinker.png

DefaultPatchListener就是檢查修復包的類,最後呼叫該類的onPatchReceived,該方法開始檢查修復包,檢查完成之後就開始合成補丁包。

tinker.png

在onPatchReceived裡面呼叫patchCheck方法檢查修復包

 /**
     * when we receive a patch, what would we do?
     * you can overwrite it
     *
     * @param path 補丁路徑
     * @return
     */
    @Override
    public int onPatchReceived(String path) {
        File patchFile = new File(path);

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

        //一切準備好之後,呼叫runPatchService方法,啟動補丁的Service服務
        if (returnCode == ShareConstants.ERROR_PATCH_OK) {
            TinkerPatchService.runPatchService(context, path);
        } else {
            Tinker.with(context).getLoadReporter().onLoadPatchListenerReceiveFail(new File(path), returnCode);
        }
        return returnCode;
    }

    /**
     *
     * @param path 補丁路徑
     * @param patchMd5 //補丁檔案的md5值
     * @return
     */
    protected int patchCheck(String path, String patchMd5) {
        Tinker manager = Tinker.with(context);
        //check SharePreferences also
        //檢查是否開啟補丁修復
        if (!manager.isTinkerEnabled() || !ShareTinkerInternals.isTinkerEnableWithSharedPreferences(context)) {
            return ShareConstants.ERROR_PATCH_DISABLE;
        }
        File file = new File(path);

        //檢查補丁是否存在
        if (!SharePatchFileUtil.isLegalFile(file)) {
            return ShareConstants.ERROR_PATCH_NOTEXIST;
        }

        //patch service can not send request
        //補丁服務不能傳送請求
        if (manager.isPatchProcess()) {
            return ShareConstants.ERROR_PATCH_INSERVICE;
        }

        //if the patch service is running, pending
        //如果補丁服務的servicec正在執行,就等待
        if (TinkerServiceInternals.isTinkerPatchServiceRunning(context)) {
            return ShareConstants.ERROR_PATCH_RUNNING;
        }
        //檢查JIT
        if (ShareTinkerInternals.isVmJit()) {
            return ShareConstants.ERROR_PATCH_JIT;
        }

        //獲取tinker物件
        Tinker tinker = Tinker.with(context);

        //tinker載入完成
        if (tinker.isTinkerLoaded()) {
            //載入完成結果
            TinkerLoadResult tinkerLoadResult = tinker.getTinkerLoadResultIfPresent();
            if (tinkerLoadResult != null && !tinkerLoadResult.useInterpretMode) {
                String currentVersion = tinkerLoadResult.currentVersion;
                //補丁沒有變和當前版本相同
                if (patchMd5.equals(currentVersion)) {
                    return ShareConstants.ERROR_PATCH_ALREADY_APPLY;
                }
            }
        }

        if (!UpgradePatchRetry.getInstance(context).onPatchListenerCheck(patchMd5)) {
            return ShareConstants.ERROR_PATCH_RETRY_COUNT_LIMIT;
        }

        return ShareConstants.ERROR_PATCH_OK;
    }
複製程式碼

經過一系列的檢查,包括檢查是否開啟補丁修復、補丁是否存在和補丁服務是否正在執行等等,最後返回ERROR_PATCH_OK表示補丁驗證通過。

補丁的檢驗通過之後會呼叫TinkerPatchService.runPatchService來啟動合成補丁的服務。否則呼叫DefaultLoadReporter的onLoadPatchListenerReceiveFail方法,報告載入補丁包失敗。

//一切準備好之後,呼叫runPatchService方法,啟動補丁的Service服務
        if (returnCode == ShareConstants.ERROR_PATCH_OK) {
            TinkerPatchService.runPatchService(context, path);
        } else {
            Tinker.with(context).getLoadReporter()
                    .onLoadPatchListenerReceiveFail
                            (new File(path), returnCode);
        }
複製程式碼

我們主要看TinkerPatchService的runPatchService方法,啟動服務。

tinker.png

該方法接受的引數就是將補丁的路徑和上下文傳過去,啟動TinkerPatchService服務,而TinkerPatchService該服務是屬於:patch程式的。TinkerPatchService繼承IntentService,需要重寫onHandleIntent方法,該方法是執行在子執行緒當中,可以做一些耗時任務,任務完成之後會自己結束掉服務。

啟動服務之後,我們主要看TinkerPatchService的onHandleIntent方法。

@Override
    protected void onHandleIntent(Intent intent) {
        final Context context = getApplicationContext();
        //獲取tinker物件
        Tinker tinker = Tinker.with(context);
        //呼叫DefaultPatchReporter的onPatchServiceStart方法
        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);
        //獲取從裝置boot後經歷的時間值。
        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;

        //關閉patch程式,如果更新成功就要刪掉rawPatchFilePath
        AbstractResultService.runResultService(
                context,
                patchResult, 
                getPatchResultExtra(intent));

    }
複製程式碼

在onHandleIntent方法裡面,首先會呼叫DefaultPatchReporter的onPatchServiceStart方法,而DefaultPatchReporter主要是打修復包過程中的報告類。看下onPatchServiceStart方法做了那些事情。

tinker.png

主要做的工作就是報告TinkerPatchService開始時的一些工作。

繼續往下看,最後呼叫UpgradePatchRetry的onPatchServiceStart方法。

/**
     * 啟動服務要做的事情
     * 包括把檔案搬到/data/data/包名下
     * @param intent
     */
    public void onPatchServiceStart(Intent intent) {
        if (!isRetryEnable) {
            TinkerLog.w(TAG, 
                    "onPatchServiceStart retry disabled, just return");
            return;
        }

        if (intent == null) {
            TinkerLog.e(TAG, 
                    "onPatchServiceStart intent is null, just return");
            return;
        }

        String path = TinkerPatchService.getPatchPathExtra(intent);

        if (path == null) {
            TinkerLog.w(TAG, 
                    "onPatchServiceStart patch path is null, just return");
            return;
        }

        RetryInfo retryInfo;
        File patchFile = new File(path);

        String patchMd5 = SharePatchFileUtil.getMD5(patchFile);
        if (patchMd5 == null) {
            TinkerLog.w(TAG, 
                    "onPatchServiceStart patch md5 is null, just return");
            return;
        }

        if (retryInfoFile.exists()) {
            retryInfo = RetryInfo.readRetryProperty(retryInfoFile);
            if (retryInfo.md5 == null || 
                    retryInfo.times == null || 
                    !patchMd5.equals(retryInfo.md5)) {
                copyToTempFile(patchFile);
                retryInfo.md5 = patchMd5;
                retryInfo.times = "1";
            } else {
                int nowTimes = Integer.parseInt(retryInfo.times);
                if (nowTimes >= maxRetryCount) {
                    SharePatchFileUtil.safeDeleteFile(tempPatchFile);
                    TinkerLog.w(TAG, 
                            "onPatchServiceStart retry more than max count" +
                            ", delete retry info file!");
                    return;
                } else {
                    retryInfo.times = String.valueOf(nowTimes + 1);
                }
            }

        } else {
            copyToTempFile(patchFile);
            retryInfo = new RetryInfo(patchMd5, "1");
        }

        //重寫屬性
        RetryInfo.writeRetryProperty(retryInfoFile, retryInfo);
    }
複製程式碼

該方法也經過一些列的判斷,往下看,我們看到copyToTempFile(patchFile)這個方法。

tinker.png

方法內呼叫SharePatchFileUtil.copyFileUsingStream方法。

/**
     * 將補丁檔案copy到dest檔案下
     * @param source
     * @param dest
     * @throws IOException
     */
    public static void copyFileUsingStream(File source, 
                                           File dest)
            throws IOException {
        if (!SharePatchFileUtil.isLegalFile(source) || 
                dest == null) {
            return;
        }
        if (source.getAbsolutePath().
                equals(dest.getAbsolutePath())) {
            return;
        }
        FileInputStream is = null;
        FileOutputStream os = null;
        File parent = dest.getParentFile();
        if (parent != null && (!parent.exists())) {
            parent.mkdirs();
        }
        try {
            is = new FileInputStream(source);
            os = new FileOutputStream(dest, false);

            byte[] buffer = new byte[ShareConstants.
                    BUFFER_SIZE];
            int length;
            while ((length = is.read(buffer)) > 0) {
                os.write(buffer, 0, length);
            }
        } finally {
            closeQuietly(is);
            closeQuietly(os);
        }
    }
複製程式碼

該方法就是將補丁檔案copy到dest檔案,在UpgradePatchRetry建立物件的時候就初始化了tempPatchFile檔案(/data/data/包名/tinker_temp/temp.apk)。

看回onPatchServiceStart方法,最後呼叫writeRetryProperty方法。

tinker.png

更新RetryInfo物件的屬性,儲存著補丁的md5和time。

onPatchServiceStart:該方法主要的作用就是將補丁檔案拷貝一份到(/data/data/包名/tinker_temp/temp.apk)temp.apk檔案,然後更新補丁檔案的md5和time並且儲存起來。

繼續看onHandleIntent方法,經過intent判斷,通過onHandleIntent方法拿到path之後,然後通過increasingPriority()方法,將服務設定到前臺來,為了就是讓該無法不被系統殺死。

service設定到前臺

tinker.png

startForeground(notificationId, notification)

tinker.png

startForeground(notificationId, notification):後臺服務置於前臺,就像音樂播放器的播放服務一樣,不會被系統殺死。如果Build.VERSION.SDK_INT > 18,開啟一個InnerService降低被殺死的概率。

將service調到前臺之後,接著呼叫UpgradePatch的tryPatch方法,該方法就是合成補丁包的過程。

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

        final File patchFile = new File(tempPatchPath);

        if (!manager.isTinkerEnabled() || 
                !ShareTinkerInternals
                        .isTinkerEnableWithSharedPreferences(context)) {
            TinkerLog.e(TAG, 
                    "UpgradePatch tryPatch:patch is disabled, just return");
            return false;
        }

        if (!SharePatchFileUtil.isLegalFile(patchFile)) {
            TinkerLog.e(TAG, 
                    "UpgradePatch tryPatch:patch file is not found, just return");
            return false;
        }
        //check the signature, we should create a new checker
        ShareSecurityCheck signatureCheck = new ShareSecurityCheck(context);

        int returnCode = ShareTinkerInternals.checkTinkerPackage(context, 
                manager.getTinkerFlags(), patchFile, signatureCheck);
        if (returnCode != ShareConstants.ERROR_PACKAGE_CHECK_OK) {
            TinkerLog.e(TAG, "UpgradePatch tryPatch:onPatchPackageCheckFail");
            manager.getPatchReporter().onPatchPackageCheckFail(patchFile, 
                    returnCode);
            return false;
        }

        String patchMd5 = SharePatchFileUtil.getMD5(patchFile);
        if (patchMd5 == null) {
            TinkerLog.e(TAG, 
                    "UpgradePatch tryPatch:patch md5 is null, just return");
            return false;
        }
        //use md5 as version
        patchResult.patchVersion = patchMd5;

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

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

        try {
            // check md5 first
            if (!patchMd5.equals(SharePatchFileUtil.getMD5(destPatchFile))) {
                SharePatchFileUtil.copyFileUsingStream(patchFile, destPatchFile);
                TinkerLog.w(TAG, 
                        "UpgradePatch copy patch file, src file: %s size: %d, " +
                                "dest file: %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;
        }

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

        // check dex opt file at last, 
        // some phone such as VIVO/OPPO like to change dex2oat to interpreted
        if (!DexDiffPatchInternal.waitAndCheckDexOptFile(
                patchFile, 
                manager)) {
            TinkerLog.e(TAG, "UpgradePatch tryPatch:new patch recover, " +
                    "check dex opt file failed");
            return false;
        }

        if (!SharePatchInfo.rewritePatchInfoFileWithLock(
                patchInfoFile, 
                newInfo, 
                patchInfoLockFile)) {
            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;
    }
複製程式碼

微信團隊寫程式碼是非常嚴謹的,在tryPatch方法中,需要經過一系列的判斷(包括判斷是否開啟熱修復,是否存在補丁包,md5是否為null等)之後,然後呼叫補丁合成的方法。

tinker.png

在此之前new了一個名為destPatchFile的檔案,如果之前沒有存在該檔案,就需要將補丁複製寫入,合成的時候需要用destPatchFile檔案,因為如果使用原來的補丁檔案,在合成的過程中,使用者有可能刪除補丁,所以為了安全需要使用destPatchFile檔案來進行合成。

注意:

1、補丁包必須拷貝到/data/data/包名/目錄下,通過從下載目錄檔案通過流讀出寫入到該目錄下,因為修復替換patch需要在/data/data/包名/目錄下進行。

2、在合成的時候,分別使用DexDiff合成dex、BsDiff合成library和ResDiff合成resource。

最後一部就是拷貝SharePatchInfo到PatchInfoFile中,使用SharePatchInfo的rewritePatchInfoFileWithLock方法。

執行完補丁的合成之後,在TinkerPatchService的onHandleIntent方法中,會呼叫AbstractResultService的runResultService方法,而runResultService方法啟動的service就是我們在呼叫tinker.install方法傳入的service。在TinkerSample中傳入的是SampleResultService,而SampleResultService的onPatchResult方法,主要做的就是:

1、呼叫killTinkerPatchServiceProcess關閉patchService的程式

2、呼叫deleteRawPatchFile刪掉補丁檔案。

tinker.png

以上就是補丁合成的過程,但是沒有深入到合成演算法的分析,簡單的合成流程分析,演算法還需要時間慢慢啃。

參考文章:《微信熱補丁Tinker – 補丁流程》

相關文章