【腾讯优测干货分享】如何降低App的待机内存(五)——优化dex相关内存及本章总结

版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/Tencent_Bugly/article/details/54287565

本文来自于腾讯优测公众号(wxutest),未经作者同意,请勿转载,原文地址:http://mp.weixin.qq.com/s/01Abwe0p1h3WLh28Tzg_Dw

1.5案例:优化dex相关内存

在上一节我们提到,随着代码功能的增加,代码复杂度也在不断地变大,这时候我们往往会发现Dalvik Other和dex mmap这两部分消耗的内存也在不断的增加。在之前的例子里,我们知道这两部分的内存已经接近总内存的一半。在Dalvik Heap已经充分优化的情况下,我们有必要继续对这部分内存进行研究如何优化。

我们已经知道Dalvik Other存放的是类的数据结构及关系,而dex mmap是类函数的代码和常量。通常情况下,想要减少这部分的内存,需要从代码出发,精简无用代码,或者将功能插件化。但如果我们深入理解了系统,也能够找到一些其他的方法来降低这部分的内存消耗。

1.5.1 从class对象说起

在MAT的对象实例列表中,我们往往能很多Class条目,如图1-16所示:

图1-16 MAT中似乎不消耗内存的Class类

这些对象是各种类型的元数据。从MAT的信息看来,它们只是保存了各个类的静态成员,所以对于没有静态成员的类型,shallow heap的值为0,并不消耗内存。但实际上,这只是class消耗内存的冰山一角。我们从下面的例子开始:

这段代码是一个数学处理库提供的函数,代码十分简单,只是新建了两个对象,但将这段代码在一个空应用中执行后,我们能够观察到以下的内存增长:

  • Dalvik heap增长约1.8M
  • Dalvik other增长约60K
  • .dex mmap增长约300K

Dalvik Heap的增长是我们能预期的。通常来说,能够从代码的逻辑中分析出执行这段代码总共需要分配多少内存,也能够在MAT中看到new对象消耗的内存。当应用使用完new的对象后,就会将heap内存释放,但dalvik other和.dex mmap部分是不会释放的。接下来我们首先分析一下这两部分为什么消耗了这么多内存。

1.5.2 一个类的内存消耗

首先,如果我们在代码中要使用一个类,例如以下代码:

Foo f = new Foo();

虚拟机在执行到这步时会做什么呢?

第一步是loadClass操作,将类信息从dex文件加载进内存:

1)读取.dex mmap中class对应的数据。
2)分配native-heap和dalvik-heap内存创建class对象。
3)分配dalvik-LinearAlloc存放class数据。
4)分配dalvik-aux-structure存放class数据。

第二步是new instance操作,创建对象实例。

1)执行.dex mmap中和的代码。
2)分配dalvik-heap创建class对象实例。

在这个过程中,可能还会分配dalvik-bitmap和jit-code-cache内存。如果class Foo引用了其他类型,那就还需要先按照同样的逻辑创建被引用的class。由此可见,在创建一个类实例的每一步都需要消耗内存。我们接下来大概计算一下new操作需要消耗的内存。

根据Dalvik虚拟机的代码,能够得知class根据类成员和函数的数目分配LinearAlloc和aux-structure的多少,以及class本身及函数需要的字节数。我们再根据一个应用中所有class的总量进行平均计算,得到以下一组数据:

第一步是loadClass操作,加载类信息。
- .dex mmap(class def + class data): 载入一个类需要先读取259字节的mmap。
- dalvik-LinearAlloc: 在LinearAlloc区域分配437字节,存放类静态数据。
- dalvik-aux-structure: 在aux区域分配88字节,存放各种指针。

第二步是new instance操作,创建对象实例。
- .dex mmap(code):为了执行类构造函数,还需要读取252字节的mmap。
- dalvik-heap: 根据类的具体内容而变化。

可见在new对象实例的操作中,dalvik other和dex mmap部分就各需要约500字节的内存空间。但是考虑到4K页面的问题,由于这些内存并不是连续分布的,所以可能需要分配多个4K页面。当然由于很多类会在一起使用,使得实际的页面值不会那么多。

以我们举例的应用为例,总共有7042个类,启动后载入了1522个类,这时侯应用的dex mmap内存消耗大约是5M,平均后约为3.4K。Dalvik other的部分会少一些,但依然是远远超出需要使用的大小。

1.5.3 Dex mmap

dex mmap在Android应用中的作用是映射classes.dex文件。dalvik虚拟机需要从dex文件中加载类信息,字符串常量等;还需要在调用函数的时候直接从mmap内存中读取函数代码(dvm bytecode)来执行,所以该部分内存是程序运行必不可少的。

以一个示例应用为例,我们能够在MAT中看到,应用加载了大约1500个class类型,而dex文件的class类型共有10635个。使用dex mmap动态统计功能统计后发现,虽然只加载了1500个类,但dex内存通常却高达4-6M,差不多是dex文件大小的一半。如表1-1所示。

表1-1 dex内存的利用率

以上数据中可以看到,很大一部分dex内存空间被浪费了,实际使用到的数据和代码并没有那么多,这是为什么呢?这是由于dex文件在生成时是按字母顺序排列。由于4K页面加载的原因,实际运行时会加载许多相邻但不会被用到的数据。例如在代码中使用了A1类,虚拟机就需要加载包含A1类数据的页面。但由于A1的数据只有1K,那在加载的4K页面中,还会有A2A3A4类,总共占用了4K内存。

假设我们的代码里在用到A1类后,还会用到B1C1D1类,那么如果能在dex文件中将A1B1C1D1类放在一起,虚拟机就只需要加载一个4K页面,不仅减少了内存使用,还对程序的运行速度有好处。因此,优化的思路就是调整Dex文件中数据的顺序,将能够用到的数据紧密排列在一起。

1.5.4 Dex文件优化

为了达到优化的目的,我们需要先了解Dex文件的结构。Dex文件结构如表1-2所示:

表1-2 dex文件结构

简而言之,为了节约空间,dex将原先在各个class文件中重复的信息集中放置在一起,并以索引和指针的形式支持快速访问。虚拟机能够通过索引表在Data区域中找到需要的信息。下面我们看一个访问字符串的例子:

在dex文件结构中,读取字符串需要先到StringIdList中查表,然后根据查到的地址到Data区读取内容。StringIdList的数据结构如下:

struct DexStringId {
    u4 stringDataOff;
};

现在我们模拟虚拟机读取一个字符串,来观察内存的消耗。

假设有一个字符串的id = 6728,对应的地址就会是112 + 6728 = 6990。因此虚拟机首先根据string id读取0x006990 - 0x006994的内容,此时系统会加载0x006000-0x006fff的整页内存,从PSS角度来看,会增加4K。

虚拟机读到的内容是stringDataOff = 0x531ed4,随后虚拟机会继续从0x531ed4读取字符串内容,假设字符串长度是45字节,则虚拟机会读取0x531ed4 - 0x531f04的内容,但此时系统也必须加载0x531000 - 0x531fff的整页内存,从PSS角度来看,会再次增加4K。

由此可见,在有些情况下,虚拟机读取data区的一个数据,就至少要消耗8K物理内存。如果多次读取的分散在文件各处的数据,就可能会以4K的倍数快速消耗内存。

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文件的顺序后,往往能够节约1M以上的mmap内存。
  • 引入SDK库和调用新的系统API时需要考虑成本。有可能一些不常用的功能会导致大量的消耗。这时候有可能需要多进程方案,将这些影响内存的操作放入临时进程执行。

1.6本章小结

在这一章里,我们通过对几个案例的分析,基本了解了Android应用的各种内存组成,以及这些成分是如何被消耗的,也总结出了一些节约和优化内存的经验。在这一小节里我们把经验都列出来供读者参考。

内存的主要组成索引
- Native Heap:Native代码分配的内存,虚拟机和Android框架本身也会分配
- Dalvik Heap:Java代码分配的对象
- Dalvik Other:类的数据结构和索引
- so mmap:Native代码和常量
- dex mmap:Java代码和常量

内存工具
- Android Studio/Memory Monitor:观察Dalvik内存
- dumpsys meminfo:观察整体内存
- smaps:观察整体内存的详细组成
- Eclipse Memory Analyzer:详细分析Dalvik内存

测试经验
- MAT是探索Java堆并发现问题的好帮手,能够迅速发现常见的图片和大数组等问题。
- 仅靠MAT提供的功能也不是万能的,比如内存碎片问题就隐藏在对象的地址中。
- 要测试非Dalvik部分,有必要了解Linux的进程和内存原理,内存共享机制,熟悉常用命令行工具。
- 内存分配的最小单位是页面,通常为4K,这个限制往往会引发各种碎片问题。
- 碎片不仅仅是Dalvik内存,包括各种文件的mmap也有可能产生碎片。

性能优化
- 尽量不要在循环中创建很多临时变量。
- 可以将大型的循环拆散,分段或者按需执行。
- 引入SDK库和调用新的系统API时需要考虑成本。有可能一些不常用的功能会导致大量的消耗。这时候有可能需要多进程方案,将这些影响内存的操作放入临时进程执行。
- 除了Dalvik堆内存,还有其他类型的内存在了解了原理后也能够进行分析和优化。
- Dex文件有很多优化空间。在仔细统计并调整了Dex文件的顺序后,往往能够节约1M以上的mmap内存。


更多精彩内容欢迎关注腾讯优测的微信公众账号:

腾讯优测是专业的移动云测试平台,为应用、游戏、H5混合应用的研发团队提供产品质量检测与问题解决服务。不仅在线上平台提供app自动化测试、云真机远程操控与调试、私有自动化测试工具XTest等多种质量检测工具,更为VIP客户配备了专家团队提供定制化综合测试解决方案。

展开阅读全文

没有更多推荐了,返回首页