
                            
                            因收到Google相关通知，网站将会择期关闭。相关通知内容
                            
                            
                            03 内存优化（上）：4GB内存时代，再谈内存优化
                            在写今天这篇文章前，我又翻了翻三年前我在WeMobileDev公众号写过的《Android内存优化杂谈》，今天再看，对里面的一句话更有感触：“我们并不能将内存优化中用到的所有技巧都一一说明，而且随着Android版本的更替，可能很多方法都会变的过时”。

三年过去了，4GB内存的手机都变成了主流。那内存优化是不是变得不重要了？如今有哪些技巧已经淘汰，而我们又要升级什么技能呢？

今天在4GB内存时代下，我就再来谈谈“内存优化”这个话题。

移动设备发展

Facebook有一个叫device-year-class的开源库，它会用年份来区分设备的性能。可以看到，2008年的手机只有可怜的140MB内存，而今年的华为Mate 20 Pro手机的内存已经达到了8GB。



内存看起来好像是我们都非常熟悉的概念，那请问问自己，手机内存和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。



目前市面上的手机，主流的运行内存有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.两个问题



内存造成的第一个问题是异常。在前面的崩溃分析我提到过“异常率”，异常包括OOM、内存分配失败这些崩溃，也包括因为整体内存不足导致应用被杀死、设备重启等问题。不知道你平时是否在工作中注意过，如果我们把用户设备的内存分成2GB以下和2GB以上两部分，你可以试试分别计算他们的异常率或者崩溃率，看看差距会有多大。

内存造成的第二个问题是卡顿。Java内存不足会导致频繁GC，这个问题在Dalvik虚拟机会更加明显。而ART虚拟机在内存管理跟回收策略上都做大量优化，内存分配和GC效率相比提升了5～10倍。如果想具体测试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这样一个绝对的数值。当系统内存充足的时候，我们可以多用一些获得更好的性能。当系统内存不足的时候，希望可以做到“用时分配，及时释放”，就像下面这张图一样，当系统内存出现压力时，能够迅速释放各种缓存来减少系统压力。



现在手机已经有6GB和8GB的内存出现了，Android系统也希望去提升内存的利用率，因此我们有必要简单回顾一下Android Bitmap内存分配的变化。


在Android 3.0之前，Bitmap对象放在Java堆，而像素数据是放在Native内存中。如果不手动调用recycle，Bitmap Native内存的回收完全依赖finalize函数回调，熟悉Java的同学应该知道，这个时机不太可控。

Android 3.0～Android 7.0将Bitmap对象和像素数据统一放到Java堆中，这样就算我们不调用recycle，Bitmap内存也会随着对象一起被回收。不过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。



low memory killer的设计，是假定我们都遵守Android规范，但并没有考虑到中国国情。国内很多应用就像是打不死的小强，杀死一个拉起五个。频繁的杀死、拉起进程，又会导致system server卡死。当然在Android 8.0以后应用保活变得困难很多，但依然有一些方法可以突破。

既然讲到了将图片的内存放到Native中，我们比较熟悉的是Fresco图片库在Dalvik会把图片放到Native内存中。事实上在Android 5.0～Android 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使用情况》。

adb shell dumpsys meminfo <package_name|pid> [-d]


1. Java内存分配

有些时候我们希望跟踪Java堆内存的使用情况，这个时候最常用的有Allocation Tracker和MAT这两个工具。

在我曾经写过的《Android内存申请分析》里，提到过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 。遵循“谁最痛，谁最需要，谁优化”，所以Chromium出品了一大堆Native相关的工具。Android之前对AddressSanitize支持的不太好，需要root和一大堆的操作，但在Android 8.0之后，我们可以根据这个指南来使用AddressSanitize。目前AddressSanitize内存泄漏检测只支持x86_64 Linux和OS X系统，不过相信Google很快就可以支持直接在Android上进行检测了。

那我们有没有类似Allocation Tracker那样的Native内存分配工具呢？在这方面，Android目前的支持还不是太好，但Android Developer近来也补充了一些相关的文档，你可以参考《调试本地内存使用》。关于Native内存的问题，有两种方法，分别是Malloc调试和Malloc钩子。

Malloc调试可以帮助我们去调试Native内存的一些使用问题，例如堆破坏、内存泄漏、非法地址等。Android 8.0之后支持在非root的设备做Native内存调试，不过跟AddressSanitize一样，需要通过wrap.sh做包装。

adb shell setprop wrap.<APP> '"LIBC_DEBUG_MALLOC_OPTIONS=backtrace logwrapper"'


Malloc钩子是在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就提供了一个自定义的Allocation Tracker实现的示例，目前已经兼容到Android 8.1。你可以用它练习实现自动化的内存分析，有哪些对象占用了大量内存，以及它们是如何导致GC等。

欢迎你点击“请朋友读”，把今天的内容分享给好友，邀请他一起学习。最后别忘了在评论区提交今天的作业，我也为认真完成作业的同学准备了丰厚的“学习加油礼包”，期待与你一起切磋进步哦。



                        
                        
                            