com.tencent.tinker.build.decoder.DexDiffDecoder Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of tinker-patch-lib Show documentation
Show all versions of tinker-patch-lib Show documentation
Tinker is a hot-fix solution library for Android, it supports dex, library and resources update without reinstalling apk.
/*
* Tencent is pleased to support the open source community by making Tinker available.
*
* Copyright (C) 2016 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is
* distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
* either express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.tencent.tinker.build.decoder;
import com.tencent.tinker.android.dex.ClassDef;
import com.tencent.tinker.android.dex.Dex;
import com.tencent.tinker.android.dex.DexFormat;
import com.tencent.tinker.build.dexpatcher.DexPatchGenerator;
import com.tencent.tinker.build.dexpatcher.util.ChangedClassesDexClassInfoCollector;
import com.tencent.tinker.build.info.InfoWriter;
import com.tencent.tinker.build.patch.Configuration;
import com.tencent.tinker.build.util.DexClassesComparator;
import com.tencent.tinker.build.util.DexClassesComparator.DexClassInfo;
import com.tencent.tinker.build.util.DexClassesComparator.DexGroup;
import com.tencent.tinker.build.util.ExcludedClassModifiedChecker;
import com.tencent.tinker.build.util.FileOperation;
import com.tencent.tinker.build.util.Logger;
import com.tencent.tinker.build.util.MD5;
import com.tencent.tinker.build.util.TinkerPatchException;
import com.tencent.tinker.build.util.TypedValue;
import com.tencent.tinker.build.util.Utils;
import com.tencent.tinker.commons.dexpatcher.DexPatchApplier;
import com.tencent.tinker.commons.dexpatcher.DexPatcherLogger.IDexPatcherLogger;
import org.jf.dexlib2.builder.BuilderMutableMethodImplementation;
import org.jf.dexlib2.dexbacked.DexBackedDexFile;
import org.jf.dexlib2.iface.DexFile;
import org.jf.dexlib2.iface.Field;
import org.jf.dexlib2.iface.Method;
import org.jf.dexlib2.iface.MethodImplementation;
import org.jf.dexlib2.writer.builder.BuilderField;
import org.jf.dexlib2.writer.builder.BuilderMethod;
import org.jf.dexlib2.writer.builder.DexBuilder;
import org.jf.dexlib2.writer.io.FileDataStore;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.jar.JarFile;
import java.util.zip.ZipEntry;
/**
* Created by zhangshaowen on 2016/3/23.
*/
public class DexDiffDecoder extends BaseDecoder {
private static final String TEST_DEX_NAME = "test.dex";
private static final String CHANGED_CLASSES_DEX_NAME_PREFIX = "changed_classes";
private final InfoWriter logWriter;
private final InfoWriter metaWriter;
private final ExcludedClassModifiedChecker excludedClassModifiedChecker;
private final Map addedClassDescToDexNameMap;
private final Map deletedClassDescToDexNameMap;
private final List> oldAndNewDexFilePairList;
private final Map dexNameToRelatedInfoMap;
private boolean hasDexChanged = false;
private DexPatcherLoggerBridge dexPatcherLoggerBridge = null;
public DexDiffDecoder(Configuration config, String metaPath, String logPath) throws IOException {
super(config);
if (metaPath != null) {
metaWriter = new InfoWriter(config, config.mTempResultDir + File.separator + metaPath);
} else {
metaWriter = null;
}
if (logPath != null) {
logWriter = new InfoWriter(config, config.mOutFolder + File.separator + logPath);
} else {
logWriter = null;
}
if (logWriter != null) {
this.dexPatcherLoggerBridge = new DexPatcherLoggerBridge(logWriter);
}
excludedClassModifiedChecker = new ExcludedClassModifiedChecker(config);
addedClassDescToDexNameMap = new HashMap<>();
deletedClassDescToDexNameMap = new HashMap<>();
oldAndNewDexFilePairList = new ArrayList<>();
dexNameToRelatedInfoMap = new HashMap<>();
}
@Override
public void onAllPatchesStart() throws IOException, TinkerPatchException {
}
/**
* Provide /oldFileRoot/dir/to/oldDex, /newFileRoot/dir/to/newDex,
* return dir/to/oldDex or dir/to/newDex if any one is not null.
*/
protected String getRelativeDexName(File oldDexFile, File newDexFile) {
return oldDexFile != null ? getRelativePathStringToOldFile(oldDexFile) : getRelativePathStringToNewFile(newDexFile);
}
@SuppressWarnings("NewApi")
@Override
public boolean patch(final File oldFile, final File newFile) throws IOException, TinkerPatchException {
final String dexName = getRelativeDexName(oldFile, newFile);
// first of all, we should check input files if excluded classes were modified.
Logger.d("Check for loader classes in dex: %s", dexName);
try {
excludedClassModifiedChecker.checkIfExcludedClassWasModifiedInNewDex(oldFile, newFile);
} catch (IOException e) {
throw new TinkerPatchException(e);
} catch (TinkerPatchException e) {
if (config.mIgnoreWarning) {
Logger.e("Warning:ignoreWarning is true, but we found %s", e.getMessage());
} else {
Logger.e("Warning:ignoreWarning is false, but we found %s", e.getMessage());
throw e;
}
} catch (Exception e) {
e.printStackTrace();
}
// If corresponding new dex was completely deleted, just return false.
// don't process 0 length dex
if (newFile == null || !newFile.exists() || newFile.length() == 0) {
return false;
}
File dexDiffOut = getOutputPath(newFile).toFile();
final String newMd5 = getRawOrWrappedDexMD5(newFile);
//new add file
if (oldFile == null || !oldFile.exists() || oldFile.length() == 0) {
hasDexChanged = true;
copyNewDexAndLogToDexMeta(newFile, newMd5, dexDiffOut);
return true;
}
final String oldMd5 = getRawOrWrappedDexMD5(oldFile);
if ((oldMd5 != null && !oldMd5.equals(newMd5)) || (oldMd5 == null && newMd5 != null)) {
hasDexChanged = true;
if (oldMd5 != null) {
collectAddedOrDeletedClasses(oldFile, newFile);
}
}
RelatedInfo relatedInfo = new RelatedInfo();
relatedInfo.oldMd5 = oldMd5;
relatedInfo.newMd5 = newMd5;
// collect current old dex file and corresponding new dex file for further processing.
oldAndNewDexFilePairList.add(new AbstractMap.SimpleEntry<>(oldFile, newFile));
dexNameToRelatedInfoMap.put(dexName, relatedInfo);
return true;
}
@Override
public void onAllPatchesEnd() throws Exception {
if (!hasDexChanged) {
Logger.d("No dexes were changed, nothing needs to be done next.");
return;
}
if (config.mIsProtectedApp) {
generateChangedClassesDexFile();
} else {
generatePatchInfoFile();
}
addTestDex();
}
@SuppressWarnings("NewApi")
private void generateChangedClassesDexFile() throws IOException {
final String dexMode = config.mDexRaw ? "raw" : "jar";
List oldDexList = new ArrayList<>();
List newDexList = new ArrayList<>();
for (AbstractMap.SimpleEntry oldAndNewDexFilePair : oldAndNewDexFilePairList) {
File oldDexFile = oldAndNewDexFilePair.getKey();
File newDexFile = oldAndNewDexFilePair.getValue();
if (oldDexFile != null) {
oldDexList.add(oldDexFile);
}
if (newDexFile != null) {
newDexList.add(newDexFile);
}
}
DexGroup oldDexGroup = DexGroup.wrap(oldDexList);
DexGroup newDexGroup = DexGroup.wrap(newDexList);
ChangedClassesDexClassInfoCollector collector = new ChangedClassesDexClassInfoCollector();
collector.setExcludedClassPatterns(config.mDexLoaderPattern);
collector.setLogger(dexPatcherLoggerBridge);
collector.setIncludeRefererToRefererAffectedClasses(true);
Set classInfosInChangedClassesDex = collector.doCollect(oldDexGroup, newDexGroup);
Set owners = new HashSet<>();
Map> ownerToDescOfChangedClassesMap = new HashMap<>();
for (DexClassInfo classInfo : classInfosInChangedClassesDex) {
owners.add(classInfo.owner);
Set descOfChangedClasses = ownerToDescOfChangedClassesMap.get(classInfo.owner);
if (descOfChangedClasses == null) {
descOfChangedClasses = new HashSet<>();
ownerToDescOfChangedClassesMap.put(classInfo.owner, descOfChangedClasses);
}
descOfChangedClasses.add(classInfo.classDesc);
}
StringBuilder metaBuilder = new StringBuilder();
int changedDexId = 1;
for (Dex dex : owners) {
Set descOfChangedClassesInCurrDex = ownerToDescOfChangedClassesMap.get(dex);
DexFile dexFile = new DexBackedDexFile(org.jf.dexlib2.Opcodes.forApi(20), dex.getBytes());
boolean isCurrentDexHasChangedClass = false;
for (org.jf.dexlib2.iface.ClassDef classDef : dexFile.getClasses()) {
if (descOfChangedClassesInCurrDex.contains(classDef.getType())) {
isCurrentDexHasChangedClass = true;
break;
}
}
if (!isCurrentDexHasChangedClass) {
continue;
}
DexBuilder dexBuilder = DexBuilder.makeDexBuilder();
for (org.jf.dexlib2.iface.ClassDef classDef : dexFile.getClasses()) {
if (!descOfChangedClassesInCurrDex.contains(classDef.getType())) {
continue;
}
Logger.d("Class %s will be added into changed classes dex ...", classDef.getType());
List builderFields = new ArrayList<>();
for (Field field : classDef.getFields()) {
final BuilderField builderField = dexBuilder.internField(
field.getDefiningClass(),
field.getName(),
field.getType(),
field.getAccessFlags(),
field.getInitialValue(),
field.getAnnotations()
);
builderFields.add(builderField);
}
List builderMethods = new ArrayList<>();
for (Method method : classDef.getMethods()) {
MethodImplementation methodImpl = method.getImplementation();
if (methodImpl != null) {
methodImpl = new BuilderMutableMethodImplementation(dexBuilder, methodImpl);
}
BuilderMethod builderMethod = dexBuilder.internMethod(
method.getDefiningClass(),
method.getName(),
method.getParameters(),
method.getReturnType(),
method.getAccessFlags(),
method.getAnnotations(),
methodImpl
);
builderMethods.add(builderMethod);
}
dexBuilder.internClassDef(
classDef.getType(),
classDef.getAccessFlags(),
classDef.getSuperclass(),
classDef.getInterfaces(),
classDef.getSourceFile(),
classDef.getAnnotations(),
builderFields,
builderMethods
);
}
// Write constructed changed classes dex to file and record it in meta file.
final String changedDexName = CHANGED_CLASSES_DEX_NAME_PREFIX + changedDexId + ".dex";
final File dest = new File(config.mTempResultDir + "/" + changedDexName);
final FileDataStore fileDataStore = new FileDataStore(dest);
dexBuilder.writeTo(fileDataStore);
final String md5 = MD5.getMD5(dest);
appendMetaLine(metaBuilder, changedDexName, "", md5, md5, 0, 0, 0, dexMode);
++changedDexId;
}
final String meta = metaBuilder.toString();
Logger.d("\nDexDecoder:write changed classes dex meta file data:\n%s", meta);
metaWriter.writeLineToInfoFile(meta);
}
private void appendMetaLine(StringBuilder sb, Object... vals) {
if (vals == null || vals.length == 0) {
return;
}
boolean isFirstItem = true;
for (Object val : vals) {
if (isFirstItem) {
isFirstItem = false;
} else {
sb.append(',');
}
sb.append(val);
}
sb.append('\n');
}
@SuppressWarnings("NewApi")
private void generatePatchInfoFile() throws IOException {
generatePatchedDexInfoFile();
// generateSmallPatchedDexInfoFile is blocked by issue we found in ART environment
// which indicates that if inline optimization is done on patched class, some error
// such as crash, ClassCastException, mistaken string fetching, etc. would happen.
//
// Instead, we will log all classN dexes as 'copy directly' in dex-meta, so that
// tinker patch applying procedure will copy them out and load them in ART environment.
//generateSmallPatchedDexInfoFile();
logDexesToDexMeta();
checkCrossDexMovingClasses();
}
@SuppressWarnings("NewApi")
private void logDexesToDexMeta() throws IOException {
Map dexNameToClassNOldDexFileMap = new HashMap<>();
Set realClassNDexFiles = new HashSet<>();
for (AbstractMap.SimpleEntry oldAndNewDexFilePair : oldAndNewDexFilePairList) {
File oldFile = oldAndNewDexFilePair.getKey();
final String dexName = getRelativeDexName(oldFile, null);
if (isDexNameMatchesClassNPattern(dexName)) {
dexNameToClassNOldDexFileMap.put(dexName, oldFile);
}
}
// If we meet a case like:
// classes.dex, classes2.dex, classes4.dex, classes5.dex
// Since classes3.dex is missing, according to the logic in AOSP, we should not treat
// rest dexes as part of class N dexes.
for (int i = 0; i < dexNameToClassNOldDexFileMap.size(); ++i) {
final String expectedDexName = (i == 0 ? DexFormat.DEX_IN_JAR_NAME : "classes" + (i + 1) + ".dex");
if (dexNameToClassNOldDexFileMap.containsKey(expectedDexName)) {
File oldDexFile = dexNameToClassNOldDexFileMap.get(expectedDexName);
realClassNDexFiles.add(oldDexFile);
} else {
break;
}
}
for (AbstractMap.SimpleEntry oldAndNewDexFilePair : oldAndNewDexFilePairList) {
final File oldDexFile = oldAndNewDexFilePair.getKey();
final File newDexFile = oldAndNewDexFilePair.getValue();
final String dexName = getRelativeDexName(oldDexFile, newDexFile);
final RelatedInfo relatedInfo = dexNameToRelatedInfoMap.get(dexName);
if (!relatedInfo.oldMd5.equals(relatedInfo.newMd5)) {
logToDexMeta(newDexFile, oldDexFile, relatedInfo.dexDiffFile, relatedInfo.newOrFullPatchedMd5, relatedInfo.newOrFullPatchedMd5, relatedInfo.dexDiffMd5, relatedInfo.newOrFullPatchedCRC);
} else {
// For class N dexes, if new dex is the same as old dex, we should log it as 'copy directly'
// in dex meta to fix problems in Art environment.
if (realClassNDexFiles.contains(oldDexFile)) {
// Bugfix: However, if what we would copy directly is main dex, we should do an additional diff operation
// so that patch applier would help us remove all loader classes of it in runtime.
if (dexName.equals(DexFormat.DEX_IN_JAR_NAME)) {
Logger.d("\nDo additional diff on main dex to remove loader classes in it.");
diffDexPairAndFillRelatedInfo(oldDexFile, newDexFile, relatedInfo);
logToDexMeta(newDexFile, oldDexFile, relatedInfo.dexDiffFile, relatedInfo.newOrFullPatchedMd5, relatedInfo.newOrFullPatchedMd5, relatedInfo.dexDiffMd5, relatedInfo.newOrFullPatchedCRC);
} else {
logToDexMeta(newDexFile, oldDexFile, null, "0", relatedInfo.oldMd5, "0", relatedInfo.newOrFullPatchedCRC);
}
}
}
}
}
@SuppressWarnings("NewApi")
private void generatePatchedDexInfoFile() throws IOException {
// Generate dex diff out and full patched dex if a pair of dex is different.
for (AbstractMap.SimpleEntry oldAndNewDexFilePair : oldAndNewDexFilePairList) {
File oldFile = oldAndNewDexFilePair.getKey();
File newFile = oldAndNewDexFilePair.getValue();
final String dexName = getRelativeDexName(oldFile, newFile);
RelatedInfo relatedInfo = dexNameToRelatedInfoMap.get(dexName);
if (!relatedInfo.oldMd5.equals(relatedInfo.newMd5)) {
diffDexPairAndFillRelatedInfo(oldFile, newFile, relatedInfo);
} else {
// In this case newDexFile is the same as oldDexFile, but we still
// need to treat it as patched dex file so that the SmallPatchGenerator
// can analyze which class of this dex should be kept in small patch.
relatedInfo.newOrFullPatchedFile = newFile;
relatedInfo.newOrFullPatchedMd5 = relatedInfo.newMd5;
relatedInfo.newOrFullPatchedCRC = FileOperation.getFileCrc32(newFile);
}
}
}
private void diffDexPairAndFillRelatedInfo(File oldDexFile, File newDexFile, RelatedInfo relatedInfo) {
File tempFullPatchDexPath = new File(config.mOutFolder + File.separator + TypedValue.DEX_TEMP_PATCH_DIR);
final String dexName = getRelativeDexName(oldDexFile, newDexFile);
File dexDiffOut = getOutputPath(newDexFile).toFile();
ensureDirectoryExist(dexDiffOut.getParentFile());
try {
DexPatchGenerator dexPatchGen = new DexPatchGenerator(oldDexFile, newDexFile);
dexPatchGen.setAdditionalRemovingClassPatterns(config.mDexLoaderPattern);
logWriter.writeLineToInfoFile(
String.format(
"Start diff between [%s] as old and [%s] as new:",
getRelativeStringBy(oldDexFile, config.mTempUnzipOldDir),
getRelativeStringBy(newDexFile, config.mTempUnzipNewDir)
)
);
dexPatchGen.executeAndSaveTo(dexDiffOut);
} catch (Exception e) {
throw new TinkerPatchException(e);
}
if (!dexDiffOut.exists()) {
throw new TinkerPatchException("can not find the diff file:" + dexDiffOut.getAbsolutePath());
}
relatedInfo.dexDiffFile = dexDiffOut;
relatedInfo.dexDiffMd5 = MD5.getMD5(dexDiffOut);
Logger.d("\nGen %s patch file:%s, size:%d, md5:%s", dexName, relatedInfo.dexDiffFile.getAbsolutePath(), relatedInfo.dexDiffFile.length(), relatedInfo.dexDiffMd5);
File tempFullPatchedDexFile = new File(tempFullPatchDexPath, dexName);
if (!tempFullPatchedDexFile.exists()) {
ensureDirectoryExist(tempFullPatchedDexFile.getParentFile());
}
try {
new DexPatchApplier(oldDexFile, dexDiffOut).executeAndSaveTo(tempFullPatchedDexFile);
Logger.d(
String.format("Verifying if patched new dex is logically the same as original new dex: %s ...", getRelativeStringBy(newDexFile, config.mTempUnzipNewDir))
);
Dex origNewDex = new Dex(newDexFile);
Dex patchedNewDex = new Dex(tempFullPatchedDexFile);
checkDexChange(origNewDex, patchedNewDex);
relatedInfo.newOrFullPatchedFile = tempFullPatchedDexFile;
relatedInfo.newOrFullPatchedMd5 = MD5.getMD5(tempFullPatchedDexFile);
relatedInfo.newOrFullPatchedCRC = FileOperation.getFileCrc32(tempFullPatchedDexFile);
} catch (Exception e) {
e.printStackTrace();
throw new TinkerPatchException(
"Failed to generate temporary patched dex, which makes MD5 generating procedure of new dex failed, either.", e
);
}
if (!tempFullPatchedDexFile.exists()) {
throw new TinkerPatchException("can not find the temporary full patched dex file:" + tempFullPatchedDexFile.getAbsolutePath());
}
Logger.d("\nGen %s for dalvik full dex file:%s, size:%d, md5:%s", dexName, tempFullPatchedDexFile.getAbsolutePath(), tempFullPatchedDexFile.length(), relatedInfo.newOrFullPatchedMd5);
}
private void addTestDex() throws IOException {
//write test dex
String dexMode = "jar";
if (config.mDexRaw) {
dexMode = "raw";
}
final InputStream is = DexDiffDecoder.class.getResourceAsStream("/" + TEST_DEX_NAME);
String md5 = MD5.getMD5(is, 1024);
is.close();
String meta = TEST_DEX_NAME + "," + "" + "," + md5 + "," + md5 + "," + 0 + "," + 0 + "," + 0 + "," + dexMode;
File dest = new File(config.mTempResultDir + "/" + TEST_DEX_NAME);
FileOperation.copyResourceUsingStream(TEST_DEX_NAME, dest);
Logger.d("\nAdd test install result dex: %s, size:%d", dest.getAbsolutePath(), dest.length());
Logger.d("DexDecoder:write test dex meta file data: %s", meta);
metaWriter.writeLineToInfoFile(meta);
}
private void checkCrossDexMovingClasses() {
// Here we will check if any classes that were deleted in one dex
// would be added to another dex. e.g. classA is deleted in dex0 and
// added in dex1.
// Since DexClassesComparator will guarantee that a class can be either 'added'
// or 'deleted' between two files it compares. We can achieve our checking works
// by calculating the intersection of deletedClassDescs and addedClassDescs.
Set deletedClassDescs = new HashSet(deletedClassDescToDexNameMap.keySet());
Set addedClassDescs = new HashSet(addedClassDescToDexNameMap.keySet());
deletedClassDescs.retainAll(addedClassDescs);
// So far deletedClassNames only contains the intersect elements between
// deletedClassNames and addedClassNames.
Set movedCrossFilesClassDescs = deletedClassDescs;
if (!movedCrossFilesClassDescs.isEmpty()) {
Logger.e("Warning:Class Moved. Some classes are just moved from one dex to another. "
+ "This behavior may leads to unnecessary enlargement of patch file. you should try to check them:");
for (String classDesc : movedCrossFilesClassDescs) {
StringBuilder sb = new StringBuilder();
sb.append('{');
sb.append("classDesc:").append(classDesc).append(',');
sb.append("from:").append(deletedClassDescToDexNameMap.get(classDesc)).append(',');
sb.append("to:").append(addedClassDescToDexNameMap.get(classDesc));
sb.append('}');
Logger.e(sb.toString());
}
}
}
/**
* Before starting real diff works, we collect added class descriptor
* and deleted class descriptor for further analysing in {@code checkCrossDexMovingClasses}.
*/
private void collectAddedOrDeletedClasses(File oldFile, File newFile) throws IOException {
Dex oldDex = new Dex(oldFile);
Dex newDex = new Dex(newFile);
Set oldClassDescs = new HashSet<>();
for (ClassDef oldClassDef : oldDex.classDefs()) {
oldClassDescs.add(oldDex.typeNames().get(oldClassDef.typeIndex));
}
Set newClassDescs = new HashSet<>();
for (ClassDef newClassDef : newDex.classDefs()) {
newClassDescs.add(newDex.typeNames().get(newClassDef.typeIndex));
}
Set addedClassDescs = new HashSet<>(newClassDescs);
addedClassDescs.removeAll(oldClassDescs);
Set deletedClassDescs = new HashSet<>(oldClassDescs);
deletedClassDescs.removeAll(newClassDescs);
for (String addedClassDesc : addedClassDescs) {
if (addedClassDescToDexNameMap.containsKey(addedClassDesc)) {
throw new TinkerPatchException(
String.format(
"Class Duplicate. Class [%s] is added in both new dex: [%s] and [%s]. Please check your newly apk.",
addedClassDesc,
addedClassDescToDexNameMap.get(addedClassDesc),
newFile.toString()
)
);
} else {
addedClassDescToDexNameMap.put(addedClassDesc, newFile.toString());
}
}
for (String deletedClassDesc : deletedClassDescs) {
if (deletedClassDescToDexNameMap.containsKey(deletedClassDesc)) {
throw new TinkerPatchException(
String.format(
"Class Duplicate. Class [%s] is deleted in both old dex: [%s] and [%s]. Please check your base apk.",
deletedClassDesc,
addedClassDescToDexNameMap.get(deletedClassDesc),
oldFile.toString()
)
);
} else {
deletedClassDescToDexNameMap.put(deletedClassDesc, newFile.toString());
}
}
}
private boolean isDexNameMatchesClassNPattern(String dexName) {
return (dexName.matches("^classes[0-9]*\\.dex$"));
}
private void copyNewDexAndLogToDexMeta(File newFile, String newMd5, File output) throws IOException {
FileOperation.copyFileUsingStream(newFile, output);
final long newFileCrc = FileOperation.getFileCrc32(newFile);
logToDexMeta(newFile, null, null, newMd5, newMd5, "0", newFileCrc);
}
private void checkDexChange(Dex originDex, Dex newDex) {
DexClassesComparator classesCmptor = new DexClassesComparator("*");
classesCmptor.setIgnoredRemovedClassDescPattern(config.mDexLoaderPattern);
classesCmptor.startCheck(originDex, newDex);
List addedClassInfos = classesCmptor.getAddedClassInfos();
boolean isNoClassesAdded = addedClassInfos.isEmpty();
Map changedClassDescToClassInfosMap;
boolean isNoClassesChanged;
if (isNoClassesAdded) {
changedClassDescToClassInfosMap = classesCmptor.getChangedClassDescToInfosMap();
isNoClassesChanged = changedClassDescToClassInfosMap.isEmpty();
} else {
throw new TinkerPatchException(
"some classes was unexpectedly added in patched new dex, check if there's any bugs in "
+ "patch algorithm. Related classes: " + Utils.collectionToString(addedClassInfos)
);
}
if (isNoClassesChanged) {
List deletedClassInfos = classesCmptor.getDeletedClassInfos();
if (!deletedClassInfos.isEmpty()) {
throw new TinkerPatchException(
"some classes that are not matched to loader class pattern "
+ "was unexpectedly deleted in patched new dex, check if there's any bugs in "
+ "patch algorithm. Related classes: "
+ Utils.collectionToString(deletedClassInfos)
);
}
} else {
throw new TinkerPatchException(
"some classes was unexpectedly changed in patched new dex, check if there's any bugs in "
+ "patch algorithm. Related classes: "
+ Utils.collectionToString(changedClassDescToClassInfosMap.keySet())
);
}
}
/**
* Construct dex meta-info and write it to meta file and log.
*
* @param newFile
* New dex file.
* @param oldFile
* Old dex file.
* @param dexDiffFile
* Dex diff file. (Generated by DexPatchGenerator.)
* @param destMd5InDvm
* Md5 of output dex in dvm environment, could be full patched dex md5 or new dex.
* @param destMd5InArt
* Md5 of output dex in dvm environment, could be small patched dex md5 or new dex.
* @param dexDiffMd5
* Md5 of dex patch info file.
* @param newOrFullPatchedCrc
* CRC32 of new dex or full patched dex.
*
* @throws IOException
*/
protected void logToDexMeta(File newFile, File oldFile, File dexDiffFile, String destMd5InDvm, String destMd5InArt, String dexDiffMd5, long newOrFullPatchedCrc) {
if (metaWriter == null && logWriter == null) {
return;
}
String parentRelative = getParentRelativePathStringToNewFile(newFile);
String relative = getRelativePathStringToNewFile(newFile);
if (metaWriter != null) {
String fileName = newFile.getName();
String dexMode = "jar";
if (config.mDexRaw) {
dexMode = "raw";
}
//new file
String oldCrc;
if (oldFile == null) {
oldCrc = "0";
Logger.d("DexDecoder:add newly dex file: %s", parentRelative);
} else {
oldCrc = FileOperation.getZipEntryCrc(config.mOldApkFile, relative);
if (oldCrc == null || oldCrc.equals("0")) {
throw new TinkerPatchException(
String.format("can't find zipEntry %s from old apk file %s", relative, config.mOldApkFile.getPath())
);
}
}
String meta = fileName + "," + parentRelative + "," + destMd5InDvm + ","
+ destMd5InArt + "," + dexDiffMd5 + "," + oldCrc + "," + newOrFullPatchedCrc + "," + dexMode;
Logger.d("DexDecoder:write meta file data: %s", meta);
metaWriter.writeLineToInfoFile(meta);
}
if (logWriter != null) {
String log = relative + ", oldSize=" + FileOperation.getFileSizes(oldFile) + ", newSize="
+ FileOperation.getFileSizes(newFile) + ", diffSize=" + FileOperation.getFileSizes(dexDiffFile);
logWriter.writeLineToInfoFile(log);
}
}
@Override
public void clean() {
metaWriter.close();
logWriter.close();
}
private String getRawOrWrappedDexMD5(File dexOrJarFile) {
final String name = dexOrJarFile.getName();
if (name.endsWith(".dex")) {
return MD5.getMD5(dexOrJarFile);
} else {
JarFile dexJar = null;
try {
dexJar = new JarFile(dexOrJarFile);
ZipEntry classesDex = dexJar.getEntry(DexFormat.DEX_IN_JAR_NAME);
// no code
if (classesDex == null) {
throw new TinkerPatchException(
String.format("Jar file %s do not contain 'classes.dex', it is not a correct dex jar file!", dexOrJarFile.getAbsolutePath())
);
}
return MD5.getMD5(dexJar.getInputStream(classesDex), 1024 * 100);
} catch (IOException e) {
throw new TinkerPatchException(
String.format("File %s is not end with '.dex', but it is not a correct dex jar file !", dexOrJarFile.getAbsolutePath()), e
);
} finally {
if (dexJar != null) {
try {
dexJar.close();
} catch (Exception e) {
// Ignored.
}
}
}
}
}
private String getRelativeStringBy(File file, File reference) {
File actualReference = reference.getParentFile();
if (actualReference == null) {
actualReference = reference;
}
return actualReference.toPath().relativize(file.toPath()).toString().replace("\\", "/");
}
private void ensureDirectoryExist(File dir) {
if (!dir.exists()) {
if (!dir.mkdirs()) {
throw new TinkerPatchException("failed to create directory: " + dir);
}
}
}
private final class RelatedInfo {
File newOrFullPatchedFile = null;
/**
* This field could be null if old dex and new dex
* are the same.
*/
File dexDiffFile = null;
String oldMd5 = "0";
String newMd5 = "0";
String dexDiffMd5 = "0";
/**
* This field could be one of the following value:
* fullPatchedDex md5, if old dex and new dex are different;
* newDex md5, if new dex is marked to be copied directly.
*/
String newOrFullPatchedMd5 = "0";
/**
* This field is used to generate class-N dex jar on app runtime.
* It could be one of the following value:
* CRC32 of full patched dex, if old dex and new dex are different;
* CRC32 of new dex, if new dex is marked to be copied directly.
*/
long newOrFullPatchedCRC = 0;
}
private final class DexPatcherLoggerBridge implements IDexPatcherLogger {
private final InfoWriter logWriter;
DexPatcherLoggerBridge(InfoWriter logWritter) {
this.logWriter = logWritter;
}
@Override
public void v(String msg) {
this.logWriter.writeLineToInfoFile(msg);
}
@Override
public void d(String msg) {
this.logWriter.writeLineToInfoFile(msg);
}
@Override
public void i(String msg) {
this.logWriter.writeLineToInfoFile(msg);
}
@Override
public void w(String msg) {
this.logWriter.writeLineToInfoFile(msg);
}
@Override
public void e(String msg) {
this.logWriter.writeLineToInfoFile(msg);
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy