You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

201 lines
16 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 03 | 内存优化4GB内存时代再谈内存优化
在写今天这篇文章前我又翻了翻三年前我在WeMobileDev公众号写过的[《Android内存优化杂谈》](http://mp.weixin.qq.com/s/Z7oMv0IgKWNkhLon_hFakg)今天再看对里面的一句话更有感触“我们并不能将内存优化中用到的所有技巧都一一说明而且随着Android版本的更替可能很多方法都会变的过时”。
三年过去了4GB内存的手机都变成了主流。那内存优化是不是变得不重要了如今有哪些技巧已经淘汰而我们又要升级什么技能呢
今天在4GB内存时代下我就再来谈谈“内存优化”这个话题。
## 移动设备发展
Facebook有一个叫[device-year-class](http://github.com/facebook/device-year-class)的开源库它会用年份来区分设备的性能。可以看到2008年的手机只有可怜的140MB内存而今年的华为Mate 20 Pro手机的内存已经达到了8GB。
![](https://static001.geekbang.org/resource/image/8d/f1/8d1367526799c38d525910bfb5a618f1.png)
内存看起来好像是我们都非常熟悉的概念那请问问自己手机内存和PC内存有哪什么差异呢8GB内存是不是就一定会比4GB内存更好我想可能很多人都不一定能回答正确。
手机运行内存RAM其实相当于我们的PC中的内存是手机中作为App运行过程中临时性数据暂时存储的内存介质。不过考虑到体积和功耗手机不使用PC的DDR内存采用的是LPDDR RAM全称是“低功耗双倍数据速率内存”其中LP就是“Lower Power”低功耗的意思。
以LPDDR4为例带宽 = 时钟频率 × 内存总线位数 ÷ 8即1600 × 64 ÷ 8 = 12.8GB/s因为是DDR内存是双倍速率所以最后的带宽是12.8 × 2 = 25.6GB/s。
![](https://static001.geekbang.org/resource/image/2f/44/2f26e93ac941f30bb4037648640aca44.png)
目前市面上的手机主流的运行内存有LPDDR3、LPDDR4以及LPDDR4X。可以看出LPDDR4的性能要比LPDDR3高出一倍而LPDDR4X相比LPDDR4工作电压更低所以也比LPDDR4省电20%40%。当然图中的数据是标准数据,不同的生成厂商会有一些低频或者高频的版本,性能方面高频要好于低频。
那手机内存是否越大越好呢?
如果一个手机使用的是4GB的LPDDR4X内存另外一个使用的是6GB的LPDDR3内存那么无疑选择4GB的运行内存手机要更加实用一些。
但是内存并不是一个孤立的概念它跟操作系统、应用生态这些因素都有关。同样是1GB内存使用Android 9.0系统会比Android 4.0系统流畅使用更加封闭、规范的iOS系统也会比“狂野”的Android系统更好。今年发布的iPhone XR和iPhone XS使用的都是LPDDR4X的内存不过它们分别只有3GB和4GB的大小。
## 内存问题
在前面所讲的崩溃分析中我提到过“内存优化”是崩溃优化工作中非常重要的一部分。类似OOM很多的“异常退出”其实都是由内存问题引起。那么内存究竟能引发什么样的问题呢
1.两个问题
![](https://static001.geekbang.org/resource/image/c2/ce/c26a9351868bb82abd7ada028275f5ce.png)
内存造成的第一个问题是**异常**。在前面的崩溃分析我提到过“异常率”异常包括OOM、内存分配失败这些崩溃也包括因为整体内存不足导致应用被杀死、设备重启等问题。不知道你平时是否在工作中注意过如果我们把用户设备的内存分成2GB以下和2GB以上两部分你可以试试分别计算他们的异常率或者崩溃率看看差距会有多大。
内存造成的第二个问题是**卡顿**。Java内存不足会导致频繁GC这个问题在Dalvik虚拟机会更加明显。而ART虚拟机在内存管理跟回收策略上都做大量优化内存分配和GC效率相比提升了510倍。如果想具体测试GC的性能例如暂停挂起时间、总耗时、GC吞吐量我们可以通过发送**SIGQUIT信号获得ANR日志**。
```
adb shell kill -S QUIT PID
adb pull /data/anr/traces.txt
```
它包含一些ANR转储信息以及GC的详细性能信息。
```
sticky concurrent mark sweep paused: Sum: 5.491ms 99% C.I. 1.464ms-2.133ms Avg: 1.830ms Max: 2.133ms // GC 暂停时间
Total time spent in GC: 502.251ms // GC 总耗时
Mean GC size throughput: 92MB/s // GC 吞吐量
Mean GC object throughput: 1.54702e+06 objects/s
```
另外我们还可以使用systrace来观察GC的性能耗时这部分内容在专栏后面会详细讲到。
除了频繁GC造成卡顿之外物理内存不足时系统会触发low memory killer机制系统负载过高是造成卡顿的另外一个原因。
2.两个误区
除了内存引起的异常和卡顿,在日常做内存优化和架构设计时,很多同学还非常容易陷入两个误区之中。
**误区一:内存占用越少越好**
VSS、PSS、Java堆内存不足都可能会引起异常和卡顿。有些同学认为内存是洪水猛兽占用越少应用的性能越好这种认识在具体的优化过程中很容易“用力过猛”。
应用是否占用了过多的内存跟设备、系统和当时情况有关而不是300MB、400MB这样一个绝对的数值。当系统内存充足的时候我们可以多用一些获得更好的性能。当系统内存不足的时候希望可以做到**“用时分配,及时释放”**,就像下面这张图一样,当系统内存出现压力时,能够迅速释放各种缓存来减少系统压力。
![](https://static001.geekbang.org/resource/image/07/97/0739a98bfafdd9f59539ddbbf403f097.png)
现在手机已经有6GB和8GB的内存出现了Android系统也希望去提升内存的利用率因此我们有必要简单回顾一下Android Bitmap内存分配的变化。
* 在Android 3.0之前Bitmap对象放在Java堆而像素数据是放在Native内存中。如果不手动调用recycleBitmap Native内存的回收完全依赖finalize函数回调熟悉Java的同学应该知道这个时机不太可控。
* Android 3.0Android 7.0将Bitmap对象和像素数据统一放到Java堆中这样就算我们不调用recycleBitmap内存也会随着对象一起被回收。不过Bitmap是内存消耗的大户把它的内存放到Java堆中似乎不是那么美妙。即使是最新的华为Mate 20最大的Java堆限制也才到512MB可能我的物理内存还有5GB但是应用还是会因为Java堆内存不足导致OOM。Bitmap放到Java堆的另外一个问题会引起大量的GC对系统内存也没有完全利用起来。
* 有没有一种实现可以将Bitmap内存放到Native中也可以做到和对象一起快速释放同时GC的时候也能考虑这些内存防止被滥用NativeAllocationRegistry可以一次满足你这三个要求Android 8.0正是使用这个辅助回收Native内存的机制来实现像素数据放到Native内存中。Android 8.0还新增了硬件位图Hardware Bitmap它可以减少图片内存并提升绘制效率。
**误区二Native内存不用管**
虽然Android 8.0重新将Bitmap内存放回到Native中那么我们是不是就可以随心所欲地使用图片呢
答案当然是否定的。正如前面所说当系统物理内存不足时lmk开始杀进程从后台、桌面、服务、前台直到手机重启。系统构想的场景就像下面这张图描述的一样大家有条不絮的按照优先级排队等着被kill。
![](https://static001.geekbang.org/resource/image/b8/98/b8d160f8d487bcb377e0c38ff9a0ac98.png)
low memory killer的设计是假定我们都遵守Android规范但并没有考虑到中国国情。国内很多应用就像是打不死的小强杀死一个拉起五个。频繁的杀死、拉起进程又会导致system server卡死。当然在Android 8.0以后应用保活变得困难很多,但依然有一些方法可以突破。
既然讲到了将图片的内存放到Native中我们比较熟悉的是Fresco图片库在Dalvik会把图片放到Native内存中。事实上在Android 5.0Android 7.0,也能做到相同的效果,只是流程相对复杂一些。
步骤一通过直接调用libandroid\_runtime.so中Bitmap的构造函数可以得到一张空的Bitmap对象而它的内存是放到Native堆中。但是不同Android版本的实现有那么一点差异这里都需要适配。
步骤二通过系统的方法创建一张普通的Java Bitmap。
步骤三将Java Bitmap的内容绘制到之前申请的空的Native Bitmap中。
步骤四将申请的Java Bitmap释放实现图片内存的“偷龙转凤”。
```
// 步骤一:申请一张空的 Native Bitmap
Bitmap nativeBitmap = nativeCreateBitmap(dstWidth, dstHeight, nativeConfig, 22);
// 步骤二:申请一张普通的 Java Bitmap
Bitmap srcBitmap = BitmapFactory.decodeResource(res, id);
// 步骤三:使用 Java Bitmap 将内容绘制到 Native Bitmap 中
mNativeCanvas.setBitmap(nativeBitmap);
mNativeCanvas.drawBitmap(srcBitmap, mSrcRect, mDstRect, mPaint);
// 步骤四:释放 Java Bitmap 内存
srcBitmap.recycle();
srcBitmap = null
```
虽然最终图片的内存的确是放到Native中了不过这个“黑科技”有两个主要问题一个是兼容性问题另外一个是频繁申请释放Java Bitmap容易导致内存抖动。
## 测量方法
在日常开发中有时候我们需要去排查应用程序中的内存问题。对于系统内存和应用内存的使用情况你可以参考Android Developer中 [《调查RAM使用情况》。](http://developer.android.com/studio/profile/investigate-ram?hl=zh-cn)
```
adb shell dumpsys meminfo <package_name|pid> [-d]
```
**1\. Java内存分配**
有些时候我们希望跟踪Java堆内存的使用情况这个时候最常用的有Allocation Tracker和MAT这两个工具。
在我曾经写过的[《Android内存申请分析》](http://mp.weixin.qq.com/s/b_lFfL1mDrNVKj_VAcA2ZA)里提到过Allocation Tracker的三个缺点。
* 获取的信息过于分散,中间夹杂着不少其他的信息,很多信息不是应用申请的,可能需要进行不少查找才能定位到具体的问题。
* 跟Traceview一样无法做到自动化分析每次都需要开发者手工开始/结束,这对于某些问题的分析可能会造成不便,而且对于批量分析来说也比较困难。
* 虽然在Allocation Tracking的时候不会对手机本身的运行造成过多的性能影响但是在停止的时候直到把数据dump出来之前经常会把手机完全卡死如果时间过长甚至会直接ANR。
因此我们希望可以做到脱离Android Studio实现一个自定义的“Allocation Tracker”实现对象内存的自动化分析。通过这个工具可以获取所有对象的申请信息大小、类型、堆栈等可以找到一段时间内哪些对象占用了大量的内存。
但是这个方法需要考虑的兼容性问题会比较多在Dalvik和ART中Allocation Tracker的处理流程差异就非常大。下面是在Dalvik和ART中Allocation Tacker的开启方式。
```
// dalvik
bool dvmEnableAllocTracker()
// art
void setAllocTrackingEnabled()
```
我们可以用自定义的“Allocation Tracker”来监控Java内存的监控也可以拓展成实时监控Java内存泄漏。这方面经验不多的同学也不用担心我在今天的“课后作业”提供了一个自定义的“Allocation Tracker”供你参考。**不过任何一个工具如果只需要做到线下自动化测试,实现起来会相对简单,但想要移植到线上使用,那就要更加关注兼容性、稳定性和性能,付出的努力要远远高于实验室方案。**
在课后作业中我们会提供一个简单的例子在熟悉Android Studio中Profiler各种工具的实现原理后我们就可以做各种各样的自定义改造在后面的文章中也会有大量的例子供你参考和练习。
**2\. Native内存分配**
Android的Native内存分析是一直做得非常不好当然Google在近几个版本也做了大量努力让整个过程更加简单。
首先Google之前将Valgrind弃用建议我们使用Chromium的[AddressSanitize](http://source.android.com/devices/tech/debug/asan.html) 。遵循**“谁最痛,谁最需要,谁优化”**所以Chromium出品了一大堆Native相关的工具。Android之前对AddressSanitize支持的不太好需要root和一大堆的操作但在Android 8.0之后,我们可以根据这个[指南](http://github.com/google/sanitizers/wiki/AddressSanitizerOnAndroid)来使用AddressSanitize。目前AddressSanitize内存泄漏检测只支持x86\_64 Linux和OS X系统不过相信Google很快就可以支持直接在Android上进行检测了。
那我们有没有类似Allocation Tracker那样的Native内存分配工具呢在这方面Android目前的支持还不是太好但Android Developer近来也补充了一些相关的文档你可以参考[《调试本地内存使用》](http://source.android.com/devices/tech/debug/native-memory)。关于Native内存的问题有两种方法分别是**Malloc调试**和**Malloc钩子**。
[Malloc调试](http://android.googlesource.com/platform/bionic/+/master/libc/malloc_debug/README.md)可以帮助我们去调试Native内存的一些使用问题例如堆破坏、内存泄漏、非法地址等。Android 8.0之后支持在非root的设备做Native内存调试不过跟AddressSanitize一样需要通过[wrap.sh](http://developer.android.com/ndk/guides/wrap-script.html)做包装。
```
adb shell setprop wrap.<APP> '"LIBC_DEBUG_MALLOC_OPTIONS=backtrace logwrapper"'
```
[Malloc钩子](http://android.googlesource.com/platform/bionic/+/master/libc/malloc_hooks/README.md)是在Android P之后Android的libc支持拦截在程序执行期间发生的所有分配/释放调用,这样我们就可以构建出自定义的内存检测工具。
```
adb shell setprop wrap.<APP> '"LIBC_HOOKS_ENABLE=1"'
```
但是在使用“Malloc调试”时感觉整个App都会变卡有时候还会产生ANR。如何在Android上对应用Native内存分配和泄漏做自动化分析也是我最近想做的事情。据我了解微信最近几个月在Native内存泄漏监控上也做了一些尝试我会在专栏下一期具体讲讲。
## 总结
LPDDR5将在明年进入量产阶段移动内存一直向着更大容量、更低功耗、更高带宽的方向发展。伴随内存的发展内存优化的挑战和解决方案也不断变化。而内存优化又是性能优化重要的一部分今天我讲到了很多的异常和卡顿都是因为内存不足引起的并在最后讲述了如何在日常开发中分析和测量内存的使用情况。
一个好的开发者并不满足于做完需求,我们在设计方案的时候,还需要考虑要使用多少的内存,应该怎么去管理这些内存。在需求完成之后,我们也应该去回归需求的内存情况,是否存在使用不当的地方,是否出现内存泄漏。
## 课后作业
内存优化是一个非常“古老”的话题大家在工作中也会遇到各种各样内存相关的问题。今天的课后作业是分享一下你在工作中遇到的内存问题总结一下通过Sample的练习有什么收获。
在今天文章里我提到希望可以脱离Android Studio实现一个自定义的Allocation Tracker这样就可以将它用到自动化分析中。本期的[Sample](http://github.com/AndroidAdvanceWithGeektime/Chapter03)就提供了一个自定义的Allocation Tracker实现的示例目前已经兼容到Android 8.1。你可以用它练习实现自动化的内存分析有哪些对象占用了大量内存以及它们是如何导致GC等。
欢迎你点击“请朋友读”,把今天的内容分享给好友,邀请他一起学习。最后别忘了在评论区提交今天的作业,我也为认真完成作业的同学准备了丰厚的“[学习加油礼包](http://time.geekbang.org/column/article/70250)”,期待与你一起切磋进步哦。
![](https://static001.geekbang.org/resource/image/24/c0/24c190870d71c3daa203a939d67358c0.jpg)![](https://static001.geekbang.org/resource/image/30/aa/306ef8892cc985a19fdd36534e7c5daa.png)