1.5.4 dex文件优化
为了达到优化的目的,我们需要先了解dex文件的结构。dex文件结构如表1-2所示。
表1-2 dex文件结构
区 域 描 述 内 容
Header
索引区 String Id list 指向Data的偏移量
Type Id list
Method Prototype Id list
Field Id list
Method Id list
Class Definition list
Data区 ClassData 类数据 常量及变量定义Id
接口Id
成员函数Prototype Id
类Annotation的偏移量
StringData 字符串数据 类名
Proto字符串
常量字符串
Code 函数代码 Dalvik字节码
函数Debuginfo的偏移量
函数Annotations的偏移量
StaticValues 静态变量初始值
Debuginfo Debug信息
Annotation Annotations
Map list
简而言之,为了节约空间,dex将原先在各个class文件中重复的信息集中放置在一起,并以索引和指针的形式支持快速访问。虚拟机能够通过索引表在Data区域中找到需要的信息。
下面我们看一个访问字符串的例子。在dex文件结构中,读取字符串需要先到StringIdList中查表,然后根据查到的地址到Data区读取内容。StringIdList的数据结构如下:
struct DexStringId {
u4 stringDataOff;
};
现在我们模拟虚拟机读取一个字符串,来观察内存的消耗。假设有一个字符串的id = 6728,对应的地址就会是112 + 6728 = 6990。因此虚拟机首先根据string ID读取0x006990 - 0x006994的内容,此时系统会加载0x006000~0x006fff的整页内存,从Pss角度来看,会增加4KB。
虚拟机读到的内容是stringDataOff = 0x531ed4,随后虚拟机会继续从0x531ed4读取字符串内容,假设字符串长度是45字节,则虚拟机会读取0x531ed4~0x531f04的内容,但此时系统也必须加载0x531000~0x531fff的整页内存,从Pss角度来看,会再次增加4KB。
由此可见,在有些情况下,虚拟机读取data区的一个数据,就至少要消耗8KB物理内存。如果多次读取的分散在文件各处的数据,就可能会以4KB的倍数快速消耗内存。
Android SDK提供了dexdump工具来观察dex文件内容,我们以此工具来看看dex的数据内容:
dexdump classes.dex
Processing 'classes.dex'...
Opened 'classes.dex', DEX version '035'
Class #0 header:
...
Class #0 -
Class descriptor : 'Laaa/aaa;'
...
Class #1 -
Class descriptor : 'Laaa/bbb;'
...
Class #2 -
Class descriptor : 'Lbbb/ccc;'
...
根据对dex数据的观察,我们发现dex文件中数据基本是按类名的字母顺序进行排列的,这样同样包名的类会排在一起。但在实际程序执行中,同一个package下的类并不会全部一起调用,而是和很多其他package下的类进行交互,但mmap加载了整个页面,可能会有很多无用数据。为了减少这样的情况,我们在生成文件时要尽量将使用到的数据内容排布在一起。在APK的编译流程中,Proguard混淆工具正好是能够对类名进行修改的,可以根据程序运行的逻辑,将那些会互相调用的类改为同一个package名,这样就可以使它们的数据排布在一起。
以上表数据为例,Class的排列顺序是aaa/aaa、aaa/bbb、bbb/ccc。假设我们的应用运行逻辑是aaa/aaa、bbb/ccc,而aaa/bbb在某些特殊时候才能用到。但在当前的排列情况下,加载了aaa/aaa和bbb/ccc就必然要加载aaa/bbb。我们可以用Proguard等工具来控制类名,将aaa/bbb等不常用的类放在后面,则aaa/bbb平时就不会加载。如下所示:
dexdump classes.dex
Processing 'classes.dex'...
Opened 'classes.dex', DEX version '035'
Class #0 header:
...
Class #0 -
Class descriptor : 'La0;' # 原aaa/aaa
...
Class #1 -
Class descriptor : 'La1;' # 原bbb/ccc
...
Class #2 -
Class descriptor : 'La2;' # ...
...
Class #100 -
Class descriptor : 'La100;' # 平时用不到的aaa/bbb
...
经验总结
根据上述的流程,我们探讨了Dalvik Other和.dex mmap部分的内存,大致搞清楚了它们被消耗的机制,以及一些能够减少消耗的方法。经验如下:
在优化内存时,不只有堆内存,还有其他许多类型的内存能够进行分析和优化。
dex文件有很多优化空间。在仔细统计并调整了dex文件的顺序后,往往能够节约1MB以上的mmap内存。
引入SDK库和调用新的系统API时需要考虑成本。有可能一些不常用的功能会导致大量的内存消耗。这时有可能需要多进程方案,将这些影响内存的操作放入临时进程执行。