Android 微信熱補丁Tinker -- 補丁流程

作爲開發人員,會用別人的框架是遠遠不夠的,我們可以學習別人的設計思想、實驗原理,積累知識,才能不斷提升自己。今天這一章主要給大家介紹Tinker補丁流程,深入到代碼中去探索Tinker。

補丁流程

在文章[Android熱補丁方案][7]中介紹了Tinker的原理框架,那裏只是簡單介紹一下Tinker框架的補丁流程,這裏我重新整理了一份Tinker補丁流程:

上面的流程圖中,已經把Tinker的補丁流程和集成Tinker需要注意的邏輯都已經表示的很清楚了。需要注意的地方就是需要開發者自己編寫下載過程,然後調用Tinker的Api傳入補丁,以及補丁成功後重啓應用的邏輯,這個開發者需要結合自己項目的需求來制定,我自己項目裏面是通過熄屏監聽來殺掉應用進程,進行重啓。

在合成補丁流程中,總結了以下幾點最重要的部分:

  • 安全校驗: 無論在補丁合成還是加載,我們都需要有必要的安全校驗。
  • 版本管理: Tinker 支持補丁升級,甚至是多個補丁不停的切換。這裏我們需要保證所有進程版本的一致性;
  • 補丁加載: 如果通過反射系統加載我們合成好的 dex,so 與資源;
  • 補丁合成: 這些都在單獨的 patch 進程工作,這裏包括 dex,so 還有資源,主要完成補丁包的合成以及升級;
  • 監控回調: 在合成與加載過程中,出現問題及時回調;

經過源碼分析,總結出了以下Tinker補丁流程圖(上圖虛線部分):

以上圖片中,標出了加載流程以及它實現的類和方法,本章也是主要着重這個流程分析。

源碼跟蹤

###一、收到補丁
當補丁下載完成後,即調用下面的函數開始補丁:

/**
 * 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, true);
}

此時的Tinker已經被初始化,在ApplicationLike中調用:

TinkerManager.installTinker(this);  

初始化的函數:

/**
 * install tinker with custom config, you must install tinker before you use their api
 * or you can just use {@link TinkerApplicationHelper}'s api
 *
 * @param applicationLike
 * @param loadReporter
 * @param patchReporter
 * @param listener
 * @param resultServiceClass
 * @param upgradePatchProcessor
 * @param repairPatchProcessor
 */
public static void install(ApplicationLike applicationLike, LoadReporter loadReporter, PatchReporter patchReporter,
                           PatchListener listener, Class<? extends AbstractResultService> resultServiceClass,
                           AbstractPatch upgradePatchProcessor, AbstractPatch repairPatchProcessor) {

    Tinker tinker = new Tinker.Builder(applicationLike.getApplication())
        .tinkerFlags(applicationLike.getTinkerFlags())
        .loadReport(loadReporter)
        .listener(listener)
        .patchReporter(patchReporter)
        .tinkerLoadVerifyFlag(applicationLike.getTinkerLoadVerifyFlag()).build();

    Tinker.create(tinker);
    tinker.install(applicationLike.getTinkerResultIntent(), resultServiceClass, upgradePatchProcessor, repairPatchProcessor);
}

傳入的參數有:

  • ApplicationLike:應用的代理application
  • LoadReporter:加載合成的包的報告類
  • PatchReporter:打修復包過程中的報告類
  • PatchListener:對修復包最開始的檢查
  • ResultService:從合成進程取合成結果監聽
  • UpgradePatchProcessor:生成一個新的patch合成包
  • ReparePatchProcessor:修復上一次合成失敗的修復包

看一下Tinker中核心的install方法:

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

    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!");
    }
}

主要做了以下的工作:

  • 設置自定義的ResultService
  • 設置自定義的UpgradePatch和ReparePatch
  • 創建TinkerLoadResult調用parseTinkerResult(Context context, Intent intentResult)解析上次合成之後的信息:花費時間,返回值等。
  • 調用LoaderReporter的onLoadResult方法,通知,加載結果。

二、補丁校驗

準備工作都做完了,下面就是校驗補丁包操作了。

調用第一步中的收到補丁函數,則PatchListener接收到補丁包:

