内容简介:随着Android生态的多年发展,现在4GB 内存的手机都变成了主流,2008 年的手机只有可怜的 140MB 内存,而今年的华为Mate 20 Pro 手机的内存已经达到了 8GB,在以前低内存设备更容易出现内存不足引起的异常和卡顿,我们也可以通过查看应用中用户的手机内存在 2GB 以下所占的比例来评估,所以在优化前要先定好自己的目标,这一点非常关键。比如针对2GB 以上的设备,完全是两种不同的优化思路。1、内存占用越少越好。应用是否占用了过多的内存,跟设备、系统和当时情况有关,而不是 300MB、400
随着Android生态的多年发展,现在4GB 内存的手机都变成了主流,2008 年的手机只有可怜的 140MB 内存,而今年的华为Mate 20 Pro 手机的内存已经达到了 8GB,在以前低内存设备更容易出现内存不足引起的异常和卡顿,我们也可以通过查看应用中用户的手机内存在 2GB 以下所占的比例来评估,所以在优化前要先定好自己的目标,这一点非常关键。比如针对2GB 以上的设备,完全是两种不同的优化思路。
内存优化误区:
1、内存占用越少越好。应用是否占用了过多的内存,跟设备、系统和当时情况有关,而不是 300MB、400MB 这样一个绝对的数值。当系统内存充足的时候,我们可以多用一些获得更好的性能。当系统内存不足的时候,希望可以做到“用时分配,及时释放”,当系统内存出现压力时,能够迅速释放各种缓存来减少系统压力。
- 在 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,它可以减少图片内存并提升绘制效率。
2、Native 内存不用管。虽然 Android 8.0 重新将 Bitmap 内存放回到Native中,那么我们是不是就可以随心所欲地使用图片呢?答案当然是否定的。正如前面所说当系统物理内存不足时,从后台、桌面、服务、前台,直到手机重启。lmk 开始杀进程,系统构想的场景就像下面这张图描述的一样,大家有条不絮的按照优先级排队等着被 kill。low memory killer 的设计,是假定我们都遵守 Android 规范,但并没有考虑到中国国情。国内很多应用就像是打不死的小强,杀死一个拉起五个。频繁的杀死、拉起进程,又会导致 system server 卡死。当然在 Android 8.0 以后应用保活变得困难很多,但依然有一些方法可以突破。只是流程复杂些。
内存造成问题:
之前的崩溃优化中提到“内存优化”是崩溃优化工作中非常重要的一部分,类似 OOM,很多的“异常退出”其实都是由内存问题引起。内存主要会引发两方面问题
1、 异常。 异常包括 OOM、内存分配失败这些崩溃,也包括因为整体内存不足导致应用被杀死、设备重启等问题。
2、卡顿。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 的详细性能信息,另外我们还可以使用 systrace 来观察 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 复制代码
着手内存优化:
1、设备分级。
- 相信你肯定遇到过,同一个应用在 4GB 内存的手机运行得非常流畅,但在 1GB 内存的手机就不一定可以做到,而且在系统空闲和繁忙的时候表现也不太一样。所以内存优化首先需要根据设备环境来综合考虑,Facebook 有一个叫 device-year-class 的开源库,它会用年份来区分设备的性能。使用类似 device-year-class 的策略对设备分级,对于低端机用户可以关闭复杂的动画,或者是某些功能;使用 565 格式的图片,使用更小的缓存内存等。在现实环境下,不是每个用户的设备都跟我们的测试机一样高端,在开发过程我们要学会思考功能要不要对低端机开启、在系统资源吃紧的时候能不能做降级。
- 缓存管理。我们需要有一套统一的缓存管理机制,可以适当地使用内存;当“系统有难”时,也要义不容辞地归还。我们可以使用 OnTrimMemory 回调,根据不同的状态决定释放多少内存。对于大项目来说,可能存在几十上百个模块,统一缓存管理可以更好地监控每个模块的缓存大小。
- 安装包大小。安装包中的代码、资源、图片以及 so 库的体积,跟它们占用的内存有很大的关系。一个 80MB 的应用很难在 512MB 内存的手机上流畅运行,这种情况我们需要考虑针对低端机用户推出 4MB 的轻量版本,例如 Facebook Lite、今日头条极速版都是这个思路。
Bitmap 内存一般占应用总内存很大一部分,所以做内存优化永远无法避开图片内存这个“永恒主题”。
- 统一图片库。图片内存优化的前提是收拢图片的调用,这样我们可以做整体的控制策略。例如低端机使用 565 格式、更加严格的缩放算法,可以使用 Glide、Fresco 或者采取自研都可以。而且需要进一步将所有 Bitmap.createBitmap、BitmapFactory 相关的接口也一并收拢。
- 统一监控。第一是大图片监控:我们需要注意某张图片内存占用是否过大,例如长宽远远大于 View 甚至是屏幕的长宽。在开发过程中,如果检测到不合规的图片使用,应该立即弹出对话框提示图片所在的 Activity 和堆栈,让开发同学更快发现并解决问题。在灰度和线上环境下可以将异常信息上报到后台,我们可以计算有多少比例的图片会超过屏幕的大小,也就是图片的“超宽率”。第二是重复图片监控:重复图片指的是 Bitmap 的像素数据完全一致,但是有多个不同的对象存在。这个监控不需要太多的样本量,一般只在内部使用。第三是图片总内存:通过收拢图片使用,我们还可以统计应用所有图片占用的内存,这样在线上就可以按不同的系统、屏幕分辨率等维度去分析图片内存的占用情况。在 OOM 崩溃的时候,也可以把图片占用的总内存、Top N 图片的内存都写到崩溃日志中,帮助我们排查问题。
- Java内存泄漏。建立类似LeakCanary 自动化检测方案,至少做到 Activity 和 Fragment 的泄漏检测。在开发过程,我们希望出现泄漏时可以弹出对话框,让开发者更加容易去发现和解决问题。内存泄漏监控放到线上并不容易,我们可以对生成的 Hprof 内存快照文件做一些优化裁剪大部分图片对应的 byte 数组减少文件大小。比如一个 100MB 的文件裁剪后一般只剩下 30MB 左右。使用 7zip 压缩最后小于 10MB,增加了文件上传的成功率。
- OOM监控。美团有一个 Android 内存泄露自动化链路分析组件Probe,它在发生 OOM 的时候生成 Hprof 内存快照,然后通过单独进程对这个文件做进一步的分析。不过在线上使用这个 工具 风险还是比较大,在崩溃的时候生成内存快照有可能会导致二次崩溃,而且部分手机生成 Hprof 快照可能会耗时几分钟,对用户造成的体验影响会比较大。另外,部分 OOM 是因为虚拟内存不足导致,这块需要具体问题具体分析。
- Native内存泄漏监控。Malloc 调试(Malloc Debug)和 Malloc钩子(Malloc Hook)似乎还不是那么稳定。在 WeMobileDev 最近的一篇文章 《微信 Android 终端内存优化实践》 中,微信也做了一些其他方案上面的尝试。
- 针对无法重编so的情况。使用 PLT Hook 拦截库的内存分配函数,其中 PLT Hook 是 Native Hook 的一种方案,然后重定向到我们自己的实现后记录分配的内存地址、大小、来源 so 库路径等信息,定期扫描分配与释放是否配对,对于不配对的分配输出我们记录的信息。
- 针对可重编的so情况。通过 GCC 的“-finstrument-functions"参数给所有函数插桩,桩中模拟调用栈入栈出栈操作;通过 ld 的“–wrap”参数拦截内存分配和释放函数,重定向到我们自己的实现后记录分配的内存地址、大小、来源 so 以及插桩记录的调用栈此刻的内容,定期扫描分配与释放是否配对,对于不配对的分配输出我们记录的信息。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 1.内存优化(一)内存泄漏
- 内存泄露与内存溢出的区别
- 谈谈对物理内存和虚拟内存的理解以及内存分配原理,一文彻底搞懂
- [译] 图解 Go 内存管理与内存清理
- Swoole 源码分析——内存模块之共享内存
- Swoole 源码分析——内存模块之内存池
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。