1 Low Memory Killer机制
在Android系统中,进程的生命周期都是由系统来控制的。出于体验和性能上的考虑,即使对一个App进行Home键还是Back键退出的操作,系统并不会真正杀掉该App的进程,它的进程依然存在于内存之中。因为这样在下次要启动这个App时就能更加快速。随着系统运行的时间增长,打开的App越来越多,内存中的进程也就会越来越多,这样系统的可用内存就会越来越少。在系统内存减少到达一个阈值时,系统就会开始根据进程的优先级来进行回收机制杀掉一部分进程来释放出内存供后面需要启动的App使用。这套回收内存的机制就叫Low Memory Killer。Android的Low Memory Killer机制是基于Linux内核的OOM(Out Of Memory)规则改进的。
2 内存阈值
上面提到当系统内存减少到达一个阈值时就开始回收进程,这个内存阈值在不同的手机上会不一样,我们可以通过adb命令:adb shell cat /sys/module/lowmemorykiller/parameters/minfree来查看它(高版本的系统需要权限):
可见输出了6个值,这些值的单位是Page,1Page = 4KB,所以上面6个值换算成MB的话应该是:72、90、108、126、144、180。也就是说,当系统内存减少到达180MB时,系统就会开始结束优先级最低的进程,当减少到达144MB时,系统又会开始结束对应优先级较低的进程,由此类推。
3 oom_adj值
刚才提到回收进程是根据进程的优化级来决定,这个优先级其实是Linux内核分配给每个系统进程的一个值,我们叫它做oom_adj值。oom_adj值定义在\sources\android-23\com\android\server\am\ProcessList.java类中。此值越大,进程的优先级越低,越容易被回收,普通应用进程的oom_adj值是>=0,而系统进程的oom_adj值是<0的。要查看进程的oom_adj值,可以使用adb命令:adb sehll cat /proc/进程id/oom_adj。下图中我对一个普通的应用进行了三次查看,三次的值分别是:0,6,8,第一次是打开了一个Activity,第二次是按下的Home键,而第三次是按下了Back键。
4 进程划分
进程的优先级从高到低可分为5种,分别是:Foreground process、Visible process、Service process、Background process、Empty process。所以它们的oom_adj值也是从小到大。
Foreground process
前台进程,就是用户正在使用中的应用当前的进程,例如:正在与用户交互的Activity所在的进程; 一个与正在交互的Activity绑定的Service所在的进程; 一个调用了startForeground的前台Service所在的进程,等。一般地前台进程是不会被系统回收杀死的。
Visible process
可见进程,就是用户正在使用,但不能直接操作的进程,例如:不在前台但仍可见的Activity,像已调用了onPause()回调的Activity,等。一般情况下,系统不会杀死可见进程,除非要在资源吃紧的情况下。
Service process
服务进程,就是没有界面一直在后台工作的进程,例如:通过startService()启动的Service正在后台中工作的所在进程。当系统内存不足以维持所有前台进程和可见进程同时运行的情况下,服务进程就会被杀死。
Background process
后台进程,就是用户按了Home键或Back键后,应用完全进入了后台,但还在运行的进程。这类进程是随时都会被系统杀死的。
Empty process
空进程,某个进程不包含任何活跃组件时就会被置为空进程,此类进程已经处于完全无用状态。
5 进程保活方案
进程保活是很多应用开发者都希望对自己App做的事情,目前除了手机厂商对像微信、QQ之类的大哥级App设置了防被杀白名单之外,是没有其他有效的技术手段能保证一个App进程一直活着不会被杀。如果真的这样的黑科技的话,那么所有的App都使用上了,这些App都在后台干些不安份的事情以及占用着内存不释放,那么我们的手机将会变得非常的恐怖了。
5.1 提升进程优先级
虽然没有什么有效的手段可使App进程不会被杀,但是使App的进程等级提升没那么容易被杀还是有些办法的。
方法一、使用前台Service
我们知道,通过系统API的startForeground方法可启动一个前台的Service,也就是启动了一个常驻的通知栏。因为前面进程划分中也提过,一个调用了startForeground的前台Service所在的进程就是最高级别的前台进程,在一般情况下都不会被轻易地杀死,所以曾经App保活党在前台Service上没少下功夫。
情况一、在过去Android API 19之前,可以直接调用startForeground并传入一个new Nofification()便能启动一个用户完全看不到没有感知的常驻通知栏,不过漏洞终究是漏洞,在Android API 19及以后系统版本中若使用该方法会在通知栏中出现“XXX正在运行”的通知栏,再也不是无感知了。
情况二、在Android API 25之前,又出了新的办法,就是同时启动两个id相同的前台Service,然后在后启动的Service执行stopForeground来停止。这样做又再一次可以神不知鬼不觉地启动了一个前台Service了,不过天网恢恢,此办法在Android API 25及以后系统版本中又再次失效了。
情况三、与其偷偷摸摸暗地里做事,不如光明正大干一回。像当下一些天气类App、音乐类App、安全类App,它们都是使用startForeground明着出现一个常驻通知栏,并带些自身功能,就是要告诉用户,我其实还活着,我在通知栏这里为你干事。其实主要你的App有足够的理由常驻通知栏,这种方法是最直接最有效的保活方法。
方法二、锁屏一像素Activity
该方法是监听系统熄屏和亮屏广播,在熄屏的时候启动一个透明且1个像素的Activity,使得在熄屏状态时还能假装App在前台,App所在进程变成了最高优先级了,然后在亮屏时再将这个Activity退出。这种方法也避开了一些国内手机在锁屏后一段时间后对后台进程的优化以达到优化性能和省电的机制。笔者在多个版本的原生系统虚拟机上运行是可行,但未在实际开发中使用过,以及未在国内流行手机厂商Rom中验证过,貌似国内有部分手机系统中已经将熄屏和亮屏广播给阉割了。顺便一提,听说手机QQ曾经有在使用此方法!
方法三、循环播放无声音乐
顾名思义,该方法就是耍流氓地在后台某个时机初始化MediaPlayer对象去播放一段无声的音乐,但据说会在某些国内手机上锁屏界面可能会出现音乐播放器界面。不管怎么样,该方法是非常不建议去使用的,因为就算在播放音乐时进程优先级被提高了,但会拖累了内存使用与手机的耗电,往往就会适得其反地被第三方安全应用或系统安全应用检测出来然后杀掉,也会使用户反感。
5.2 进程复活
还有另外一种保活的方式,就是在App进程死掉后,通过别的手段重新启动。这些手段目前也有很多,我们来看看。
方法一、帐户同步
Android系统本身提供了帐户同步功能,任何第三方App都可以通过此功能将自己的数据在一定时间内同步到服务器中去。然而在同步数据是通过我们自己代码来实现的,所以在同步数据时会把已结束掉的App再次拉活起来。此做法的过程我们在前面的文章《Android里帐户同步的实现》中有详细介绍过,但由于国内手机厂商的各种订制和阉割,经验证在某部分手机上是通不通的。
方法二、JobScheduler
Android5.0后提供了一个叫JobScheduler作业调度器的功能。它一种可让系统在某个时刻某个特定条件下批处理一些App的任务请求的机制。所以同样的也可以将已经结束的App在系统触发此机制时可再次拉活起来。此做法的过程我们在前面的文章《Android里JobScheduler的实现》中有详细介绍过,也是由于国内手机厂商的各种订制和阉割,并不是所有厂商手机系统都能够实现这种拉活机制,像小米手机上是验证行不通的。
方法三、双进程守护
双进程守护简单说就是在你的App中起动两个不同进程,在其中一个进程被杀掉的时候,另外的进程会立即检测到,然后再次把被杀的进程启动起来。这种办法在Android5.0之前或许是比较流行的保活手段,但在后来高版本的Android系统中,系统回收策略已经改成进程组的形式了,那就是说,如果系统要回收一个进程,必然会杀死同属于该App的所有进程,因此双进程守护也只能是一个历史性漏洞,只可以在低版本手机上使用。
方法四、 监听系统广播
通过监听一些全局的系统广播,比如开机广播、解锁屏广播、充电状态变化广播等,来启动一个App的后台服务在过去不失为一个好办法。在Android 3.1开始,广播机制加入了两个标记位,分别是FLAGE_INCLUDE_STOPPED_PACKGES(包含已经停止的应用,这个时候广播会发送给已停止的应用) 和 FLAG_EXCLUDE_STOPPED_PACKAGES(不包含已经停止的应用,这个时候广播不会发送给已停止的应用),用来控制广播是否要对处于停止状态的应用起作用,而系统广播默认是加上了FLAG_EXCLUDE_STOPPED_PACKAGES标志,这样做是为了防止广播无意间或者在不必要时调起停止运行的应用。在原生系统中,当App初始启动后就会被认为是非停止状态,那么就是说只要启动过一次后就能无论进程是否存在都能接收到系统广播。但是请别开心得太早,在国内大多厂商手机系统中还是行不通。例如像小米长按Back键强行停止App后,它是一定收不到系统广播的。
方法五、家族应用相互唤醒
这个很好理解,就是同一个公司中有开发了多款App,而各款App都做了相互拉活的接口,哪个App在做事的同时,顺便检测一下自家公司的其它App是否也在该台手机上有安装,是否进程没有启动,如果是就通过约定好的接口将其拉活。
方法六、推送拉活
像MiPush、MeizuPush、HuaweiPush这类消息推送服务,它们通过在云端到手机端之间建立一条稳定、可靠的长连接消息推送通道。它们在针对各自家的Rom表现也是相当出色的,通常在被强制停止的App中是无法接弹出通知栏的,但是它们在各自系统中因为存在系统级推送应用,可以通过服务端下发消息,然后该系统应用接收消息进行了代理式地给已停止的App创建出通知栏消息,从而有效地帮助App拉动用户活跃度,改善产品体验。除了手机厂商官方的推送外,还有一些第三方的推送,如友盟、个推等,他们一般对一些较为出名的大手机厂商的官方PushSDK进行封装成一个集成版的大PushSDK,使开发者可以一步到位接入多家手机厂商的Push,而对于小众的手机厂商由于系统中并不存在系统级应用进立的长连接,所以他们会在接入了集成的PushSDK中选择一个当好此时活跃的应用跟第三方推送服务端建立一条临时的长连接,然后接收属于该手机当时的所有消息,最后通过应用相互唤醒的方式进行消息传递到各个已接入他们集成PushSDK的应用去。
5.3 授予系统权限
授予系统权限简单说就是让用户心甘情愿为你的App授予一些系统稳定性权限。比如大多数手机中的“自启动权限“、小米手机中的”神隐模式加白“、华为中的“受保护应用权限”、Vivo手机中的“清理白名单“和”后台高耗电“、Oppo手机中的”后台冻结”,等。当你的App开启了对应这类权限后,就会大大增加不会被杀的机率,是很好的保活手段。还有一个所有手机都有的”通知读取权限“,经过验证,开启此权限的App它的进程优化级会被提高,也是一个很好的保活手段。还有一个最强的保活手段,就是让用户在多任务切换中,将你的App锁定,这样系统的一键清理后台任务也就不会对你的App进行清理了。
6 总结
上面介绍了不少目前或过去在国内App开发中比较流行的保活方案。其实说句实在话,随着Android系统的升级,不断对后台应用的限制和不断完善用户体验,与其费尽周折使用各利擦边球式的保活小手段瞒天过海一时,还不如安安份份地做好产品体验和性能优化。当你的App有足够的理由常驻通知栏或者说有足够的理由要在后台运行的时候,只要稍加一些小引导用户自然会给你的App授予系统的稳定性权限。这才是长治久安的好办法!做好了性能优化,就算系统要随时回收资源,也没那么快轮到你的App,因为首先要杀也是杀占用内存大的、高耗电的App。而且并不是所有用户都是愿意让不相关的App任其在后台常驻的,有时候我们为了私欲做得过多时反而会适得其反地让用户反感。所以说,所有的歪门邪道的保活都是耍流氓的,我们还是应该尊重用户的意愿,养成良好的开发信仰。