系列文章
Java 内存模型
Android 系统内存管理机制
Android 性能优化(三)之内存管理
Android 性能优化(四)之内存优化实战
系统层内存管理:针对的是多个进程的管理
应用层内存管理:针对的是本应用进程的管理
关键字
LMK (Low Memory Killer)
oom_adj (out of memory, adjuster)
ProcessList
ProcessRecord
1. Android 内存特点
不关程序会不会更耗电?
安卓应用切换到后台,其实已经暂停了,并不会消耗cpu资源,只保留了运行状态,所以有的程序切出去重进会到主界面。
服务可以在后台持续运行,所有耗电只有带服务的应用。
在内存少的时候运行大型程序会慢?
因为内存不足会触发进程调度策略选择性关闭,这是十分消耗系统资源的操作。频繁的调度自然会拖慢系统。
Android 属于 Linux 系统
现象:Linux 经常发现空闲内存很少,似乎所有的内存都被系统占用了,表面是内存不足,实际是 Linux 内存管理的优秀特性。
特点:无论物理内存多大,Linux 都将其充分利用,缓存到内存提高性能,充分发挥硬件投资带来的好处。
而 window 的内存管理,只在需要内存时,才为应用分配内存,不能充分利用,硬件投资相当于摆设。
Android 每一个应用都带有独立的虚拟机,由垃圾回收机制来管理内存。
内存调度有个阈值,当剩余内存低于这个值,系统才会根据策略关闭用户不需要的东西。当这个阈值很小时,内存总会在此值徘徊,但事实上不影响速度,相反加快了下次启动速度。
这种设计,下次启动应用会更快,因为不需要读取界面资源,所以用户平时不用习惯一键清理抹杀掉安卓这个优点。
2. 进程分类和优先级
两个角度进程的分类:LMK 和 AMS
以 LMK 角度划分
每一类别的进程会有对应的oom_adj的值,oom_adj 越高代表进程越不重要,LMK会先从值高的开始杀。
系统 LMK 机制下对于进程的级别以变量的形式定义在类 ProcessList
中,可总结如下:
注意:oom_adj 的值和
ProcessList
的值不同
名称 | oom_adj | 描述 | 备注 |
---|---|---|---|
cached | 9~16 | 缓存进程 | 第一类: 只有activity的进程退到后台; 第二类:empty进程,已执行receiver、service、provider; 第三类:没有stopService且内含activity的进程 |
B service | 8 | 服务 | 同SERVICE_ADJ,只是优先级低 |
previous | 7 | 上一个进程 | - |
home | 6 | Home进程 | - |
A service | 5 | 服务 | startService且不包含activity的进程 |
heavy weight | 4 | 后台重量级进程 | - |
backup | 3 | 备份进程 | - |
perceptible | 2 | 可感知进程 | startService且调用了 startForeground |
visible | 1 | 可见进程 | 第一类:非前台但可见的应用; 第二类:使用service或provider的常驻进程 |
foreground | 0 | 前台进程 | 第一类:当前正在前台的应用; 第二类:应用正在运行onReceive或Service.onCreate/onStartCommand |
persistent | -11 | 常驻进程 | AndroidManifest声明persistent=true |
system | -16 | 系统进程 | system进程 |
native | -17 | Native进程 | - |
注意:
没有 stopService 其内含 activity 的后台进程
从 LMK 的角度划分为 cached,因为这类进程含有 activity 往往占有较大的内存,即使包含了Service,LMK 也会更倾向于杀死这类进程。
从 AMS 的角度划分为 PROCESS_STATE_SERVICE,即视为服务进程,以服务进程执行相关逻辑。
此外使用dumpsys meminfo
查看所有进程,这类进程被列在 B Service 类别。perceptible 的标准
名为可感知进程,标准为服务是否执行了 startForeground 动作,与自己的 "感知" 无关。A Service 和 B Service 的区别?
所有启动了服务进程,且该服务所在的进程没有显示过UI,且该服务未执行 startForeground动作(执行后为perceptible),那么该进程为A Service 或 B Service中的一种。
根据这类服务进程所处于 Lru 进程表中的位置,前 1/3 为 A Service,其余的为 B Service。
以 AMS 角度划分
上表带分级只是从 LMK 角度来分的,时用于 LMK 执行杀进程操作,但是从android的系统管理角度看,即是从 AMS 执行相关逻辑时,又有一套自己的分级机制,当然这两套机制也有着很多互通的点。AMS角度的级别划分以变量的形式定义在 ActivityManager
类中,以 PROCESS_STATE
开头的变量。
内存优化配置
除了上述程序重要性分类,Android系统还维护着一张表,以 N1 测试机为例:
名称 | adj | 剩余内存警戒值(以4K为单位) |
---|---|---|
前台进程 | 0 | 1536 |
用户可见进程 | 1 | 2048 |
次要服务 | 2 | 4096 |
后台进程 | 7 | 5120 |
内容提供者 | 14 | 5632 |
空进程 | 15 | 6144 |
上述表由 /sys/module/lowmemorykiller/parameters/adj
和 /sys/module/lowmemorykiller/parameters/minfree
两个文件组成。
举例:当可用内存小于 6144 * 4K = 24M 时,开始杀空进程。
内存优化模式配置:
1:游戏配置
Foreground:6
Visible:8
Secondary Server:16
Hiden App:80
Content Provider:90
Empty:100
配置理念:压榨后台进程,内容供应节点和空进程,将内存尽可能多得留给前台进程和系统,提升游
戏速度和浏览器体验。
优点:程序启动和运行的速度最快
缺点:多任务处理不理想,开启程序较多时,后台进程会被终止
- 多任务配置
Foreground:6
Visible:8
Secondary Server:16
Hiden App:20
Content Provider:60
Empty:100
配置理念:压榨空进程,给内容供应节点留有一定空间,最大限度提升后台程序的使用空间,提升多
任务的处理能力
优点:运行多个程序时,由于可支配内存较多,后台程序不容易被终止
缺点:程序启动的速度和整体系统的运行速度可能会比游戏玩家配置略慢一些,由于经常运行多任务,平时系统的响应速度会受到一定影响。
- 轻度用户/女生专用配置
Foreground:6
Visible:8
Secondary Server:16
Hiden App:24
Content Provider:32
Empty:48
配置理念:压榨空进程,给内容供应节点留有一定空间,最大限度提升后台程序的使用空间,提升多
任务的处理能力
优点:比较均衡的配置,提升了系统的可用内存,使得系统的整体速度得到了提高,拉开了各级进程
的管理策略层次,使得管理机制更有效率
缺点:比较均衡的配置,无明显缺点
查看应用 oom_adj 级别方式
- dumpsys meminfo
- cat /proc/[PID]/oom_adj
示例如下:
▶ adb shell
* daemon not running; starting now at tcp:5037
* daemon started successfully
shell@hwALE-H:/ $ dumpsys meminfo
Applications Memory Usage (kB):
Uptime: 5115041 Realtime: 402373211
Total PSS by process:
138697 kB: system (pid 3691)
...
7 kB: filebackup (pid 2335)
Total PSS by OOM adjustment:
131659 kB: Native
64551 kB: surfaceflinger (pid 2316)
11285 kB: zygote64 (pid 2348)
4846 kB: zygote (pid 2349)
4761 kB: /init (pid 1)
320 kB: dumpsys (pid 30978)
312 kB: adbd (pid 3095)
136 kB: lmkd (pid 2313)
109 kB: installd (pid 2337)
90 kB: watchdogd (pid 5626)
86 kB: watchdogd (pid 3162)
78 kB: systeminfo (pid 2333)
...
138697 kB: System
138697 kB: system (pid 3691)
237136 kB: Persistent
84924 kB: com.android.keyguard (pid 4371)
48353 kB: com.android.systemui (pid 4478)
23573 kB: com.huawei.android.powermonitor (pid 4809)
22288 kB: com.android.supl (pid 4752)
20486 kB: com.android.phone (pid 4718)
14955 kB: com.huawei.powergenie (pid 4656)
9339 kB: com.android.contacts:cacheservice (pid 5143)
7123 kB: com.huawei.ca (pid 4783)
6095 kB: com.android.server.telecom (pid 4689)
74413 kB: Foreground
50407 kB: com.huawei.android.launcher (pid 4834 / activities)
24006 kB: com.huawei.systemmanager:service (pid 5233)
137104 kB: Visible
48904 kB: com.kingroot.kinguser:service (pid 4399)
32678 kB: com.google.process.gapps (pid 30506)
26773 kB: com.google.process.location (pid 30473)
18463 kB: com.huawei.lcagent (pid 5901)
6165 kB: com.baidu.map.location (pid 4975)
4121 kB: com.android.smspush (pid 5340)
183306 kB: Perceptible
81549 kB: com.huawei.hwid.persistent (pid 6324)
42141 kB: com.tencent.mm:push (pid 8269)
34894 kB: com.tencent.mm (pid 5373)
7750 kB: com.huawei.android.hsf (pid 27584)
...
61296 kB: A Services
33319 kB: com.huawei.appmarket (pid 26853)
27977 kB: com.tencent.mobileqq:MSF (pid 6268)
87169 kB: Previous
87169 kB: com.tencent.mobileqq (pid 24079 / activities)
111384 kB: B Services
62802 kB: com.chengdudaily.activity (pid 11891)
12519 kB: com.huawei.android.pushagent.PushService (pid 9649)
8971 kB: android.process.media (pid 30586)
7545 kB: com.android.settings (pid 28373)
...
136887 kB: Cached
10925 kB: com.android.email (pid 26878)
4618 kB: com.huawei.android.ds (pid 27434)
...
Total PSS by category:
378913 kB: Dalvik
357987 kB: Native
...
0 kB: Memtrack
Total RAM: 1904064 kB (status normal)
Free RAM: 697383 kB (136887 cached pss + 529120 cached + 31376 free)
Used RAM: 1299280 kB (1162164 used pss + 8080 buffers + 1192 shmem + 127844 slab)
Lost RAM: -92599 kB
ZRAM: 8472 kB physical used for 41500 kB in swap (374780 kB total swap)
KSM: 60680 kB saved from shared 2504 kB
66380 kB unshared; 158400 kB volatile
Tuning: 192 (large 512), oom 325000 kB, restore limit 108333 kB (high-end-gfx)
3. 未控制好 oom_adj 的案例
3.1 UI 进程启动 Service 的隐患
案例a:备份进程启动一个服务开始执行备份,备份服务运行在ui进程(服务未调用startForeground())
备份服务一般需要很长时间。
隐患:易导致备份失败。
用户点击 Home 键退出到后台,备份进程处于 Previous进程。
继续使用其他应用,会是使得备份进程处于cch-started-ui-services的状态,即是启动了服务并且包含ui的进程退到后台状态,此时划分为 Cache 进程。
如果在较长的备份过程,触发了 LMK,很容易导致备份失败。
案例b:备份进程启动一个服务开始执行备份,备份服务运行在ui进程(服务调用了startForeground())
隐患:增加了卡顿场景出现的几率。
此进程划分为 Perceptible 进程,基本上不会被 LMK 杀掉,但含ui的进程占用了较大的内存,慢慢会导致系统进入内存较低的等级,触发系统进程回收动作,容易导致卡顿场景的出现。
此外,调用了 startForeground() 会被判定为前景进程,抢占前台应用的 cpu 资源,增加卡顿场景的几率。
** 解决方法:将Service运行在独立的进程,没有UI**
这样退出到后台,备份进程就会处于 A Service 慢慢掉落到 B Service,比 Cached 优先级高,不容易被 LMK 杀掉。
是否需要 startForeground()? 如果需要尽快完成,可牺牲一些用户体验,将服务推到前景应用,但切记做到 UI 和 Service 分离。
3.2 使用线程解决耗时操作导致的ANR问题的隐患
案例:短信、邮件、或笔记本应用,在用户按BACK键时存下草稿
方式1:直接存储
public class MyActivity extents Activity {
public void onPause(){
//存储草稿
}
}
问题:由于存储草稿定操作一般时保存到数据库,某些情况下可能会占用较长时间,这里就有可能导致anr的隐患
方式2: 切换子线程存储
public class MyActivity extents Activity {
public void onPause(){
new Thread() {
public void run() {
//存储草稿
}
}.start()
}
}
线程无法影响OOM_ADJ,只有进程的行为能影响
问题:当用户以back键离开应用时(以home键离开会处于previous状态),应用退到后台处于empty-cached状态,内存不足时,可能会立刻杀。
方式3: 采用服务
问题: 如果用服务来存储草稿,即将存储草稿动作写在onStartCommand中,由于onStartCommmand操作依旧是执行在主线程的,所以在其中执行耗时操作时,依旧可能会导致ANR
最终方案:使用IntentService来执行保存草稿的动作
public class MyActivity extents Activity {
public void onPause(){
...
startService(new Intent(this, MyIntentService.class));
...
}
}
public class MyIntentService extends IntentService {
protected void onHandleIntent(Intent intent) {
//保存草稿
}
}
3.2 provider 被绑定导致的隐患
案例:systemui进程获取手机中的手机管家应用提供的content provider,用于获取当前应用相关信息
问题:管家应用的UID为System,在Android机制中,System UID进程提供的provider一旦被访问,即使访问完成关掉provider后,连接依旧存在,所有管家应用由于其provider持续被persistent进程咬住,所以管家应用便会长时间处于foreground级别的应用中,oom_adj为0,导致管家应用占用的大量内存很难被回收
解决方案:单独使用一个进程提供provider,提供provider进程由于占用内存较小,所以即使无法被回收也影响较小,这样管家应用的UI进程能够按照系统正常的回收流程在需要时被回收
4. OOM_ADJ与编程
线程无法影响OOM_ADJ,只有进程的行为能影响
4.1 Broadcast Receiver
- 在执行onReceive() 时,应用的OOM_ADJ 为0
- 离开onReceive() 后,视应用其他的状态决定OOM_ADJ
4.2 Started Service
在执行onCreate()、onStartCommand() 时,应用的OOM_ADJ 为0。之后依序经历几种变化:
- Service A:Service 刚运行时
- Service B:若系统同时运行的Service 过多,较早运行的Service 会放到Service B
- Cached:运行超过30分钟、或曾经启动过activity
- Foreground service 能保持OOM_ADJ 处于2,且跟前台应用抢占CPU 时有相同优先级。但滥用foreground service,会导致内存问题
4.3 Bind Service
- 若进程A bind 住进程B 的service,ActivityManager会将B的重要性提升至与A 相同。如果A使用完服务后,忘了调用unbindService(),B的重要性就降不下来,极端情况是,若A是常驻的,会导致B也变成常驻了,引发系统性内存问题
- 若进程A bind住的service位在同一进程,则OOM_ADJ 不会因此有任何改变
4.4 Provider
- 若进程A查询进程B的provider得到Cursor,在进程A关闭Cursor之前,ActivityManager会保持provider connection,此时B的OOM_ADJ将提升至与A相同。若进程A属于常驻应用,则B也跟着变成常驻了,就形成严重的内存问题
- 若应用B的UID为system UID,不可开放content provider给其他应用使用。在安卓的特殊机制中,只要进程A存取(insert/update/delete/pquery) 了B的provider,provider connection 就不再结束
4.5 常驻应用
狭义: 在 AndroidManifest.xml 设定 persistent =true 的应用
广义:应用透过某种方式,直接、或间接持续运行在内存都算,例如:
- 不停止的started service
- 常驻应用在后台透过bind service 绑住另一进程、且不释放
- 常驻应用在后台透过provider 绑住另一进程、且不释放
- 应用透过AlarmManager或类似方式,不断启动,导致被砍了也频繁重启运行在后台
5. 日常开发注意事项
- 避免服务进程常驻内存,任务完成及时调用 stopService 或 stopSelf。
- ui 和 service 分离,避免后台进程占用较大的 ui 内存资源
- 常驻进程(oom_adj <=6 HOME),避免长时间绑定其他进程服务或provider,用完马上释放。
- 不可使用System UID 进程内的provider,在Android设计中,即使已关掉provider仍会保持连接
5.利用onStartCommand方法回传值(START_STICKY,START_NOT_STICKY等4种模式)控制好服务的重启属性,在设计时充分考虑进程被lmk杀死的情况
6.IntentService继承自Service,针对CPU scheduling、工作排程等都有完整实现,建议多采用IntentnService进行功能实现
参考资料
Android内存管理机制
Android内存管理篇 - adj的概念与进程adj级别控制
Android的OOM_ADJ
Overview of Android Memory Management
Investigating Your RAM Usage
Android内存优化之OOM