1. TinkerInstaller # install()
TinkerInstaller主要提供了兩個install()方法,一個簡單的,另一個復雜一點的適用于需要自定義更多功能的。
1-1 多參數(shù)的install()
先看這個多參數(shù)的install(),主要:
- 使用傳入?yún)?shù)構建了一個Tinker對象,Tinker使用了構建者模式。
- 調(diào)用了Tinker的create()和install()。
public static Tinker install(ApplicationLike applicationLike, LoadReporter loadReporter, PatchReporter patchReporter,
PatchListener listener, Class<? extends AbstractResultService> resultServiceClass, AbstractPatch upgradePatchProcessor) {
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);
return tinker;
}
1-2 單參數(shù)的install()
單參數(shù)的install()也是相同的,只是構建Tinker對象時沒有設置那些傳入的參數(shù)。
public static Tinker install(ApplicationLike applicationLike) {
Tinker tinker = new Tinker.Builder(applicationLike.getApplication()).build();
Tinker.create(tinker);
tinker.install(applicationLike.getTinkerResultIntent());
return tinker;
}
這里就能看出來TinkerInstaller是一個外觀模式,并沒有執(zhí)行初始化的工作,真正工作的是Tinker類,所以我們來看Tinker。
1-3 Tinker的單例模式
Tinker類是一個單例,用得是DCL,沒有處理DCL失效問題,可能是因為發(fā)生的概率太小了且處理會讓效率降低。最后是用Builder去構造對象,使用了構建者模式,Builder是Tinker內(nèi)部類,去管理Tinker的參數(shù)。
public static Tinker with(Context context) {
if (!sInstalled) {
throw new TinkerRuntimeException("you must install tinker before get tinker sInstance");
}
if (sInstance == null) {
synchronized (Tinker.class) {
if (sInstance == null) {
sInstance = new Builder(context).build();
}
}
}
return sInstance;
}
Builder在Tinker的內(nèi)部,統(tǒng)一管理了一些參數(shù),包括多參數(shù)install()傳入的Reporter和Listener等。
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;
1-4 Builder的構造器
在構造方法中會先初始化一些,主要是context所在線程的判斷和各種目錄的初始。
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);
}
1-5 Builder # patchReporter()
之后就是構建者模式,提供了一系列設置參數(shù)的方法。比如這個設置patchReporter的方法。設置的方法不要多次調(diào)用,并不會覆蓋掉之前的設置,只會拋已經(jīng)設置過的異常。
public Builder patchReporter(PatchReporter patchReporter) {
if (patchReporter == null) {
throw new TinkerRuntimeException("patchReporter must not be null.");
}
if (this.patchReporter != null) {
throw new TinkerRuntimeException("patchReporter is already set.");
}
this.patchReporter = patchReporter;
return this;
}
1-6 Builder # build()
在build()中會判空,如果是之前還沒有初始賦值的參數(shù),就賦默認值。最后調(diào)用Tinker構造器傳入初始化的參數(shù)創(chuàng)建Tinker對象并返回。
public Tinker build() {
if (status == -1) {
status = ShareConstants.TINKER_ENABLE_ALL;
}
// 如果調(diào)用的是但參數(shù)的install()就會在這里賦默認值
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);
}
1-7 Tinker # create()
回到1-2,Tinker對象初始化好后傳入Tinker的create(),來看create()。這個方法就是賦值單例sInstance。
public static void create(Tinker tinker) {
if (sInstance != null) {
throw new TinkerRuntimeException("Tinker instance is already set.");
}
sInstance = tinker;
}
1-8 Tinker # install()
1-2在調(diào)用完create()后就會調(diào)用install(),Tinker的install()也有兩個,單參數(shù)的TinkerInstaller#install()調(diào)用單參數(shù)的install(),多參數(shù)的調(diào)用多參數(shù)的,單參數(shù)也是調(diào)用多參數(shù)的install(),后面兩個參數(shù)就生成默認對象傳入了。
public void install(Intent intentResult) {
install(intentResult, DefaultTinkerResultService.class, new UpgradePatch());
}
再看多參數(shù)的install(),它完成真正install的邏輯。主要工作:
- 置標記sInstalled為true。
- 將兩個參數(shù)注入到TinkerPatchService中。
- 初始化TinkerLoadResult,調(diào)用onLoadResult()。
public void install(Intent intentResult, Class<? extends AbstractResultService> serviceClass,
AbstractPatch upgradePatch) {
sInstalled = true;
TinkerPatchService.setPatchProcessor(upgradePatch, serviceClass);
TinkerLog.i(TAG, "try to install tinker, isEnable: %b, version: %s", isTinkerEnabled(), ShareConstants.TINKER_VERSION);
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!");
}
}
2. TinkerInstaller # onReceiveUpgradePatch()
同樣因為外觀模式,所以TinkerInstaller沒有任何處理,直接交給Tinker對象中已經(jīng)初始化好的PatchListener的onPatchReceived()處理。
public static void onReceiveUpgradePatch(Context context, String patchLocation) {
Tinker.with(context).getPatchListener().onPatchReceived(patchLocation);
}
2-1 PatchListener # onPatchReceived()
PatchListener是一個接口,只有一個onPatchReceived()接口方法。DefaultPatchListener是PatchListener實現(xiàn)類。
public interface PatchListener {
int onPatchReceived(String path);
}
2-2 DefaultPatchListener # onPatchReceived()
可以自己去實現(xiàn)PatchListener,但總歸需要處理的工作是相似的。我們看一個默認實現(xiàn),DefaultPatchListener。在onPatchReceived()方法里:
- 調(diào)用patchCheck()對patch文件進行一系列的安全性檢查,去重寫這個方法也就可以實現(xiàn)自己的檢查邏輯了。
- 如果檢查是安全地就開啟TinkerPatchService,開始合并補丁包。
- 如果檢查沒通過就調(diào)用LoadReporter的相關方法通知。
@Override
public int onPatchReceived(String path) {
File patchFile = new File(path);
int returnCode = patchCheck(path, SharePatchFileUtil.getMD5(patchFile));
if (returnCode == ShareConstants.ERROR_PATCH_OK) {
TinkerPatchService.runPatchService(context, path);
} else {
Tinker.with(context).getLoadReporter().onLoadPatchListenerReceiveFail(new File(path), returnCode);
}
return returnCode;
}
到這里中心就要轉去TinkerPatchService了。
總結一下:
- TinkerInstaller使用了外觀模式,沒有真正邏輯處理,只是封裝了Tinker的各種調(diào)用,真正處理的邏輯在Tinker中。
- Tinker使用了單例模式(DCL) + 構建者模式。
- TinkerInstaller提供了兩個api:
- install()用來創(chuàng)建并初始化Tinker對象,并調(diào)用了Tinker對象的create()和install()。
- onReceiveUpgradePatch()中調(diào)用了Tinker對象中PatchListener的onPatchReceived()。
- Tinker的install()中也初始了TinkerPatchService,為后面做準備。
- PatchListener的onPatchReceived()用來檢查patch文件合法性并開啟執(zhí)行修復工作的TinkerPatchService。
3. TinkerPatchService
TinkerPatchService就是加載合并patch文件的Service,繼承了IntentService。
3-1 TinkerPatchService # setPatchProcessor()
回顧一下Tinker的install(),在install()中就傳入了兩個參數(shù)調(diào)用了setPatchProcessor()。再追溯參數(shù)來源其實是在我們自己編寫代碼調(diào)用TinkerInstaller的復雜install()傳入的或是調(diào)用簡單install()時Tinker的Builder為我們默認創(chuàng)建的。
多參數(shù)的源頭:
// 回顧我們自己編寫的代碼,調(diào)用 TinkerInstaller#install()。
// 最終最后兩個參數(shù)被注入到TinkerPatchService中。
AbstractPatch upgradePatchProcessor = new UpgradePatch();
TinkerInstaller.install(mAppLike, loadReporter,
patchReporter, mPatchListener,
CustomResultService.class,// 決定在patch文件安裝完畢后的操作
upgradePatchProcessor// 決定patch文件的安裝策略
);// 復雜的初始化方法
單參數(shù)的源頭:
// Tinker # install()
public void install(Intent intentResult) {
// 傳入的是為我們默認實例的對象。
install(intentResult, DefaultTinkerResultService.class, new UpgradePatch());
}
一直傳遞到setPatchProcessor(),最后向TinkerPatchService注入了這兩個對象。
- upgradePatch是我們直接創(chuàng)建的UpgradePatch()對象,表示patch文件的安裝策略。
- serviceClass是我們自定義的DefaultTinkerResultService類,表示修復完畢后的動作。
public static void setPatchProcessor(AbstractPatch upgradePatch, Class<? extends AbstractResultService> serviceClass) {
upgradePatchProcessor = upgradePatch;
resultServiceClass = serviceClass;
//try to load
try {
Class.forName(serviceClass.getName());
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
3-2 TinkerPatchService # runPatchService()
接著上面看一下runPatchService(),就是啟動TinkerPatchService的代碼,intent中還傳了一個patch文件路徑path和一個在Tinker#install()中傳入的一個ResultService類名。
public static void runPatchService(Context context, String path) {
try {
Intent intent = new Intent(context, TinkerPatchService.class);
intent.putExtra(PATCH_PATH_EXTRA, path);
intent.putExtra(RESULT_CLASS_EXTRA, resultServiceClass.getName());
context.startService(intent);
} catch (Throwable throwable) {
TinkerLog.e(TAG, "start patch service fail, exception:" + throwable);
}
}
3-3 TinkerPatchService # onHandleIntent()
- 在這個方法中最重要的就是調(diào)用了tryPatch(),也就是更近一步的修復邏輯,因為onHandleIntent()是支持耗時操作的,所以完全可以猜想tryPatch()是同步方法。
- 其次重要就是在處理完tryPatch()之后會開啟ResultService,執(zhí)行修復完畢后的工作。
@Override
protected void onHandleIntent(Intent intent) {
final Context context = getApplicationContext();
Tinker tinker = Tinker.with(context);
// PatchReporter回調(diào)
tinker.getPatchReporter().onPatchServiceStart(intent);
// ...一些異常判斷處理...
// 提升進程優(yōu)先級,盡可能保證此Service不被kill
increasingPriority();
PatchResult patchResult = new PatchResult();
try {
if (upgradePatchProcessor == null) {
throw new TinkerRuntimeException("upgradePatchProcessor is null.");
}
// 核心調(diào)用tryPatch()
result = upgradePatchProcessor.tryPatch(context, path, patchResult);
} catch (Throwable throwable) {
e = throwable;
result = false;
// PatchReporter回調(diào)
tinker.getPatchReporter().onPatchException(patchFile, e);
}
cost = SystemClock.elapsedRealtime() - begin;
// PatchReporter回調(diào)
tinker.getPatchReporter().onPatchResult(patchFile, result, cost);
patchResult.isSuccess = result;
patchResult.rawPatchFilePath = path;
patchResult.costTime = cost;
patchResult.e = e;
// 開始執(zhí)行ResultService,執(zhí)行修復完畢后的工作
AbstractResultService.runResultService(context, patchResult, getPatchResultExtra(intent));
}
3-4 TinkerPatchService # increasingPriority()
在說兩個核心過程之前先來看這個方法,這個方法在tryPatch()之前被調(diào)用,主要是利用系統(tǒng)漏洞讓Service優(yōu)先級高一些避免輕易被回收。
private void increasingPriority() {
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);
}
}
public static class InnerService extends Service {
@Override
public void onCreate() {
super.onCreate();
try {
startForeground(notificationId, new Notification());
} catch (Throwable e) {
TinkerLog.e(TAG, "InnerService set service for push exception:%s.", e);
}
// kill
stopSelf();
}
@Override
public void onDestroy() {
stopForeground(true);
super.onDestroy();
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
}
4. UpgradePatch # tryPatch()
在上面分析的TinkerPatchService#onHanleIntent():
result = upgradePatchProcessor.tryPatch(context, path, patchResult);
4-1 AbstractPatch
upgradePatchProcessor是AbstractPatch類的,只有一個抽象方法tryPatch()。
public abstract class AbstractPatch {
public abstract boolean tryPatch(Context context, String tempPatchPath, PatchResult patchResult);
}
upgradePatchProcessor是AbstractPatch的實例對象,在Tinker的install()調(diào)用時傳入,也就是多參數(shù)TinkerInstaller的install()傳入的,實現(xiàn)類是UpgradePatch,看tryPatch()實現(xiàn),真的是非常長,我們分成兩個部分來看,顯示檢查再是算法調(diào)用。
4-2 UpgradePatch # tryPatch()中的檢查邏輯
tryPatch()的返回值便是合成Patch成功與否,在方法的開始都是一些判斷,對Tinker的檢查、文件的檢查、簽名檢查、TinkerId檢查、文件md5檢查等,一旦檢查不安全就直接返回false。
@Override
public boolean tryPatch(Context context, String tempPatchPath, PatchResult patchResult) {
Tinker manager = Tinker.with(context);
final File patchFile = new File(tempPatchPath);
// 檢查Tinker參數(shù)和SharedPreferences是否可用
if (!manager.isTinkerEnabled() || !ShareTinkerInternals.isTinkerEnableWithSharedPreferences(context)) {
TinkerLog.e(TAG, "UpgradePatch tryPatch:patch is disabled, just return");
return false;
}
// 判斷patch存在、可讀、是文件、大小大于0
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);
// 解壓patch去檢查簽名、TinkerId、和patch壓縮包中文件全不全
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;
}
// 獲取patch的md5
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
// 存patch文件的md5
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);
// 看之前是不是有patch
SharePatchInfo oldInfo = SharePatchInfo.readAndCheckPropertyWithLock(patchInfoFile, patchInfoLockFile);
//it is a new patch, so we should not find a exist
SharePatchInfo newInfo;
//already have patch
// 構建newInfo
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;
}
// 檢查md5不空且長度正確
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);
}
// ......
//copy file
File destPatchFile = new File(patchVersionDirectory + "/" + SharePatchFileUtil.getPatchVersionFile(patchMd5));
try {
// check md5 first
if (!patchMd5.equals(SharePatchFileUtil.getMD5(destPatchFile))) {
// 檢查md5正確后拷貝文件,因為后面操作可能會發(fā)生以外而刪除patch文件。
// 所以在這里拷貝一份,后面的操作對拷貝的patch來操作。
SharePatchFileUtil.copyFileUsingStream(patchFile, destPatchFile);
TinkerLog.w(TAG, "UpgradePatch copy patch file, src file: %s size: %d, dest file: %s size:%d", patchFile.getAbsolutePath(), patchFile.lengt
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;
}
// ......檢查成功的后序合并算法調(diào)用
}
4-3 UpgradePatch # tryPatch()中合并算法的調(diào)用
在通過了這一系列檢查之后就到了真正合并文件算法的時候了,合并的文件分為三種:dex文件、.so文件和資源文件分別對應下面三個調(diào)用,只要一個修復工作失敗了,就返回false,修復算法我們在下一篇分析。
@Override
public boolean tryPatch(Context context, String tempPatchPath, PatchResult patchResult) {
// ......一系列檢查工作
//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;
}
// ......寫新的合并Patch信息
}
5. AbstractResultService
回顧3-3,在tryPatch()調(diào)用完成后,最后一句:
AbstractResultService.runResultService(context, patchResult, getPatchResultExtra(intent));
開啟了修復完成后的工作Service,DefaultTinkerResultService是默認實現(xiàn),或者也可以自定義,在TinkerInstaller#install()傳入。
5-1 AbstractResultService # runResultService()
resultServiceClass是一路傳遞過來的類名,到這里就是啟動了,默認給的是DefaultTinkerResultService
public static void runResultService(Context context, PatchResult result, String resultServiceClass) {
if (resultServiceClass == null) {
throw new TinkerRuntimeException("resultServiceClass is null.");
}
try {
Intent intent = new Intent();
intent.setClassName(context, resultServiceClass);
intent.putExtra(RESULT_EXTRA, result);
context.startService(intent);
} catch (Throwable throwable) {
TinkerLog.e(TAG, "run result service fail, exception:" + throwable);
}
}
5-2 DefaultTinkerResultService # onHandleIntent()
DefaultTinkerResultService沒有重寫該方法,父類實現(xiàn)直接調(diào)用的onPatchResult()
@Override
protected void onHandleIntent(Intent intent) {
if (intent == null) {
TinkerLog.e(TAG, "AbstractResultService received a null intent, ignoring.");
return;
}
PatchResult result = (PatchResult) ShareIntentUtil.getSerializableExtra(intent, RESULT_EXTRA);
onPatchResult(result);
}
5-3 DefaultTinkerResultService # onPatchResult()
首先會關閉PatchService,然后刪除patch文件,最后將應用進程殺死,再開就生效了。但是這樣的體驗不好,所以如果想要自己的邏輯,就可以自定義DefaultTinkerResultService,重寫onPatchService()。
@Override
public void onPatchResult(PatchResult result) {
// ......一些判斷和日志打印
//first, we want to kill the recover process
// 關閉TinkerPatchService
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) {
// 如果修復成功了,就把patch刪掉
deleteRawPatchFile(new File(result.rawPatchFilePath));
if (checkIfNeedKill(result)) {
// 這就是為什么不自定義ResultService時,修復完成應用會閃退
android.os.Process.killProcess(android.os.Process.myPid());
} else {
TinkerLog.i(TAG, "I have already install the newly patch version!");
}
}
}
到這里整個流程就結束了,默認的話此時進程已被殺死,再次啟動才能夠生效。就再分析分析啟動過程中發(fā)生的事情。