分析并优化 Android 应用内存

作者: cff70524f5cf | 来源:发表于2019-03-19 19:51 被阅读8次
    image

    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 进程也是优先级最低的进程:

    image

    如上图所示,当已用内存超过 LMK 阈值时,LMK 将从 Cached 列表底部开始杀死进程。如果可用内存还是不满足分配需要,那么将会按照上表所示优先级自底向上杀死进程,直到准备 Kill SystemServer 进程,这将导致手机重启。

    所以,你可以想象 LMK 在低内存手机上的情景:

    image

    如上图所示,LMK 将一直处于活跃状态,具体表现就是应用卡顿、桌面黑屏重启,手机死机等等。如此,OEM 将不愿生产这些设备。

    评估应用内存使用情况

    那么,我们怎么知道 App 使用了多少内存呢?

    • 物理内存追踪

    之前提到,设备的物理内存被分为很多页(Page),Linux Kernel 将会持续跟踪每个进程使用的 Pages,所以只要对进程使用的 Pages 进行计数即可:

    image

    但实际情况远比这要复杂的多,因为有些 Pages 是进程间共享的:

    image

    共享内存页计数方法

    RSS(Resident Set Size):App 完全负责

    image

    PSS(Proportional Set Size):App 按比例负责,比如下图所示两个进程共享,那就负责一半。如果三个进程共享,那就负责三分之一:

    image

    USS(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进阶实践技术

    相关文章

      网友评论

        本文标题:分析并优化 Android 应用内存

        本文链接:https://www.haomeiwen.com/subject/xilimqtx.html