keyTitle_dexsectiondiffalgorithm类设计
keyTitle_dexsectionpatchalgorithm类设计
1:构建基准apk(仅仅发版时需要打开tinkerEnabled,开发自己debug时不需要打开)
1.1: Copy apk,map,R文件到bakApk目录下
1.2: 创建tinkerPatchDebug和tinkerPatchRelease的task执行的必要参数
//tinker/tinker-sample-android/app/build.gradle
def bakPath = file("${buildDir}/bakApk/")
/**
\* you can use assembleRelease to build you base apk
\* use tinkerPatchRelease -POLD_APK= -PAPPLY_MAPPING= -PAPPLY_RESOURCE= to build patch
\* add apk from the build/bakApk
*/
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-0917-16-24-41.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-0424-15-02-56-R.txt"
//only use for build all flavor, if not, just ignore this field
tinkerBuildFlavorDirectory = "${bakPath}/app-1018-17-32-47"
}
if (buildWithTinker()) {
apply plugin: 'com.tencent.tinker.patch'
tinkerPatch {
oldApk = getOldApkPath()
ignoreWarning = false
useSign = true
tinkerEnable = buildWithTinker()
buildConfig {
applyMapping = getApplyMappingPath()
applyResourceMapping = getApplyResourceMappingPath()
tinkerId = getTinkerIdValue()
keepDexApply = false
isProtectedApp = false
supportHotplugComponent = false
}
dex {
dexMode = "jar"
pattern = ["classes*.dex",
"assets/secondary-dex-?.jar"]
loader = [
"tinker.sample.android.app.BaseBuildInfo"
]
}
lib {
pattern = ["lib/*/*.so"]
}
res {
pattern = ["res/*", "assets/*", "resources.arsc", "AndroidManifest.xml"]
ignoreChange = ["assets/sample_meta.txt"]
largeModSize = 100
}
packageConfig {
configField("patchMessage", "tinker is sample to use")
configField("platform", "all")
configField("patchVersion", "1.0")
}
sevenZip {
zipArtifact = "com.tencent.mm:SevenZip:1.1.10"
}
}
android.applicationVariants.all { variant ->
/**
\* task type, you want to bak
*/
def taskName = variant.name
tasks.all {
if ("assemble${taskName.capitalize()}".equalsIgnoreCase(it.name)) {
it.doLast {
copy {
def fileNamePrefix = "${project.name}-${variant.baseName}"
def newFileNamePrefix = hasFlavors ? "${fileNamePrefix}" : "${fileNamePrefix}-${date}"
def destPath = hasFlavors ? file("${bakPath}/${project.name}-${date}/${variant.flavorName}") : bakPath
if (variant.metaClass.hasProperty(variant, 'packageApplicationProvider')) {
def packageAndroidArtifact = variant.packageApplicationProvider.get()
if (packageAndroidArtifact != null) {
try {
from new File(packageAndroidArtifact.outputDirectory.getAsFile().get(), variant.outputs.first().apkData.outputFileName)
} catch (Exception e) {
from new File(packageAndroidArtifact.outputDirectory, variant.outputs.first().apkData.outputFileName)
}
} else {
from variant.outputs.first().mainOutputFile.outputFile
}
} else {
from variant.outputs.first().outputFile
}
into destPath
rename { String fileName ->
fileName.replace("${fileNamePrefix}.apk", "${newFileNamePrefix}.apk")
}
from "${buildDir}/outputs/mapping/${variant.dirName}/mapping.txt"
into destPath
rename { String fileName ->
fileName.replace("mapping.txt", "${newFileNamePrefix}-mapping.txt")
}
from "${buildDir}/intermediates/symbols/${variant.dirName}/R.txt"
from "${buildDir}/intermediates/symbol_list/${variant.dirName}/R.txt"
into destPath
rename { String fileName ->
fileName.replace("R.txt", "${newFileNamePrefix}-R.txt")
}
}
}
}
}
}
#tinker/tinker-build/tinker-patch-gradle-plugin/src/main/resources/META-INF/gradle-plugins/com.tencent.tinker.patch.properties
implementation-class=com.tencent.tinker.build.gradle.TinkerPatchPlugin
class TinkerPatchPlugin implements Plugin<Project> {
@Override
public void apply(Project project) {
mProject = project
mProject.extensions.create('tinkerPatch', TinkerPatchExtension)
mProject.tinkerPatch.extensions.create('buildConfig', TinkerBuildConfigExtension, mProject)
//mProject.tinkerPatch.extensions
mProject.tinkerPatch.extensions.create('dex', TinkerDexExtension, mProject)
mProject.tinkerPatch.extensions.create('lib', TinkerLibExtension)
mProject.tinkerPatch.extensions.create('res', TinkerResourceExtension)
mProject.tinkerPatch.extensions.create("arkHot", TinkerArkHotExtension)
mProject.tinkerPatch.extensions.create('packageConfig', TinkerPackageConfigExtension, mProject)
mProject.tinkerPatch.extensions.create('sevenZip', TinkerSevenZipExtension, mProject)
android.applicationVariants.all { variant ->
def variantOutput = variant.outputs.first()
def variantName = variant.name.capitalize()
def variantData = variant.variantData
//创建task: tinkerPatchDebug或tinkerPatchRelease, main
TinkerPatchSchemaTask tinkerPatchBuildTask = mProject.tasks.create("tinkerPatch${variantName}", TinkerPatchSchemaTask)
//创建task: tinkerProcessDebugManifest,main
// Create a task to add a build TINKER_ID to AndroidManifest.xml
// This task must be called after "process${variantName}Manifest", since it
// requires that an AndroidManifest.xml exists in `build/intermediates`.
TinkerManifestTask manifestTask = mProject.tasks.create("tinkerProcess${variantName}Manifest", TinkerManifestTask)
tinkerPatchBuildTask.signConfig = variantData.variantConfiguration.signingConfig
variant.outputs.each { output ->
setPatchNewApkPath(configuration, output, variant, tinkerPatchBuildTask)
setPatchOutputFolder(configuration, output, variant, tinkerPatchBuildTask)
}
manifestTask.mustRunAfter variantOutput.processManifest
variantOutput.processResources.dependsOn manifestTask
//main
TinkerResourceIdTask applyResourceTask = mProject.tasks.create("tinkerProcess${variantName}ResourceId", TinkerResourceIdTask)
applyResourceTask.resDir = variantOutput.processResources.inputResourcesDir.getFiles().first()
applyResourceTask.mustRunAfter manifestTask
variantOutput.processResources.dependsOn applyResourceTask
applyResourceTask.dependsOn mergeResourcesTask
if (multiDexEnabled) {//main
TinkerMultidexConfigTask multidexConfigTask = mProject.tasks.create("tinkerProcess${variantName}MultidexKeep", TinkerMultidexConfigTask)
multidexConfigTask.applicationVariant = variant
multidexConfigTask.multiDexKeepProguard = getManifestMultiDexKeepProguard(variant)
multidexConfigTask.mustRunAfter manifestTask
multidexConfigTask.mustRunAfter variantOutput.processResources
multidexTask.dependsOn multidexConfigTask
if (configuration.buildConfig.keepDexApply
&& FileOperation.isLegalFile(mProject.tinkerPatch.oldApk)) {
com.tencent.tinker.build.gradle.transform.ImmutableDexTransform.inject(mProject, variant)//main
}
}
void setPatchNewApkPath(configuration, output, variant, tinkerPatchBuildTask) {
tinkerPatchBuildTask.dependsOn variant.assemble//assembleDebug
}
ImmutableDexTransform
@Override
void transform(TransformInvocation transformInvocation) throws TransformException, IOException, InterruptedException {}
TinkerPatchExtension configuration
def android
String buildApkPath
String outputFolder
def signConfig
public TinkerPatchSchemaTask() {
description = 'Assemble Tinker Patch'
group = 'tinker'
outputs.upToDateWhen { false }
configuration = project.tinkerPatch
android = project.extensions.android
}
@TaskAction
def tinkerPatch() {
configuration.checkParameter()
configuration.buildConfig.checkParameter()
configuration.res.checkParameter()
configuration.dex.checkDexMode()
configuration.sevenZip.resolveZipFinalPath()
builder.setOldApk(configuration.oldApk)
.setNewApk(buildApkPath)
.setOutBuilder(outputFolder)
.setIgnoreWarning(configuration.ignoreWarning)
.setAllowLoaderInAnyDex(configuration.allowLoaderInAnyDex)
.setRemoveLoaderForAllDex(configuration.removeLoaderForAllDex)
.setDexFilePattern(new ArrayList<String>(configuration.dex.pattern))
.setIsProtectedApp(configuration.buildConfig.isProtectedApp)
.setIsComponentHotplugSupported(configuration.buildConfig.supportHotplugComponent)
.setDexLoaderPattern(new ArrayList<String>(configuration.dex.loader))
.setDexIgnoreWarningLoaderPattern(new ArrayList<String>(configuration.dex.ignoreWarningLoader))
.setDexMode(configuration.dex.dexMode)
.setSoFilePattern(new ArrayList<String>(configuration.lib.pattern))
.setResourceFilePattern(new ArrayList<String>(configuration.res.pattern))
.setResourceIgnoreChangePattern(new ArrayList<String>(configuration.res.ignoreChange))
.setResourceIgnoreChangeWarningPattern(new ArrayList<String>(configuration.res.ignoreChangeWarning))
.setResourceLargeModSize(configuration.res.largeModSize)
.setUseApplyResource(configuration.buildConfig.usingResourceMapping)
.setConfigFields(new HashMap<String, String>(configuration.packageConfig.getFields()))
.setSevenZipPath(configuration.sevenZip.path)
.setUseSign(configuration.useSign)
.setArkHotPath(configuration.arkHot.path)
.setArkHotName(configuration.arkHot.name)
InputParam inputParam = builder.create()
Runner.gradleRun(inputParam);
public static void gradleRun(InputParam inputParam) {
mBeginTime = System.currentTimeMillis();
Runner m = new Runner();
m.run(inputParam);
}
private void run(InputParam inputParam) {
loadConfigFromGradle(inputParam);
try {
//tinker-sample-android\app\build\outputs\apk/tinkerPatch/debug\log.txt
Logger.initLogger(config);
tinkerPatch();//main
} catch (IOException e) {
e.printStackTrace();
goToError();
} finally {
Logger.closeLogger();
}
}
protected void tinkerPatch() {
Logger.d("-----------------------Tinker patch begin-----------------------");
Logger.d(config.toString());
try {
//gen patch
ApkDecoder decoder = new ApkDecoder(config);
decoder.onAllPatchesStart();
decoder.patch(config.mOldApkFile, config.mNewApkFile);//main
decoder.onAllPatchesEnd();
//gen meta file and version file
PatchInfo info = new PatchInfo(config);
info.gen();//输出assets/package_meta.txt文件记录版本信息
//build patch
PatchBuilder builder = new PatchBuilder(config);
builder.buildPatch();
} catch (Throwable e) {
e.printStackTrace();
goToError();
}
Logger.d("Tinker patch done, total time cost: %fs", diffTimeFromBegin());
Logger.d("Tinker patch done, you can go to file to find the output %s", config.mOutFolder);
Logger.d("-----------------------Tinker patch end-------------------------");
}
public void checkIfExcludedClassWasModifiedInNewDex(File oldFile, File newFile) throws IOException, TinkerPatchException {
int stmCode = STMCODE_START;
while (stmCode != STMCODE_END) {
switch (stmCode) {
case STMCODE_START: {
boolean isPrimaryDex = isPrimaryDex((oldFile == null ? newFile : oldFile));
if (isPrimaryDex) {
if (oldFile == null) {
stmCode = STMCODE_ERROR_PRIMARY_OLD_DEX_IS_MISSING;
} else if (newFile == null) {
stmCode = STMCODE_ERROR_PRIMARY_NEW_DEX_IS_MISSING;
} else {
dexCmptor.startCheck(oldDex, newDex);//main
public void startCheck(Dex oldDex, Dex newDex) {
startCheck(DexGroup.wrap(oldDex), DexGroup.wrap(newDex));
}
public void startCheck(DexGroup oldDexGroup, DexGroup newDexGroup) {
//add, delete, change三个列表必须都是empty,才算是逻辑上一致
Set<String> deletedClassDescs = new HashSet<>(oldDescriptorOfClassesToCheck);
deletedClassDescs.removeAll(newDescriptorOfClassesToCheck);
Set<String> addedClassDescs = new HashSet<>(newDescriptorOfClassesToCheck);
addedClassDescs.removeAll(oldDescriptorOfClassesToCheck);
Set<String> mayBeChangedClassDescs = new HashSet<>(oldDescriptorOfClassesToCheck);
mayBeChangedClassDescs.retainAll(newDescriptorOfClassesToCheck);
for (String desc : mayBeChangedClassDescs) {
DexClassInfo oldClassInfo = oldClassDescriptorToClassInfoMap.get(desc);
DexClassInfo newClassInfo = newClassDescriptorToClassInfoMap.get(desc);
switch (compareMode) {
case COMPARE_MODE_NORMAL: {
if (!isSameClass(//main
oldClassInfo.owner,
newClassInfo.owner,
oldClassInfo.classDef,
newClassInfo.classDef
)) {
if (Utils.isStringMatchesPatterns(desc, patternsOfIgnoredRemovedClassDesc)) {
logger.i(TAG, "Ignored changed class: %s", desc);
} else {
logger.i(TAG, "Changed class: %s", desc);
changedClassDescToClassInfosMap.put(
desc, new DexClassInfo[]{oldClassInfo, newClassInfo}
);
}
private boolean isSameClass(
Dex oldDex,
Dex newDex,
ClassDef oldClassDef,
ClassDef newClassDef
) {//内部会有各种界别same的判断,内部还会调用isSameMethod,isSameCode等等,判断是否逻辑一致
if (oldClassDef.accessFlags != newClassDef.accessFlags) {
return false;
}
if (!isSameClassDesc(
oldDex, newDex, oldClassDef.supertypeIndex, newClassDef.supertypeIndex
)) {
return false;
}
short[] oldInterfaceIndices = oldDex.interfaceTypeIndicesFromClassDef(oldClassDef);
short[] newInterfaceIndices = newDex.interfaceTypeIndicesFromClassDef(newClassDef);
if (oldInterfaceIndices.length != newInterfaceIndices.length) {
return false;
} else {
for (int i = 0; i < oldInterfaceIndices.length; ++i) {
if (!isSameClassDesc(oldDex, newDex, oldInterfaceIndices[i], newInterfaceIndices[i])) {
return false;
}
}
}
if (!isSameName(oldDex, newDex, oldClassDef.sourceFileIndex, newClassDef.sourceFileIndex)) {
return false;
}
if (!isSameAnnotationDirectory(
oldDex,
newDex,
oldClassDef.annotationsOffset,
newClassDef.annotationsOffset
)) {
return false;
}
if (!isSameClassData(
oldDex,
newDex,
oldClassDef.classDataOffset,
newClassDef.classDataOffset
)) {
return false;
}
return isSameStaticValue(
oldDex,
newDex,
oldClassDef.staticValuesOffset,
newClassDef.staticValuesOffset
);
}
dexPatchGen.executeAndSaveTo(dexDiffOut);//Dex差分
二路归并算法是较小的指针+1,从而判断是删除old还是新增new
private DexSectionDiffAlgorithm<StringData> stringDataSectionDiffAlg;
private DexSectionDiffAlgorithm<Integer> typeIdSectionDiffAlg;
private DexSectionDiffAlgorithm<ProtoId> protoIdSectionDiffAlg;
private DexSectionDiffAlgorithm<FieldId> fieldIdSectionDiffAlg;
private DexSectionDiffAlgorithm<MethodId> methodIdSectionDiffAlg;
private DexSectionDiffAlgorithm<ClassDef> classDefSectionDiffAlg;
private DexSectionDiffAlgorithm<TypeList> typeListSectionDiffAlg;
private DexSectionDiffAlgorithm<AnnotationSetRefList> annotationSetRefListSectionDiffAlg;
private DexSectionDiffAlgorithm<AnnotationSet> annotationSetSectionDiffAlg;
private DexSectionDiffAlgorithm<ClassData> classDataSectionDiffAlg;
private DexSectionDiffAlgorithm<Code> codeSectionDiffAlg;
private DexSectionDiffAlgorithm<DebugInfoItem> debugInfoSectionDiffAlg;
private DexSectionDiffAlgorithm<Annotation> annotationSectionDiffAlg;
private DexSectionDiffAlgorithm<EncodedValue> encodedArraySectionDiffAlg;
private DexSectionDiffAlgorithm<AnnotationsDirectory> annotationsDirectorySectionDiffAlg;
public DexPatchGenerator(File oldDexFile, File newDexFile) throws IOException {
this(new Dex(oldDexFile), new Dex(newDexFile));
}
public DexPatchGenerator(Dex oldDex, Dex newDex) {
this.oldDex = oldDex;
this.newDex = newDex;
SparseIndexMap oldToNewIndexMap = new SparseIndexMap();
SparseIndexMap oldToPatchedIndexMap = new SparseIndexMap();
SparseIndexMap newToPatchedIndexMap = new SparseIndexMap();
SparseIndexMap selfIndexMapForSkip = new SparseIndexMap();
this.stringDataSectionDiffAlg = new StringDataSectionDiffAlgorithm(
oldDex, newDex,
oldToNewIndexMap,
oldToPatchedIndexMap,
newToPatchedIndexMap,
selfIndexMapForSkip
);
this.typeIdSectionDiffAlg = new TypeIdSectionDiffAlgorithm(
oldDex, newDex,
oldToNewIndexMap,
oldToPatchedIndexMap,
newToPatchedIndexMap,
selfIndexMapForSkip
);
public void executeAndSaveTo(File file) throws IOException {
OutputStream os = null;
try {
os = new BufferedOutputStream(new FileOutputStream(file));
executeAndSaveTo(os);
} finally {
StreamUtil.closeQuietly(os);
}
}
public void executeAndSaveTo(OutputStream out) throws IOException {
// Firstly, collect information of items we want to remove additionally
// in new dex and set them to corresponding diff algorithm implementations.
//首先移除需要ignore变更的class,如loader包中的,SampleApplication和BaseBuildInfo,这些类的差异无法通过tinker修复
......
((ClassDefSectionDiffAlgorithm) this.classDefSectionDiffAlg)
.setTypeIdOfClassDefsToRemove(typeIdOfClassDefsToRemove);
((ClassDataSectionDiffAlgorithm) this.classDataSectionDiffAlg)
.setOffsetOfClassDatasToRemove(offsetOfClassDatasToRemove);
// Then, run diff algorithms according to sections' dependencies.
// The diff works on each sections obey such procedure:
// 1. Execute diff algorithms to calculate indices of items we need to add, del and replace.
// 2. Execute patch algorithm simulation to calculate indices and offsets mappings that is
// necessary to next section's diff works.
// Immediately do the patch simulation so that we can know:
// 1. Indices and offsets mapping between old dex and patched dex.
// 2. Indices and offsets mapping between new dex and patched dex.
// These information will be used to do next diff works.
this.patchedStringIdsOffset = patchedHeaderOffset + patchedheaderSize;
if (this.oldDex.getTableOfContents().stringIds.isElementFourByteAligned) {
this.patchedStringIdsOffset = SizeOf.roundToTimesOfFour(this.patchedStringIdsOffset);
}
this.stringDataSectionDiffAlg.execute();//执行二路归并算法,结果存储到三种(add/del/replace)map中//main
...
this.stringDataSectionDiffAlg.simulatePatchOperation(this.patchedStringDataItemsOffset);//main
......
// Finally, write results to patch file.
writeResultToStream(out);//main
private void writeResultToStream(OutputStream os) throws IOException {
DexDataBuffer buffer = new DexDataBuffer();
buffer.write(DexPatchFile.MAGIC);
buffer.writeShort(DexPatchFile.CURRENT_VERSION);
buffer.writeInt(this.patchedDexSize);
// we will return here to write firstChunkOffset later.
int posOfFirstChunkOffsetField = buffer.position();
buffer.writeInt(0);
buffer.writeInt(this.patchedStringIdsOffset);
buffer.writeInt(this.patchedTypeIdsOffset);
buffer.writeInt(this.patchedProtoIdsOffset);
buffer.writeInt(this.patchedFieldIdsOffset);
buffer.writeInt(this.patchedMethodIdsOffset);
buffer.writeInt(this.patchedClassDefsOffset);
buffer.writeInt(this.patchedMapListOffset);
buffer.writeInt(this.patchedTypeListsOffset);
buffer.writeInt(this.patchedAnnotationSetRefListItemsOffset);
buffer.writeInt(this.patchedAnnotationSetItemsOffset);
buffer.writeInt(this.patchedClassDataItemsOffset);
buffer.writeInt(this.patchedCodeItemsOffset);
buffer.writeInt(this.patchedStringDataItemsOffset);
buffer.writeInt(this.patchedDebugInfoItemsOffset);
buffer.writeInt(this.patchedAnnotationItemsOffset);
buffer.writeInt(this.patchedEncodedArrayItemsOffset);
buffer.writeInt(this.patchedAnnotationsDirectoryItemsOffset);
buffer.write(this.oldDex.computeSignature(false));
int firstChunkOffset = buffer.position();
buffer.position(posOfFirstChunkOffsetField);
buffer.writeInt(firstChunkOffset);
buffer.position(firstChunkOffset);
writePatchOperations(buffer, this.stringDataSectionDiffAlg.getPatchOperationList());
writePatchOperations(buffer, this.typeIdSectionDiffAlg.getPatchOperationList());
writePatchOperations(buffer, this.typeListSectionDiffAlg.getPatchOperationList());
writePatchOperations(buffer, this.protoIdSectionDiffAlg.getPatchOperationList());
writePatchOperations(buffer, this.fieldIdSectionDiffAlg.getPatchOperationList());
writePatchOperations(buffer, this.methodIdSectionDiffAlg.getPatchOperationList());
writePatchOperations(buffer, this.annotationSectionDiffAlg.getPatchOperationList());
writePatchOperations(buffer, this.annotationSetSectionDiffAlg.getPatchOperationList());
writePatchOperations(buffer, this.annotationSetRefListSectionDiffAlg.getPatchOperationList());
writePatchOperations(buffer, this.annotationsDirectorySectionDiffAlg.getPatchOperationList());
writePatchOperations(buffer, this.debugInfoSectionDiffAlg.getPatchOperationList());
writePatchOperations(buffer, this.codeSectionDiffAlg.getPatchOperationList());
writePatchOperations(buffer, this.classDataSectionDiffAlg.getPatchOperationList());
writePatchOperations(buffer, this.encodedArraySectionDiffAlg.getPatchOperationList());
writePatchOperations(buffer, this.classDefSectionDiffAlg.getPatchOperationList());
byte[] bufferData = buffer.array();
os.write(bufferData);
os.flush();
}
private <T extends Comparable<T>> void writePatchOperations(
DexDataBuffer buffer, List<PatchOperation<T>> patchOperationList
) {
......
buffer.writeUleb128(delOpIndexList.size());
int lastIndex = 0;
for (Integer index : delOpIndexList) {
buffer.writeSleb128(index - lastIndex);
lastIndex = index;
}
buffer.writeUleb128(addOpIndexList.size());
lastIndex = 0;
for (Integer index : addOpIndexList) {
buffer.writeSleb128(index - lastIndex);
lastIndex = index;
}
buffer.writeUleb128(replaceOpIndexList.size());
lastIndex = 0;
for (Integer index : replaceOpIndexList) {
buffer.writeSleb128(index - lastIndex);
lastIndex = index;
}
for (T newItem : newItemList) {
if (newItem instanceof StringData) {
buffer.writeStringData((StringData) newItem);
} else
......
new DexPatchApplier(oldDexFile, dexDiffOut).executeAndSaveTo(tempFullPatchedDexFile);//Dex差分合成
public DexPatchApplier(File oldDexIn, File patchFileIn) throws IOException {
this(new Dex(oldDexIn), new DexPatchFile(patchFileIn));
}
public DexPatchApplier(Dex oldDexIn, DexPatchFile patchFileIn) {
this.oldDex = oldDexIn;
this.patchFile = patchFileIn;
this.patchedDex = new Dex(patchFileIn.getPatchedDexSize());
this.oldToPatchedIndexMap = new SparseIndexMap();
}
public void executeAndSaveTo(File file) throws IOException {
OutputStream os = null;
try {
os = new BufferedOutputStream(new FileOutputStream(file));
executeAndSaveTo(os);
} finally {
StreamUtil.closeQuietly(os);
}
}
public void executeAndSaveTo(OutputStream out) throws IOException {
// Before executing, we should check if this patch can be applied to
// old dex we passed in.
// Firstly, set sections' offset after patched, sort according to their offset so that
// the dex lib of aosp can calculate section size.
TableOfContents patchedToc = this.patchedDex.getTableOfContents();
patchedToc.header.off = 0;
patchedToc.header.size = 1;
patchedToc.mapList.size = 1;
patchedToc.stringIds.off = this.patchFile.getPatchedStringIdSectionOffset();
patchedToc.typeIds.off = this.patchFile.getPatchedTypeIdSectionOffset();
patchedToc.typeLists.off = this.patchFile.getPatchedTypeListSectionOffset();
patchedToc.protoIds.off = this.patchFile.getPatchedProtoIdSectionOffset();
patchedToc.fieldIds.off = this.patchFile.getPatchedFieldIdSectionOffset();
patchedToc.methodIds.off = this.patchFile.getPatchedMethodIdSectionOffset();
patchedToc.classDefs.off = this.patchFile.getPatchedClassDefSectionOffset();
patchedToc.mapList.off = this.patchFile.getPatchedMapListSectionOffset();
patchedToc.stringDatas.off = this.patchFile.getPatchedStringDataSectionOffset();
patchedToc.annotations.off = this.patchFile.getPatchedAnnotationSectionOffset();
patchedToc.annotationSets.off = this.patchFile.getPatchedAnnotationSetSectionOffset();
patchedToc.annotationSetRefLists.off = this.patchFile.getPatchedAnnotationSetRefListSectionOffset();
patchedToc.annotationsDirectories.off = this.patchFile.getPatchedAnnotationsDirectorySectionOffset();
patchedToc.encodedArrays.off = this.patchFile.getPatchedEncodedArraySectionOffset();
patchedToc.debugInfos.off = this.patchFile.getPatchedDebugInfoSectionOffset();
patchedToc.codes.off = this.patchFile.getPatchedCodeSectionOffset();
patchedToc.classDatas.off = this.patchFile.getPatchedClassDataSectionOffset();
patchedToc.fileSize = this.patchFile.getPatchedDexSize();
Arrays.sort(patchedToc.sections);
patchedToc.computeSizesFromOffsets();
// Secondly, run patch algorithms according to sections' dependencies.
this.stringDataSectionPatchAlg = new StringDataSectionPatchAlgorithm(patchFile, oldDex, patchedDex, oldToPatchedIndexMap);
....
this.stringDataSectionPatchAlg.execute();
this.typeIdSectionPatchAlg.execute();
this.typeListSectionPatchAlg.execute();
this.protoIdSectionPatchAlg.execute();
this.fieldIdSectionPatchAlg.execute();
this.methodIdSectionPatchAlg.execute();
this.annotationSectionPatchAlg.execute();
this.annotationSetSectionPatchAlg.execute();
this.annotationSetRefListSectionPatchAlg.execute();
this.annotationsDirectorySectionPatchAlg.execute();
this.debugInfoSectionPatchAlg.execute();
this.codeSectionPatchAlg.execute();
this.classDataSectionPatchAlg.execute();
this.encodedArraySectionPatchAlg.execute();
this.classDefSectionPatchAlg.execute();
// Thirdly, write header, mapList. Calculate and write patched dex's sign and checksum.
Dex.Section headerOut = this.patchedDex.openSection(patchedToc.header.off);
patchedToc.writeHeader(headerOut);
Dex.Section mapListOut = this.patchedDex.openSection(patchedToc.mapList.off);
patchedToc.writeMap(mapListOut);
this.patchedDex.writeHashes();
// Finally, write patched dex to file.
this.patchedDex.writeTo(out);
}
public DexPatchFile(File file) throws IOException {
this.buffer = new DexDataBuffer(ByteBuffer.wrap(FileUtils.readFile(file)));
init();
}
private void init() {
byte[] magic = this.buffer.readByteArray(MAGIC.length);
if (CompareUtils.uArrCompare(magic, MAGIC) != 0) {
throw new IllegalStateException("bad dex patch file magic: " + Arrays.toString(magic));
}
this.version = this.buffer.readShort();
if (CompareUtils.uCompare(this.version, CURRENT_VERSION) != 0) {
throw new IllegalStateException("bad dex patch file version: " + this.version + ", expected: " + CURRENT_VERSION);
}
this.patchedDexSize = this.buffer.readInt();
this.firstChunkOffset = this.buffer.readInt();
this.patchedStringIdSectionOffset = this.buffer.readInt();
this.patchedTypeIdSectionOffset = this.buffer.readInt();
this.patchedProtoIdSectionOffset = this.buffer.readInt();
this.patchedFieldIdSectionOffset = this.buffer.readInt();
this.patchedMethodIdSectionOffset = this.buffer.readInt();
this.patchedClassDefSectionOffset = this.buffer.readInt();
this.patchedMapListSectionOffset = this.buffer.readInt();
this.patchedTypeListSectionOffset = this.buffer.readInt();
this.patchedAnnotationSetRefListSectionOffset = this.buffer.readInt();
this.patchedAnnotationSetSectionOffset = this.buffer.readInt();
this.patchedClassDataSectionOffset = this.buffer.readInt();
this.patchedCodeSectionOffset = this.buffer.readInt();
this.patchedStringDataSectionOffset = this.buffer.readInt();
this.patchedDebugInfoSectionOffset = this.buffer.readInt();
this.patchedAnnotationSectionOffset = this.buffer.readInt();
this.patchedEncodedArraySectionOffset = this.buffer.readInt();
this.patchedAnnotationsDirectorySectionOffset = this.buffer.readInt();
this.oldDexSignature = this.buffer.readByteArray(SizeOf.SIGNATURE);
this.buffer.position(firstChunkOffset);
}
public DexDataBuffer getBuffer() {
return buffer;
}
checkDexChange(origNewDex, patchedNewDex);//检查是否patchedNewDex(打完补丁的new dex)和原本的newDex在逻辑上保持一致
tinker-build/tinker-patch-lib/src/main/resources/test.dex代码中固定的文件通过getResourceAsStream复制到当前patch的输出目录mTempResultDir
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);
}
public void buildPatch() throws Exception {
final File resultDir = config.mTempResultDir;
generateUnsignedApk(unSignedApk);//将tinker_result文件夹下的内容打包到patch apk
signApk(unSignedApk, signedApk);执行jarsigner命令进行sign
use7zApk(signedApk, signedWith7ZipApk, sevenZipOutPutDir);
}
aosp-dexutils-1.9.14.jar中包含dex字节码的处理库如Code类
/**
\* Creates a new dex buffer from the dex file {@code file}.
*/
public Dex(File file) throws IOException {
if (file.getName().endsWith(".dex")) {
InputStream in = null;
try {
in = new BufferedInputStream(new FileInputStream(file));
loadFrom(in, (int) file.length());
} catch (Exception e) {
throw new DexException(e);
}
}
}
private void loadFrom(InputStream in, int initSize) throws IOException {
byte[] rawData = FileUtils.readStream(in, initSize);//rawData表示dex文件的byte数组
this.data = ByteBuffer.wrap(rawData);
this.data.order(ByteOrder.LITTLE_ENDIAN);
this.tableOfContents.readFrom(this);//main
}
public void writeTo(OutputStream out) throws IOException {
byte[] rawData = data.array();
out.write(rawData);
out.flush();
}
/**
\* The file header and map.
*/
TableOfContents {
public final Section header = new Section(SECTION_TYPE_HEADER, true);//无Algorithm对应
public final Section stringIds = new Section(SECTION_TYPE_STRINGIDS, true);//ref StringDataSectionDiffAlgorithm
public final Section typeIds = new Section(SECTION_TYPE_TYPEIDS, true);//ref TypeIdSectionDiffAlgorithm
public final Section protoIds = new Section(SECTION_TYPE_PROTOIDS, true);//ref ProtoIdSectionDiffAlgorithm
public final Section fieldIds = new Section(SECTION_TYPE_FIELDIDS, true);//ref FieldIdSectionDiffAlgorithm
public final Section methodIds = new Section(SECTION_TYPE_METHODIDS, true);//ref MethodIdSectionDiffAlgorithm
public final Section classDefs = new Section(SECTION_TYPE_CLASSDEFS, true);//ref ClassDefSectionDiffAlgorithm
public final Section mapList = new Section(SECTION_TYPE_MAPLIST, true);//无Algorithm对应
public final Section typeLists = new Section(SECTION_TYPE_TYPELISTS, true);//TypeListSectionDiffAlgorithm
public final Section annotationSetRefLists = new Section(SECTION_TYPE_ANNOTATIONSETREFLISTS, true);//ref ...
public final Section annotationSets = new Section(SECTION_TYPE_ANNOTATIONSETS, true);ref...
public final Section classDatas = new Section(SECTION_TYPE_CLASSDATA, false);ref...
public final Section codes = new Section(SECTION_TYPE_CODES, true);ref...
public final Section stringDatas = new Section(SECTION_TYPE_STRINGDATAS, false);ref...
public final Section debugInfos = new Section(SECTION_TYPE_DEBUGINFOS, false);ref...
public final Section annotations = new Section(SECTION_TYPE_ANNOTATIONS, false);ref...
public final Section encodedArrays = new Section(SECTION_TYPE_ENCODEDARRAYS, false);//无Algorithm对应
public final Section annotationsDirectories = new Section(SECTION_TYPE_ANNOTATIONSDIRECTORIES, true);ref...
//18个section,对应map size = 18,其中15个都一一对应一个SectionDiffAlgorithm类进行差分,在Algorithm子类的getTocSection方法中返回对应的Section
public final Section[] sections = {
header, stringIds, typeIds, protoIds, fieldIds, methodIds, classDefs, mapList,
typeLists, annotationSetRefLists, annotationSets, classDatas, codes, stringDatas,
debugInfos, annotations, encodedArrays, annotationsDirectories
};
public int checksum;
public byte[] signature;
public int fileSize;
public int linkSize;
public int linkOff;
public int dataSize;
public int dataOff;
}
public void readFrom(Dex dex) throws IOException {
readHeader(dex.openSection(header));
// special case, since mapList.byteCount is available only after
// computeSizesFromOffsets() was invoked, so here we can't use
// dex.openSection(mapList) to get dex section. Or
// an {@code java.nio.BufferUnderflowException} will be thrown.
readMap(dex.openSection(mapList.off));
computeSizesFromOffsets();
}
//Dex
public Section openSection(TableOfContents.Section tocSec) {
int position = tocSec.off;
if (position < 0 || position >= data.capacity()) {
throw new IllegalArgumentException(
"position=" + position + " length=" + data.capacity()
);
}
ByteBuffer sectionData = data.duplicate();
sectionData.order(ByteOrder.LITTLE_ENDIAN); // necessary?
sectionData.position(position);//设定指定section的起始地址position和大小byteCount
sectionData.limit(position + tocSec.byteCount);
return new Section("section", sectionData);
}
private void readHeader(Dex.Section headerIn) throws UnsupportedEncodingException {
byte[] magic = headerIn.readByteArray(8);
int apiTarget = DexFormat.magicToApi(magic);
if (apiTarget != DexFormat.API_NO_EXTENDED_OPCODES) {
throw new DexException("Unexpected magic: " + Arrays.toString(magic));
}
checksum = headerIn.readInt();
signature = headerIn.readByteArray(20);
fileSize = headerIn.readInt();
......
stringIds.size = headerIn.readInt();
stringIds.off = headerIn.readInt();
typeIds.size = headerIn.readInt();
typeIds.off = headerIn.readInt();
protoIds.size = headerIn.readInt();
protoIds.off = headerIn.readInt();
......
dataSize = headerIn.readInt();
dataOff = headerIn.readInt();
private void readMap(Dex.Section in) throws IOException {
int mapSize = in.readInt();
Section previous = null;
for (int i = 0; i < mapSize; i++) {
short type = in.readShort();
in.readShort(); // unused
Section section = getSection(type);
int size = in.readInt();
int offset = in.readInt();
if ((section.size != 0 && section.size != size)
|| (section.off != Section.UNDEF_OFFSET && section.off != offset)) {//末尾map中的size和offset应该和header中的size,offset一致
throw new DexException("Unexpected map value for 0x" + Integer.toHexString(type));
}
section.size = size;
section.off = offset;
if (previous != null && previous.off > section.off) {
throw new DexException("Map is unsorted at " + previous + ", " + section);
}
previous = section;
}
header.off = 0;
Arrays.sort(sections);
// Skip header section, since its offset must be zero.
for (int i = 1; i < sections.length; ++i) {
if (sections[i].off == Section.UNDEF_OFFSET) {
sections[i].off = sections[i - 1].off;
}
}
}
public void computeSizesFromOffsets() {
int end = fileSize;
for (int i = sections.length - 1; i >= 0; i--) {//18个Section反向迭代,计算出每个Section的byteCount
Section section = sections[i];
if (section.off == Section.UNDEF_OFFSET) {
continue;
}
if (section.off > end) {
throw new DexException("Map is unsorted at " + section);
}
section.byteCount = end - section.off;
end = section.off;
}
dataOff = header.byteCount
\+ stringIds.byteCount
\+ typeIds.byteCount
\+ protoIds.byteCount
\+ fieldIds.byteCount
\+ methodIds.byteCount
\+ classDefs.byteCount;
dataSize = fileSize - dataOff;
}
public static class Section implements Comparable<Section> {
public final short type;
public boolean isElementFourByteAligned;
public int size = 0;//数组大小
public int off = UNDEF_OFFSET;//起始地址
public int byteCount = 0;//整个Section所占区域大小
public Section(int type, boolean isElementFourByteAligned) {
this.type = (short) type;
this.isElementFourByteAligned = isElementFourByteAligned;
if (type == SECTION_TYPE_HEADER) {
off = 0;
size = 1;
byteCount = SizeOf.HEADER_ITEM;
} else
if (type == SECTION_TYPE_MAPLIST) {
size = 1;
}
}
public static abstract class Item<T> implements Comparable<T> {
public int off;
public Item(int off) {
this.off = off;
}
}
public final class Section extends DexDataBuffer {
private final String name;
private Section(String name, ByteBuffer data) {
super(data);
this.name = name;
}
}
class DexDataBuffer {
public DexDataBuffer(ByteBuffer data) {
this.data = data;
this.data.order(ByteOrder.LITTLE_ENDIAN);
this.dataBound = data.limit();
this.isResizeAllowed = false;
}
}
public Code adjust(Code code) {
int adjustedDebugInfoOffset = adjustDebugInfoItemOffset(code.debugInfoOffset);
short[] adjustedInstructions = adjustInstructions(code.instructions);//main
Code.CatchHandler[] adjustedCatchHandlers = adjustCatchHandlers(code.catchHandlers);
return new Code(
code.off, code.registersSize, code.insSize, code.outsSize,
adjustedDebugInfoOffset, adjustedInstructions, code.tries, adjustedCatchHandlers
);
}
private short[] adjustInstructions(short[] instructions) {
if (instructions == null || instructions.length == 0) {
return instructions;
}
InstructionTransformer insTrans = new InstructionTransformer(this);
return insTrans.transform(instructions);
}
public short[] transform(short[] encodedInstructions) throws DexException {
ShortArrayCodeOutput out = new ShortArrayCodeOutput(encodedInstructions.length);//因为每个指令的长度是u1 也就是0~255
InstructionPromoter ipmo = new InstructionPromoter();//地址转换,应对类似const-string 到const-string/jumbo的地址扩展情况
InstructionWriter iw = new InstructionWriter(out, ipmo);
InstructionReader ir = new InstructionReader(new ShortArrayCodeInput(encodedInstructions));
try {
// First visit, we collect mappings from original target address to promoted target address.
ir.accept(new InstructionTransformVisitor(ipmo));//main
// Then do the real transformation work.
ir.accept(new InstructionTransformVisitor(iw));//main
} catch (EOFException e) {
throw new DexException(e);
}
return out.getArray();
}
private final ShortArrayCodeInput codeIn;
public InstructionReader(ShortArrayCodeInput in) {
this.codeIn = in;
}
public void accept(InstructionVisitor iv) throws EOFException {
codeIn.reset();
while (codeIn.hasMore()) {
int currentAddress = codeIn.cursor();
int opcodeUnit = codeIn.read();
int opcodeForSwitch = Opcodes.extractOpcodeFromUnit(opcodeUnit);//main
switch (opcodeForSwitch) {
case Opcodes.SPECIAL_FORMAT: {
iv.visitZeroRegisterInsn(currentAddress, opcodeUnit, 0, InstructionCodec.INDEX_TYPE_NONE, 0, 0L);
break;
}
case Opcodes.GOTO: {
int opcode = InstructionCodec.byte0(opcodeUnit);//main
int target = (byte) InstructionCodec.byte1(opcodeUnit); // sign-extend
iv.visitZeroRegisterInsn(currentAddress, opcode, 0, InstructionCodec.INDEX_TYPE_NONE, currentAddress + target, 0L);
break;
}
......
case Opcodes.INVOKE_STATIC:
case Opcodes.INVOKE_INTERFACE: {
int opcode = InstructionCodec.byte0(opcodeUnit);
int e = InstructionCodec.nibble2(opcodeUnit);
int registerCount = InstructionCodec.nibble3(opcodeUnit);
int index = codeIn.read();
int abcd = codeIn.read();
int a = InstructionCodec.nibble0(abcd);
int b = InstructionCodec.nibble1(abcd);
int c = InstructionCodec.nibble2(abcd);
int d = InstructionCodec.nibble3(abcd);
int indexType = InstructionCodec.getInstructionIndexType(opcode);
switch (registerCount) {
case 0: {
iv.visitZeroRegisterInsn(currentAddress, opcode, index, indexType, 0, 0L);
break;
}
case 1: {
iv.visitOneRegisterInsn(currentAddress, opcode, index, indexType, 0, 0L, a);
break;
}
case 2: {
iv.visitTwoRegisterInsn(currentAddress, opcode, index, indexType, 0, 0L, a, b);
break;
}
case 3: {
iv.visitThreeRegisterInsn(currentAddress, opcode, index, indexType, 0, 0L, a, b, c);
break;
}
case 4: {
iv.visitFourRegisterInsn(currentAddress, opcode, index, indexType, 0, 0L, a, b, c, d);
break;
}
case 5: {
iv.visitFiveRegisterInsn(currentAddress, opcode, index, indexType, 0, 0L, a, b, c, d, e);
break;
}
default: {
throw new DexException("bogus registerCount: " + Hex.uNibble(registerCount));
}
classDiagram
class InstructionVisitor {
+visitZeroRegisterInsn(int currentAddress, int opcode, int index, int indexType, int target, long literal) void
+visitOneRegisterInsn(int currentAddress, int opcode, int index, int indexType, int target, long literal, int a) void
}
InstructionVisitor<|--InstructionTransformVisitor
InstructionTransformVisitor--*InstructionPromoter:delegate
InstructionVisitor<|--InstructionPromoter
InstructionVisitor<|--InstructionWriter
InstructionTransformVisitor--*InstructionWriter:delegate
private final InstructionVisitor prevIv;
public InstructionVisitor(InstructionVisitor iv) {
this.prevIv = iv;
}
public void visitZeroRegisterInsn(int currentAddress, int opcode, int index, int indexType, int target, long literal) {
if (prevIv != null) {
prevIv.visitZeroRegisterInsn(currentAddress, opcode, index, indexType, target, literal);
}
}
public void visitOneRegisterInsn(int currentAddress, int opcode, int index, int indexType, int target, long literal, int a) {
if (prevIv != null) {
prevIv.visitOneRegisterInsn(currentAddress, opcode, index, indexType, target, literal, a);
}
}
@Override
public void visitZeroRegisterInsn(int currentAddress, int opcode, int index, int indexType, int target, long literal) {
int mappedIndex = transformIndexIfNeeded(index, indexType);
super.visitZeroRegisterInsn(currentAddress, opcode, mappedIndex, indexType, target, literal);
}
@Override
public void visitOneRegisterInsn(int currentAddress, int opcode, int index, int indexType, int target, long literal, int a) {
int mappedIndex = transformIndexIfNeeded(index, indexType);//这里index为old index, mappedIndex为new dex中的 index
super.visitOneRegisterInsn(currentAddress, opcode, mappedIndex, indexType, target, literal, a);
}
public void visitZeroRegisterInsn(int currentAddress, int opcode, int index, int indexType, int target, long literal) {
}
public void visitOneRegisterInsn(int currentAddress, int opcode, int index, int indexType, int target, long literal, int a) {
switch (opcode) {
case Opcodes.CONST_STRING: {
if (this.hasPromoter) {
if (index > 0xFFFF) {
codeOut.write(
InstructionCodec.codeUnit(Opcodes.CONST_STRING_JUMBO, a),
InstructionCodec.unit0(index),
InstructionCodec.unit1(index)
);
} else {
short indexUnit = (short) index;
codeOut.write(InstructionCodec.codeUnit(opcode, a), indexUnit);
//写入到out字节码中的字符串 index 是new dex中的索引,这里把旧的 Code 字节码强行改变,所以在做对比的时候这两个的字节码是完全一样的,这个方法也不会将该方法打入补丁包中,这个过程对补丁包的大小影响很大,能减少好多原本没有改变的方法打入补丁包中
}