/**
 * when we receive a patch, what would we do?
 * you can overwrite it
 *
 * @param path
 * @param isUpgrade
 * @return
 */
@Override
public int onPatchReceived(String path, boolean isUpgrade) {

    int returnCode = patchCheck(path, isUpgrade);

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

DefaultPatchListener的onPatchReceived方法:

  • 對補丁包進行檢查;
  • 如果修復包校驗通過,則開啓一個單獨的進程合成全量包;
  • 如果修復包不完整或者有其它問題,則調用LoadReporter的onLoadPatchListenerReceiveFail(File patchFile, int errorCode, boolean isUpgrade)方法通知,並將原有賦予errorCode傳達,這裏跟我上面總結的流程分析圖中一樣。

下面我們來看下上述的對補丁的具體校驗:

protected int patchCheck(String path, boolean isUpgrade) {
    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 (!file.isFile() || !file.exists() || file.length() == 0) {
        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
    if (TinkerServiceInternals.isTinkerPatchServiceRunning(context)) {
        return ShareConstants.ERROR_PATCH_RUNNING;
    }
    return ShareConstants.ERROR_PATCH_OK;
}

這個是DefaultPatchListener的patchCheck方法,這裏主要做了四件事:

  • 檢查Tinker開關是否開啓,需要打開
  • 檢查Patch文件是否存在,需要存在
  • 檢查是否是合成進程的操作,需要否
  • 檢查合成進程是否正在執行,需要否

這裏的合成進程指的是TinkerPatchService,這個需要檢查校驗完後才能去啓動一個單獨的進程去合成補丁。
除了DefaultPatchListener,我們還可以在SimplePatchListener中自定義一些補丁校驗,官方Demo中還檢查了以下方面:

  • 檢查Rom空間
  • 檢查Crash次數
  • 檢查新舊補丁PatchVersion
  • 檢查補丁Patch條件是否符合

經過所有的檢查,當返回ERROR_PATCH_OK時纔會去開啓合成進程;若不是則通過LoadReporter的onLoadPatchListenerReceiveFail(File patchFile, int errorCode, boolean isUpgrade)方法通知,並將原有賦予errorCode傳達。

###三、補丁加載

當檢查OK後,即開啓

public static void runPatchService(Context context, String path, boolean isUpgradePatch) {
    Intent intent = new Intent(context, TinkerPatchService.class);
    intent.putExtra(PATCH_PATH_EXTRA, path);
    intent.putExtra(PATCH_NEW_EXTRA, isUpgradePatch);

    context.startService(intent);
}

在TinkerPatchService中開啓合成進程的服務。它是一個IntentSerivce,是在單獨的線程中進行的操作。下面來看一下onHandleIntent中的操作。

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

    boolean isUpgradePatch = getPatchUpgradeExtra(intent);

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

    increasingPriority();
    PatchResult patchResult = new PatchResult();
    try {
        if (isUpgradePatch) {
            if (upgradePatchProcessor == null) {
                throw new TinkerRuntimeException("upgradePatchProcessor is null.");
            }
            result = upgradePatchProcessor.tryPatch(context, path, patchResult);

        } else {
            //just recover from exist patch
            if (repairPatchProcessor == null) {
                throw new TinkerRuntimeException("upgradePatchProcessor is null.");
            }
            result = repairPatchProcessor.tryPatch(context, path, patchResult);
        }
    } catch (Throwable throwable) {
        e = throwable;
        result = false;
        tinker.getPatchReporter().onPatchException(patchFile, e, isUpgradePatch);
    }

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

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

    AbstractResultService.runResultService(context, patchResult);
}  

這裏主要有幾個操作:

  • 調用PatchRepoter的onPatchServiceStart(intent),表示合成Patch的Service開啓,用戶可以自定義一些標記,用於跟蹤合成進程;
  • 調用increasingPriority()方法,讓服務置於前臺服務;
  • 第一,startForeground(notificationId, notification); 這個方法,可以讓後臺服務置於前臺,就像音樂播放器的,播放服務一樣,不會被系統殺死。
  • 第二,開啓一個InnerService降低被殺死的概率。
  • 調用tryPatch(context, path, patchResult)方法來執行合併操作,並將結果返回,具體的下面再分析這個合成方法,如果兩個AbstractPatch爲空,則會捕獲異常。並且調用PatchReporter的onPatchException來通知。
  • 修復成功後調用PatchReporter的onPatchResult()來通知,有花費時間等信息。
  • 調用AbstactResultService的runResultService(Context context, PatchResult result)方法。也是在TinkerInstaller的Install的時候賦值。也是一個IntentService。這個Service會將補丁合成進程返回的結果返回給主進程,在單獨的線程中執行。onHandleIntent中只是回調了runResultService(Context context, PatchResult result)方法。通過IntentService完成了進程間的通信。

最後我們先來預覽一下合成方法:

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 (!patchFile.isFile() || !patchFile.exists()) {
        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, true, returnCode);
        return false;
    }

    patchResult.patchTinkerID = signatureCheck.getNewTinkerID();
    patchResult.baseTinkerID = signatureCheck.getTinkerID();

    //it is a new patch, so we should not find a exist
    SharePatchInfo oldInfo = manager.getTinkerLoadResultIfPresent().patchInfo;
    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;

    SharePatchInfo newInfo;

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

        if (oldInfo.oldVersion.equals(patchMd5) || oldInfo.newVersion.equals(patchMd5)) {
            TinkerLog.e(TAG, "UpgradePatch tryPatch:onPatchVersionCheckFail");
            manager.getPatchReporter().onPatchVersionCheckFail(patchFile, oldInfo, patchMd5, true);
            return false;
        }
        newInfo = new SharePatchInfo(oldInfo.oldVersion, patchMd5);
    } else {
        newInfo = new SharePatchInfo("", patchMd5);
    }

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

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

    final String patchName = SharePatchFileUtil.getPatchVersionDirectory(patchMd5);

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

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

    //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, true);
        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, true)) {
        TinkerLog.e(TAG, "UpgradePatch tryPatch:new patch recover, try patch dex failed");
        return false;
    }

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

    if (!ResDiffPatchInternal.tryRecoverResourceFiles(manager, signatureCheck, context, patchVersionDirectory, destPatchFile, true)) {
        TinkerLog.e(TAG, "UpgradePatch tryPatch:new patch recover, try patch resource 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, true);
        return false;
    }


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

看到這麼長的方法是不是很慌,不要慌,Tinker開發者寫代碼非常有條理性,從前面的代碼分析中可以發現了,好了,下面來分析一下這段代碼:

  • 首先,還是常規的Tinker開關和Patch文件檢查,隨後ShareTinkerInternals的靜態方法checkTinkerPackage(Context context, int tinkerFlag,File patchFile, ShareSecurityCheck securityCheck)傳入了ShareSecurityCheck,主要做了一下檢查操作,如果檢查失敗通過PatchReporter拋出onPatchPackageCheckFail:
  • 簽名檢查,ShareSecurityCheck是檢查簽名的類,裏面封裝了簽名檢查的方法;
  • TinkerId檢查
  • 檢查Tinker開關類型,dex、resource、libriry 的支持性。
  • SharePatchInfo,存儲了修復包的版本信息,有oldVersion和newVersion,newVersion使用的是修復包的md5值。
  • oldInfo何時會存在呢?加載成功過一次,也修復成功了,再次執行合成的時候,如果下載的包還是之前的包,則會報告onPatchVersionCheckFail。如果是新的修復包,則會把 oldVersion賦值給 SharePatchInfo(String oldVer, String newVew)中的ondVersion。到此爲止,所有的檢查已經完成。
  • 拷貝修復包到data/data目錄,從下載目錄文件通過流讀出寫入data/data目錄。
  • DexDiff合成dex、BsDiff合成library、ResDiff合成res。
  • 拷貝SharePatchInfo到PatchInfoFile中,PatchInfoFile在TinkerInstaller的install方法中初始化。

到這裏,整個Tinker修復的流程已經走完,雖然感覺很複雜,但是條理很清晰,安全措施很齊全,也提現了Tinker的穩定性,最後合成補丁到下一篇研究。


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