修改日期 | 修改人 | 备注 |
2020-09-11 15:23:46[当前版本] | 朱凡 | 修改标题 |
2020-09-11 15:23:06 | 朱凡 | 创建版本 |
Dex格式解析与增量更新
dex文件是Android系统的可执行文件,包含应用程序的全部操du作指令以及运行时数据。
当java程序编译成class后,还需要使用dx工具将所有的class文件整合到一个dex文件,目的是其中各个类能够共享数据,在一定程度上降低了冗余,同时也是文件结构更加经凑,实验表明,dex文件是传统jar文件大小的50%左
右。
文件布局
dex文件可以分为3个模块,头文件、索引区、数据区。头文件概况的描述了整个dex文件的分布,包括每一个索引区的大小跟偏移。索引区表示每个数据的标识,索引区主要是指向数据区的偏移。
1
我们可以使用16进制查看工具打开一个dex来同步分析。(建议使用010Editor)。
1598320740980
010Editor中除了数据区(data)没有显示出来,其他区段都有显示,另外link_data在此处被定为map_list
大小端
一般的,文件一般使用小端字节序存储(Dex文件也不例外),网络传输一般使用大端字节序。
大端模式(Big-endian),是指数据的高字节保存在内存的低地址中,而数据的低字节保存在内存的高地址中 。
小端模式(Little-endian),是指数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中 。
假如有一个4字节的数据为
(十进制:
,为高字节,
为低字节),若将其
存放于地址中:
内存地址 |
0x1000(低地址) |
0x1001 |
0x1002 |
0x1003(高地址) |
大端模式 |
0x12(高字节) |
0x34 |
0x56 |
0x78(低字节) |
小端模式 |
0x78(低字节) |
0x56 |
0x34 |
0x12(高字节) |
Header
整个dex文件以16进制打开,前112个字节为头文件数据。Header描述了dex文件信息,和其他各个区的索引。
1598320920528
此处数据,最开始为魔数,数据为:
字段 |
字节数 |
说明 |
dex |
3 |
文件格式:dex |
newLine |
1 |
换行:"\n" |
ver |
3 |
版本:035 |
zero |
1 |
无意义,00 |
uint为4字节数据
checksum:文件校验码,使用alder32算法校验文件除去maigc、checksum外余下的所有文件区域,用于检 查文件错误。
signature:使用SHA-1算法hash除去magic、checksum和signature外余下的所有文件区域, 用于唯一识别本文件 。
file_size: dex文件大小
header_size: header区域的大小,固定为0x70
endian_tag:大小端标签,dex文件格式为小端,固定值为0x12345678
map_off: map_item的偏移地址,该item属于data区里的内容,值要大于等于data_off的大小,处于dex
文件的末端。
其他,成对出现,为对于数据的偏移与数据个数。对应Header数据解析代码为:
public finalintprotoIdsSize;public finalintprotoIdsOff;public finalintfieldIdsSize;public finalintfieldIdsOff;public finalintmethodIdsSize;public finalintmethodIdsOff;public finalintclassDefsSize;public finalintclassDefsOff;public finalintdataSize;public finalintdataOff;publicintmapOff;
publicintfileSize;
publicHeader(ByteBuffer data) {
byte[]magic=BufferUtil.readBytes(data,8);//魔数:文件格式、版本intchecksum=data.getInt();//校验码
byte[]signature=BufferUtil.readBytes(data,20);//签名
fileSize=data.getInt();
intheaderSize=data.getInt();//一定是112
intendianTag=data.getInt();//一定是0x12345678
intlinkSize=data.getInt();intlinkOff=data.getInt();
//mapList部分偏移
mapOff=data.getInt();stringIdsSize=data.getInt();stringidsOff=data.getInt();typeIdsSize=data.getInt();typeIdsOff=data.getInt();protoIdsSize=data.getInt();protoIdsOff=data.getInt();fieldIdsSize=data.getInt();fieldIdsOff=data.getInt();methodIdsSize=data.getInt();methodIdsOff=data.getInt();classDefsSize=data.getInt();classDefsOff=data.getInt();dataSize=data.getInt();dataOff=data.getInt();
}
public staticHeader readFrom(ByteBuffer in) {
//拷贝一份ByteBuffer
ByteBuffer sectionData=in.duplicate();sectionData.order(ByteOrder.LITTLE_ENDIAN);//小端序sectionData.position(0);
//可操作数据长度为112字节sectionData.limit(SIZE_OF_HEADER);return newHeader(sectionData);
}
}
在解析完之后,就能够获得接下来数据的偏移与长度,按照对应的值定位位置解析。
StringIds
string_ids区段描述了dex文件中所有的字符串。记录的数据只有一个偏移量,偏移量指向了 数据区Data中 的一个字符串:
stringids
根据解析结果得知,StringIds中有15个数据。
//dex对应的ByteBuffer、stringids个数与stringids数据区域偏移
string_ids=StringIdItem.readFrom(data,header.stringIdsSize,header.stringidsOff);
publicstaticMap(Integer,StringIdItem>readFrom(ByteBufferin,intsize,intoff)throwsUTFDataFormatException{
ByteBuffer sectionData=in.duplicate();sectionData.order(ByteOrder.LITTLE_ENDIAN);
sectionData.position(off);//偏移此处为stringids
Map(Integer,StringIdItem>map=newHashMap(>();for(inti=0;i(size;i++) {
//字符串数据内容偏移
intstring_data_off=sectionData.getInt();intposition=sectionData.position();
//定位到数据内容对应偏移
sectionData.position(string_data_off);
//解析字符串数据 : 下面说明
intutf16_size=BufferUtil.readUnsignedLeb128(sectionData);Stringdata=BufferUtil.readMutf8(sectionData,utf16_size);sectionData.position(position);
StringIdItemstringItem=newStringIdItem(string_data_off,utf16_size,data);map.put(i,stringItem);
}
returnmap;
}
后续数据同样的方式进行解析。体力活~~~后续数据格式参考:
http://gnaixx.cc/2016/11/26/20161126dex-file/ https://source.android.google.cn/devices/tech/dalvik/dex-format
DexDiff
dexDiff是微信结合Dex文件格式设计的一个专门针对Dex的差分算法。根据Dex的文件格式,对两个Dex中每一项数据进行差分记录。整个实现过程其实很繁琐,我们以字符串StringIds区域的差分举例
StringIds差分计算
Dex文件中的段落数据都是经过排序的。 如存在"a","b"与"c"三个字符串。那么在StringIds中Item顺序也为abc。对照两个dex文件数据:oldDex与newDex。
|
0 |
1 |
2 |
old |
a |
b |
c |
new |
b |
c |
e |
old dex中
与new dex中的b比较。
: old dex中的a标记为:del,oldIndex++继续比较。
|
0 |
1 |
2 |
old |
a |
b |
c |
new |
b |
c |
e |
old dex中b与new dex中的b比较。
,不处理。oldIndex++,new Index++
|
0 |
1 |
2 |
old |
a |
b |
c |
new |
b |
c |
e |
old dex中c与new dex中的c比较。
,不处理。oldIndex++,newIndex++
|
0 |
1 |
2 |
old |
a |
b |
c |
new |
b |
c |
e |
因此new dex剩余的Item全部记为:add
intoldIndex=0;intnewIndex=0;
intoldStrCount=oldDex.string_ids.size();//olddex中解析的字符串集合
intnewStrCount=newDex.string_ids.size();//newdex中解析的字符串集合
//记录操作集合
List(PatchOperation>patchOperationList=newArrayList(>();while(oldIndex(oldStrCount||newIndex(newStrCount){
if(oldIndex>=oldStrCount) {//old下标记超过old数据元素个数了
//表示new还有,则全是新的
patchOperationList.add(newPatchOperation(PatchOperation.OP_ADD,newIndex,newDex.string_ids.get(newIndex)));
newIndex++;
}else if(newIndex>=newStrCount) {
// old需要remove
patchOperationList.add(newPatchOperation(PatchOperation.OP_DEL,oldIndex,oldDex.string_ids.get(oldIndex)));
oldIndex++;
}else{
StringIdItemnewItem=newDex.string_ids.get(newIndex);StringIdItemoldItem=oldDex.string_ids.get(oldIndex);
//比较StringIdItem对象内部实现位:比较字符串数据
intcmpRes=oldItem.compareTo(newItem);if(cmpRes(0) {
// old:a new:b此时应该是删除old的a,new的b继续比较old后续的字符串
patchOperationList.add(newPatchOperation(PatchOperation.OP_DEL,oldIndex,oldDex.string_ids.get(oldIndex)));
oldIndex++;
}else if(cmpRes>0) {
// old:b new:a此时应该是增加new的a,old的b继续对比new后续的字符串
patchOperationList.add(newPatchOperation(PatchOperation.OP_ADD,newIndex,newDex.string_ids.get(newIndex)));
newIndex++;
}else{
oldIndex++;newIndex++;
}
}
}
增量更新
自从Android4.1开始,GooglePlay引入了应用程序的增量更新功能,App使用该升级方式,可节省约2/3的流量。现在国内主流的应用市场也都支持应用的增量更新。
增量更新的关键在于增量一词。平时我们的开发过程,往往都是今天在昨天的基础上修改一些代码,app的更新也是类似的:往往都是在旧版本的app上进行修改。这样看来,增量更新就是原有app的基础上只更新发生变化的地方,其余保持原样。
与之前每次更新都要下载完整apk包的做法相比,这样做的好处显而易见:每次变化的地方总是比较少,因此更新 包的体积就会小很多。比某APK的体积在60m左右,如果不采用增量更新,用户每次更新都需要下载大约60m左右的安装包,而采用增量更新这种方案之后每次只需要下载2m左右的更新包即可,相比原来做法大大减少了用户下载等待的时间和流量,同时也可以因为更新变得更简单也能够缩短产品版本覆盖周期。
使用BSDiff
http://www.daemonology.net/bsdiff/
此处下载源码,包含两个程序:bsdiff(比较两个文件的二进制数据,生成差分包)与bspatch(合并旧的文件与差分包,生成新的文件)。
Linux/Mac
已经安装bzip2打开Makefile
内容也不多。但是这个Makefile有一个问题,Makefile中Target指令必须以tab开头,所以我们需要修改为
现在保存退出。
我们直接进入目录执行make
报错了,不认识u_char这个类型。打开bspatch.c
然后在执行make,将得到bsdiff与bspatch工具。
Windows
Windows的同学可以到附件中获得可执行程序。
测试
Android中使用需要NDK开发
我们先来使用bsdiff工具生成差分补丁包:
可以看到使用方式是
bsdiff老文件 新文件 输出的补丁
创建一个空的Android Native工程生成apk。--- 1.0版本
然后我们修改一些代码,再增加一些图片,生成新的apk --- 2.0版本
运行:(apk文件在bsdiff上一层)
然后会在我们指定的目录生成
这里说明下我们看到补丁包2.3+oldapp 2=4.3比我们的app2.0要大。因为bsdiff的补丁大小并不是简单的加减,还会存在一些上下文环境等额外的信息。
但是我们从上面能够看出,如果在安装了1.0的情况下,我们升级,能够节省1.5M的内存。接下来我们来验证下这个补丁包是不是有效
bspatch命令是:
我们合并后的文件生成了
目前来看至少大小一样。
我们可以测试能否正常安装。