Android内存管理篇 - adj的概念与进程adj级别控制
本文主要介绍Android的lowmemorykiller的oom_adj的相关概念,以及根据一些案例来阐述了解oom_adj对于做Android应用开发的重要意义。
一、lowmeorykiller中进程的分类以及各类进程的adj值
在Android的lowmemroykiller机制中,会对于所有进程进行分类,对于每一类别的进程会有其oom_adj值的取值范围,oom_adj值越高则代表进程越不重要,在系统执行低杀操作时,会从oom_adj值越高的开始杀。系统lowmemeorykiller机制下对于进程的级别的以变量的形式定义在framework/base/core/java/com/android/server/am/ProcessList.java类中,可总结成下表:
再补充介绍一下:
1.AMS角度对于进程的分级
上表带分级只是从lowmemroykiller角度来分的,时用于lowmemeorykiller执行杀进程操作,但是从android的系统管理角度看,即是从AMS执行相关逻辑时,又有一套自己的分级机制,当然这两套机制也有着很多互通的点。AMS角度的级别划分以变量的形式定义在framework/base/core/java/android/app/ActivityManager.java类中,以PROCESS_STATE开头的变量。
2.没有stopService其内含activity的后台进程
这类进程从lowmemorykiller角度是划分为cached,因为如果这类进程往往占有较大的内存,这类含有activity的后台进程往往占有较大内存,所以即使这类进程包含了Service,lowmemorykiller的机制也会更加倾向于优先杀死这类进程。
但是一般启动了服务的进程往往是希望服务在后台能够执行某些任务,这样看是不希望这些服务因为进程被杀而过早的被终止的,那如何调和这种矛盾呢?正确的做法是,对于期望较长时间留在后台的服务,应该将服务运行在单独的进程里,即是UI进程与Servie进程分离,这样期望长时间留在后台的Serivce会存在与一个被lmk分类为Service 进程的服务而获得较小的Adj值,而占有大量内存的UI进程则会分类为Cached进程,能够在需要的时候更快地被回收。
还有一点,这类进程虽然被lmk划分为cached进程,但是从ams角度是被划分为PROCESS_STATE_SERVICE这个类别的,即视为服务进程,在ams相关流程中也是以服务进程来执行相关逻辑的,此外在使用dumpsys meminfo查看所有进程时,这类进程也是被列在B service这个类别的。
3.A-Service与B-Service的划分
所有启动了服务的进程,且该服务所在的进程没有显示过UI,且该服务未执行startForeground(执行后会变为perveptible服务)动作,那该进程则为A-Service与B-Service中的一种。然后根据这类服务进程所处于Lru进程表中的位置,前1/3点服务为A-Service,其余的则为B-Service。
4.perceptible的标准
perceptible名为可感知的进程,但并不是说能够感知到进程就一定表示该进程属于perveptible进程,比如播放音乐的进程活着状态栏上有通知的进程,虽然能够感知到进程的存在,但是不代表进程一定时perceptible类别的进程。决定该进程是否属于perceptible进程并未进程的可感知性,而是该进程的服务是否执行了startForeground动作。
二、如何查询应用的adj级别
1.dumpsys meminfo
使用dumpsys meminfo命令时,会列出当前系统的所有进程,不同进程放入不同的分类,对应的分类名基本与lmk的分类一致。有一点不同的就是,退到后台启动了服务且显示过UI的进程,在dumpsys meminfo命令中会归为b service一类,但从lmk角度分配的oom_adj值为9~16的范围,属于cached一类
2.cat /proc/[PID]/oom_adj: 使用该命令会直接显示出对应进程号的adj值
三、未控制好oom_adj的案例
1.ui进程启动service的隐患
案例a:备份进程启动一个服务开始执行备份,备份服务运行在ui进程(服务未调用startForeground())
隐患:备份服务一般需要较长时间,在用户按Home键退出后台后,备份进程会处于previous状态,继续使用手机其他应用,会是使得备份进程处于cch-started-ui-services的状态,即是启动了服务并且包含ui的进程退到后台状态,此时进程的adj值处于9~16,随着时间推移逐渐增大。如果在较长的备份过程中,触发了lowmemorykiller,很容易导致备份进程被杀掉,从而导致备份的失败。
案例b:备份进程启动一个服务开始执行备份,备份服务运行在ui进程(服务调用了startForeground())
隐患:这种情况下备份进程会被划分为perceptible进程,基本上是不会被lowmemorykiller杀掉的,但是这也导致内存占用较大的备份常驻了,从内存管理角度来将,备份进程的UI部分是并不期望他常驻的,而大量内存的常驻也容易导致lowmemorykiller的出现,从而导致系统进入内存较低的等级,而当系统处于内存较低等级时,会触发系统回调所有进程进行进程回收动作,容易导致系统卡顿场景的出现。此外,调用了startForeground()会导致进程被系统判定为前景进程,这样备份进程便会抢占用户操作手机时前台应用的cpu资源,增加了卡顿场景出现的几率。
解决方法:将Service运行在独立的进程,这样应用退到后台后,备份服务进程会处于A-Service中(逐渐掉落到B-Service),而B-Service进程一般也是很难被lowmemorykiller砍。该独立是否要startForeground()?如果期望保证备份尽快到完成,便可以牺牲一些用户在操作其他应用时到用户体验,将服务推为前景应用;对于很多需要保证功能的流畅运行的服务进程,例如音乐播放,录音等,则需要将这类服务进程通过startForeground()设置为前景进程,但前提还是需要做到ui与Service分离。
2.使用线程解决耗时操作造成anr问题的隐患
案例:短信、邮件、或笔记本应用,在用户按BACK键时存下草稿
public class MyActivity extents Activity { public void onPause(){ //存储草稿 } }
问题(1):由于存储草稿定操作一般时保存到数据库,某些情况下可能会占用较长时间,这里就有可能导致anr的隐患
解决方案1:
public class MyActivity extents Activity { public void onPause(){ new Thread() { public void run() { //存储草稿 } }.start() } }
问题(2):当用户以back键离开应用时(以home键离开会处于previous状态),应用退到后台处于empty-cached状态,内存不足时,可能会立刻杀。
解决方案2:如果线程有这问题,是否可以用服务来完成存储草稿的动作呢?
问题(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.provider被binder导致的隐患
案例:systemui进程获取手机中的手机管家应用提供的content provider,用于获取当前应用相关信息
问题:管家应用的UID为System,在Android机制中,System UID进程提供的provider一旦被访问,即使访问完成关掉provider后,连接依旧存在,所有管家应用由于其provider持续被persistent进程咬住,所以管家应用便会长时间处于foreground级别的应用中,oom_adj为0,导致管家应用占用的大量内存很难被回收
解决方案:单独使用一个进程提供provider,提供provider进程由于占用内存较小,所以即使无法被回收也影响较小,这样管家应用的UI进程能够按照系统正常的回收流程在需要时被回收
四、总结一些经验
1.进程在启动服务后,在事情做完后,必须呼叫stopService或stopSelf通知框架,避免事情做完了,服务进程依旧常驻内存
2.对于需要长时间停留在后台的服务,且服务设置为具有重启特性时,需要做到ui与service分离,一避免占用内存较大的ui进程的常驻
3.对于需要长时间停留在后台的服务,且服务设置为具有重启特性时,不可长时间bind住其他进程的service或provider,避免其他进程常驻;
4.常驻性质的进程(oom_adj<=6),不可一直bind住其他进程的服务或provider,用完必须马上放掉
5.不可使用SystemUID进程内的provider,在Android设计中,若针对System UID的进程使用provider,即使已关掉provider,但框架仍会保持provider connection
6.利用onStartCommand方法回传值(START_STICKY,START_NOT_STICKY等)控制好服务的重启属性,在设计时充分考虑进程被lmk杀死的情况
7.IntentService继承自Service,针对CPU scheduling、工作排程等都有完整实现,建议多采用IntentnService进行功能实现