作為開發人員,會用别人的架構是遠遠不夠的,我們可以學習别人的設計思想、實驗原理,積累知識,才能不斷提升自己。今天這一章主要給大家介紹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的穩定性,最後合成更新檔到下一篇研究。