Tinker實(shí)現(xiàn)原理和源碼分析
Tinker工程結(jié)構(gòu)
直接從github上clone Tinker的源碼進(jìn)行食用如下:
接入流程
- gradle相關(guān)配置主項(xiàng)目中
build.gradle
加入
buildscript {
dependencies {
classpath ('com.tencent.tinker:tinker-patch-gradle-plugin:1.8.1')
}
}
在app工程中build.gradle
加入
dependencies {
//可選筐喳,用于生成application類
provided('com.tencent.tinker:tinker-android-anno:1.8.1')
//tinker的核心庫(kù)
compile('com.tencent.tinker:tinker-android-lib:1.8.1')
}
...
...
//apply tinker插件
apply plugin: 'com.tencent.tinker.patch'
這里需要注意tinker編譯階段會(huì)判斷一個(gè)TinkerId
的字段羞秤,該字段默認(rèn)由git提交記錄生成HEAD(git rev-parse --short HEAD
)而且是在rootproject中執(zhí)行的git命令,所以個(gè)別工程可能在rootproject目錄沒(méi)有g(shù)it init過(guò)靴庆,可以選擇在那初始化git或者自定義gradle修改gitSha
方法。
出包還是使用正常的build過(guò)程,測(cè)試階段選擇assembleDebug
,Tinker產(chǎn)出patch使用gradle tinkerPatchDebug
同樣也支持Flavor和Variant,Tiner會(huì)在主工程build
目錄下創(chuàng)建bakApk,下面會(huì)有一個(gè)app-yydd-hh-mm-ss
的目錄里面對(duì)應(yīng)有Favor子目錄里面包含了通過(guò)assemble
出的apk包沽翔。在build
目錄下的outputs
中有tinkerPatch
里面同樣也區(qū)分了build variant
產(chǎn)物。
需要注意的是在debug出包測(cè)試過(guò)程中需要修改gradle的參數(shù)
ext {
//for some reason, you may want to ignore tinkerBuild, such as instant run debug build?
tinkerEnabled = true
//for normal build
//old apk file to build patch apk
tinkerOldApkPath = "${bakPath}/app-debug-1018-17-58-54.apk"
//proguard mapping file to build patch apk
tinkerApplyMappingPath = "${bakPath}/app-debug-1018-17-32-47-mapping.txt"
//resource R.txt to build patch apk, must input if there is resource changed
tinkerApplyResourcePath = "${bakPath}/app-debug-1018-17-32-47-R.txt"
//使用buildvariants修改此處app信息作為基準(zhǔn)包
tinkerBuildFlavorDirectory = "${bakPath}/app-1020-11-52-37"
}
而release
出包可以直接在gradle命令帶上后綴-POLD_APK= -PAPPLY_MAPPING= -PAPPLY_RESOURCE=
- Application改造
Tinker采用了代碼框架的方案來(lái)解決應(yīng)用啟動(dòng)加載默認(rèn)Application導(dǎo)致patch無(wú)法修復(fù)它窿凤。原理就是使用一個(gè)ApplicationLike
代理類來(lái)完成原Application的功能仅偎,把所有原理Application中的代碼邏輯移動(dòng)到ApplicationLike
中,然后刪除原來(lái)的Application類通過(guò)注解讓Tinker自動(dòng)生成默認(rèn)Application卷玉。
@DefaultLifeCycle(application = "com.*.Application",
flags = ShareConstants.TINKER_ENABLE_ALL,
loadVerifyFlag = false)
public class ApplicationLike extends DefaultApplicationLike {
@Override
public void onBaseContextAttached(Context base) {
super.onBaseContextAttached(base);
//you must install multiDex whatever tinker is installed!
MultiDex.install(base);
TinkerManager.setTinkerApplicationLike(this);
TinkerManager.initFastCrashProtect();
//should set before tinker is installed
TinkerManager.setUpgradeRetryEnable(true);
//installTinker after load multiDex
//or you can put com.tencent.tinker.** to main dex
TinkerManager.installTinker(this);
}
}
TinkerManager.java
public static void installTinker(ApplicationLike appLike) {
if (isInstalled) {
TinkerLog.w(TAG, "install tinker, but has installed, ignore");
return;
}
//or you can just use DefaultLoadReporter
LoadReporter loadReporter = new TinkerLoadReporter(appLike.getApplication());
//or you can just use DefaultPatchReporter
PatchReporter patchReporter = new TinkerPatchReporter(appLike.getApplication());
//or you can just use DefaultPatchListener
PatchListener patchListener = new TinkerPatchListener(appLike.getApplication());
//you can set your own upgrade patch if you need
AbstractPatch upgradePatchProcessor = new UpgradePatch();
TinkerInstaller.install(appLike,
loadReporter, patchReporter, patchListener,
TinkerResultService.class, upgradePatchProcessor);
isInstalled = true;
}
其中參數(shù)application
代表自動(dòng)生成的application包名路徑哨颂,flags
代表tinker作用域包括res、so相种、dex威恼,loadVerifyFlag
代表是否開(kāi)啟加載patch前各個(gè)文件進(jìn)行md5校驗(yàn),還有一個(gè)loaderClass
默認(rèn)是"com.tencent.tinker.loader.TinkerLoader"
表示加載Tinker的主類名。
在onBaseContextAttached
方法里需要初始化一些Tinker相關(guān)回調(diào)(在installTinker
方法中)PatchReporter
是對(duì)patch進(jìn)程中合成過(guò)程的回調(diào)接口實(shí)現(xiàn)寝并,LoadReporter
是對(duì)主進(jìn)程加載patch dex補(bǔ)丁過(guò)程的回調(diào)接口實(shí)現(xiàn)箫措。PatchListener
可以對(duì)接收到patch補(bǔ)丁后做自定義的check操作比如渠道檢查和存儲(chǔ)空間檢查。
設(shè)置AbstractResultService
的實(shí)現(xiàn)類TinkerResultService
作為合成補(bǔ)丁完成后的處理重啟邏輯的IntentService衬潦。
設(shè)置AbstractPatch
的實(shí)現(xiàn)類UpgradePatch
類作為合成patch方法tryPatch
實(shí)現(xiàn)類斤蔓。
Tinker原理
先上github官方首頁(yè)的圖
BaseApk就是我們的基準(zhǔn)包,也就是渠道上線的包镀岛。
NewApk就是我們的hotfix包弦牡,包括修復(fù)的代碼資源以及so文件。
Tinker做了對(duì)應(yīng)的DexDiff漂羊、ResDiff驾锰、BsDiff來(lái)產(chǎn)出一個(gè)patch.apk,里面具體內(nèi)容也是由lib、res和dex文件組成走越,assets中還有對(duì)應(yīng)的dex椭豫、res和so信息
然后Tinker通過(guò)找到基準(zhǔn)包data/app/packagename/base.apk通過(guò)DexPatch合成新的dex,并且合成一個(gè)tinker_classN.apk
(其實(shí)就是包含了所有合成dex的zip包)接著在運(yùn)行時(shí)通過(guò)反射把這個(gè)合成dex文件插入到PathClassLoader
中的dexElements
數(shù)組的前面,保證類加載時(shí)優(yōu)先加載補(bǔ)丁dex中的class赏酥。
接下來(lái)我們就從加載patch和合成patch來(lái)弄清Tinker的整個(gè)工作流程喳整。
Tinker源碼分析之加載補(bǔ)丁Patch流程
默認(rèn)情況如果使用了Tinker注解產(chǎn)生Application
可以看到它繼承了TinkerApplication
/**
*
* Generated application for tinker life cycle
*
*/
public class Application extends TinkerApplication {
public Application() {
super(7, "com.jiuyan.infashion.ApplicationLike", "com.tencent.tinker.loader.TinkerLoader", false);
}
}
跟蹤到TinkerApplication
在方法attachBaseContext
中找到最終會(huì)調(diào)用loadTinker
方法來(lái),最后反射調(diào)用了變量loaderClassName
定義類中的tryLoad
方法,默認(rèn)是com.tencent.tinker.loader.TinkerLoader
這個(gè)類中的tryLoad
方法裸扶。該方法調(diào)用tryLoadPatchFilesInternal
來(lái)執(zhí)行相關(guān)代碼邏輯框都。
private void tryLoadPatchFilesInternal(TinkerApplication app, Intent resultIntent) {
//..省略一大段校驗(yàn)相關(guān)邏輯代碼
//now we can load patch jar
if (isEnabledForDex) {
boolean loadTinkerJars = TinkerDexLoader.loadTinkerJars(app, patchVersionDirectory, oatDex, resultIntent, isSystemOTA);
if (isSystemOTA) {
// update fingerprint after load success
patchInfo.fingerPrint = Build.FINGERPRINT;
patchInfo.oatDir = loadTinkerJars ? ShareConstants.INTERPRET_DEX_OPTIMIZE_PATH : ShareConstants.DEFAULT_DEX_OPTIMIZE_PATH;
// reset to false
oatModeChanged = false;
if (!SharePatchInfo.rewritePatchInfoFileWithLock(patchInfoFile, patchInfo, patchInfoLockFile)) {
ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_REWRITE_PATCH_INFO_FAIL);
Log.w(TAG, "tryLoadPatchFiles:onReWritePatchInfoCorrupted");
return;
}
// update oat dir
resultIntent.putExtra(ShareIntentUtil.INTENT_PATCH_OAT_DIR, patchInfo.oatDir);
}
if (!loadTinkerJars) {
Log.w(TAG, "tryLoadPatchFiles:onPatchLoadDexesFail");
return;
}
}
//now we can load patch resource
if (isEnabledForResource) {
boolean loadTinkerResources = TinkerResourceLoader.loadTinkerResources(app, patchVersionDirectory, resultIntent);
if (!loadTinkerResources) {
Log.w(TAG, "tryLoadPatchFiles:onPatchLoadResourcesFail");
return;
}
}
// kill all other process if oat mode change
if (oatModeChanged) {
ShareTinkerInternals.killAllOtherProcess(app);
Log.i(TAG, "tryLoadPatchFiles:oatModeChanged, try to kill all other process");
}
//all is ok!
ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_OK);
Log.i(TAG, "tryLoadPatchFiles: load end, ok!");
return;
}
這里省略了非常多的Tinker校驗(yàn),一共有包括tinker自身enable屬性以及md5和文件存在等相關(guān)檢查呵晨。
先看加載dex部分瞬项,TinkerDexLoader.loadTinkerJars
傳入四個(gè)參數(shù),分別為application何荚,patchVersionDirectory當(dāng)前patch文件目錄,oatDir當(dāng)前patch的oat文件目錄猪杭,intent餐塘,當(dāng)前patch是否需要進(jìn)行oat(由于系統(tǒng)OTA更新需要dex oat重新生成緩存)。
/**
* Load tinker JARs and add them to
* the Application ClassLoader.
*
* @param application The application.
*/
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
public static boolean loadTinkerJars(final TinkerApplication application, String directory, String oatDir, Intent intentResult, boolean isSystemOTA) {
if (loadDexList.isEmpty() && classNDexInfo.isEmpty()) {
Log.w(TAG, "there is no dex to load");
return true;
}
PathClassLoader classLoader = (PathClassLoader) TinkerDexLoader.class.getClassLoader();
if (classLoader != null) {
Log.i(TAG, "classloader: " + classLoader.toString());
} else {
Log.e(TAG, "classloader is null");
ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_VERSION_DEX_CLASSLOADER_NULL);
return false;
}
String dexPath = directory + "/" + DEX_PATH + "/";
ArrayList<File> legalFiles = new ArrayList<>();
for (ShareDexDiffPatchInfo info : loadDexList) {
//for dalvik, ignore art support dex
if (isJustArtSupportDex(info)) {
continue;
}
String path = dexPath + info.realName;
File file = new File(path);
//...check md5
legalFiles.add(file);
}
//... verify merge classN.apk
File optimizeDir = new File(directory + "/" + oatDir);
if (isSystemOTA) {
final boolean[] parallelOTAResult = {true};
final Throwable[] parallelOTAThrowable = new Throwable[1];
String targetISA;
try {
targetISA = ShareTinkerInternals.getCurrentInstructionSet();
} catch (Throwable throwable) {
Log.i(TAG, "getCurrentInstructionSet fail:" + throwable);
// try {
// targetISA = ShareOatUtil.getOatFileInstructionSet(testOptDexFile);
// } catch (Throwable throwable) {
// don't ota on the front
deleteOutOfDateOATFile(directory);
intentResult.putExtra(ShareIntentUtil.INTENT_PATCH_INTERPRET_EXCEPTION, throwable);
ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_GET_OTA_INSTRUCTION_SET_EXCEPTION);
return false;
// }
}
deleteOutOfDateOATFile(directory);
Log.w(TAG, "systemOTA, try parallel oat dexes, targetISA:" + targetISA);
// change dir
optimizeDir = new File(directory + "/" + INTERPRET_DEX_OPTIMIZE_PATH);
TinkerDexOptimizer.optimizeAll(
legalFiles, optimizeDir, true, targetISA,
new TinkerDexOptimizer.ResultCallback() {
//... callback
}
);
if (!parallelOTAResult[0]) {
Log.e(TAG, "parallel oat dexes failed");
intentResult.putExtra(ShareIntentUtil.INTENT_PATCH_INTERPRET_EXCEPTION, parallelOTAThrowable[0]);
ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_OTA_INTERPRET_ONLY_EXCEPTION);
return false;
}
}
try {
SystemClassLoaderAdder.installDexes(application, classLoader, optimizeDir, legalFiles);
} catch (Throwable e) {
Log.e(TAG, "install dexes failed");
// e.printStackTrace();
intentResult.putExtra(ShareIntentUtil.INTENT_PATCH_EXCEPTION, e);
ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_VERSION_DEX_LOAD_EXCEPTION);
return false;
}
return true;
}
省略了幾處md5校驗(yàn)代碼皂吮,首先獲取到PathClassLoader
并且通過(guò)判斷系統(tǒng)是否art過(guò)濾出對(duì)應(yīng)legalFiles
戒傻,如果發(fā)現(xiàn)系統(tǒng)進(jìn)行過(guò)OTA升級(jí)則通過(guò)ProcessBuilder
命令行執(zhí)行dex2oat
進(jìn)行并行的oat優(yōu)化dex,最后調(diào)用installDexes
來(lái)安裝dex蜂筹。
@SuppressLint("NewApi")
public static void installDexes(Application application, PathClassLoader loader, File dexOptDir, List<File> files)
throws Throwable {
Log.i(TAG, "installDexes dexOptDir: " + dexOptDir.getAbsolutePath() + ", dex size:" + files.size());
if (!files.isEmpty()) {
files = createSortedAdditionalPathEntries(files);
ClassLoader classLoader = loader;
if (Build.VERSION.SDK_INT >= 24 && !checkIsProtectedApp(files)) {
classLoader = AndroidNClassLoader.inject(loader, application);
}
//because in dalvik, if inner class is not the same classloader with it wrapper class.
//it won't fail at dex2opt
if (Build.VERSION.SDK_INT >= 23) {
V23.install(classLoader, files, dexOptDir);
} else if (Build.VERSION.SDK_INT >= 19) {
V19.install(classLoader, files, dexOptDir);
} else if (Build.VERSION.SDK_INT >= 14) {
V14.install(classLoader, files, dexOptDir);
} else {
V4.install(classLoader, files, dexOptDir);
}
//install done
sPatchDexCount = files.size();
Log.i(TAG, "after loaded classloader: " + classLoader + ", dex size:" + sPatchDexCount);
if (!checkDexInstall(classLoader)) {
//reset patch dex
SystemClassLoaderAdder.uninstallPatchDex(classLoader);
throw new TinkerRuntimeException(ShareConstants.CHECK_DEX_INSTALL_FAIL);
}
}
}
針對(duì)不同的Android版本需要對(duì)DexPathList
中的dexElements
生成方法makeDexElements
進(jìn)行適配需纳。
主要做的事情就是獲取當(dāng)前app運(yùn)行時(shí)PathClassLoader
的父類BaseDexClassLoader
中的pathList
對(duì)象,通過(guò)反射它的makePathElements
方法傳入對(duì)應(yīng)的path參數(shù)構(gòu)造出Element[]
數(shù)組對(duì)象艺挪,然后拿到pathList
中的Element[]
數(shù)組對(duì)象dexElements
兩者進(jìn)行合并排序不翩,把patch的相關(guān)dex信息放在數(shù)組前端,最后合并數(shù)組結(jié)果賦值給pathList
保證classloader優(yōu)先到patch中查找加載麻裳。
Tinker源碼分析之合成補(bǔ)丁Patch流程
合并代碼入口
Tinker.with(context).getPatchListener().onPatchReceived(patchLocation);
傳入patch文件所在位置即可口蝠,推薦通過(guò)服務(wù)端下發(fā)下載到對(duì)應(yīng)的/data/data/
應(yīng)用目錄下防止被三方軟件清理,onPatchReceived
方法在DefaultPatchListener.java
中津坑。
@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;
}
先進(jìn)行tinker的一些初始化配置檢查還有patch文件的md5校驗(yàn)妙蔗。如果check通過(guò)returnCode
為0則執(zhí)行runPatchService
啟動(dòng)一個(gè)IntentService
的子類TinkerPatchService
來(lái)處理patch的合成。接下來(lái)看Service執(zhí)行任務(wù)代碼:
@Override
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);
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;
AbstractResultService.runResultService(context, patchResult, getPatchResultExtra(intent));
}
回調(diào)PatchReporter
接口的onPatchServiceStart
方法疆瑰,然后取到patch文件同時(shí)調(diào)用increasingPriority
啟動(dòng)一個(gè)不可見(jiàn)前臺(tái)Service泵挤矗活這個(gè)TinkerPatchService
,最后開(kāi)始合成patchupgradePatchProcessor.tryPatch
穆役。同樣省略一些常規(guī)check代碼:
@Override
public boolean tryPatch(Context context, String tempPatchPath, PatchResult patchResult) {
Tinker manager = Tinker.with(context);
final File patchFile = new File(tempPatchPath);
//...省略
//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));
//...省略
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;
}
//...省略
}
1.檢查是否有之前的patch信息oldInfo
,查看舊補(bǔ)丁是否正在執(zhí)行oat過(guò)程,后續(xù)會(huì)等待主進(jìn)程oat執(zhí)行完畢寸五。
2.拷貝new patch到app的data目錄的tinker目錄下,防止被三方軟件刪除孵睬。
3.分別判斷執(zhí)行tryRecoverDexFiles
合成dex播歼,tryRecoverLibraryFiles
合成so以及tryRecoverResourceFiles
合成資源。
主要看下dex合成過(guò)程,這也是我們最關(guān)心的地方秘狞。
protected static boolean tryRecoverDexFiles(Tinker manager, ShareSecurityCheck checker, Context context,
String patchVersionDirectory, File patchFile) {
if (!manager.isEnabledForDex()) {
TinkerLog.w(TAG, "patch recover, dex is not enabled");
return true;
}
String dexMeta = checker.getMetaContentMap().get(DEX_META_FILE);
if (dexMeta == null) {
TinkerLog.w(TAG, "patch recover, dex is not contained");
return true;
}
long begin = SystemClock.elapsedRealtime();
boolean result = patchDexExtractViaDexDiff(context, patchVersionDirectory, dexMeta, patchFile);
long cost = SystemClock.elapsedRealtime() - begin;
TinkerLog.i(TAG, "recover dex result:%b, cost:%d", result, cost);
return result;
}
讀取patch包assets/dex_meta.txt
信息轉(zhuǎn)換成String
叭莫,進(jìn)入patchDexExtractViaDexDiff
方法。
private static boolean patchDexExtractViaDexDiff(Context context, String patchVersionDirectory, String meta, final File patchFile) {
String dir = patchVersionDirectory + "/" + DEX_PATH + "/";
if (!extractDexDiffInternals(context, dir, meta, patchFile, TYPE_DEX)) {
TinkerLog.w(TAG, "patch recover, extractDiffInternals fail");
return false;
}
File dexFiles = new File(dir);
File[] files = dexFiles.listFiles();
List<File> dexList = files != null ? Arrays.asList(files) : null;
final String optimizeDexDirectory = patchVersionDirectory + "/" + DEX_OPTIMIZE_PATH + "/";
return dexOptimizeDexFiles(context, dexList, optimizeDexDirectory, patchFile);
}
首先執(zhí)行方法extractDexDiffInternals
傳入了合成后dex
路徑,前面讀取的dex_meta
信息,patch文件以及type類型dex烁试。為了節(jié)約篇幅只提取了主要的代碼雇初,詳細(xì)代碼參考github。
private static boolean extractDexDiffInternals(Context context, String dir, String meta, File patchFile, int type) {
//parse
patchList.clear();
ShareDexDiffPatchInfo.parseDexDiffPatchInfo(meta, patchList);
//獲取base.apk
String apkPath = applicationInfo.sourceDir;
apk = new ZipFile(apkPath);
patch = new ZipFile(patchFile);
for (ShareDexDiffPatchInfo info : patchList) {
String patchRealPath;
if (infoPath.equals("")) {
patchRealPath = info.rawName;
} else {
patchRealPath = info.path + "/" + info.rawName;
}
File extractedFile = new File(dir + info.realName);
//..省略
ZipEntry patchFileEntry = patch.getEntry(patchRealPath);
ZipEntry rawApkFileEntry = apk.getEntry(patchRealPath);
patchDexFile(apk, patch, rawApkFileEntry, patchFileEntry, info, extractedFile);
}
if (!mergeClassNDexFiles(context, patchFile, dir)) {
return false;
}
}
1.解析dex_meta
內(nèi)容
對(duì)應(yīng)的
ShareDexDiffPatchInfo
信息
final String name = kv[0].trim();
final String path = kv[1].trim();
final String destMd5InDvm = kv[2].trim();
final String destMd5InArt = kv[3].trim();
final String dexDiffMd5 = kv[4].trim();
final String oldDexCrc = kv[5].trim();
final String newDexCrc = kv[6].trim();
final String dexMode = kv[7].trim();
2.循環(huán)遍歷獲取到patch中各個(gè)classes.dex的crc和md5信息以及一大片校驗(yàn)代碼减响,調(diào)用patchDexFile
方法對(duì)base.apk和patch中的dex做合并生成新的dex靖诗。
3.把合成的dex壓縮為一個(gè)tinker_classN.apk
接下來(lái)看patchDexFile
方法,同樣只提取了關(guān)鍵代碼支示。
private static void patchDexFile(
ZipFile baseApk, ZipFile patchPkg, ZipEntry oldDexEntry, ZipEntry patchFileEntry,
ShareDexDiffPatchInfo patchInfo, File patchedDexFile) throws IOException {
InputStream oldDexStream = null;
InputStream patchFileStream = null;
oldDexStream = new BufferedInputStream(baseApk.getInputStream(oldDexEntry));
patchFileStream = (patchFileEntry != null ? new BufferedInputStream(patchPkg.getInputStream(patchFileEntry)) : null);
//...省略判斷dex是否是jar類型或者是raw類型刊橘,做不同處理
new DexPatchApplier(oldDexStream, patchFileStream).executeAndSaveTo(patchedDexFile);
}
下面是github官網(wǎng)上對(duì)raw和jar區(qū)別的解釋
Tinker中的dex配置'raw'與'jar'模式應(yīng)該如何選擇?
它們應(yīng)該說(shuō)各有優(yōu)劣勢(shì)颂鸿,大概應(yīng)該有以下幾條原則:
如果你的minSdkVersion小于14, 那你務(wù)必要選擇'jar'模式促绵;
以一個(gè)10M的dex為例,它壓縮成jar大約為4M嘴纺,即'jar'模式能節(jié)省6M的ROM空間败晴。
對(duì)于'jar'模式,我們需要驗(yàn)證壓縮包流中dex的md5,這會(huì)更耗時(shí)栽渴,在小米2S上數(shù)據(jù)大約為'raw'模式126ms, 'jar'模式為246ms尖坤。
因?yàn)樵诤铣蛇^(guò)程中我們已經(jīng)校驗(yàn)了各個(gè)文件的Md5,并將它們存放在/data/data/..目錄中闲擦。默認(rèn)每次加載時(shí)我們并不會(huì)去校驗(yàn)tinker文件的Md5,但是你也可通過(guò)開(kāi)啟loadVerifyFlag強(qiáng)制每次加載時(shí)校驗(yàn)慢味,但是這會(huì)帶來(lái)一定的時(shí)間損耗。
簡(jiǎn)單來(lái)說(shuō)墅冷,'jar'模式更省空間贮缕,但是運(yùn)行時(shí)校驗(yàn)的耗時(shí)大約為'raw'模式的兩倍。如果你沒(méi)有打開(kāi)運(yùn)行時(shí)校驗(yàn)俺榆,推薦使用'jar'模式感昼。
最后通過(guò)ZipFile拿到base.apk和patch中對(duì)應(yīng)dex文件進(jìn)行合成為patchedDexFile
。核心部分是如何把差分的dex和基準(zhǔn)dex做合成處理產(chǎn)生新的dex罐脊,這部分涉及到了dex文件結(jié)構(gòu)定嗓、DexDiff和DexPatch算法,官方wiki里提供了一篇分析文章萍桌。