熱修復是什麼
答:
熱修復無疑是這2年較火的新技術,是作爲安卓工程師必學的技能之一。在熱修復出現之前,一個已經上線的app中如果出現了bug,即使是一個非常小的bug,不及時更新的話有可能存在風險,若要及時更新就得將app重新打包發佈到應用市場後,讓用戶再一次下載,這樣就大大降低了用戶體驗,當熱修復出現之後,這樣的問題就不再是問題了。
目前較火的熱修復方案大致分爲兩派,分別是:
- 阿里系:spohix、andfix:從底層二進制入手(c語言)。
- 騰訊系:tinker:從java加載機制入手。
有接觸過tinker嗎
答: 有接觸過Tinker的 Tinker是一個比較優異修復架構
修復的原理是什麼
答: 關於bug的概念自己百度百科吧,我認爲的bug一般有2種(可能不太準確):
- 代碼功能不符合項目預期,即代碼邏輯有問題。
- 程序代碼不夠健壯導致App運行時崩潰。
這兩種情況一般是一個或多個class出現了問題,在一個理想的狀態下,我們只需將修復好的這些個class更新到用戶手機上的app中就可以修復這些bug了。但說着簡單,要怎麼才能動態更新這些class呢?其實,不管是哪種熱修復方案,肯定是如下幾個步驟:
- 下發補丁(內含修復好的class)到用戶手機,即讓app從服務器上下載(網絡傳輸)
- app通過**"某種方式"**,使補丁中的class被app調用(本地更新)
這裏的**"某種方式"**,對本篇而言,就是使用Android的類加載器,通過類加載器加載這些修復好的class,覆蓋對應有問題的class,理論上就能修復bug了。所以,下面就先來了解和分析Android中的類加載器吧。
Tinker源碼分析
Tinker工程結構
直接從github上clone Tinker的源碼進行食用如下:
接入流程
- gradle相關配置主項目中
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的核心庫
compile('com.tencent.tinker:tinker-android-lib:1.8.1')
}
...
...
//apply tinker插件
apply plugin: 'com.tencent.tinker.patch'
這裏需要注意tinker編譯階段會判斷一個TinkerId
的字段,該字段默認由git提交記錄生成HEAD(git rev-parse --short HEAD
)而且是在rootproject中執行的git命令,所以個別工程可能在rootproject目錄沒有git init過,可以選擇在那初始化git或者自定義gradle修改gitSha
方法。
出包還是使用正常的build過程,測試階段選擇assembleDebug
,Tinker產出patch使用gradle tinkerPatchDebug
同樣也支持Flavor和Variant,Tiner會在主工程build
目錄下創建bakApk,下面會有一個app-yydd-hh-mm-ss
的目錄裏面對應有Favor子目錄裏面包含了通過assemble
出的apk包。在build
目錄下的outputs
中有tinkerPatch
裏面同樣也區分了build variant
產物。
需要注意的是在debug出包測試過程中需要修改gradle的參數
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信息作爲基準包
tinkerBuildFlavorDirectory = "${bakPath}/app-1020-11-52-37"
}
而release
出包可以直接在gradle命令帶上後綴-POLD_APK= -PAPPLY_MAPPING= -PAPPLY_RESOURCE=
- Application改造
Tinker採用了代碼框架的方案來解決應用啓動加載默認Application導致patch無法修復它。原理就是使用一個ApplicationLike
代理類來完成原Application的功能,把所有原理Application中的代碼邏輯移動到ApplicationLike
中,然後刪除原來的Application類通過註解讓Tinker自動生成默認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;
}
其中參數application
代表自動生成的application包名路徑,flags
代表tinker作用域包括res、so、dex,loadVerifyFlag
代表是否開啓加載patch前各個文件進行md5校驗,還有一個loaderClass
默認是"com.tencent.tinker.loader.TinkerLoader"
表示加載Tinker的主類名。
在onBaseContextAttached
方法裏需要初始化一些Tinker相關回調(在installTinker
方法中)PatchReporter
是對patch進程中合成過程的回調接口實現,LoadReporter
是對主進程加載patch dex補丁過程的回調接口實現。PatchListener
可以對接收到patch補丁後做自定義的check操作比如渠道檢查和存儲空間檢查。
設置AbstractResultService
的實現類TinkerResultService
作爲合成補丁完成後的處理重啓邏輯的IntentService。
設置AbstractPatch
的實現類UpgradePatch
類作爲合成patch方法tryPatch
實現類。
Tinker原理
先上github官方首頁的圖
BaseApk就是我們的基準包,也就是渠道上線的包。
NewApk就是我們的hotfix包,包括修復的代碼資源以及so文件。
Tinker做了對應的DexDiff、ResDiff、BsDiff來產出一個patch.apk,裏面具體內容也是由lib、res和dex文件組成,assets中還有對應的dex、res和so信息
然後Tinker通過找到基準包data/app/packagename/base.apk通過DexPatch合成新的dex,並且合成一個tinker_classN.apk
(其實就是包含了所有合成dex的zip包)接着在運行時通過反射把這個合成dex文件插入到PathClassLoader
中的dexElements
數組的前面,保證類加載時優先加載補丁dex中的class。
接下來我們就從加載patch和合成patch來弄清Tinker的整個工作流程。
Tinker源碼分析之加載補丁Patch流程
默認情況如果使用了Tinker註解產生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
中找到最終會調用loadTinker
方法來,最後反射調用了變量loaderClassName
定義類中的tryLoad
方法,默認是com.tencent.tinker.loader.TinkerLoader
這個類中的tryLoad
方法。該方法調用tryLoadPatchFilesInternal
來執行相關代碼邏輯。
private void tryLoadPatchFilesInternal(TinkerApplication app, Intent resultIntent) {
//..省略一大段校驗相關邏輯代碼
//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校驗,一共有包括tinker自身enable屬性以及md5和文件存在等相關檢查。
先看加載dex部分,TinkerDexLoader.loadTinkerJars
傳入四個參數,分別爲application,patchVersionDirectory當前patch文件目錄,oatDir當前patch的oat文件目錄,intent,當前patch是否需要進行oat(由於系統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校驗代碼,首先獲取到PathClassLoader
並且通過判斷系統是否art過濾出對應legalFiles
,如果發現系統進行過OTA升級則通過ProcessBuilder
命令行執行dex2oat
進行並行的oat優化dex,最後調用installDexes
來安裝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);
}
}
}
針對不同的Android版本需要對DexPathList
中的dexElements
生成方法makeDexElements
進行適配。
主要做的事情就是獲取當前app運行時PathClassLoader
的父類BaseDexClassLoader
中的pathList
對象,通過反射它的makePathElements
方法傳入對應的path參數構造出Element[]
數組對象,然後拿到pathList
中的Element[]
數組對象dexElements
兩者進行合併排序,把patch的相關dex信息放在數組前端,最後合併數組結果賦值給pathList
保證classloader優先到patch中查找加載。
Tinker源碼分析之合成補丁Patch流程
合併代碼入口
Tinker.with(context).getPatchListener().onPatchReceived(patchLocation);
傳入patch文件所在位置即可,推薦通過服務端下發下載到對應的/data/data/
應用目錄下防止被三方軟件清理,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;
}
先進行tinker的一些初始化配置檢查還有patch文件的md5校驗。如果check通過returnCode
爲0則執行runPatchService
啓動一個IntentService
的子類TinkerPatchService
來處理patch的合成。接下來看Service執行任務代碼:
@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));
}
回調PatchReporter
接口的onPatchServiceStart
方法,然後取到patch文件同時調用increasingPriority
啓動一個不可見前臺Service保活這個TinkerPatchService
,最後開始合成patchupgradePatchProcessor.tryPatch
。同樣省略一些常規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
,查看舊補丁是否正在執行oat過程,後續會等待主進程oat執行完畢。 2.拷貝new patch到app的data目錄的tinker目錄下,防止被三方軟件刪除。 3.分別判斷執行tryRecoverDexFiles
合成dex,tryRecoverLibraryFiles
合成so以及tryRecoverResourceFiles
合成資源。
主要看下dex合成過程,這也是我們最關心的地方。
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
信息轉換成String
,進入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);
}
首先執行方法extractDexDiffInternals
傳入了合成後dex
路徑,前面讀取的dex_meta
信息,patch文件以及type類型dex。爲了節約篇幅只提取了主要的代碼,詳細代碼參考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
內容
對應的
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.循環遍歷獲取到patch中各個classes.dex的crc和md5信息以及一大片校驗代碼,調用patchDexFile
方法對base.apk和patch中的dex做合併生成新的dex。
3.把合成的dex壓縮爲一個tinker_classN.apk
接下來看patchDexFile
方法,同樣只提取了關鍵代碼。
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官網上對raw和jar區別的解釋
Tinker中的dex配置'raw'與'jar'模式應該如何選擇? 它們應該說各有優劣勢,大概應該有以下幾條原則: 如果你的minSdkVersion小於14, 那你務必要選擇'jar'模式; 以一個10M的dex爲例,它壓縮成jar大約爲4M,即'jar'模式能節省6M的ROM空間。 對於'jar'模式,我們需要驗證壓縮包流中dex的md5,這會更耗時,在小米2S上數據大約爲'raw'模式126ms, 'jar'模式爲246ms。 因爲在合成過程中我們已經校驗了各個文件的Md5,並將它們存放在/data/data/..目錄中。默認每次加載時我們並不會去校驗tinker文件的Md5,但是你也可通過開啓loadVerifyFlag強制每次加載時校驗,但是這會帶來一定的時間損耗。 簡單來說,'jar'模式更省空間,但是運行時校驗的耗時大約爲'raw'模式的兩倍。如果你沒有打開運行時校驗,推薦使用'jar'模式。
最後通過ZipFile拿到base.apk和patch中對應dex文件進行合成爲patchedDexFile
。核心部分是如何把差分的dex和基準dex做合成處理產生新的dex,這部分涉及到了dex文件結構、DexDiff和DexPatch算法
扣扣掃碼加入粉絲羣,領取福利