分析并优化 Android 应用内存

本文深入探讨Android内存管理机制,解析LowMemoryKiller工作原理,提供应用内存使用情况评估方法及内存优化策略。

640?wx_fmt=png


今日科技快讯


1月15日,据CNET报道,在美国联邦贸易委员会(FTC)周一对高通提起的反垄断诉讼中,苹果公司首席运营官杰夫·威廉姆斯(Jeff Williams)证实,苹果曾希望在最新款iPhone手机中使用高通的4G LTE处理器,但后者拒绝向苹果出售该芯片,这拖累了苹果向5G转型的速度。 


作者简介


本篇转载自 Yuloran 的翻译文章。文章对Android 应用内存占用方面的知识进行了不错的讲解,希望对大家有所帮助。

Yuloran 的博客地址:

https://juejin.im/user/57137c67df0eea00649d01b5


开始


演讲人介绍

Rechard Uhler,Android Runtime 开发工程师。为便于写作,笔者将以第一人称视角对视频内容进行概述。

640?wx_fmt=other

视频地址

https://www.youtube.com/watch?v=w7K0jio8afM&list=PLpvKQarSfUV0l-fMxTrJIDF3IvwoXlGOA


正文


想要进行内存优化,就必须对 Android 内存管理机制有比较深入的了解,这样才能保证应用在低端机上也能有良好的表现。不同的内存类型,包括 Shared Memory,Dex Memory 以及 GPU Memory, 都会对用户体验产生影响。

我在过去的三年时间里,都在致力于深入理解 Android 应用内存管理机制。那么,为什么 App 开发工程师也要关注内存占用呢?于我而言,主要是因为 Android 生态系统。如果一个 Android 应用在低端设备上用户体验不好(比如经常卡顿),那么 OEM(Original Entrusted Manufacture) 就不愿再生产这样的设备,进而导致这部分用户被排除在 Android 生态系统之外。

本次课题主要讨论三点内容:

  • 低内存时 Android 系统的工作机制

  • 如何评估应用内存使用情况

  • 如何减少应用内存占用

低内存时 Android 系统的工作机制

首先,需要介绍物理内存的概念,然后引入 Android Low Memory Killer。

  • 物理内存

设备的物理内存被分为很多页(Page),每页 4KB。不同的页用来做不同的事情:

640?wx_fmt=other

