Tinker原理解析
补丁包生成原理
Tinker打补丁主要为两个步骤:1.通过执行gradke task tinkerPatchDebug(Release)生成补丁包。2.通过tinkerInstaller将补丁包进行合成。
我们先来分析第一步:tinkerPatchDebug task包括两个步骤,1.执行debug生成修复代码之后的apk。2.执行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 | public class Hello { |
生成Hello.class文件:
然后把java文件和class文件放到sdk->build-tools->23.0.1中,生成dex文件:
会发现放在23.0.1下边出错,我把它放到28.0.2下边就正常了,可能低版本有点问题。最后会有3个文件:
dex的内部结构如下
下边这张图更直观一点:
dex是使用LEB128编码的,用010Editor可以看dex文件。可以看到010Editor解析出来的数据基本跟dex文件结构对应,link_data在010Editor中表示为map_list
这里我们主要对dex_header和map_list做大致的分析:
dex_header:
magic:魔法值DEX_FILE_MAGIC。用来标识是dex文件,java的魔法值是CAFFEEBABE。
其他相关字段含义:
注意,从下面的图中可以看到,dex_header中的string_ids_size,string_ids_off这种成对出现的分别代表大小和偏移量,对应着下面出现的其他表的数据。
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 | private void diffDexPairAndFillRelatedInfo(File oldDexFile, File newDexFile, RelatedInfo relatedInfo) { |
这里有两个步骤。
1:DexPatchGenerator dexPatchGen = new DexPatchGenerator(oldDexFile, newDexFile);
2:dexPatchGen.executeAndSaveTo(dexDiffOut);
接下来我们先看1:
1 | public class DexPatchGenerator { |
1 | public final class Dex { |
1 | public final class TableOfContents { |
这里的核心就在于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 | public class DexPatchGenerator { |
这里初始化每个算法之后主要执行3个操作,一个是execute方法,另一个是simulatePatchOperation方法,最后是执行writeResultToStream方法。这里先看execute方法:
1 | public abstract class DexSectionDiffAlgorithm<T extends Comparable<T>> { |
这边首先拿到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 | public void execute() { |
上面得到了一个patchOperationList对象之后,先对其进行排序。
然后执行一些优化操作,同一个index的item的OP_DEL操作后边紧跟着OP_ADD操作会被替换成OP_REPLACE。
然后最后生成3个Map:indexToDelOperationMap,indexToAddOperationMap,indexToReplaceOperationMap.
至此,完成了需要删除,添加,替换的item收集。
接着看simulatePatchOperation方法:
参数传入的偏移量为data区域的偏移量。
1 | public void simulatePatchOperation(int 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 | private void writeResultToStream(OutputStream os) throws IOException { |
这里主要写入dex中的header的相关字段,然后写入所有的跟maplist各个区域相关的offset。然后调用各个区域对应算法写入信息,最后生成patch文件。这里以stringDataSectionDiffAlg为例,看下其源码:
1 | private <T extends Comparable<T>> void writePatchOperations( |
这里将我们的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 | public class DexPatchApplier { |
1 | public final class DexPatchFile { |
构造函数中首先oldDex被封装为Dex对象,就是上面分析过的readHeader和readMap。不过patchFile是转化为一个DexPatchFile对象。首先将patch file读取为byte[],然后调用其init方法,init方法首先判断MAGIC和Version,然后对patcheDexSize和各区域进行赋值,最后定位到数据区。
接下来的方法步骤较多,分为几个流程:
1 | public DexPatchApplier( |
1.这里读取patchFile中记录的值给patchedDex的TableOfContent中各种Section赋值,然后根据它们的偏移进行排序。
1 | public void executeAndSaveTo(OutputStream out) throws IOException { |
2.这里初始化了很多算法,依次调用execute()方法。这里依旧来看stringDataSectionPatchAlg的execute方法:
1 | public void execute() { |
这里1.读取del的数量,将index存储在int数组中。2.读取add的数量,将index存储在int数组中。3.读取replace的数量,将index存储在int数组中。
然后就拿着deletedIndices,addedIndices,replacedIndices,oldSection, oldItemCount就能进行完整的patch了。
1 | private void doFullPatch( |
这个方法就是往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 | public void executeAndSaveTo(OutputStream out) throws IOException { |
最后一个步骤就是写header,mapList的数据。这样就完成了完整的dex的恢复,最后将内存中的所有数据写到文件中。
补丁合成的原理
前面我们已经生成了补丁包,接着通过服务器下发或者本地放入sd卡中,通过应用启动之后进行加载合成。这一节中就来讲这个具体的过程。
程序启动时会加载默认的Application类,将导致无法对它进行补丁修复。Tinker中为了避免这个问题,是通过将原来的Application类隔离起来,将Application类以及它的继承类的所有代码移至自己的ApplicationLike继承类中。并且把attachBaseContext方法实现单独移动到onBaseContextAttached中,这里的onBaseContextAttached并不是Application的生命周期,这个方法是在自己生成的TinkerApplication中的attachBaseContext方法中调用的,这里这样处理的目的就是为了将Application类隔离起来。
1 | @SuppressWarnings("unused") |
这里通过注解生成我们真正的application类:SampleApplication.而DefaultApplicationLike是我们的代理类。这里看TinkerApplication类的实现可以看出这里用的是代理模式。
这里重点看TinkerApplication中的方法,
1 | private void onBaseContextAttached(Context base) { |
这里通过传入的loader类,通过反射调用tryLoad方法,这里的loader类默认是TinkerLoader,这里内部会调用到tryLoadPatchFilesInternal,会判断是否开启dex修复,lib修复,资源修复,然后再分别调用TinkerDexLoader,TinkerResourceLoader等方法进行对应的修复,这里我们看dex修复。
1 | public class SampleApplication extends TinkerApplication { |
第一步,先调用checkComplete方法从assets/dex_meta.txt中检查记录的dex信息,检查对应的dex文件是否存在,并保存到loadDexList中。
第二部调用TinkerDexLoader.loadTinkerJars方法会首先根据是否支持dalvik或者art做对应的处理,最后调用installDexes方法:
1 | public class SystemClassLoaderAdder { |
这里根据不同的系统版本,反射处理dexElements。看一下最新版本的SDK实现,就是V23.install方法
1 | private static final class V23 { |
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)方法来加载补丁,其流程如下:
1-5:加载补丁文件
6-9:启动一个JobSeheduler来处理任务
10-13:启动一个AsyncTask在执行补丁包合成的任务
14-18:补丁包合成的核心流程,补丁包的验证,dex修复,lib修复,res修复等等
1 | protected static boolean tryRecoverDexFiles(Tinker manager, ShareSecurityCheck checker, Context context, |
首先解析meta中的信息,meta中包含了patch中每个dex的相关数据。然后拿到apk和patch的文件路径,根据文件信息进行crc校验和md5校验,然后调用patchDexFile对两个dex文件合并。
1 | private static void patchDexFile( |
通过ZipFile拿到文件流,读取本地apk中的dex文件和patch中的dex文件,然后通过executeAndSaveTo合并到最终的patchedDexFile,也就是上面所说的生成差分包的原理。
具体案例分析
我分别编写了2个java文件,Hello.java和World.java
1 | public class Hello { |
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