Tinker原理解析

Tinker原理解析

补丁包生成原理

Tinker打补丁主要为两个步骤:1.通过执行gradke task tinkerPatchDebug(Release)生成补丁包。2.通过tinkerInstaller将补丁包进行合成。

我们先来分析第一步:tinkerPatchDebug task包括两个步骤,1.执行debug生成修复代码之后的apk。2.执行tinkerPatch生成补丁包。

tinkerPatch相关流程

1-7:构建gradle tinkerPatch task

8-9:将新旧apk对比构建补丁包:将两个apk的内容读取到流中,对比是否有新添加的class文件

10-15:将apk以流的形式读取,进行对比

16-19:patch补丁包生成

20-23:将patch补丁和原来的apk合成新的修复过后的apk

可以看出,这里的核心在于16-23,就是如何生成补丁包进行补丁包的修复。下面我会主要从这两两个角度进行原理剖析。

在具体讲tinker是如何实现这部分模块之前先介绍一下准备知识。

知识准备
生成dex

编写一个Hello.java文件:

1
2
3
4
5
public class Hello {
public static void main(String[] args){
System.out.println("hello dex!");
}
}

生成Hello.class文件:

生成class文件

然后把java文件和class文件放到sdk->build-tools->23.0.1中,生成dex文件:

生成dex出错

会发现放在23.0.1下边出错,我把它放到28.0.2下边就正常了,可能低版本有点问题。最后会有3个文件:

java class dex文件

dex的内部结构如下

dex文件结构(1)

dex文件结构(2)

下边这张图更直观一点:

dex文件结构

dex是使用LEB128编码的,用010Editor可以看dex文件。可以看到010Editor解析出来的数据基本跟dex文件结构对应,link_data在010Editor中表示为map_list

dex大致结构

这里我们主要对dex_header和map_list做大致的分析:

dex_header

magic:魔法值DEX_FILE_MAGIC。用来标识是dex文件,java的魔法值是CAFFEEBABE。

其他相关字段含义:

dex_header字段含义

注意,从下面的图中可以看到,dex_header中的string_ids_size,string_ids_off这种成对出现的分别代表大小和偏移量,对应着下面出现的其他表的数据。

dex具体结构

dex_map_list

其对应的结构如下:

dex_map_list结构

每个map_item_list包含一个enum表示类型,一个ushort未使用的成员,一个uint表示当前类型的个数,一个uint表示当前类型偏移量。

首先是TYPE_HEADER_ITEM类型,包含1个header(size=1),偏移量为0

然后是TYPE_STRING_ID_ITEM,包含14个string_id_item(size=14),偏移量为112.跟前面dex_header中的数据是一样的。

以此类推。通过map_list,可以将一个完整的dex文件划分成固定的区域,且知道每个区域的开始,以及该区域对应的数据格式的个数。

一种可能的tinkerPatch算法

在tinker中,我们是生成了新旧apk,其内部是通过对比差异来得出一个差量包,其对比的文件就是dex为单位。

在对比前,有两个dex:1.old dex.2.new dex。对比生成一个patch文件,然后patch文件可以和old dex通过算法合成一个新的dex。

那么patch + old dex->new dex的步骤就包括几个部分:

1.header不作处理,因为可以根据其他数据生成;

2.map list。获取各个区域的offset偏移值

3.知道了各个区域的offset后,在生成new dex的时候,定位各个区域的开始和结束为止,往各个区域写数据即可。

假设针对一个区域的diff,假设有个string 区域,用于存储字符串:

old dex区域的字符串:Hello World

new dex区域的字符串:Hello dex

可以看出该区域,我们删除了World,增加了dex,那么patch中针对该区域可以作如下记录:“del World,add dex”(实际转换成对应的编码)那么在patch的时候可以很容易的计算出new dex最后的结果为:Hello dex。这样就是一个大致的diff和patch算法。

tinker就是基于上面这样的思想来实现的,当然实际情况比这个要复杂的多。

生成差分包的原理

从前面的流程图可以看到补丁包的生成过程是在16-19。

15的代码如下:

以下代码为了方便理解都有一定的删减。

