00 前言
演讲人介绍
Rechard Uhler,Android Runtime 开发工程师。为便于写作,笔者将以第一人称视角对视频内容进行概述。
image
视频地址:
https://www.youtube.com/watch?v=w7K0jio8afM&list=PLpvKQarSfUV0l-fMxTrJIDF3IvwoXlGOA
02 正文
想要进行内存优化,就必须对 Android 内存管理机制有比较深入的了解,这样才能保证应用在低端机上也能有良好的表现。不同的内存类型,包括 Shared Memory,Dex Memory 以及 GPU Memory, 都会对用户体验产生影响。
我在过去的三年时间里,都在致力于深入理解 Android 应用内存管理机制。那么,为什么 App 开发工程师也要关注内存占用呢?于我而言,主要是因为 Android 生态系统。如果一个 Android 应用在低端设备上用户体验不好(比如经常卡顿),那么 OEM(Original Entrusted Manufacture) 就不愿再生产这样的设备,进而导致这部分用户被排除在 Android 生态系统之外。
本次课题主要讨论三点内容:
-
低内存时 Android 系统的工作机制
-
如何评估应用内存使用情况
-
如何减少应用内存占用
低内存时 Android 系统的工作机制
首先,需要介绍物理内存的概念,然后引入 Android Low Memory Killer。
-
物理内存
设备的物理内存被分为很多页(Page),每页 4KB。不同的页用来做不同的事情:
image橘黄色的是已使用页,黄色的是缓存页(数据在磁盘上有备份,所以 Cache Pages 是可以被回收的),绿色的是空闲页。
用于回收 Cached pages 的 kswapd 进程
这是一个 2G 内存的手机,X 轴表示使用时间,Y 轴表示内存使用情况。随着打开的应用越来越多,Used Pages 也越来越多,而 Cached Pages 和 Free Pages 则越来越少。当 Free Pages 低于 kswapd 的阈值时,Linux 内核就会通过 kswapd 进程对 Cached Pages 进行回收。当应用再次访问 Cached Pages 上的内容时,就需要从磁盘上重新加载。如果 Cached Pages 太少的话,设备就可能死机:
image所以,在 Android 上我们有个机制叫 Low Memory Killer,当 Cached Pages 太少时,就会被触发。它的工作方式是根据进程的优先级,选择性地杀死某个进程,释放该进程占用的所有资源以满足内存分配需要:
image如上图所示,当 Cached Pages 低于 LMK 阈值时,将会触发低内存杀死机制。
LMK(Low Memory Killer)
如果 LMK 杀掉的是用户正在交互或可以感知的进程,将会导致非常不友好的用户体验。所以 Android SystemServer 进程维护了一张进程优先级列表,LMK 根据这张表来决定先杀死哪个进程:
image-
Perceptible 指的是非用户直接交互的进程,比如在后台播放音乐的音乐播放器进程;
-
Previous 指的是切换至当前前台应用前的应用进程;
-
Cached 指缓存的进程,这可能是退至后台的应用进程,也可能是已经退出的应用进程,目的是为了实现应用间的快速切换。所以,Cached 进程也是优先级最低的进程:
如上图所示,当已用内存超过 LMK 阈值时,LMK 将从 Cached 列表底部开始杀死进程。如果可用内存还是不满足分配需要,那么将会按照上表所示优先级自底向上杀死进程,直到准备 Kill SystemServer 进程,这将导致手机重启。
所以,你可以想象 LMK 在低内存手机上的情景:
image如上图所示,LMK 将一直处于活跃状态,具体表现就是应用卡顿、桌面黑屏重启,手机死机等等。如此,OEM 将不愿生产这些设备。
评估应用内存使用情况
那么,我们怎么知道 App 使用了多少内存呢?
-
物理内存追踪
之前提到,设备的物理内存被分为很多页(Page),Linux Kernel 将会持续跟踪每个进程使用的 Pages,所以只要对进程使用的 Pages 进行计数即可:
image但实际情况远比这要复杂的多,因为有些 Pages 是进程间共享的:
image共享内存页计数方法
RSS(Resident Set Size):App 完全负责
imagePSS(Proportional Set Size):App 按比例负责,比如下图所示两个进程共享,那就负责一半。如果三个进程共享,那就负责三分之一:
imageUSS(Unique Set Size):App 无责:
image但实际上,至少需要系统级别的上下文才能知道识别 RSS 与 USS。所以通常都是使用 PSS 来计算,这也可以避免多计或者少计 Shared Pages。你可以使用:
adb shell dumpsys meminfo -s [process]
命令来查看一个进程的 PSS 使用情况:
image最底部的 TOTAL 代表的就是应用按比例占用的总内存大小。
应用内存占用分析
如果想要应用支持的功能越多,UI 越炫酷,那就需要更多的内存分配。既想马儿跑,又想马儿不吃草的事情是不存在的:
image- 内存占用影响因素
应用使用场景:很好理解,哪个页面比较炫、动效多、或者使用了 webview,那这个时候 App 占用的内存就高:
image平台配置:很好理解,比如手机的分辨率越高,相同 dp 的图片占用的内存就越大,所以高档手机上,App 的内存占用肯定比低档手机高:
image设备内存压力:设备内存越紧张,越可能触发 GC,导致 App 占用内存比设备内存充裕时低:
image所以,你应当在相同的内存压力下评估你的 App 内存占用:
image由于内存压力不好控制,所以建议评估前,先一键清理所有进程,然后再测试。
减少应用内存占用
使用 Android Studio 的 Memory Profiler,可以查看当前 Java 堆上分配了哪些对象、对象大小以及对象引用链和被引用链等很多信息。Live Allocation 中有 image heap、zygote heap、app heap 等可以选择,但是我建议你只关注 app heap。因为 image heap 和 zygote heap 是 App 启动时从系统继承过来的,对于这部分内存占用,我们基本上无能为力: image关于 Memory Profiler 的细节我不会讲太多,因为明天中午 12:30 Esteban 将会详细讲解 Profiler 的用法,毕竟这是他们团队开发的。所以,我强力推荐你们也参加一下明天的宣讲会。
Java Heap 以外的内存占用分析
上面提到,TOTAL 是 PSS,那么这张图中,除了 Java Heap,其它的是什么意思呢?对于这部分内存占用,我们又能做什么呢?
image
这就比较好玩了,因为这部分大多是由 Android 平台产生的,如果你真的想理解他们,那么你需要学习很多专业知识。比如 Framework 是如何实现 View 系统及 Resource 管理的,Native Code 是如何执行的,WebView 是如何工作的,Android Runtime 是如何执行你的代码的,HAL 如何管理你的 Graphics 以及 Linux 内核的虚拟内存管理方式等等。
顺便说一下,我生活在这儿,这个橘黄色的方块里(Android Runtime):
image
Android 平台产生的内存占用诊断
那么,对于平台产生的内存占用,我们需要使用工具来诊断吗?首先,我们可以使用:
adb shell dumpsys meminfo -a [process]
来查看更详细的信息(以下数据为笔者自己开发的 App 的内存占用情况):
Applications Memory Usage (in Kilobytes):Uptime: 498024399 Realtime: 1230430304** MEMINFO in pid 10898 [com.yuloran.wanandroid_java] ** Pss Pss Shared Private Shared Private SwapPss Heap Heap Heap Total Clean Dirty Dirty Clean Clean Dirty Size Alloc Free ------ ------ ------ ------ ------ ------ ------ ------ ------ ------ Native Heap 35822 0 824 35764 32 24 8740 75776 38786 36989 Dalvik Heap 4001 0 304 3552 72 412 240 6847 3424 3423 Dalvik Other 5256 0 48 5256 0 0 0 Stack 120 0 4 120 0 0 0 Ashmem 130 0 4 128 4 0 0 Gfx dev 2596 0 0 2596 0 0 0 Other dev 16 0 104 0 0 16 0 .so mmap 23782 22188 1132 504 13320 22188 15 .jar mmap 68 0 8 68 0 0 0 .apk mmap 8029 24 0 7684 1872 24 0 .ttf mmap 223 20 0 0 956 20 0 .dex mmap 21974 19864 0 20 13080 19864 0 .oat mmap 377 64 0 0 3620 64 0 .art mmap 6547 404 868 5852 7584 404 24 Other mmap 408 0 12 8 644 376 0 EGL mtrack 24660 0 0 24660 0 0 0 GL mtrack 4524 0 0 4524 0 0 0 Unknown 2130 0 184 2124 0 0 0 TOTAL 140702 42564 3492 92860 41184 43392 39 82623 42210 40412 Dalvik Details .Heap 3308 0 0 3308 0 0 0 .LOS 42 0 16 12 4 28 4 .LinearAlloc 4020 0 20 4020 0 0 0 .GC 384 0 16 384 0 0 0 .JITCache 596 0 0 596 0 0 0 .Zygote 583 0 288 164 68 384 0 .NonMoving 68 0 0 68 0 0 0 .IndirectRef 256 0 12 256 0 0 0 App Summary Pss(KB) ------ Java Heap: 9808 Native Heap: 35764 Code: 50436 Stack: 120 Graphics: 31780 Private Other: 8344 System: 4450 TOTAL: 140702 TOTAL SWAP PSS: 39 Objects Views: 207 ViewRootImpl: 1 AppContexts: 3 Activities: 1 Assets: 18 AssetManagers: 3 Local Binders: 24 Proxy Binders: 23 Parcel memory: 8 Parcel count: 34 Death Recipients: 3 OpenSSL Sockets: 0 WebViews: 0 SQL MEMORY_USED: 345 PAGECACHE_OVERFLOW: 55 MALLOC_SIZE: 117 DATABASES pgsz dbsz Lookaside(b) cache Dbname 4 20 41 17/38/5 /data/user/0/com.yuloran.wanandroid_java/databases/app_database.db 4 12 0/0/0 (attached) temp 4 20 40 3/19/4 /data/user/0/com.yuloran.wanandroid_java/databases/app_database.db (1)
-
Private Dirty Memory 类似于之前说过的 Used Memory;
-
Private Clean Memory 类似于 之前说过的 Cached Memory。
下面又介绍了几种工具,showmap、ahat、debug malloc等,略。。。因为他下面说到:
image总的来说就是:可以,但没必要。因为这需要了解很多专业知识,而且很多数据是可见但不可控的。
内存优化建议
- 优化 Java 堆上的对象
很多内存虽然不在 Java 堆分配,但是其生命周期跟 Java 堆上分配的对象相绑定:
image所以,优化 Java Heap 上的对象,也有助于其它类型内存的回收。
- 减小 apk 体积
因为很多在 apk 中占据磁盘空间的文件,在运行期也会占据内存空间:
image因为 apk 占据的磁盘空间大小是固定的,所以压缩 apk 大小比降低内存占用更容易。更多 apk 大小优化方法请查看 Best Practices to Slim Down Your App Size,视频地址为:
https://www.youtube.com/watch?v=AdfKNgyT438
02 结论言
本期视频主要讲述了 Android 的 Low Memory Killer 机制、如何评估应用的内存使用情况以及如何减少应用内存占用,来源于 Google Android Runtime 开发工程师 Rechard Uhler 的经验总结,可以说很靠谱了。
就笔者自身的开发经验来看,内存泄露比较容易解决,只是有的泄露是由于第三方 SDK 或者 Framework 导致的,此时只能通过反射来修复。如果反射也修复不了,但是不存在持续泄露,即仅泄露一次,也可以不作处理,或者通过商务推动去解决。而减少内存占用则比较困难,毕竟要想 App 功能丰富,那势必会占用更多的内存。而且现在很多项目是多人团队开发,每个人可能只负责一小块,对整个应用的掌控能力不足,进行内存调休就更困难了。所以,内存调优工作需要丰富的编程经验及架构经验,除了 Java 以外,还需要对 Android 的很多 UI 控件有比较深入的理解,因为在 Android 平台上,内存占用大头永远是 UI,主要是 Bitmap。
内存优化,任重而道远。
【附】相关架构及资料
image资料领取
点赞+加群免费获取 Android 架构设计③群
加群领取获取往期Android高级架构资料、源码、笔记、视频。高级UI、性能优化、架构师课程、NDK、混合式开发(ReactNative+Weex)微信小程序、Flutter全方面的Android进阶实践技术
网友评论