2020-09-11 15:23:06 版本 : Dex格式简析与增量更新
作者: 朱凡 于 2020年09月11日 发布在分类 / FM组 / FM_App 下,并于 2020年09月11日 编辑
 历史版本

修改日期 修改人 备注
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命令是:


我们合并后的文件生成了

目前来看至少大小一样。

我们可以测试能否正常安装。

历史版本-目录  [回到顶端]
    知识分享平台 -V 4.8.7 -wcp