patch res 生成
1、ResDiffDecoder.java patch 新资源处理, Md5 生成,AndroidManifest.xml文件忽略
@Override
public boolean patch(File oldFile, File newFile) throws IOException, TinkerPatchException {
String name = getRelativePathStringToNewFile(newFile);
//actually, it won't go below
if (newFile == null || !newFile.exists()) {
String relativeStringByOldDir = getRelativePathStringToOldFile(oldFile);
if (Utils.checkFileInPattern(config.mResIgnoreChangePattern, relativeStringByOldDir)) {
Logger.e("found delete resource: " + relativeStringByOldDir + " ,but it match ignore change pattern, just ignore!");
return false;
}
deletedSet.add(relativeStringByOldDir);
writeResLog(newFile, oldFile, TypedValue.DEL);
return true;
}
File outputFile = getOutputPath(newFile).toFile();
if (oldFile == null || !oldFile.exists()) {
if (Utils.checkFileInPattern(config.mResIgnoreChangePattern, name)) {
Logger.e("found add resource: " + name + " ,but it match ignore change pattern, just ignore!");
return false;
}
FileOperation.copyFileUsingStream(newFile, outputFile);
addedSet.add(name);
writeResLog(newFile, oldFile, TypedValue.ADD);
return true;
}
//both file length is 0
if (oldFile.length() == 0 && newFile.length() == 0) {
return false;
}
//new add file
String newMd5 = MD5.getMD5(newFile);
String oldMd5 = MD5.getMD5(oldFile);
//oldFile or newFile may be 0b length
if (oldMd5 != null && oldMd5.equals(newMd5)) {
return false;
}
if (Utils.checkFileInPattern(config.mResIgnoreChangePattern, name)) {
Logger.d("found modify resource: " + name + ", but it match ignore change pattern, just ignore!");
return false;
}
if (name.equals(TypedValue.RES_MANIFEST)) {
Logger.d("found modify resource: " + name + ", but it is AndroidManifest.xml, just ignore!");
return false;
}
if (name.equals(TypedValue.RES_ARSC)) {
if (AndroidParser.resourceTableLogicalChange(config)) {
Logger.d("found modify resource: " + name + ", but it is logically the same as original new resources.arsc, just ignore!");
return false;
}
}
dealWithModifyFile(name, newMd5, oldFile, newFile, outputFile);
return true;
}
2、ResDiffDecoder.java dealWithModifyFile BSDiff.bsdiff 生成差分文件
private boolean dealWithModifyFile(String name, String newMd5, File oldFile, File newFile, File outputFile) throws IOException {
if (checkLargeModFile(newFile)) {
if (!outputFile.getParentFile().exists()) {
outputFile.getParentFile().mkdirs();
}
BSDiff.bsdiff(oldFile, newFile, outputFile);
//treat it as normal modify
if (Utils.checkBsDiffFileSize(outputFile, newFile)) {
LargeModeInfo largeModeInfo = new LargeModeInfo();
largeModeInfo.path = newFile;
largeModeInfo.crc = FileOperation.getFileCrc32(newFile);
largeModeInfo.md5 = newMd5;
largeModifiedSet.add(name);
largeModifiedMap.put(name, largeModeInfo);
writeResLog(newFile, oldFile, TypedValue.LARGE_MOD);
return true;
}
}
modifiedSet.add(name);
FileOperation.copyFileUsingStream(newFile, outputFile);
writeResLog(newFile, oldFile, TypedValue.MOD);
return false;
}
3、ResDiffDecoder.onAllPatchesEnd() 生成res_meta.txt
@Override
public void onAllPatchesEnd() throws IOException, TinkerPatchException {
//only there is only deleted set, we just ignore
if (addedSet.isEmpty() && modifiedSet.isEmpty() && largeModifiedSet.isEmpty()) {
return;
}
if (!config.mResRawPattern.contains(TypedValue.RES_ARSC)) {
throw new TinkerPatchException("resource must contain resources.arsc pattern");
}
if (!config.mResRawPattern.contains(TypedValue.RES_MANIFEST)) {
throw new TinkerPatchException("resource must contain AndroidManifest.xml pattern");
}
//check gradle build
if (config.mUsingGradle) {
final boolean ignoreWarning = config.mIgnoreWarning;
final boolean resourceArscChanged = modifiedSet.contains(TypedValue.RES_ARSC)
|| largeModifiedSet.contains(TypedValue.RES_ARSC);
if (resourceArscChanged && !config.mUseApplyResource) {
if (ignoreWarning) {
//ignoreWarning, just log
Logger.e("Warning:ignoreWarning is true, but resources.arsc is changed, you should use applyResourceMapping mode to build the new apk, otherwise, it may be crash at some times");
} else {
Logger.e("Warning:ignoreWarning is false, but resources.arsc is changed, you should use applyResourceMapping mode to build the new apk, otherwise, it may be crash at some times");
throw new TinkerPatchException(
String.format("ignoreWarning is false, but resources.arsc is changed, you should use applyResourceMapping mode to build the new apk, otherwise, it may be crash at some times")
);
}
} /*else if (config.mUseApplyResource) {
int totalChangeSize = addedSet.size() + modifiedSet.size() + largeModifiedSet.size();
if (totalChangeSize == 1 && resourceArscChanged) {
Logger.e("Warning: we are using applyResourceMapping mode to build the new apk, but there is only resources.arsc changed, you should ensure there is actually resource changed!");
}
}*/
}
//add delete set
deletedSet.addAll(getDeletedResource(config.mTempUnzipOldDir, config.mTempUnzipNewDir));
//we can't modify AndroidManifest file
addedSet.remove(TypedValue.RES_MANIFEST);
deletedSet.remove(TypedValue.RES_MANIFEST);
modifiedSet.remove(TypedValue.RES_MANIFEST);
largeModifiedSet.remove(TypedValue.RES_MANIFEST);
//remove add, delete or modified if they are in ignore change pattern also
removeIgnoreChangeFile(modifiedSet);
removeIgnoreChangeFile(deletedSet);
removeIgnoreChangeFile(addedSet);
removeIgnoreChangeFile(largeModifiedSet);
// after ignore-changes resource files are being removed, we now check if there's any anim
// resources in added and modified files.
checkIfSpecificResWasAnimRes(addedSet);
checkIfSpecificResWasAnimRes(modifiedSet);
checkIfSpecificResWasAnimRes(largeModifiedSet);
// last add test res in assets for user cannot ignore it;
addAssetsFileForTestResource();
File tempResZip = new File(config.mOutFolder + File.separator + TEMP_RES_ZIP);
final File tempResFiles = config.mTempResultDir;
//gen zip resources_out.zip
FileOperation.zipInputDir(tempResFiles, tempResZip, null);
File extractToZip = new File(config.mOutFolder + File.separator + TypedValue.RES_OUT);
String resZipMd5 = Utils.genResOutputFile(extractToZip, tempResZip, config,
addedSet, modifiedSet, deletedSet, largeModifiedSet, largeModifiedMap);
Logger.e("Final normal zip resource: %s, size=%d, md5=%s", extractToZip.getName(), extractToZip.length(), resZipMd5);
logWriter.writeLineToInfoFile(
String.format("Final normal zip resource: %s, size=%d, md5=%s", extractToZip.getName(), extractToZip.length(), resZipMd5)
);
//delete temp file
FileOperation.deleteFile(tempResZip);
//first, write resource meta first
//use resources.arsc's base crc to identify base.apk
String arscBaseCrc = FileOperation.getZipEntryCrc(config.mOldApkFile, TypedValue.RES_ARSC);
String arscMd5 = FileOperation.getZipEntryMd5(extractToZip, TypedValue.RES_ARSC);
if (arscBaseCrc == null || arscMd5 == null) {
throw new TinkerPatchException("can't find resources.arsc's base crc or md5");
}
String resourceMeta = Utils.getResourceMeta(arscBaseCrc, arscMd5);
writeMetaFile(resourceMeta);
//pattern
String patternMeta = TypedValue.PATTERN_TITLE;
HashSet<String> patterns = new HashSet<>(config.mResRawPattern);
//we will process them separate
patterns.remove(TypedValue.RES_MANIFEST);
writeMetaFile(patternMeta + patterns.size());
//write pattern
for (String item : patterns) {
writeMetaFile(item);
}
//add store files
getCompressMethodFromApk();
//write meta file, write large modify first
writeMetaFile(largeModifiedSet, TypedValue.LARGE_MOD);
writeMetaFile(modifiedSet, TypedValue.MOD);
writeMetaFile(addedSet, TypedValue.ADD);
writeMetaFile(deletedSet, TypedValue.DEL);
writeMetaFile(storedSet, TypedValue.STORED);
}
patch res 合成
1、TinkerPatchService.java doApplyPatch
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)) {
TinkerLog.w(TAG, "TinkerPatchService doApplyPatch is running by another runner.");
return;
}
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;
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));
sIsPatchApplying.set(false);
}
2、UpgradePatch.java tryPatch分别对dex、so、res patch加载
if (!ResDiffPatchInternal.tryRecoverResourceFiles(manager, signatureCheck, context, patchVersionDirectory, destPatchFile)) {
TinkerLog.e(TAG, "UpgradePatch tryPatch:new patch recover, try patch resource failed");
return false;
}
// check dex opt file at last, some phone such as VIVO/OPPO like to change dex2oat to interpreted
if (!DexDiffPatchInternal.waitAndCheckDexOptFile(patchFile, manager)) {
TinkerLog.e(TAG, "UpgradePatch tryPatch:new patch recover, check dex opt file failed");
return false;
}
if (!SharePatchInfo.rewritePatchInfoFileWithLock(patchInfoFile, newInfo, patchInfoLockFile)) {
TinkerLog.e(TAG, "UpgradePatch tryPatch:new patch recover, rewrite patch info failed");
manager.getPatchReporter().onPatchInfoCorrupted(patchFile, newInfo.oldVersion, newInfo.newVersion);
return false;
}
3、ResDiffPatchInternal.java extractResourceDiffInternals checkAndExtractResourceLargeFile 大文件 BSPatch.patchFast(oldStream, newStream, largeModeInfo.file);
InputStream oldStream = null;
InputStream newStream = null;
try {
oldStream = apkFile.getInputStream(baseEntry);
newStream = patchZipFile.getInputStream(patchEntry);
BSPatch.patchFast(oldStream, newStream, largeModeInfo.file);
} finally {
StreamUtil.closeQuietly(oldStream);
StreamUtil.closeQuietly(newStream);
}
patch res 加载
1、TinkerResourceLoader.java loadTinkerResources
public static boolean loadTinkerResources(TinkerApplication application, String directory, Intent intentResult) {
if (resPatchInfo == null || resPatchInfo.resArscMd5 == null) {
return true;
}
String resourceString = directory + "/" + RESOURCE_PATH + "/" + RESOURCE_FILE;
File resourceFile = new File(resourceString);
long start = System.currentTimeMillis();
if (application.isTinkerLoadVerifyFlag()) {
if (!SharePatchFileUtil.checkResourceArscMd5(resourceFile, resPatchInfo.resArscMd5)) {
Log.e(TAG, "Failed to load resource file, path: " + resourceFile.getPath() + ", expect md5: " + resPatchInfo.resArscMd5);
ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_VERSION_RESOURCE_MD5_MISMATCH);
return false;
}
Log.i(TAG, "verify resource file:" + resourceFile.getPath() + " md5, use time: " + (System.currentTimeMillis() - start));
}
try {
TinkerResourcePatcher.monkeyPatchExistingResources(application, resourceString);
Log.i(TAG, "monkeyPatchExistingResources resource file:" + resourceString + ", use time: " + (System.currentTimeMillis() - start));
} catch (Throwable e) {
Log.e(TAG, "install resources failed");
//remove patch dex if resource is installed failed
try {
SystemClassLoaderAdder.uninstallPatchDex(application.getClassLoader());
} catch (Throwable throwable) {
Log.e(TAG, "uninstallPatchDex failed", e);
}
intentResult.putExtra(ShareIntentUtil.INTENT_PATCH_EXCEPTION, e);
ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_VERSION_RESOURCE_LOAD_EXCEPTION);
return false;
}
return true;
}
2、TinkerResourcePatcher.java monkeyPatchExistingResources 依靠反射,通过AssertManager addAssetPath函数,加入外部的资源路径
public static void monkeyPatchExistingResources(Context context, String externalResourceFile) throws Throwable {
if (externalResourceFile == null) {
return;
}
final ApplicationInfo appInfo = context.getApplicationInfo();
final Field[] packagesFields;
if (Build.VERSION.SDK_INT < 27) {
packagesFields = new Field[]{packagesFiled, resourcePackagesFiled};
} else {
packagesFields = new Field[]{packagesFiled};
}
for (Field field : packagesFields) {
final Object value = field.get(currentActivityThread);
for (Map.Entry<String, WeakReference<?>> entry
: ((Map<String, WeakReference<?>>) value).entrySet()) {
final Object loadedApk = entry.getValue().get();
if (loadedApk == null) {
continue;
}
final String resDirPath = (String) resDir.get(loadedApk);
if (appInfo.sourceDir.equals(resDirPath)) {
resDir.set(loadedApk, externalResourceFile);
}
}
}
// Create a new AssetManager instance and point it to the resources installed under
if (((Integer) addAssetPathMethod.invoke(newAssetManager, externalResourceFile)) == 0) {
throw new IllegalStateException("Could not create new AssetManager");
}
// Kitkat needs this method call, Lollipop doesn't. However, it doesn't seem to cause any harm
// in L, so we do it unconditionally.
if (stringBlocksField != null && ensureStringBlocksMethod != null) {
stringBlocksField.set(newAssetManager, null);
ensureStringBlocksMethod.invoke(newAssetManager);
}
for (WeakReference<Resources> wr : references) {
final Resources resources = wr.get();
if (resources == null) {
continue;
}
// Set the AssetManager of the Resources instance to our brand new one
try {
//pre-N
assetsFiled.set(resources, newAssetManager);
} catch (Throwable ignore) {
// N
final Object resourceImpl = resourcesImplFiled.get(resources);
// for Huawei HwResourcesImpl
final Field implAssets = findField(resourceImpl, "mAssets");
implAssets.set(resourceImpl, newAssetManager);
}
clearPreloadTypedArrayIssue(resources);
resources.updateConfiguration(resources.getConfiguration(), resources.getDisplayMetrics());
}
// Handle issues caused by WebView on Android N.
// Issue: On Android N, if an activity contains a webview, when screen rotates
// our resource patch may lost effects.
// for 5.x/6.x, we found Couldn't expand RemoteView for StatusBarNotification Exception
if (Build.VERSION.SDK_INT >= 24) {
try {
if (publicSourceDirField != null) {
publicSourceDirField.set(context.getApplicationInfo(), externalResourceFile);
}
} catch (Throwable ignore) {
// Ignored.
}
}
if (!checkResUpdate(context)) {
throw new TinkerRuntimeException(ShareConstants.CHECK_RES_INSTALL_FAIL);
}
}