1
2
3
4
private void diffDexPairAndFillRelatedInfo(File oldDexFile, File newDexFile, RelatedInfo relatedInfo) {
DexPatchGenerator dexPatchGen = new DexPatchGenerator(oldDexFile, newDexFile);
dexPatchGen.executeAndSaveTo(dexDiffOut);

这里有两个步骤。

1:DexPatchGenerator dexPatchGen = new DexPatchGenerator(oldDexFile, newDexFile);

2:dexPatchGen.executeAndSaveTo(dexDiffOut);

接下来我们先看1:

1
2
3
4
5
public class DexPatchGenerator {
public DexPatchGenerator(File oldDexFile, File newDexFile) throws IOException {
this(new Dex(oldDexFile), new Dex(newDexFile));
}

1
2
3
4
5
6
7
8
9
10
11
public final class Dex {
public Dex(File file) throws IOException {
loadFrom(inputStream, (int) entry.getSize());

private void loadFrom(InputStream in, int initSize) throws IOException {
byte[] rawData = FileUtils.readStream(in, initSize);
this.data = ByteBuffer.wrap(rawData);
this.data.order(ByteOrder.LITTLE_ENDIAN);
this.tableOfContents.readFrom(this);
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public final class TableOfContents {
public void readFrom(Dex dex) throws IOException {
readHeader(dex.openSection(header));
readMap(dex.openSection(mapList.off));
computeSizesFromOffsets();
}
//这里就是将dex中的字段读取出来保存起来
private void readHeader(Dex.Section headerIn) throws UnsupportedEncodingException {
byte[] magic = headerIn.readByteArray(8);
checksum = headerIn.readInt();
signature = headerIn.readByteArray(20);
fileSize = headerIn.readInt();
int headerSize = headerIn.readInt();
int endianTag = headerIn.readInt();
linkSize = headerIn.readInt();
linkOff = headerIn.readInt();
mapList.off = 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();
section.size = size;
section.off = offset;
previous = section;
}
header.off = 0;
}

这里的核心就在于readHeader和readMap,在这两个步骤完成之后就完成了dex文件到dex对象的初始化。

在读取header的时候,已经读取了除了map list区域的offset,并存储在mapList.off中。所以readMap中传入的是这个参数,开始读取map list的数据。首先读取的是map_list_item的个数,然后在for循环中便利读取每个map_list_item的实际数据。

依次读取的数据为:type,unused,size,offset,这些都是跟我们前面讲的map_list_item对应的,在Tinker中对应的对象为TableContents.Section对象。

至此,拿到了两个Dex对象,接下来开始做diff操作。

步骤1我们已经分析完了,接下来看步骤2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class DexPatchGenerator {
public DexPatchGenerator(Dex oldDex, Dex newDex) {
this.oldDex = oldDex;
this.newDex = newDex;
//这里初始化15个算法,每个算法代表dex的每个字段,算法的作用就是我们之前说的那种算法,用来计算增加了哪些,需要删除哪些
this.stringDataSectionDiffAlg = new StringDataSectionDiffAlgorithm
this.typeIdSectionDiffAlg = new TypeIdSectionDiffAlgorithm
this.protoIdSectionDiffAlg = new ProtoIdSectionDiffAlgorithm
this.fieldIdSectionDiffAlg = new FieldIdSectionDiffAlgorithm
this.methodIdSectionDiffAlg = new MethodIdSectionDiffAlgorithm
···(省略)
this.codeSectionDiffAlg = new CodeSectionDiffAlgorithm(
oldDex, newDex,
oldToNewIndexMap,
oldToPatchedIndexMap,
newToPatchedIndexMap,
selfIndexMapForSkip
);

public void executeAndSaveTo(File file) throws IOException {
OutputStream os = null;
os = new BufferedOutputStream(new FileOutputStream(file));
executeAndSaveTo(os);
}
public void executeAndSaveTo(OutputStream out) throws IOException {
//1.执行execute方法
this.stringDataSectionDiffAlg.execute();
//2.执行simulatePatchOperation方法
this.stringDataSectionDiffAlg.simulatePatchOperation(this.patchedStringDataItemsOffset);

this.typeIdSectionDiffAlg.execute();
this.typeIdSectionDiffAlg.simulatePatchOperation(this.patchedTypeIdsOffset);
this.typeListSectionDiffAlg.execute();
this.typeListSectionDiffAlg.simulatePatchOperation(this.patchedTypeListsOffset);
...

// Finally, write results to patch file.
writeResultToStream(out);


这里初始化每个算法之后主要执行3个操作,一个是execute方法,另一个是simulatePatchOperation方法,最后是执行writeResultToStream方法。这里先看execute方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
public abstract class DexSectionDiffAlgorithm<T extends Comparable<T>> {
public void execute() {
this.patchOperationList.clear();
//1.拿到oldDex和newDex的itemList,进行调整排序
this.adjustedOldIndexedItemsWithOrigOrder = collectSectionItems(this.oldDex, true);
this.oldItemCount = this.adjustedOldIndexedItemsWithOrigOrder.length;

AbstractMap.SimpleEntry<Integer, T>[] adjustedOldIndexedItems = new AbstractMap.SimpleEntry[this.oldItemCount];
System.arraycopy(this.adjustedOldIndexedItemsWithOrigOrder, 0, adjustedOldIndexedItems, 0, this.oldItemCount);
Arrays.sort(adjustedOldIndexedItems, this.comparatorForItemDiff);

AbstractMap.SimpleEntry<Integer, T>[] adjustedNewIndexedItems = collectSectionItems(this.newDex, false);
this.newItemCount = adjustedNewIndexedItems.length;
Arrays.sort(adjustedNewIndexedItems, this.comparatorForItemDiff);

int oldCursor = 0;
int newCursor = 0;
//2.遍历,对比,收集patch操作
while (oldCursor < this.oldItemCount || newCursor < this.newItemCount) {
if (oldCursor >= this.oldItemCount) {
// rest item are all newItem.
while (newCursor < this.newItemCount) {
//剩下的都是newIten,做ADD操作
AbstractMap.SimpleEntry<Integer, T> newIndexedItem = adjustedNewIndexedItems[newCursor++];
this.patchOperationList.add(new PatchOperation<>(PatchOperation.OP_ADD, newIndexedItem.getKey(), newIndexedItem.getValue()));
}
} else
if (newCursor >= newItemCount) {
// rest item are all oldItem.
while (oldCursor < oldItemCount) {
//剩下的都是oldItem,做DEL操作
AbstractMap.SimpleEntry<Integer, T> oldIndexedItem = adjustedOldIndexedItems[oldCursor++];
int deletedIndex = oldIndexedItem.getKey();
int deletedOffset = getItemOffsetOrIndex(deletedIndex, oldIndexedItem.getValue());
this.patchOperationList.add(new PatchOperation<T>(PatchOperation.OP_DEL, deletedIndex));
markDeletedIndexOrOffset(this.oldToPatchedIndexMap, deletedIndex, deletedOffset);
}
} else {
AbstractMap.SimpleEntry<Integer, T> oldIndexedItem = adjustedOldIndexedItems[oldCursor];
AbstractMap.SimpleEntry<Integer, T> newIndexedItem = adjustedNewIndexedItems[newCursor];
int cmpRes = oldIndexedItem.getValue().compareTo(newIndexedItem.getValue());
if (cmpRes < 0) {
int deletedIndex = oldIndexedItem.getKey();
int deletedOffset = getItemOffsetOrIndex(deletedIndex, oldIndexedItem.getValue());
this.patchOperationList.add(new PatchOperation<T>(PatchOperation.OP_DEL, deletedIndex));
markDeletedIndexOrOffset(this.oldToPatchedIndexMap, deletedIndex, deletedOffset);
++oldCursor;
} else
if (cmpRes > 0) {
this.patchOperationList.add(new PatchOperation<>(PatchOperation.OP_ADD, newIndexedItem.getKey(), newIndexedItem.getValue()));
++newCursor;
} else {
int oldIndex = oldIndexedItem.getKey();
int newIndex = newIndexedItem.getKey();
int oldOffset = getItemOffsetOrIndex(oldIndexedItem.getKey(), oldIndexedItem.getValue());
int newOffset = getItemOffsetOrIndex(newIndexedItem.getKey(), newIndexedItem.getValue());

if (oldIndex != newIndex) {
this.oldIndexToNewIndexMap.put(oldIndex, newIndex);
}

if (oldOffset != newOffset) {
this.oldOffsetToNewOffsetMap.put(oldOffset, newOffset);
}

++oldCursor;
++newCursor;
}
}
}
...
}
}

这边首先拿到oldDex和newDex做调整,排序。然后开始遍历获取oldItem和newItem,对比其value:

如果<0,则认为该old item被删除,记录为PatchOperation.OP_DEL,并将该oldItem index记录到PatchOperation对象,加入到patchOperationList中。

如果>0,则认为该newItem是新增的,记录为PatchOperation.OP_ADD,并记录该newItem index和value到PatchOperation对象,加入到patchOperationList中。

如果=0,不会生成PatchOperation。

(这里看注释是<0则删除,>0是增加,但是是为什么啊?)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public void execute() {
// So far all diff works are done. Then we perform some optimize works.
// detail: {OP_DEL idx} followed by {OP_ADD the_same_idx newItem}
// will be replaced by {OP_REPLACE idx newItem}
Collections.sort(this.patchOperationList, comparatorForPatchOperationOpt);

Iterator<PatchOperation<T>> patchOperationIt = this.patchOperationList.iterator();
PatchOperation<T> prevPatchOperation = null;
while (patchOperationIt.hasNext()) {
PatchOperation<T> patchOperation = patchOperationIt.next();
if (prevPatchOperation != null
&& prevPatchOperation.op == PatchOperation.OP_DEL
&& patchOperation.op == PatchOperation.OP_ADD
) {
if (prevPatchOperation.index == patchOperation.index) {
prevPatchOperation.op = PatchOperation.OP_REPLACE;
prevPatchOperation.newItem = patchOperation.newItem;
patchOperationIt.remove();
prevPatchOperation = null;
} else {
prevPatchOperation = patchOperation;
}
} else {
prevPatchOperation = patchOperation;
}
}

// Finally we record some information for the final calculations.
patchOperationIt = this.patchOperationList.iterator();
while (patchOperationIt.hasNext()) {
PatchOperation<T> patchOperation = patchOperationIt.next();
switch (patchOperation.op) {
case PatchOperation.OP_DEL: {
indexToDelOperationMap.put(patchOperation.index, patchOperation);
break;
}
case PatchOperation.OP_ADD: {
indexToAddOperationMap.put(patchOperation.index, patchOperation);
break;
}
case PatchOperation.OP_REPLACE: {
indexToReplaceOperationMap.put(patchOperation.index, patchOperation);
break;
}
}
}
}

上面得到了一个patchOperationList对象之后,先对其进行排序。

然后执行一些优化操作,同一个index的item的OP_DEL操作后边紧跟着OP_ADD操作会被替换成OP_REPLACE。

然后最后生成3个Map:indexToDelOperationMap,indexToAddOperationMap,indexToReplaceOperationMap.

至此,完成了需要删除,添加,替换的item收集。

接着看simulatePatchOperation方法:

参数传入的偏移量为data区域的偏移量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
public void simulatePatchOperation(int baseOffset) {
boolean isNeedToMakeAlign = getTocSection(this.oldDex).isElementFourByteAligned;
int oldIndex = 0;
int patchedIndex = 0;
int patchedOffset = baseOffset;
while (oldIndex < this.oldItemCount || patchedIndex < this.newItemCount) {
if (this.indexToAddOperationMap.containsKey(patchedIndex)) {
PatchOperation<T> patchOperation = this.indexToAddOperationMap.get(patchedIndex);
if (isNeedToMakeAlign) {
patchedOffset = SizeOf.roundToTimesOfFour(patchedOffset);
}
T newItem = patchOperation.newItem;
int itemSize = getItemSize(newItem);
++patchedIndex;
patchedOffset += itemSize;
} else
if (this.indexToReplaceOperationMap.containsKey(patchedIndex)) {
PatchOperation<T> patchOperation = this.indexToReplaceOperationMap.get(patchedIndex);
T newItem = patchOperation.newItem;
int itemSize = getItemSize(newItem);
++patchedIndex;
patchedOffset += itemSize;
} else
if (this.indexToDelOperationMap.containsKey(oldIndex)) {
++oldIndex;
} else
if (this.indexToReplaceOperationMap.containsKey(oldIndex)) {
++oldIndex;
} else
if (oldIndex < this.oldItemCount) {
T oldItem = this.adjustedOldIndexedItemsWithOrigOrder[oldIndex].getValue();
int itemSize = getItemSize(oldItem);

int oldOffset = getItemOffsetOrIndex(oldIndex, oldItem);
int newIndex = oldIndex;
if (this.oldIndexToNewIndexMap.containsKey(oldIndex)) {
newIndex = this.oldIndexToNewIndexMap.get(oldIndex);
}

int newOffset = oldOffset;
if (this.oldOffsetToNewOffsetMap.containsKey(oldOffset)) {
newOffset = this.oldOffsetToNewOffsetMap.get(oldOffset);
}
++oldIndex;
++patchedIndex;
patchedOffset += itemSize;
}
}

this.patchedSectionSize = SizeOf.roundToTimesOfFour(patchedOffset - baseOffset);
}

这里遍历oldIndex和patchIndex,分别在indexToAddOperationMap,indexToReplaceOperationMap,indexToDelOperationMap中查找。

最后生成patchedSectionSize,由patchedOffset - baseOffset计算得到。

这里在3个地方会执行patchedOffset += itemSize;

分别是if (this.indexToAddOperationMap.containsKey(patchedIndex)),if (this.indexToReplaceOperationMap.containsKey(patchedIndex)),if (oldIndex < this.oldItemCount)从这几个判断条件可以看出,首先patchedSectionSize对应newDex的这个区域的size,所以,需要ADD的itemIndex,需要被替代的itemIndex,以及OLD ITEMS中没有被删除和替代的item,这三个加起来就是newIndex的itemList.

经过上面两个操作,得到PatchOperationList和对应区域的sectionSize,执行完所有的算法,就会得到针对每个算法的PatchOperationList和每个区域的sectionSize;每个区域的sectionSize再根据每个区域对应的字节单位长度计算得到每个区域的offset。

这里看最后一个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
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();
}

这里主要写入dex中的header的相关字段,然后写入所有的跟maplist各个区域相关的offset。然后调用各个区域对应算法写入信息,最后生成patch文件。这里以stringDataSectionDiffAlg为例,看下其源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
private <T extends Comparable<T>> void writePatchOperations(
DexDataBuffer buffer, List<PatchOperation<T>> patchOperationList
) {
List<Integer> delOpIndexList = new ArrayList<>(patchOperationList.size());
List<Integer> addOpIndexList = new ArrayList<>(patchOperationList.size());
List<Integer> replaceOpIndexList = new ArrayList<>(patchOperationList.size());
List<T> newItemList = new ArrayList<>(patchOperationList.size());

for (PatchOperation<T> patchOperation : patchOperationList) {
switch (patchOperation.op) {
case PatchOperation.OP_DEL: {
delOpIndexList.add(patchOperation.index);
break;
}
case PatchOperation.OP_ADD: {
addOpIndexList.add(patchOperation.index);
newItemList.add(patchOperation.newItem);
break;
}
case PatchOperation.OP_REPLACE: {
replaceOpIndexList.add(patchOperation.index);
newItemList.add(patchOperation.newItem);
break;
}
}
}

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;
}
···

这里将我们的patchOperationList转化为3个OpIndexList,分别对应DEL,ADD,REPLACE,并且将所有的item存入newItemList.

然后依次写入del操作的个数,每个del的index;add操作的个数,每个add的index;replace操作的个数,每个replace的index。最后依次写入newItemList.

总结一下上面整个的算法流程就是一个生成patch的过程:生成一个newDex,包含各个区域的offset,可以将newDex定位到各区域的起点。包含newDex各个区域的item的删除的索引,新增的索引和值,替换的索引和值。

那么Patch的逻辑可能是这样的:

根据各个区域的offset,确定各个区域的起点;读取oldDex各个区域的items,然后根据patch去除oldDex中需要删除的和需要替换的item,再加上新增的item和替换的item即可组成newDex该区域的items。

即,newDex的区域应该为:

oldItems - del -replace + addItems + replaceItems.这就是第二部分的内容。

差分包和原dex合成新的dex

上面说了补丁包的生成,接下来说补丁包修复的原理分析。也就是流程图中的20-23:

1
2
3
4
5
public class DexPatchApplier {
public DexPatchApplier(File oldDexIn, File patchFileIn) throws IOException {
this(new Dex(oldDexIn), new DexPatchFile(patchFileIn));
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public final class DexPatchFile {
public DexPatchFile(InputStream is) throws IOException {
this.buffer = new DexDataBuffer(ByteBuffer.wrap(FileUtils.readStream(is)));
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);
}

构造函数中首先oldDex被封装为Dex对象,就是上面分析过的readHeader和readMap。不过patchFile是转化为一个DexPatchFile对象。首先将patch file读取为byte[],然后调用其init方法,init方法首先判断MAGIC和Version,然后对patcheDexSize和各区域进行赋值,最后定位到数据区。

接下来的方法步骤较多,分为几个流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
public DexPatchApplier(
Dex oldDexIn,
DexPatchFile patchFileIn
) {
this.oldDex = oldDexIn;
this.patchFile = patchFileIn;
//patchedDex作为最终输出的Dex对象
this.patchedDex = new Dex(patchFileIn.getPatchedDexSize());
this.oldToPatchedIndexMap = new SparseIndexMap();
}

public void executeAndSaveTo(File file) throws IOException {
OutputStream os = null;
os = new BufferedOutputStream(new FileOutputStream(file));
executeAndSaveTo(os);
}
public void executeAndSaveTo(OutputStream out) throws IOException {
// 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();

1.这里读取patchFile中记录的值给patchedDex的TableOfContent中各种Section赋值,然后根据它们的偏移进行排序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
public void executeAndSaveTo(OutputStream out) throws IOException {
// Secondly, run patch algorithms according to sections' dependencies.
this.stringDataSectionPatchAlg = new StringDataSectionPatchAlgorithm(
patchFile, oldDex, patchedDex, oldToPatchedIndexMap
);
this.typeIdSectionPatchAlg = new TypeIdSectionPatchAlgorithm(
patchFile, oldDex, patchedDex, oldToPatchedIndexMap
);
this.protoIdSectionPatchAlg = new ProtoIdSectionPatchAlgorithm(
patchFile, oldDex, patchedDex, oldToPatchedIndexMap
);
this.fieldIdSectionPatchAlg = new FieldIdSectionPatchAlgorithm(
patchFile, oldDex, patchedDex, oldToPatchedIndexMap
);
this.methodIdSectionPatchAlg = new MethodIdSectionPatchAlgorithm(
patchFile, oldDex, patchedDex, oldToPatchedIndexMap
);
this.classDefSectionPatchAlg = new ClassDefSectionPatchAlgorithm(
patchFile, oldDex, patchedDex, oldToPatchedIndexMap
);
this.typeListSectionPatchAlg = new TypeListSectionPatchAlgorithm(
patchFile, oldDex, patchedDex, oldToPatchedIndexMap
);
this.annotationSetRefListSectionPatchAlg = new AnnotationSetRefListSectionPatchAlgorithm(
patchFile, oldDex, patchedDex, oldToPatchedIndexMap
);
this.annotationSetSectionPatchAlg = new AnnotationSetSectionPatchAlgorithm(
patchFile, oldDex, patchedDex, oldToPatchedIndexMap
);
this.classDataSectionPatchAlg = new ClassDataSectionPatchAlgorithm(
patchFile, oldDex, patchedDex, oldToPatchedIndexMap
);
this.codeSectionPatchAlg = new CodeSectionPatchAlgorithm(
patchFile, oldDex, patchedDex, oldToPatchedIndexMap
);
this.debugInfoSectionPatchAlg = new DebugInfoItemSectionPatchAlgorithm(
patchFile, oldDex, patchedDex, oldToPatchedIndexMap
);
this.annotationSectionPatchAlg = new AnnotationSectionPatchAlgorithm(
patchFile, oldDex, patchedDex, oldToPatchedIndexMap
);
this.encodedArraySectionPatchAlg = new StaticValueSectionPatchAlgorithm(
patchFile, oldDex, patchedDex, oldToPatchedIndexMap
);
this.annotationsDirectorySectionPatchAlg = new AnnotationsDirectorySectionPatchAlgorithm(
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();

2.这里初始化了很多算法,依次调用execute()方法。这里依旧来看stringDataSectionPatchAlg的execute方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public void execute() {
final int deletedItemCount = patchFile.getBuffer().readUleb128();
final int[] deletedIndices = readDeltaIndiciesOrOffsets(deletedItemCount);

final int addedItemCount = patchFile.getBuffer().readUleb128();
final int[] addedIndices = readDeltaIndiciesOrOffsets(addedItemCount);

final int replacedItemCount = patchFile.getBuffer().readUleb128();
final int[] replacedIndices = readDeltaIndiciesOrOffsets(replacedItemCount);

final TableOfContents.Section tocSec = getTocSection(this.oldDex);
Dex.Section oldSection = null;

int oldItemCount = 0;
if (tocSec.exists()) {
oldSection = this.oldDex.openSection(tocSec);
oldItemCount = tocSec.size;
}

// Now rest data are added and replaced items arranged in the order of
// added indices and replaced indices.
doFullPatch(
oldSection, oldItemCount, deletedIndices, addedIndices, replacedIndices
);
}

这里1.读取del的数量,将index存储在int数组中。2.读取add的数量,将index存储在int数组中。3.读取replace的数量,将index存储在int数组中。

然后就拿着deletedIndices,addedIndices,replacedIndices,oldSection, oldItemCount就能进行完整的patch了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
private void doFullPatch(
Dex.Section oldSection,
int oldItemCount,
int[] deletedIndices,
int[] addedIndices,
int[] replacedIndices
) {
int deletedItemCount = deletedIndices.length;
int addedItemCount = addedIndices.length;
int replacedItemCount = replacedIndices.length;
int newItemCount = oldItemCount + addedItemCount - deletedItemCount;

int deletedItemCounter = 0;
int addActionCursor = 0;
int replaceActionCursor = 0;

int oldIndex = 0;
int patchedIndex = 0;
while (oldIndex < oldItemCount || patchedIndex < newItemCount) {
if (addActionCursor < addedItemCount && addedIndices[addActionCursor] == patchedIndex) {
T addedItem = nextItem(patchFile.getBuffer());
int patchedOffset = writePatchedItem(addedItem);
++addActionCursor;
++patchedIndex;
} else
if (replaceActionCursor < replacedItemCount && replacedIndices[replaceActionCursor] == patchedIndex) {
T replacedItem = nextItem(patchFile.getBuffer());
int patchedOffset = writePatchedItem(replacedItem);
++replaceActionCursor;
++patchedIndex;
} else
if (Arrays.binarySearch(deletedIndices, oldIndex) >= 0) {
T skippedOldItem = nextItem(oldSection); // skip old item.
markDeletedIndexOrOffset(
oldToPatchedIndexMap,
oldIndex,
getItemOffsetOrIndex(oldIndex, skippedOldItem)
);
++oldIndex;
++deletedItemCounter;
} else
if (Arrays.binarySearch(replacedIndices, oldIndex) >= 0) {
T skippedOldItem = nextItem(oldSection); // skip old item.
markDeletedIndexOrOffset(
oldToPatchedIndexMap,
oldIndex,
getItemOffsetOrIndex(oldIndex, skippedOldItem)
);
++oldIndex;
} else
if (oldIndex < oldItemCount) {
T oldItem = adjustItem(this.oldToPatchedIndexMap, nextItem(oldSection));

int patchedOffset = writePatchedItem(oldItem);

updateIndexOrOffset(
this.oldToPatchedIndexMap,
oldIndex,
getItemOffsetOrIndex(oldIndex, oldItem),
patchedIndex,
patchedOffset
);

++oldIndex;
++patchedIndex;
}
}

这个方法就是往patchedDex的stringData区写数据,写入的数据就包含:1.新增的数据。2.替代的数据。3.oldDex中除去新增和被替代的数据。

所以计算得到的int newItemCount = oldItemCount + addedItemCount - deletedItemCount;然后开始遍历,写入对应的item。对应的Item写入代码就包含:1.判断该patchIndex是否包含在addedIndices中,如果包含则写入;2.判断是否在replacedIndices中,如果包含则写入;3.如果在oldIndex被delete或者replace,直接跳过;4.如果是oldIndex非delete和replace的,也就是和newDex中items相同的部分。

1.2.4三个部分即可组成完整的newDex的该区域。这样就完成了stringData区域的patch算法,其他的14个算法的execute代码是相同的,都会完成各个部分的patch算法。

1
2
3
4
5
6
7
8
9
10
11
12
13
public void executeAndSaveTo(OutputStream out) throws IOException {
// 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);
}

最后一个步骤就是写header,mapList的数据。这样就完成了完整的dex的恢复,最后将内存中的所有数据写到文件中。

补丁合成的原理

前面我们已经生成了补丁包,接着通过服务器下发或者本地放入sd卡中,通过应用启动之后进行加载合成。这一节中就来讲这个具体的过程。

程序启动时会加载默认的Application类,将导致无法对它进行补丁修复。Tinker中为了避免这个问题,是通过将原来的Application类隔离起来,将Application类以及它的继承类的所有代码移至自己的ApplicationLike继承类中。并且把attachBaseContext方法实现单独移动到onBaseContextAttached中,这里的onBaseContextAttached并不是Application的生命周期,这个方法是在自己生成的TinkerApplication中的attachBaseContext方法中调用的,这里这样处理的目的就是为了将Application类隔离起来。

1
2
3
4
5
6
7
8
9
10
11
12
@SuppressWarnings("unused")
@DefaultLifeCycle(application = "tinker.sample.android.app.SampleApplication",
flags = ShareConstants.TINKER_ENABLE_ALL,
loadVerifyFlag = false)
public class SampleApplicationLike extends DefaultApplicationLike {
private static final String TAG = "Tinker.SampleApplicationLike";

public SampleApplicationLike(Application application, int tinkerFlags, boolean tinkerLoadVerifyFlag,
long applicationStartElapsedTime, long applicationStartMillisTime, Intent tinkerResultIntent) {
super(application, tinkerFlags, tinkerLoadVerifyFlag, applicationStartElapsedTime, applicationStartMillisTime, tinkerResultIntent);
}

这里通过注解生成我们真正的application类:SampleApplication.而DefaultApplicationLike是我们的代理类。这里看TinkerApplication类的实现可以看出这里用的是代理模式。

这里重点看TinkerApplication中的方法,

1
2
3
4
5
6
7
8
9
10
11
12
private void onBaseContextAttached(Context base) {
loadTinker();
applicationLike.onBaseContextAttached(base);
}
private void loadTinker() {
//reflect tinker loader, because loaderClass may be define by user!
Class<?> tinkerLoadClass = Class.forName(loaderClassName, false, getClassLoader());

Method loadMethod = tinkerLoadClass.getMethod(TINKER_LOADER_METHOD, TinkerApplication.class);
Constructor<?> constructor = tinkerLoadClass.getConstructor();
tinkerResultIntent = (Intent) loadMethod.invoke(constructor.newInstance(), this);
}

这里通过传入的loader类,通过反射调用tryLoad方法,这里的loader类默认是TinkerLoader,这里内部会调用到tryLoadPatchFilesInternal,会判断是否开启dex修复,lib修复,资源修复,然后再分别调用TinkerDexLoader,TinkerResourceLoader等方法进行对应的修复,这里我们看dex修复。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class SampleApplication extends TinkerApplication {

public SampleApplication() {
super(7, "tinker.sample.android.app.SampleApplicationLike", "com.tencent.tinker.loader.TinkerLoader", false);
}
}
public class TinkerLoader extends AbstractTinkerLoader {
@Override
public Intent tryLoad(TinkerApplication app) {
tryLoadPatchFilesInternal(app, resultIntent);
return resultIntent;
}
private void tryLoadPatchFilesInternal(TinkerApplication app, Intent resultIntent) {
...
boolean dexCheck = TinkerDexLoader.checkComplete(patchVersionDirectory, securityCheck, oatDex, resultIntent);
boolean loadTinkerJars = TinkerDexLoader.loadTinkerJars(app, patchVersionDirectory, oatDex, resultIntent, isSystemOTA);


第一步,先调用checkComplete方法从assets/dex_meta.txt中检查记录的dex信息,检查对应的dex文件是否存在,并保存到loadDexList中。

第二部调用TinkerDexLoader.loadTinkerJars方法会首先根据是否支持dalvik或者art做对应的处理,最后调用installDexes方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class SystemClassLoaderAdder {
@SuppressLint("NewApi")
public static void installDexes(Application application, PathClassLoader loader, File dexOptDir, List<File> files)
throws Throwable {
Log.i(TAG, "installDexes dexOptDir: " + dexOptDir.getAbsolutePath() + ", dex size:" + files.size());

if (!files.isEmpty()) {
files = createSortedAdditionalPathEntries(files);
ClassLoader classLoader = loader;
if (Build.VERSION.SDK_INT >= 24 && !checkIsProtectedApp(files)) {
classLoader = AndroidNClassLoader.inject(loader, application);
}
//because in dalvik, if inner class is not the same classloader with it wrapper class.
//it won't fail at dex2opt
if (Build.VERSION.SDK_INT >= 23) {
V23.install(classLoader, files, dexOptDir);
} else if (Build.VERSION.SDK_INT >= 19) {
V19.install(classLoader, files, dexOptDir);
} else if (Build.VERSION.SDK_INT >= 14) {
V14.install(classLoader, files, dexOptDir);
} else {
V4.install(classLoader, files, dexOptDir);
}
//install done
sPatchDexCount = files.size();
Log.i(TAG, "after loaded classloader: " + classLoader + ", dex size:" + sPatchDexCount);
}
}

这里根据不同的系统版本,反射处理dexElements。看一下最新版本的SDK实现,就是V23.install方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private static final class V23 {
private static void install(ClassLoader loader, List<File> additionalClassPathEntries,
File optimizedDirectory)
throws IllegalArgumentException, IllegalAccessException,
NoSuchFieldException, InvocationTargetException, NoSuchMethodException, IOException {
/* The patched class loader is expected to be a descendant of
* dalvik.system.BaseDexClassLoader. We modify its
* dalvik.system.DexPathList pathList field to append additional DEX
* file entries.
*/
Field pathListField = ShareReflectUtil.findField(loader, "pathList");
Object dexPathList = pathListField.get(loader);
ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
ShareReflectUtil.expandFieldArray(dexPathList, "dexElements", makePathElements(dexPathList,
new ArrayList<File>(additionalClassPathEntries), optimizedDirectory,
suppressedExceptions));
}

1.拿到PathClassLoader对象中的pathList字段

2.根据pathList对象找到makePathElements方法,返回Element[]数组

3.拿到pathList对象中原本的dexElements对象

4.将步骤2和步骤3中的Element[]数组合并,将patch相关的dex放在数组前面

5.最后将合并后的数组,设置给pathList。

合成补丁

在sample中是在点击Button的时候调用TinkerInstaller.onReceiveUpgradePatch(Context context, String patchLocation)方法来加载补丁,其流程如下:

tinker打补丁包基础流程

1-5:加载补丁文件

6-9:启动一个JobSeheduler来处理任务

10-13:启动一个AsyncTask在执行补丁包合成的任务

14-18:补丁包合成的核心流程,补丁包的验证,dex修复,lib修复,res修复等等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
protected static boolean tryRecoverDexFiles(Tinker manager, ShareSecurityCheck checker, Context context,
String patchVersionDirectory, File patchFile) {
String dexMeta = checker.getMetaContentMap().get(DEX_META_FILE);

boolean result = patchDexExtractViaDexDiff(context, patchVersionDirectory, dexMeta, patchFile);
return result;
}
private static boolean patchDexExtractViaDexDiff(Context context, String patchVersionDirectory, String meta, final File patchFile) {
String dir = patchVersionDirectory + "/" + DEX_PATH + "/";

if (!extractDexDiffInternals(context, dir, meta, patchFile, TYPE_DEX)) {
TinkerLog.w(TAG, "patch recover, extractDiffInternals fail");
return false;
}

File dexFiles = new File(dir);
File[] files = dexFiles.listFiles();
List<File> legalFiles = new ArrayList<>();
// may have directory in android o
if (files != null) {
for (File file : files) {
if (file.isFile()) {
legalFiles.add(file);
}
}
}

final String optimizeDexDirectory = patchVersionDirectory + "/" + DEX_OPTIMIZE_PATH + "/";
return dexOptimizeDexFiles(context, legalFiles, optimizeDexDirectory, patchFile);

}
private static boolean extractDexDiffInternals(Context context, String dir, String meta, File patchFile, int type) {
//parse
patchList.clear();
ShareDexDiffPatchInfo.parseDexDiffPatchInfo(meta, patchList);

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();

String apkPath = applicationInfo.sourceDir;
apk = new ZipFile(apkPath);
patch = new ZipFile(patchFile);

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;

if (!isVmArt && info.destMd5InDvm.equals("0")) {
TinkerLog.w(TAG, "patch dex %s is only for art, just continue", patchRealPath);
continue;
}
String extractedFileMd5 = isVmArt ? info.destMd5InArt : info.destMd5InDvm;

File extractedFile = new File(dir + info.realName);


ZipEntry patchFileEntry = patch.getEntry(patchRealPath);
ZipEntry rawApkFileEntry = apk.getEntry(patchRealPath);

if (oldDexCrc.equals("0")) {
if (patchFileEntry == null) {
TinkerLog.w(TAG, "patch entry is null. path:" + patchRealPath);
manager.getPatchReporter().onPatchTypeExtractFail(patchFile, extractedFile, info.rawName, type);
return false;
}

//it is a new file, but maybe we need to repack the dex file
if (!extractDexFile(patch, patchFileEntry, extractedFile, info)) {
TinkerLog.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")) {
//check source crc instead of md5 for faster
String rawEntryCrc = String.valueOf(rawApkFileEntry.getCrc());
if (!rawEntryCrc.equals(oldDexCrc)) {
TinkerLog.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;
}

// Small patched dex generating strategy was disabled, we copy full original dex directly now.
//patchDexFile(apk, patch, rawApkFileEntry, null, info, smallPatchInfoFile, extractedFile);
extractDexFile(apk, rawApkFileEntry, extractedFile, info);
} else {
//check source crc instead of md5 for faster
String rawEntryCrc = String.valueOf(rawApkFileEntry.getCrc());
if (!rawEntryCrc.equals(oldDexCrc)) {
TinkerLog.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;
}

patchDexFile(apk, patch, rawApkFileEntry, patchFileEntry, info, extractedFile);

if (!SharePatchFileUtil.verifyDexFileMd5(extractedFile, extractedFileMd5)) {
TinkerLog.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;
}

TinkerLog.w(TAG, "success recover dex file: %s, size: %d, use time: %d",
extractedFile.getPath(), extractedFile.length(), (System.currentTimeMillis() - start));
}
}
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;
}

首先解析meta中的信息,meta中包含了patch中每个dex的相关数据。然后拿到apk和patch的文件路径,根据文件信息进行crc校验和md5校验,然后调用patchDexFile对两个dex文件合并。

1
2
3
4
5
6
7
8
9
10
11
12
13
private static void patchDexFile(
ZipFile baseApk, ZipFile patchPkg, ZipEntry oldDexEntry, ZipEntry patchFileEntry,
ShareDexDiffPatchInfo patchInfo, File patchedDexFile) throws IOException {
InputStream oldDexStream = null;
InputStream patchFileStream = null;
try {
oldDexStream = new BufferedInputStream(baseApk.getInputStream(oldDexEntry));
patchFileStream = (patchFileEntry != null ? new BufferedInputStream(patchPkg.getInputStream(patchFileEntry)) : null);

zos = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(patchedDexFile)));
zos.putNextEntry(new ZipEntry(ShareConstants.DEX_IN_JAR));
new DexPatchApplier(oldDexStream, patchFileStream).executeAndSaveTo(zos);

通过ZipFile拿到文件流,读取本地apk中的dex文件和patch中的dex文件,然后通过executeAndSaveTo合并到最终的patchedDexFile,也就是上面所说的生成差分包的原理。

具体案例分析

我分别编写了2个java文件,Hello.java和World.java

1
2
3
4
5
6
7
8
9
10
public class Hello {
public static void main(String[] args){
System.out.println("hello dex!");
}
}
public class World{
public static void main(String[] args){
System.out.println("nani World");
}
}

Hello.dex和World.dex结构分别如下:

Hello.dex结构

World.dex结构

按照之前的diff算法,得到以下操作:

del 1

del 2

add 1 LWorld;

add 8 World.java

del 10

add 11 nani World

然后根据索引排序:

del 1

add 1 LWorld;

del 2

add 8 World.java

del 10

add 11 nani World

将index一致且DEL和ADD相邻的操作替换为replace:

replace 1 LWorld

del 2

add 8 World.java

del 10

add 11 nani World

在write时,按照DEL,ADD,REPLACE进行分类,并将出现的item放置到newItemList:

del ops:

​ del 2

​ del 10

add ops:

​ add 8

​ add 11

replace ops:

​ 1

newItemList为:

LWorld //replace 1

World.java //add8

naniWorld //add11

然后写入,写入的顺序为:

2 //del size

2

8 //index - lastIndex

2 //add size

8

3 // index - lastIndex

1 //replace size

1

LWorld

World.java

naniWorld

参考

Android 热修复 Tinker 源码分析之DexDiff / DexPatch

Tinker