橘黄色的是已使用页,黄色的是缓存页(数据在磁盘上有备份,所以 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 太少的话,设备就可能死机:

640?wx_fmt=other

所以,在 Android 上我们有个机制叫 Low Memory Killer,当 Cached Pages 太少时,就会被触发。它的工作方式是根据进程的优先级,选择性地杀死某个进程,释放该进程占用的所有资源以满足内存分配需要:

640?wx_fmt=other

如上图所示,当 Cached Pages 低于 LMK 阈值时,将会触发低内存杀死机制。

LMK(Low Memory Killer)

如果 LMK 杀掉的是用户正在交互或可以感知的进程,将会导致非常不友好的用户体验。所以 Android SystemServer 进程维护了一张进程优先级列表,LMK 根据这张表来决定先杀死哪个进程:

640?wx_fmt=other

  • Perceptible 指的是非用户直接交互的进程,比如在后台播放音乐的音乐播放器进程;

  • Previous 指的是切换至当前前台应用前的应用进程;

  • Cached 指缓存的进程,这可能是退至后台的应用进程,也可能是已经退出的应用进程,目的是为了实现应用间的快速切换。所以,Cached 进程也是优先级最低的进程:

640?wx_fmt=other

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

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

640?wx_fmt=other

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

评估应用内存使用情况

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

  • 物理内存追踪

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

640?wx_fmt=other

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

640?wx_fmt=jpeg

共享内存页计数方法

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

640?wx_fmt=other

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

640?wx_fmt=jpeg

USS(Unique Set Size):App 无责:

640?wx_fmt=jpeg

但实际上,至少需要系统级别的上下文才能知道识别 RSS 与 USS。所以通常都是使用 PSS 来计算,这也可以避免多计或者少计 Shared Pages。你可以使用:

adb shell dumpsys meminfo -s [process] 

命令来查看一个进程的 PSS 使用情况:

640?wx_fmt=other

最底部的 TOTAL 代表的就是应用按比例占用的总内存大小。

应用内存占用分析

如果想要应用支持的功能越多,UI 越炫酷,那就需要更多的内存分配。既想马儿跑,又想马儿不吃草的事情是不存在的:

640?wx_fmt=jpeg

  • 内存占用影响因素

应用使用场景:很好理解,哪个页面比较炫、动效多、或者使用了 webview,那这个时候 App 占用的内存就高:

640?wx_fmt=other

平台配置:很好理解,比如手机的分辨率越高,相同 dp 的图片占用的内存就越大,所以高档手机上,App 的内存占用肯定比低档手机高:

640?wx_fmt=other

设备内存压力:设备内存越紧张,越可能触发 GC,导致 App 占用内存比设备内存充裕时低:

640?wx_fmt=other

所以,你应当在相同的内存压力下评估你的 App 内存占用:

640?wx_fmt=jpeg

由于内存压力不好控制,所以建议评估前,先一键清理所有进程,然后再测试。

减少应用内存占用

使用 Android Studio 的 Memory Profiler,可以查看当前 Java 堆上分配了哪些对象、对象大小以及对象引用链和被引用链等很多信息。Live Allocation 中有 image heap、zygote heap、app heap 等可以选择,但是我建议你只关注 app heap。因为 image heap 和  zygote heap 是 App 启动时从系统继承过来的,对于这部分内存占用,我们基本上无能为力:

640?wx_fmt=other

关于 Memory Profiler 的细节我不会讲太多,因为明天中午 12:30 Esteban 将会详细讲解 Profiler 的用法,毕竟这是他们团队开发的。所以,我强力推荐你们也参加一下明天的宣讲会。

Java Heap 以外的内存占用分析

上面提到,TOTAL 是 PSS,那么这张图中,除了 Java Heap,其它的是什么意思呢?对于这部分内存占用,我们又能做什么呢?

640?wx_fmt=other

这就比较好玩了,因为这部分大多是由 Android 平台产生的,如果你真的想理解他们,那么你需要学习很多专业知识。比如 Framework 是如何实现 View 系统及 Resource 管理的,Native Code 是如何执行的,WebView 是如何工作的,Android Runtime 是如何执行你的代码的,HAL 如何管理你的 Graphics 以及 Linux 内核的虚拟内存管理方式等等。

顺便说一下,我生活在这儿,这个橘黄色的方块里(Android Runtime):

640?wx_fmt=other

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等,略。。。因为他下面说到:

640?wx_fmt=jpeg

总的来说就是:可以,但没必要。因为这需要了解很多专业知识,而且很多数据是可见但不可控的。

内存优化建议

  • 优化 Java 堆上的对象

很多内存虽然不在 Java 堆分配,但是其生命周期跟 Java 堆上分配的对象相绑定:

640?wx_fmt=jpeg

所以,优化 Java Heap 上的对象,也有助于其它类型内存的回收。

  • 减小 apk 体积

因为很多在 apk 中占据磁盘空间的文件,在运行期也会占据内存空间:

640?wx_fmt=jpeg

因为 apk 占据的磁盘空间大小是固定的,所以压缩 apk 大小比降低内存占用更容易。更多 apk 大小优化方法请查看 Best Practices to Slim Down Your App Size,视频地址为:

https://www.youtube.com/watch?v=AdfKNgyT438


结语


本期视频主要讲述了 Android 的 Low Memory Killer 机制、如何评估应用的内存使用情况以及如何减少应用内存占用,来源于 Google Android Runtime 开发工程师 Rechard Uhler 的经验总结,可以说很靠谱了。

就笔者自身的开发经验来看,内存泄露比较容易解决,只是有的泄露是由于第三方 SDK 或者 Framework 导致的,此时只能通过反射来修复。如果反射也修复不了,但是不存在持续泄露,即仅泄露一次,也可以不作处理,或者通过商务推动去解决。而减少内存占用则比较困难,毕竟要想 App 功能丰富,那势必会占用更多的内存。而且现在很多项目是多人团队开发,每个人可能只负责一小块,对整个应用的掌控能力不足,进行内存调休就更困难了。所以,内存调优工作需要丰富的编程经验及架构经验,除了 Java 以外,还需要对 Android 的很多 UI 控件有比较深入的理解,因为在 Android 平台上,内存占用大头永远是 UI,主要是 Bitmap。

内存优化,任重而道远。


640?wx_fmt=jpeg

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值