preface
The analysis of important classes is fully annotated, and the code is inGithub.com/ColorfulHor…
In the last article, we analyzed the generation of patch package. This article started from the source code to briefly analyze the synthesis process of patch package. Limited to space, it focused on the analysis of Dex synthesis process, involving knowledge including Dex loading mechanism, Art compilation mechanism, etc.
Tinker a baseline package can be patched with multiple patches, because each patch package is patched on the baseline package, and the synthesized product will be kept in a separate directory without affecting each other or the original APP. It can be removed and returned to the baseline version at any time.
Initialize the Tinker,
To use the API provided by Tinker, you need to call Tinker. install to initialize it. Otherwise, you can only use the TinkerApplicationHelper API. Synthetic patch callback. The parameters are as follows
- ApplicationLike application agent
- LoadReporter The callback class that loads the patch, DefaultLoadReporter by default
- PatchReporter Callback class for synthetic patches. DefaultPatchReporter is the default
- Listener Class to receive the synthesis patch task, DefaultPatchListener by default
- ResultServiceClass patch patch synthesis process to synthesize results back to the master class, DefaultTinkerResultService by default
- UpgradePatchProcessor Class that performs patch synthesis. The default value is UpgradePatch
public static Tinker install(ApplicationLike applicationLike, LoadReporter loadReporter, PatchReporter patchReporter, PatchListener listener, Class
resultServiceClass, AbstractPatch upgradePatchProcessor) {
// Create an instance and register some callbacks
Tinker tinker = new Tinker.Builder(applicationLike.getApplication())
.tinkerFlags(applicationLike.getTinkerFlags())
.loadReport(loadReporter)
.listener(listener)
.patchReporter(patchReporter)
// Check whether the patch is verified by MD5
.tinkerLoadVerifyFlag(applicationLike.getTinkerLoadVerifyFlag()).build();
Tinker.create(tinker);
// getTinkerResultIntent is the result of the patch loading
tinker.install(applicationLike.getTinkerResultIntent(), resultServiceClass, upgradePatchProcessor);
return tinker;
}
Copy the code
public void install(Intent intentResult, Class
serviceClass, AbstractPatch upgradePatch) {
sInstalled = true;
/ / patch service set used in patch synthesis UpgradePatch instance, as well as the synthesis results callback class DefaultTinkerResultServiceTinkerPatchService.setPatchProcessor(upgradePatch, serviceClass); . tinkerLoadResult =new TinkerLoadResult();
tinkerLoadResult.parseTinkerResult(getContext(), intentResult);
// Callback patch loading result
loadReporter.onLoadResult(patchDirectory, tinkerLoadResult.loadCode, tinkerLoadResult.costTime);
}
Copy the code
Patch synthesis
After a successful download patch by calling TinkerInstaller. OnReceiveUpgradePatch composite patches, then direct call to the DefaultPatchListener. OnPatchReceived method, This method first calls patchCheck to check whether the patch should be synthesized, and then starts TinkerPatchService to synthesize the patch.
public class DefaultPatchListener implements PatchListener {
public int onPatchReceived(String path) {
final File patchFile = new File(path);
// Difference apk MD5
final String patchMD5 = SharePatchFileUtil.getMD5(patchFile);
// Verify the patch
// Check whether the patch should be synthesized
// The patch is invalid/the patch is being synthesized/the system is started for the first time after OTA
// The patch of this version has been loaded. / The patch of this version has been synthesized. / The patch merge failure exceeds the threshold
final int returnCode = patchCheck(path, patchMD5);
if (returnCode == ShareConstants.ERROR_PATCH_OK) {
// Bind TinkerPatchForeService, which runs in the :patch process
runForgService();
// Start TinkerPatchService
TinkerPatchService.runPatchService(context, path);
} else {
Tinker.with(context).getLoadReporter().onLoadPatchListenerReceiveFail(new File(path), returnCode);
}
returnreturnCode; }}Copy the code
Whether a synthetic patch is required
PatchCheck mainly checks the validity of the patch package and whether the patch of this version has been loaded/synthesized, so as to determine whether the patch needs to be synthesized. It should be noted that the patch cannot be synthesized even after the system is started for the first time after OTA upgrade, because the system is running in interpretation mode and dexopt needs to be performed again. Here is the verification process comment code. The logic related to the interpretMode used for loading patches will be analyzed separately in the loading patch, so there is no need to pay too much attention here.
protected int patchCheck(String path, String patchMd5) {
final Tinker manager = Tinker.with(context);
// Whether tinker is enabled
if(! manager.isTinkerEnabled() || ! ShareTinkerInternals.isTinkerEnableWithSharedPreferences(context)) {return ShareConstants.ERROR_PATCH_DISABLE;
}
// Md5 and file validity check
if (TextUtils.isEmpty(patchMd5)) {
return ShareConstants.ERROR_PATCH_NOTEXIST;
}
final File file = new File(path);
if(! SharePatchFileUtil.isLegalFile(file)) {return ShareConstants.ERROR_PATCH_NOTEXIST;
}
// Cannot be called in patch process
if (manager.isPatchProcess()) {
return ShareConstants.ERROR_PATCH_INSERVICE;
}
// The patch process is already running
if (TinkerServiceInternals.isTinkerPatchServiceRunning(context)) {
return ShareConstants.ERROR_PATCH_RUNNING;
}
If the JIT option is incorrectly enabled for systems up to 7.0 (Art was re-introduced after 7.0, some custom ROMs will incorrectly enable this option)
if (ShareTinkerInternals.isVmJit()) {
return ShareConstants.ERROR_PATCH_JIT;
}
// Information about patch loading during startup
final TinkerLoadResult loadResult = manager.getTinkerLoadResultIfPresent();
// Whether you are currently running in explain mode, true indicates that dexopt needs to be re-done
final booleanrepairOptNeeded = manager.isMainProcess() && loadResult ! =null && loadResult.useInterpretMode;
if(! repairOptNeeded) {if(manager.isTinkerLoaded() && loadResult ! =null) {
String currentVersion = loadResult.currentVersion;
// This version of the patch is already loaded
if (patchMd5.equals(currentVersion)) {
returnShareConstants.ERROR_PATCH_ALREADY_APPLY; }}// The patch has been synthesized, but the main process has not been restarted and loaded
final String patchDirectory = manager.getPatchDirectory().getAbsolutePath();
File patchInfoLockFile = SharePatchFileUtil.getPatchInfoLockFile(patchDirectory);
File patchInfoFile = SharePatchFileUtil.getPatchInfoFile(patchDirectory);
try {
final SharePatchInfo currInfo = SharePatchInfo.readAndCheckPropertyWithLock(patchInfoFile, patchInfoLockFile);
if(currInfo ! =null&&! ShareTinkerInternals.isNullOrNil(currInfo.newVersion) && ! currInfo.isRemoveNewVersion) {if (patchMd5.equals(currInfo.newVersion)) {
returnShareConstants.ERROR_PATCH_ALREADY_APPLY; }}}catch (Throwable ignored) {
// Ignored.}}// The number of patch retries exceeds the threshold (20)
if(! UpgradePatchRetry.getInstance(context).onPatchListenerCheck(patchMd5)) {return ShareConstants.ERROR_PATCH_RETRY_COUNT_LIMIT;
}
return ShareConstants.ERROR_PATCH_OK;
}
Copy the code
TinkerPatchService Pre-operation of a composite patch
TinkerPatchService is an IntentService, running in the patch synthesis process, mainly to do the pre-operation of patch synthesis, the main logic is in the doApplyPatch method.
- call
DefaultPatchReporter.onPatchServiceStart
Callback to record the number of retries for synthesizing the patch - call
UpgradePatch.tryPatch
Actually start crafting the patch - The synthesized callback results to
DefaultTinkerResultService
, delete the downloaded patch source file, kill the process and restart
public class TinkerPatchService extends IntentService {...public static void runPatchService(final Context context, final String path) {
ShareTinkerLog.i(TAG, "run patch service...");
Intent intent = new Intent(context, TinkerPatchService.class);
// Patch path
intent.putExtra(PATCH_PATH_EXTRA, path);
/ / synthetic results callback class name, the default DefaultTinkerResultService
intent.putExtra(RESULT_CLASS_EXTRA, resultServiceClass.getName());
try {
context.startService(intent);
} catch (Throwable thr) {
ShareTinkerLog.e(TAG, "run patch service fail, exception:"+ thr); }}@Override
protected void onHandleIntent(Intent intent) {
// Set priority for foreground service
increasingPriority();
doApplyPatch(this, intent);
}
private static AtomicBoolean sIsPatchApplying = new AtomicBoolean(false);
private static void doApplyPatch(Context context, Intent intent) {
// Since we may retry with IntentService, we should prevent
// racing here again.
if(! sIsPatchApplying.compareAndSet(false.true)) {
ShareTinkerLog.w(TAG, "TinkerPatchService doApplyPatch is running by another runner.");
return;
}
Tinker tinker = Tinker.with(context);
// Callback event
tinker.getPatchReporter().onPatchServiceStart(intent);
if (intent == null) {
ShareTinkerLog.e(TAG, "TinkerPatchService received a null intent, ignoring.");
return;
}
// Patch file path
String path = getPatchPathExtra(intent);
if (path == null) {
ShareTinkerLog.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;
PatchResult patchResult = new PatchResult();
try {
if (upgradePatchProcessor == null) {
throw new TinkerRuntimeException("upgradePatchProcessor is null.");
}
// Call UpgradePatch tryPatch
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;
/ / callback to DefaultTinkerResultService synthesis results
AbstractResultService.runResultService(context, patchResult, getPatchResultExtra(intent));
sIsPatchApplying.set(false); }}Copy the code
Start synthesizing patches
By default upgradepatch. tryPatch performs the patch composition logic as follows
ShareSecurityCheck
Class to extract xxx_meta files from the patch package and verify the signatureShareTinkerInternals
Parse and verify the validity of TinkerId, signature, MD5, and meta files of the patch- Read and parse data/data/ package name /tinker/patch.info to verify whether the patch can be synthesized. This file records patch loading information of the current version. The corresponding class is
SharePatchInfo
- Change the name of the patch package to patch-xxx.apk and copy it to the data/data/ package name /tinker/patch-xxx directory
- The dex file, resource file and SO library in the patch were synthesized respectively, and dex2OAT operation was performed on the new dex after the synthesis
This paper mainly analyzes the Dex synthesis process, so library and resource file synthesis part is relatively simple, only need to care about the output directory
public class UpgradePatch extends AbstractPatch {
private static final String TAG = "Tinker.UpgradePatch";
@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)) {return false;
}
if(! SharePatchFileUtil.isLegalFile(patchFile)) {return false;
}
// This class is used to read meta files in the patch pack
ShareSecurityCheck signatureCheck = new ShareSecurityCheck(context);
// Parse and verify the validity of patch TinkerId, signature, MD5, meta files, etc
int returnCode = ShareTinkerInternals.checkTinkerPackage(context, manager.getTinkerFlags(), patchFile, signatureCheck);
if(returnCode ! = ShareConstants.ERROR_PACKAGE_CHECK_OK) { manager.getPatchReporter().onPatchPackageCheckFail(patchFile, returnCode);return false;
}
String patchMd5 = SharePatchFileUtil.getMD5(patchFile);
if (patchMd5 == null) {
return false;
}
// Patch package MD5 as the version number
patchResult.patchVersion = patchMd5;
ShareTinkerLog.i(TAG, "UpgradePatch tryPatch:patchMd5:%s", patchMd5);
// data/data/ package name /tinker
final String patchDirectory = manager.getPatchDirectory().getAbsolutePath();
File patchInfoLockFile = SharePatchFileUtil.getPatchInfoLockFile(patchDirectory);
// data/data/ package name /tinker/patch.info
File patchInfoFile = SharePatchFileUtil.getPatchInfoFile(patchDirectory);
// Read the package_meta.txt content
final Map<String, String> pkgProps = signatureCheck.getPackagePropertiesIfPresent();
if (pkgProps == null) {
ShareTinkerLog.e(TAG, "UpgradePatch packageProperties is null, do we process a valid patch apk ?");
return false;
}
final String isProtectedAppStr = pkgProps.get(ShareConstants.PKGMETA_KEY_IS_PROTECTED_APP);
final booleanisProtectedApp = (isProtectedAppStr ! =null && !isProtectedAppStr.isEmpty() && !"0".equals(isProtectedAppStr));
// Read the information about the last synthesized patch
SharePatchInfo oldInfo = SharePatchInfo.readAndCheckPropertyWithLock(patchInfoFile, patchInfoLockFile);
SharePatchInfo newInfo;
if(oldInfo ! =null) {
// The patch has been loaded
if (oldInfo.oldVersion == null || oldInfo.newVersion == null || oldInfo.oatDir == null) {
ShareTinkerLog.e(TAG, "UpgradePatch tryPatch:onPatchInfoCorrupted");
manager.getPatchReporter().onPatchInfoCorrupted(patchFile, oldInfo.oldVersion, oldInfo.newVersion);
return false;
}
if(! SharePatchFileUtil.checkIfMd5Valid(patchMd5)) { ShareTinkerLog.e(TAG,"UpgradePatch tryPatch:onPatchVersionCheckFail md5 %s is valid", patchMd5);
manager.getPatchReporter().onPatchVersionCheckFail(patchFile, oldInfo, patchMd5);
return false;
}
// Is the patch currently loaded in explain mode (first run after OTA)
// Patch is loaded in explain mode in TinkerLoader, oatDir is set to interpret
final boolean usingInterpret = oldInfo.oatDir.equals(ShareConstants.INTERPRET_DEX_OPTIMIZE_PATH);
// Check whether the patch has been synthesized. If the current patch has been loaded, it will not be synthesized
if(! usingInterpret && ! ShareTinkerInternals.isNullOrNil(oldInfo.newVersion) && oldInfo.newVersion.equals(patchMd5) && ! oldInfo.isRemoveNewVersion) { ShareTinkerLog.e(TAG,"patch already applied, md5: %s", patchMd5);
// The patch has been synthesized successfully. The number of retries is reset to 1
UpgradePatchRetry.getInstance(context).onPatchResetMaxCheck(patchMd5);
return true;
}
// Set oatDir to "changing" when loading patches in explain mode, so that the patch will not be loaded in explain mode next time
final String finalOatDir = usingInterpret ? ShareConstants.CHANING_DEX_OPTIMIZE_PATH : oldInfo.oatDir;
newInfo = new SharePatchInfo(oldInfo.oldVersion, patchMd5, isProtectedApp, false, Build.FINGERPRINT, finalOatDir, false);
} else {
// No patches have been loaded
newInfo = new SharePatchInfo("", patchMd5, isProtectedApp, false, Build.FINGERPRINT, ShareConstants.DEFAULT_DEX_OPTIMIZE_PATH, false);
}
final String patchName = SharePatchFileUtil.getPatchVersionDirectory(patchMd5);
// data/data/ package name /tinker/patch-xxx
final String patchVersionDirectory = patchDirectory + "/" + patchName;
ShareTinkerLog.i(TAG, "UpgradePatch tryPatch:patchVersionDirectory:%s", patchVersionDirectory);
// data/data/ package name /tinker/patch-xxx/patch-xxx.apk
File destPatchFile = new File(patchVersionDirectory + "/" + SharePatchFileUtil.getPatchVersionFile(patchMd5));
try {
if(! patchMd5.equals(SharePatchFileUtil.getMD5(destPatchFile))) {// Copy the patch package to destPatchFile
SharePatchFileUtil.copyFileUsingStream(patchFile, destPatchFile);
ShareTinkerLog.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) {
ShareTinkerLog.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;
}
// Synthesize dex file and perform dex2OAT
if(! DexDiffPatchInternal.tryRecoverDexFiles(manager, signatureCheck, context, patchVersionDirectory, destPatchFile, patchResult)) { ShareTinkerLog.e(TAG,"UpgradePatch tryPatch:new patch recover, try patch dex failed");
return false;
}
// Ark compiler related processing
if(! ArkHotDiffPatchInternal.tryRecoverArkHotLibrary(manager, signatureCheck, context, patchVersionDirectory, destPatchFile)) {return false;
}
// BSDiff merge so library, verify process is similar to merge dex, finally merge bspatch.patchfast
if(! BsDiffPatchInternal.tryRecoverLibraryFiles(manager, signatureCheck, context, patchVersionDirectory, destPatchFile)) { ShareTinkerLog.e(TAG,"UpgradePatch tryPatch:new patch recover, try patch library failed");
return false;
}
// BSDiff merges resource files. The verification process is similar to merging dex. Finally, bspatch.patchfast is synthesized
Data /data/ package name /tinker/patch-xxx/res/resources.apk
if(! ResDiffPatchInternal.tryRecoverResourceFiles(manager, signatureCheck, context, patchVersionDirectory, destPatchFile)) { ShareTinkerLog.e(TAG,"UpgradePatch tryPatch:new patch recover, try patch resource failed");
return false;
}
// Check for missing dex2OAT products as Vivo/OPPO will asynchronously execute Dex2OAT
if(! DexDiffPatchInternal.waitAndCheckDexOptFile(patchFile, manager)) { ShareTinkerLog.e(TAG,"UpgradePatch tryPatch:new patch recover, check dex opt file failed");
return false;
}
// Write the composite patch information to patch.info
if(! SharePatchInfo.rewritePatchInfoFileWithLock(patchInfoFile, newInfo, patchInfoLockFile)) { ShareTinkerLog.e(TAG,"UpgradePatch tryPatch:new patch recover, rewrite patch info failed");
manager.getPatchReporter().onPatchInfoCorrupted(patchFile, newInfo.oldVersion, newInfo.newVersion);
return false;
}
// Reset the number of patch compositions recorded
UpgradePatchRetry.getInstance(context).onPatchResetMaxCheck(patchMd5);
return true; }}Copy the code
Dex preliminary synthesis
Patch synthesis is the most critical dex file synthesis, we focus on the analysis of this part of the logic, here or list a simple process
- The entrance is
DexDiffPatchInternal.tryRecoverDexFiles
Method, and then calls topatchDexExtractViaDexDiff
patchDexExtractViaDexDiff
In the methodextractDexDiffInternals
Method does some prevalidation of the dex and begins composition- The last call
dexOptimizeDexFiles
Dexopt operation was performed on the synthesized dex, and the product was stored in data/data/ package name/Tinker/patch-XXX/Odex
public class DexDiffPatchInternal extends BasePatchInternal {
private static boolean patchDexExtractViaDexDiff(Context context, String patchVersionDirectory, String meta, final File patchFile, PatchResult patchResult) {
// data/data/ Package name /tinker/patch-xxx/dex
String dir = patchVersionDirectory + "/" + DEX_PATH + "/";
/ / synthetic dex
if(! extractDexDiffInternals(context, dir, meta, patchFile, TYPE_DEX)) {return false;
}
File dexFiles = new File(dir);
File[] files = dexFiles.listFiles();
// Store the resultant file
List<File> legalFiles = new ArrayList<>();
if(files ! =null) {
for (File file : files) {
final String fileName = file.getName();
// may have directory in android o
if (file.isFile()
&& (fileName.endsWith(ShareConstants.DEX_SUFFIX)
|| fileName.endsWith(ShareConstants.JAR_SUFFIX)
|| fileName.endsWith(ShareConstants.PATCH_SUFFIX))
) {
legalFiles.add(file);
}
}
}
ShareTinkerLog.i(TAG, "legal files to do dexopt: " + legalFiles);
// Store the dexopt product, data/data/ package name /tinker/patch-xxx/odex
final String optimizeDexDirectory = patchVersionDirectory + "/" + DEX_OPTIMIZE_PATH + "/";
// The dexopt command is triggered
returndexOptimizeDexFiles(context, legalFiles, optimizeDexDirectory, patchFile, patchResult); }}Copy the code
Dex synthesis
DexDiffPatchInternal. Do the divverify extractDexDiffInternals method for dex, specific implementation in DexPatchApplier, general process is as follows
- The dex_meta file in the patch package was parsed and the changed DEX information was resolved into ShareDexDiffPatchInfo object to load the patchList
- For the Art platform,
checkClassNDexFiles
Method Check whether data/data/ package name /tinker/patch-xxx/dex/ tinker_classn. apk exists (this file is packaged by all new dex after the patch is synthesized under Art) and determine whether the patch has been loaded. For the Dalvik platform, tinker_classn. apk is not packaged, but each Dex is checked to see if it needs to be synthesized - After traversing patchList, start to synthesize dex in turn and call
patchDexFile
At last,DexPatchApplier.executeAndSaveTo
Dex is synthesized, and the internal algorithm is DexDiff. After synthesis, dex is saved to data/data/ package name/Tinker /patch-xxx/dex - After the synthesis of each dex, md5 verification is required to ensure the correctness of the synthesis of this dex. After the synthesis of all dex, all dex should be packaged into Tinker_classn. apk under Art
Code + detailed comments:
public class DexDiffPatchInternal extends BasePatchInternal {...private static boolean extractDexDiffInternals(Context context, String dir, String meta, File patchFile, int type) {
patchList.clear();
// patchList is loaded after dex_meta is parsed
ShareDexDiffPatchInfo.parseDexDiffPatchInfo(meta, patchList);
if (patchList.isEmpty()) {
return true;
}
// data/data/ Package name /tinker/patch-xxx/dex
File directory = new File(dir);
if(! directory.exists()) { directory.mkdirs(); }//I think it is better to extract the raw files from apk
Tinker manager = Tinker.with(context);
ZipFile apk = null;
ZipFile patch = null;
try {
ApplicationInfo applicationInfo = context.getApplicationInfo();
if (applicationInfo == null) {
// Looks like running on a test Context, so just return without patching.
ShareTinkerLog.w(TAG, "applicationInfo == null!!!!");
return false;
}
String apkPath = applicationInfo.sourceDir;
/ / the original apk
apk = new ZipFile(apkPath);
/ / patches
patch = new ZipFile(patchFile);
// The patch is synthesized under art, all old dex and patch dex are synthesized, and then packaged as tinker_classn. apk, dalvik does not pack dex
// Determine whether the tinker_classn. apk file needs to be generated
if (checkClassNDexFiles(dir)) {
ShareTinkerLog.w(TAG, "class n dex file %s is already exist, and md5 match, just continue", ShareConstants.CLASS_N_APK_NAME);
return true;
}
for (ShareDexDiffPatchInfo info : patchList) {
long start = System.currentTimeMillis();
final String infoPath = info.path;
String patchRealPath;
if (infoPath.equals("")) {
patchRealPath = info.rawName;
} else {
patchRealPath = info.path + "/" + info.rawName;
}
String dexDiffMd5 = info.dexDiffMd5;
String oldDexCrc = info.oldDexCrC;
DestMd5InDvm field value is "0" if the destMd5InDvm is not the primary dex and the dex is not changed. This dex does not need to be synthesized under Dalvik
if(! isVmArt && info.destMd5InDvm.equals("0")) {
ShareTinkerLog.w(TAG, "patch dex %s is only for art, just continue", patchRealPath);
continue;
}
String extractedFileMd5 = isVmArt ? info.destMd5InArt : info.destMd5InDvm;
if(! SharePatchFileUtil.checkIfMd5Valid(extractedFileMd5)) { ShareTinkerLog.w(TAG,"meta file md5 invalid, type:%s, name: %s, md5: %s", ShareTinkerInternals.getTypeString(type), info.rawName, extractedFileMd5);
manager.getPatchReporter().onPatchPackageCheckFail(patchFile, BasePatchInternal.getMetaCorruptedCode(type));
return false;
}
// data/data/ package name /tinker/patch-xxx/dex/dex name, which is used to store the synthesized dex
File extractedFile = new File(dir + info.realName);
// Check whether the synthesized dex (which has not been synthesized yet, if it exists, it has been synthesized before) already exists. If it exists, the synthesized dex has been synthesized
// If the dex exists, check whether it is consistent with the MD5 of the pre-synthesized dex recorded in the patch package. If the dex is inconsistent, delete the existing dex
if (extractedFile.exists()) {
if (SharePatchFileUtil.verifyDexFileMd5(extractedFile, extractedFileMd5)) {
//it is ok, just continue
ShareTinkerLog.w(TAG, "dex file %s is already exist, and md5 match, just continue", extractedFile.getPath());
continue;
} else {
ShareTinkerLog.w(TAG, "have a mismatch corrupted dex "+ extractedFile.getPath()); extractedFile.delete(); }}else {
extractedFile.getParentFile().mkdirs();
}
// Patch dex in the patch package
ZipEntry patchFileEntry = patch.getEntry(patchRealPath);
// old dex
ZipEntry rawApkFileEntry = apk.getEntry(patchRealPath);
if (oldDexCrc.equals("0")) {
// If oldDexCrc is 0, the dex is new
if (patchFileEntry == null) {
ShareTinkerLog.w(TAG, "patch entry is null. path:" + patchRealPath);
manager.getPatchReporter().onPatchTypeExtractFail(patchFile, extractedFile, info.rawName, type);
return false;
}
// Extract the dex from the patch package to data/data/ package name /tinker/patch-xxx/dex/
if(! extractDexFile(patch, patchFileEntry, extractedFile, info)) { ShareTinkerLog.w(TAG,"Failed to extract raw patch file " + extractedFile.getPath());
manager.getPatchReporter().onPatchTypeExtractFail(patchFile, extractedFile, info.rawName, type);
return false; }}else if (dexDiffMd5.equals("0")) {
If oldDexCrc is not "0" and dexDiffMd5 is "0", the dex is not changed
// In this case, the old dex needs to be copied to the patch dex directory in ART and ignored in Dalvik
if(! isVmArt) {continue;
}
if (rawApkFileEntry == null) {
ShareTinkerLog.w(TAG, "apk entry is null. path:" + patchRealPath);
manager.getPatchReporter().onPatchTypeExtractFail(patchFile, extractedFile, info.rawName, type);
return false;
}
//check source crc instead of md5 for faster
String rawEntryCrc = String.valueOf(rawApkFileEntry.getCrc());
// Old dex CRC check (Old dex CRC recorded in patch package and old dex CRC in current APK)
if(! rawEntryCrc.equals(oldDexCrc)) { ShareTinkerLog.e(TAG,"apk entry %s crc is not equal, expect crc: %s, got crc: %s", patchRealPath, oldDexCrc, rawEntryCrc);
manager.getPatchReporter().onPatchTypeExtractFail(patchFile, extractedFile, info.rawName, type);
return false;
}
// Copy the old dex from the current APK to data/data/ package name /tinker/patch-xxx/dex/
extractDexFile(apk, rawApkFileEntry, extractedFile, info);
if(! SharePatchFileUtil.verifyDexFileMd5(extractedFile, extractedFileMd5)) { ShareTinkerLog.w(TAG,"Failed to recover dex file when verify patched dex: " + extractedFile.getPath());
manager.getPatchReporter().onPatchTypeExtractFail(patchFile, extractedFile, info.rawName, type);
SharePatchFileUtil.safeDeleteFile(extractedFile);
return false; }}else {
// The old dex and patch dex must exist in this branch
if (patchFileEntry == null) {
ShareTinkerLog.w(TAG, "patch entry is null. path:" + patchRealPath);
manager.getPatchReporter().onPatchTypeExtractFail(patchFile, extractedFile, info.rawName, type);
return false;
}
if(! SharePatchFileUtil.checkIfMd5Valid(dexDiffMd5)) { ShareTinkerLog.w(TAG,"meta file md5 invalid, type:%s, name: %s, md5: %s", ShareTinkerInternals.getTypeString(type), info.rawName, dexDiffMd5);
manager.getPatchReporter().onPatchPackageCheckFail(patchFile, BasePatchInternal.getMetaCorruptedCode(type));
return false;
}
// Check whether the old dex exists in the APK
if (rawApkFileEntry == null) {
ShareTinkerLog.w(TAG, "apk entry is null. path:" + patchRealPath);
manager.getPatchReporter().onPatchTypeExtractFail(patchFile, extractedFile, info.rawName, type);
return false;
}
// Old dex CRC check (Old dex CRC recorded in patch package and old dex CRC in current APK)
String rawEntryCrc = String.valueOf(rawApkFileEntry.getCrc());
if(! rawEntryCrc.equals(oldDexCrc)) { ShareTinkerLog.e(TAG,"apk entry %s crc is not equal, expect crc: %s, got crc: %s", patchRealPath, oldDexCrc, rawEntryCrc);
manager.getPatchReporter().onPatchTypeExtractFail(patchFile, extractedFile, info.rawName, type);
return false;
}
// Patch synthesis, after synthesis, write synthesis result dex to extractedFile(data/data/ package name /tinker/patch-xxx/dex)
// Internal through the DexPatchApplier class to synthesize the patch, algorithm implementation code is not specific analysis
patchDexFile(apk, patch, rawApkFileEntry, patchFileEntry, info, extractedFile);
// Check whether the MD5 value of the dex is the same as the md5 value pre-synthesized during patch package installation
if(! SharePatchFileUtil.verifyDexFileMd5(extractedFile, extractedFileMd5)) { ShareTinkerLog.w(TAG,"Failed to recover dex file when verify patched dex: " + extractedFile.getPath());
manager.getPatchReporter().onPatchTypeExtractFail(patchFile, extractedFile, info.rawName, type);
SharePatchFileUtil.safeDeleteFile(extractedFile);
return false;
}
ShareTinkerLog.w(TAG, "success recover dex file: %s, size: %d, use time: %d", extractedFile.getPath(), extractedFile.length(), (System.currentTimeMillis() - start)); }}// Art packages all the synthesized dex as tinker_classn.apk
if(! mergeClassNDexFiles(context, patchFile, dir)) {return false; }}catch (Throwable e) {
throw new TinkerRuntimeException("patch " + ShareTinkerInternals.getTypeString(type) + " extract failed (" + e.getMessage() + ").", e);
} finally {
SharePatchFileUtil.closeZip(apk);
SharePatchFileUtil.closeZip(patch);
}
return true; }}Copy the code
Dex2oat was triggered to compile and synthesize dex
Dex2oat should be operated on each dex after the dex is successfully synthesized, so as not to affect the loading speed when Dex2OAT is triggered during operation. Tinker triggers Dex2OAT with the help of PathClassLoader, and the brief process is as follows
DexDiffPatchInternal.dexOptimizeDexFiles
Method does a pre-operation and then calls toTinkerDexOptimizer.optimizeAll
Method, called separately for each dex/ APk/JAR fileOptimizeWorker.run
Methods Dex2OAT was prepared- Called on android8.0 and above
NewClassLoaderInjector.triggerDex2Oat
Methods throughDelegateLastClassLoader
orTinkerClassLoader
Trigger dex2OAT, direct call below 8.0DexFile.loadDex
- Note that Android 10 will no longer call dex2OAT from the application process, only accept OAT files generated by the system, so you need to pass
triggerPMDexOptOnDemand
Method reflection calls the PMS performDexOptSecondary method to try again to trigger dex2OAT
DexDiffPatchInternal dexOptimizeDexFiles pre operation
Data /data/ package name /tinker/patch-xxx/oat//xxx.odex, 8.0 indicates data/data/ package name /tinker/patch-xxx/odex/xxx.dex, where ISA indicates the CPU architecture, such as ARM64
private static boolean dexOptimizeDexFiles(Context context, List<File> dexFiles, String optimizeDexDirectory, final File patchFile, final PatchResult patchResult) {
final Tinker manager = Tinker.with(context);
optFiles.clear();
if(dexFiles ! =null) {
// data/data/ package name /tinker/patch-xxx/odex
File optimizeDexDirectoryFile = new File(optimizeDexDirectory);
if(! optimizeDexDirectoryFile.exists() && ! optimizeDexDirectoryFile.mkdirs()) {return false;
}
// add opt files
for (File file : dexFiles) {
// Get the output path of dexopt product
// Data /data/ package name /tinker/patch-xxx/oat/
/xxx.odex
Data /data/ package name /tinker/patch-xxx/odex/xxx.dex
String outputPathName = SharePatchFileUtil.optimizedPathFor(file, optimizeDexDirectoryFile);
optFiles.add(newFile(outputPathName)); }...// Whether to use the DelegateLastClassLoader class
final boolean useDLC = TinkerApplication.getInstance().isUseDelegateLastClassLoader();
final boolean[] anyOatNotGenerated = {false};
// Start parallel dexoptTinkerDexOptimizer.optimizeAll( context, dexFiles, optimizeDexDirectoryFile, useDLC, ......) ; . }return true;
}
Copy the code
Optimizeworker.run versions trigger dex2OAT
This method NewClassLoaderInjector. TriggerDex2Oat function and the analysis of the triggerPMDexOptOnDemand will separate out. In addition, it should be noted that useInterpretMode calls interpretDex2Oat to interpretmode dex2OAT, which is to cope with the failure of odex after OTA upgrade of the system. Details will be explained in the patch loading section.
private static class OptimizeWorker {
boolean run(a) {
try{...// Data /data/ package name /tinker/patch-xxx/oat/
/xxx.odex
Data /data/ package name /tinker/patch-xxx/odex/xxx.dex
String optimizedPath = SharePatchFileUtil.optimizedPathFor(this.dexFile, this.optimizedDir);
if(! ShareTinkerInternals.isArkHotRuning()) {if (useInterpretMode) {
// This branch is executed the first time the system runs after OTA
interpretDex2Oat(dexFile.getAbsolutePath(), optimizedPath);
} else if (Build.VERSION.SDK_INT >= 26
|| (Build.VERSION.SDK_INT >= 25&& Build.VERSION.PREVIEW_SDK_INT ! =0)) {
// Trigger dex2OAT by loading dex through PathClassLoader/
NewClassLoaderInjector.triggerDex2Oat(context, optimizedDir,
useDLC, dexFile.getAbsolutePath());
// https://developer.android.google.cn/about/versions/10/behavior-changes-10?hl=zh-cn#system-only-oat
Android10 will no longer call dex2OAT from the application process, only accept OAT files generated by the system
/ / oat_file_manager. Cc OatFileManager: : OpenDexFilesFromOat oat_file_assistant. No longer call MakeUpToDate
// Here the PMS triggers the background dex2OAT again
triggerPMDexOptOnDemand(context, dexFile.getAbsolutePath(), optimizedPath);
} else {
// Trigger dex2OAT using DexFile directly under Android8.0
DexFile.loadDex(dexFile.getAbsolutePath(), optimizedPath, 0); }}if(callback ! =null) {
callback.onSuccess(dexFile, optimizedDir, newFile(optimizedPath)); }}catch (final Throwable e) {
if(callback ! =null) {
callback.onFailed(dexFile, optimizedDir, e);
return false; }}return true; }}Copy the code
Dex2oat is triggered by the PathClassLoader
NewClassLoaderInjector. TriggerDex2Oat calls to NewClassLoaderInjector. CreateNewClassLoader, Use DelegateLastClassLoader to trigger dex2OAT if useDLC=true and Android version greater than 8.1, otherwise Create TinkerClassLoader to trigger dex2OAT. In fact, when dex2OAT is triggered, the effect of the two Classloaders is the same. Finally, dex2OAT is triggered by DexPathList. The newly created ClassLoader is only used to trigger Dex2OAT.
NewClassLoaderInjector. CreateNewClassLoader another call in patch loading logic, Use the new classerLoader to replace the original Application PathClassLoader when loading patches to avoid the impact of mixed compilation and hot patches after android7.0. So I think it’s better to write this method as two separate functions to avoid confusion when reading the code. The only thing we need to know in this article is that both DelegateLastClassLoader and TinkerClassLoader trigger dex2OAT. Further analysis of them will be left for patch loading in the next article.
DelegateLastClassLoader is a new class from android8.1 that inherits from PathClassLoader and implements the last-look strategy.
- Look for classes from boot Classpath
- Look for classes from the dexPath of the classLoader
- Finally, the class is looked up from the classLoader’s parents
- Finally, the lookup policy does not comply with the parent delegate, and the class is finally looked up from the Parent ClassLoader
TinkerClassLoader does a similar job here, rewriting the findClass method so that classes are found first from TinkerClassLoader and then from the original PathClassLoader
private static ClassLoader createNewClassLoader(ClassLoader oldClassLoader,
File dexOptDir,
boolean useDLC,
String... patchDexPaths) throws Throwable {
// Reflection gets the DexPathList field of the BaseDexClassLoader
// Old oldClassLoader is the PathClassLoader of the current app
final Field pathListField = findField(
Class.forName("dalvik.system.BaseDexClassLoader".false, oldClassLoader),
"pathList");
final Object oldPathList = pathListField.get(oldClassLoader);
final StringBuilder dexPathBuilder = new StringBuilder();
final booleanhasPatchDexPaths = patchDexPaths ! =null && patchDexPaths.length > 0;
if (hasPatchDexPaths) {
for (int i = 0; i < patchDexPaths.length; ++i) {
if (i > 0) { dexPathBuilder.append(File.pathSeparator); } dexPathBuilder.append(patchDexPaths[i]); }}// Splice the dex file path that requires dex2Oat
final String combinedDexPath = dexPathBuilder.toString();
// Reflect the nativeLibraryDirectories field in the DexPathList, so library directories
final Field nativeLibraryDirectoriesField = findField(oldPathList.getClass(), "nativeLibraryDirectories"); .// splice the so library path
final String combinedLibraryPath = libraryPathBuilder.toString();
ClassLoader result = null;
if (useDLC && Build.VERSION.SDK_INT >= 27) {
// https://developer.android.google.cn/reference/dalvik/system/DelegateLastClassLoader
// https://www.androidos.net.cn/android/10.0.0_r6/xref/libcore/dalvik/src/main/java/dalvik/system/DelegateLastClassLoader.j ava
// DelegateLastClassLoader is new after android8.1 and inherits from PathClassLoader to implement the last-look policy
result = new DelegateLastClassLoader(combinedDexPath, combinedLibraryPath, ClassLoader.getSystemClassLoader());
// Set the previous PathClassLoader to be the parent of the created DelegateLastClassLoader
final Field parentField = ClassLoader.class.getDeclaredField("parent");
parentField.setAccessible(true);
parentField.set(result, oldClassLoader);
} else {
// Do the same thing as DelegateLastClassLoader
result = new TinkerClassLoader(combinedDexPath, dexOptDir, combinedLibraryPath, oldClassLoader);
}
Android8.0 replaces the original PathClassLoader with the new classLoader in the PathList
// Android 8.0 does not support multiple classloaders using the same DexFile object to define classes at the same time, so it cannot be replaced
if (Build.VERSION.SDK_INT < 26) {
findField(oldPathList.getClass(), "definingContext").set(oldPathList, result);
}
return result;
}
Copy the code
TriggerPMDexOptOnDemand ensures that dex2oat is triggered after Android10
Due to android10’s change of dex2oat strategy, further processing must be done here. The main process of tinker is as follows
- QueryPerformDexOptSecondaryTransactionCode reflex () method gets used to identify
PackageManagerService.performDexOptSecondary
The transaction code of the method - Reflection ServiceManager gets the PMS client proxy IBinder object
- Call with binder
PackageManagerService.performDexOptSecondary
And trigger Dex2OAT in Quicken mode
private static void triggerPMDexOptOnDemand(Context context, String dexPath, String oatPath) {
if (Build.VERSION.SDK_INT < 29) {
return;
}
try {
final File oatFile = new File(oatPath);
if (oatFile.exists()) {
return;
}
boolean performDexOptSecondarySuccess = true;
try {
/ / call the PMS. PerformDexOptSecondary dex2oat trigger
performDexOptSecondary(context, oatPath);
} catch (Throwable thr) {
performDexOptSecondarySuccess = false;
}
SystemClock.sleep(1000);
if(! performDexOptSecondarySuccess || ! oatFile.exists()) {// The execution fails. If the system is huawei, perform additional processing
if ("huawei".equalsIgnoreCase(Build.MANUFACTURER) || "honor".equalsIgnoreCase(Build.MANUFACTURER)) { registerDexModule(context, dexPath, oatPath); }}... }Copy the code
public static void performDexOptSecondary(Context context, String oatPath) throws IllegalStateException {
try {
final File oatFile = new File(oatPath);
/ / reflection for PMS agent used in ipc tag PMS. TransactionCode performDexOptSecondary methods
final int transactionCode = queryPerformDexOptSecondaryTransactionCode();
final String packageName = context.getPackageName();
// Dex2OAT compile mode
final String targetCompilerFilter = "quicken";
final boolean force = false;
finalClass<? > serviceManagerClazz = Class.forName("android.os.ServiceManager");
final Method getServiceMethod = ShareReflectUtil.findMethod(serviceManagerClazz, "getService", String.class);
// Get the PMS remote proxy
final IBinder pmBinder = (IBinder) getServiceMethod.invoke(null."package");
if (pmBinder == null) {
throw new IllegalStateException("Fail to get pm binder.");
}
// Retry 20 times
final int maxRetries = 20;
for (int i = 0; i < maxRetries; ++i) {
Throwable pendingThr = null;
try {
// Call PMS performDexOptSecondary directly with binder
performDexOptSecondaryImpl(pmBinder, transactionCode, packageName, targetCompilerFilter, force);
} catch (Throwable thr) {
pendingThr = thr;
}
SystemClock.sleep(3000);
if (oatFile.exists()) {
break;
}
if (i == maxRetries - 1) {
if(pendingThr ! =null) {
throw pendingThr;
}
if(! oatFile.exists()) {throw new IllegalStateException("Expected oat file: " + oatFile.getAbsolutePath() + " does not exist."); }}}... }Copy the code
Here for PackageManagerService. PerformDexOptSecondary methods will no longer opened, are interested can go to read the source code itself, since patch synthesis process has been parsed.
Afterword.
Follow the source code all the way down obviously can feel the author for framework layer understanding, different Android versions of the framework changes for Tinker’s impact is very big, manufacturers for ROM customization modification will lead to a series of problems, to solve these problems is not easy things. I do not fully understand some details, and I hope to point out any mistakes.