分析并优化 Android 应用内存

android studio 下载 | 2019-03-19 13:27

8点20分,第一时间与你相约

每日英文

The most painful goodbyes are the ones never said, but the heart already knows it's over.

最痛苦的一种再见是从未说出口,但心里却清楚,一切都已结束。

小乐有话说

永远不是一种距离,而是一种决定。我只是一个疯子,请别介意我的语无伦次。

来自:Yuloran,链接:juejin.im/user/57137c67df0eea00649d01b5

封面来自网络

00 前言

&list=PLpvKQarSfUV0l-fMxTrJIDF3IvwoXlGOA

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

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

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

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

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

如何减少应用内存占用

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

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

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

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

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

LMK(Low Memory Killer)

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

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

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

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

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

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

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

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

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

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

共享内存页计数方法

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

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

USS(Unique Set Size):App 无责:

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

adb shell dumpsys meminfo -s [process] 命令来查看一个进程的 PSS 使用情况:

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

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

内存占用影响因素

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

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

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

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

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

减少应用内存占用

那么,对于平台产生的内存占用,我们需要使用工具来诊断吗?首先,我们可以使用:

adb shell dumpsys meminfo -a [process]来查看更详细的信息(以下数据为笔者自己开发的 App 的内存占用情况):

Applications Memory Usage (in Kilobytes):Uptime: 9 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等,略。。。因为他下面说到:

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

优化 Java 堆上的对象

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

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

减小 apk 体积

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

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

02 结论言

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

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

内存优化,任重而道远。

这里有你需要的编程技术、心得、经验(数据结构与算法、源码分析等),这里不止限于技术!还有职场心得、生活感悟、以及面经等。关注公众号,第一时间送达!