手记

补丁生成之DexDiff原理简析

前言

微信的热修复框架Tinker已经在国庆节之前开源了,成为了github.com/Tecent下第一个项目,刷爆了各位开发者的朋友圈。作为一个超级APP的HotFix库,Tinker不仅值得我们compile,更值得我们read

原理

Tinker和以往的HotFix库思路不太一样,它更像是APP的增量更新,在服务器端通过差异性算法,计算出新旧dex之间的差异包,推送到客户端,进行合成。传统的差异性算法有BsDiff,而Tinker的牛逼之处就在于它自己基于Dex的文件格式,研发出了DexDiff算法。

补丁生成之DexDiff

补丁生成是在编译阶段进行的,当我们在build.gradle中配置好tinkerOldApkPath后,调用tinkerPatchDebug,补丁就开始生成了。

调用链为:

TinkerPatchSchemaTask.tinkerPatch()
->Runner.gradleRun()
->Runner.run()
->Runner.tinkerPatch()
->ApkDecoder.patch()
……
->DexPatchGenerator.executeAndSaveTo()

executeAndSaveTo()方法是DexDiff算法的真正入口,DexDiff算法的特点在于它深入分析了Dex文件格式,深度利用Dex的格式来减少差异大小。

在了解DexDiff之前,我们需要对Dex文件有个初步了解,Dex的文件格式如下表。参考自这里:Dalvik可执行格式(dex)上

了解了Dex的格式后,让我们来看一下DexDiff的基本步骤。想要深入了解Dex文件格式的可以看Google官方文档:https://source.android.com/devices/tech/dalvik/dex-format.html,《Android软件安全与逆向分析》这本书里关于Dex文件也有很详细的讲解。

DexDiff的主要步骤如下:

Step1:计算出new dex中每项Section的大小,比如string_ids在dex文件中所占大小。

int patchedStringIdsSize = newDex.getTableOfContents().stringIds.size * SizeOf.STRING_ID_ITEM;

step2:根据表中前一项的偏移地址和大小,计算出每项Section的偏移地址。

this.patchedStringIdsOffset = patchedHeaderOffset + patchedheaderSize;

step3:调用DexSectionDiffAlgorithm.execute(),将new dex与old dex中的每项section进行对比,对于每项Section,遍历其每一项Item,进行新旧对比,记录ADD,DEL标识,存放于patchOperationList中。接着遍历patchOperationList,添加REPLACE标识,最后将ADD,DEL,REPLACE操作分别记录到各自的List中。

这里面的新旧对比的算法很有趣,它是直接将oldItem.compareTo(newItem),结果小于0记为DEL,大于0记为ADD。为什么可以直接这样比较呢?别忘啦,从上面Dex文件格式表中可知,Dex文件中的Section都是经过排序的。

上图中,old dex中下标为2的item “c” 被DEL,c后面的元素前移,new dex中下标5处ADD一个item “f”,而在下标8处又ADD一个item “j”。

下标5的”f”在这里被记为ADD,但是它其实是REPLACE了”g”。在之后遍历patchOperationList时,算法会判断operation前一个opearation(prevPatchOperation)的类型,若prevPatchOperation为DEL,而自己刚好为ADD,那么就将自己的类型改为REPLACE。

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

step4:调用DexPatchGenerator.writePatchOperations(),将记录写入补丁。
对于DEL:
开辟一块DelOpIndexList大小的区域(DelOpIndexList中记录了每块要删除部分的索引),遍历记录DelOpIndexList,对于每一个DEL索引,计算出其相对于前一个DEL索引的偏移,记录偏移量。

buffer.writeUleb128(delOpIndexList.size());
    int lastIndex = 0;
    for (Integer index : delOpIndexList) {
        buffer.writeSleb128(index - lastIndex);
        lastIndex = index;
    }

对于ADD和REPLACE,也会进行和DEL一样的操作。

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

不同的一点是,ADD和REPALACE还会接着写入新增和替换的item。

 for (T newItem : newItemList) {
        if (newItem instanceof StringData) {
            buffer.writeStringData((StringData) newItem);
        } else
        if (newItem instanceof Integer) {
            // TypeId item.
            buffer.writeInt((Integer) newItem);
        } else
        if (newItem instanceof TypeList) {
            buffer.writeTypeList((TypeList) newItem);
        }
        ......
}

总结

这里只是简单的介绍了DexDiff的简要过程,中间其实省去了很多细节,而这些细节与Dex文件格式联系非常紧密,想要彻底的了解DexDiff原理,还需好好研究Dex文件。

原文链接:http://www.apkbus.com/blog-705730-62593.html

0人推荐
随时随地看视频
慕课网APP