《Android tinker熱修復——實戰接入專案》看了該文章的讀者應該懂得如何使用tinker接入自己的專案了,但是作為開發人員,會使用人家的框架還遠遠不夠,我們需要學習別人的設計思想和原理,來提高自己。
在會使用了tinker的基礎上,接下來我們深入學習一下tinker到底是如何工作,如何載入合成補丁的,如何修復bug的。由於tinker熱修復框架比較強大,而且原理和思想比較深,所以一篇文章去探索是遠遠不夠的,我們需要一個步驟一個步驟的解析,而該片文章主要是探析tinker的補丁載入合成。
我們看下tinker的工作流程:
![tinker.png](https://i.iter01.com/images/7e679ff250ba30f196f9a69658f2eefed3d205f1a2344ff1422e4188e5b902d5.png)
接下來我們分析一下原始碼的實現過程,這裡我們挑了合成patch的流程。
開始補丁合成
下載完成補丁之後,將要呼叫TinkerInstaller的onReceiveUpgradePatch方法。
![tinker.png](https://i.iter01.com/images/547f6e53ab277216024428480c8621a8e4fd6239cb1b7829b298419536bb4b11.png)
安裝合成新的補丁包,並且啟動補丁程式。
Tinker.with(context):
![tinker.png](https://i.iter01.com/images/9e9bb59d02975f0f51bed6e2965df7b2eb9dc6e333d32c8619f1f408309c3246.png)
這裡主要是初始化tinker的基本配置,並且使用同步機制來初始化配置。
new Builder(context).build():
![tinker.png](https://i.iter01.com/images/2c61e82dae8e3dfe8609ee2226d47032c10c02dc168ad3ca7e64efddc16af0ed.png)
![tinker.png](https://i.iter01.com/images/9c9670ce90e5dbacc6e736d7c024166ccde701ed47193fd68fca67e2fa5cf53d.png)
初始化的配置,我都已經在程式碼中註釋好了。在build()方法的最後,new了Tinker物件,而在Tinker構造方法裡面主要是將剛剛初始化的預設配置,快取到Tinker裡面。
我們看回onReceiveUpgradePatch方法裡面呼叫的getPatchListener,而該方法主要是返回剛剛在build裡面初始化的DefaultPatchListener物件。
![tinker.png](https://i.iter01.com/images/2dd13791fa3d4b2cca6a5a4c0098679ab3e086f3a8cb7203472a09ae085a62b6.png)
DefaultPatchListener就是檢查修復包的類,最後呼叫該類的onPatchReceived,該方法開始檢查修復包,檢查完成之後就開始合成補丁包。
![tinker.png](https://i.iter01.com/images/50ca5a1fcdfe9dc25eb15942a13818654150556d1d14e1a79dc6989352b2bc38.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](https://i.iter01.com/images/cbde355fc63eba1801c64e89edf52e139ee931a2ca34c3db09251c211ccc5e5d.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](https://i.iter01.com/images/52d8bdc1cfe43186c285f50b97702f27aefa7d099a9ce74d3debe8b1fb1c5eef.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](https://i.iter01.com/images/9d1eaa21f43537a5057aeb56865cd3197b3871e9da1cb1d3870aeb45c9af03bd.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](https://i.iter01.com/images/4769c39275a08d4d6006fc3c9f68a645203fe1016e6e303b8844005b1356143e.png)
更新RetryInfo物件的屬性,儲存著補丁的md5和time。
onPatchServiceStart:該方法主要的作用就是將補丁檔案拷貝一份到(/data/data/包名/tinker_temp/temp.apk)temp.apk檔案,然後更新補丁檔案的md5和time並且儲存起來。
繼續看onHandleIntent方法,經過intent判斷,通過onHandleIntent方法拿到path之後,然後通過increasingPriority()方法,將服務設定到前臺來,為了就是讓該無法不被系統殺死。
service設定到前臺
![tinker.png](https://i.iter01.com/images/98e7de51430fc731550183866b0eeb8374be02241ad04153792feabfc18c48dc.png)
startForeground(notificationId, notification)
![tinker.png](https://i.iter01.com/images/1db8451b05bb50d1be38ce18a7aed51a1af51aa40203bf9782c667d206a602ec.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](https://i.iter01.com/images/ade8729328123fe676d5368674004acdd37f8eb6dba906ba20c3359573f0aebb.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](https://i.iter01.com/images/3d7bd1514ec4b08d0cd45325edbadcbf0cc5fef857992a0c65b3db5999b703fb.png)
以上就是補丁合成的過程,但是沒有深入到合成演算法的分析,簡單的合成流程分析,演算法還需要時間慢慢啃。
參考文章:《微信熱補丁Tinker – 補丁流程》