您现在的位置是:首页 > 文章详情

你说,你的APP要不要保活?

日期:2018-11-08点击:361

62fec99883318bdccde732d347e062b6ec8e5288

0、前言

Android 系统为了保持系统运行流畅,在内存吃紧的情况下,会将一些进程 kill ,以释放一部分内存。然而,对于一些(如:IM-QQ 、微信,支付-支付宝等)比较重要、我们希望能及时收到消息的 APP,需要保持进程持续活跃,那么就需要实施一些保活措施来保证进程能够持续存活,即 Android 进程保活可参看前几天的一篇文章:《2018年Android的保活方案效果统计》

Android 进程保活,一般从两个方面进行:

  • 运行中保活:提高进程优先级,降低被系统 kill 的概率

  • 被 kill 后拉活:被系统 kill 后,将进程拉活(重启)

在此之前,我们先来了解下 Android 进程的一些相关概念。

1、进程

默认情况下,同一 APP 的所有组件均运行在相同的进程中,但是也可以根据需要,通过在清单文件中配置来控制某些组件的所属进程。

内存不足的情况下,Android 系统会选择 kill 某一进程来释放该进程占用的内存,供其它为用户提供更为紧急服务的进程使用。在被关闭的进程中运行的组件也会随着进程的关闭而销毁。

决定 kill 哪个进程时,Android 系统将权衡所有进程对用户的相对重要程度。例如:相对于托管可见 Activity 的进程而言,更有可能 kill 托管不可见 Activity 的进程。因此,是否终止 kill 某个进程取决于该进程中所运行组件的状态。

2、Android 进程的生命周期

Android 系统会尽量长时间地保持 APP 进程的运行,但为了新建进程或者运行更重要的进程,最终要 kill 旧进程来回收内存。为了确定保留或者 kill 哪些进程,系统会根据进程中正在运行组件的状态,将每个进程放入重要性层次结构中,必要时,系统会首先kill重要性最低的进程,其次kill重要性略低的进程,以此类推。

重要性层次结构一共有5级。以下列表按照重要程度列出了各类进程(第一类进程最重要,将是最后一类被终止的进程):

1、前台进程

用户当前操作的进程。一个进程满足以下任一条件 ,即视为前台进程:

  • 托管用户正在交互的 Activity(已调用 onResume() 方法)。

  • 托管某个 Service ,且 Service 绑定到用户正在交互的 Activity。

  • 托管正在“前台”运行的 Service(服务已调用startForeground())。

  • 托管正在执行生命周期回调的 Service( onCreate() 、 onStart() 或 onDestory() )。

  • 托管正在执行 onReceive() 方法的 BroadcastReceiver。

通常,任意时间的前台进程数据都不多。只有在内存不足以支持它们同时继续运行这一万不得已的情况下,系统才会 kill 它们。

2、可见进程

没有任何前台组件、但仍会影响用户在屏幕上所见内容的进程。 如果一个进程满足以下任一条件,即视为可见进程:

  • 托管不在前台、但仍对用户可见的 Activity(已调用 onPause() 方法)。如:前台 Activity 启动了一个对话框,允许在其后面显示上一个 Activity。

  • 托管绑定到可见(或前台)的 Activity 的 Service。

可见进程被视为及其重要的进程,除非为了维持所有前台进程同时运行而必须终止,否则系统不会kill这些进程。

3、服务进程

正在运行已使用 startService() 方法启动的 Service 且不属于上述两个更高类别进程的进程。

尽管服务进程与用户可见内容没有直接关联,但是它们通常在执行一些用户比较关心的操作(如:在后台播放音乐或从网络下载数据等),因此,除非内部不足以维持所有前台进程和可见进程同时运行,否则系统不会 kill 这些进程。

4、后台进程

托管目前对用户不可见的 Activity 的进程(已调用 Activity 的 onStop() 方法)。

后台进程对用户体验没有直接影响,系统可能随时会 kill 它们,以回收内存提供给前台进程、可见进程、服务进程使用。通常会有很多后台进程同时运行,系统将它们保存在 LRU(最近最少使用)列表中,以确保包含用户最近查看的 Activity 的进程最后一个被终止。

5、空进程

不包含任何活动组件的进程。

保留这种进程的唯一目的是缓存,以缩短下次在其中运行的组件的启动时间。为使系统总体资源在进程缓存和底层内核缓存之间保持平衡,系统往往会kill这些进程。

根据进程中当前活动的组件的重要程度,Android 系统会将进程评定为可能达到的最高级别。比如,托管服务和可见 Activity 的进程,系统会将其评定为可见进程,而不是服务进程。

此外,一个进程的级别可能会因为其他进程对其依赖而有所提高,即服务于另一进程的进程其级别不会低于其服务的进程。例如,进程 A 中的 Service 绑定到进程 B 中的组件,则进程 A 始终被视为至少和进程 B 同等级别。

由于运行 Service 的进程其级别高于托管后台 Activity 的进程,因此在要做长时间后台操作的 Activity 中最好为该操作启动 Service,而不是简单的创建子线程,当操作有可能比 Activity 更持久时更需如此。例如,需要上传较大图片或较大文件的 Activity,应该启动 Service 来执行上传操作,这样,即使 Activity 被销毁,Service 仍能在后台继续执行上传操作。使用 Service 执行较长耗时操作,可以保证不管 Activity 发生什么情况,该操作至少有服务进程的优先级。同理,使用广播接收器时,也当如此。

以上进程生命周期内容参考Android官网文档(需要科学上网(ಥ _ ಥ))。

3、Android 进程回收策略

上文提到了,Android 系统在内存不足以创建新进程或运行更重要的进程时,会 kill 重要性低的进程来回收内存。也总结了 Android 系统进程的重要级别,那么具体的进程回收策略是什么呢?

Android 系统回收进程内存的机制叫 LMS ( Low Memory Killer )机制,是一种根据 oom_adj 阈值级别触发相应力度的内存回收的机制。oom_adj 代表进程的优先级,数值越高,优先级越低,越容易被杀死。

关于 oom_adj 的说明如下:

b60960d7af978d38273750d4ba2e83410e1f29bc

OOM_ADJ

其中红色部分代表比较容易被杀死的 Android 进程( OOM_ADJ>=4 ),绿色部分表示不容易被杀死的 Android 进程,其他表示非 Android 进程(纯 Linux 进程)。在 LMS 回收内存时会根据进程的级别优先杀死 OOM_ADJ 比较大的进程,对于优先级相同的进程则进一步受到进程所占内存和进程存活时间的影响。

Android 中进程可能被杀死的情况如下:

307a142ddb414fb5602c7a61fabf708c5f1ba6b0

Android进程可能被杀死情况

4、进程保活

重复下文章开头说的 Android 进程保活的两个方案:

  • 运行中保活:提高进程优先级,降低被系统 kill 的概率

  • 被 kill 后拉活:被系统 kill 后,将进程拉活(重启)

5、运行中保活

通过前面章节的论述,我们知道,假设 APP 进程能够一直被认为是前台进程,那么系统就有可能永远不会杀死该进程。当然,这是不可能的,当我们将 APP 退回到后台,改 APP 所属进程就不属于 前台进程了。但是上述假设也让我们有了灵感不是,只要我们尽可能的提高进程的优先级,不就可以最大概率的降低被系统 kill 的可能性了。

那么,提高进程优先级的方法有哪些呢?

1、利用 Activity 提高权限

监听手机锁屏解锁事件,锁屏时启动一个1像素的 Activity ,解锁时将该 Activity 销毁。此方法能将进程在锁屏状态下提高到最高前台进程( oom_adj 为 0 )的级别。避免出现一些让用户困扰(体验不好)的情况,该 Activity 需设计成用户无感知。

此方案主要解决为了达到省电目的,一些第三方应用或者系统管理工具在检测到锁屏之后一段时间(一般是 5 分钟)内会杀死后台进程。

下面是实例代码:

1像素 Activity:

class KeepLiveActivity : AppCompatActivity() {

   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)

       KeepLiveManager.keepLiveActivity = this

       // 设置Activity在左上角
       window.setGravity(Gravity.START)

       // 设置window的像素为1
       window.attributes.run {
           x = 0
           y = 0
           width = 1
           height = 1
       }
   }
}

注意,这里一定要设置启动模式为 singleInstance,使该 Activity 单独一个 Activity 回退栈,否则在锁屏且 APP 在后台运行时,启动该 Activity 后,会进程带入前台,解锁后显示该APP界面,体验不好

<activity
   android:name=".KeepLiveActivity"
   android:launchMode="singleInstance"/>

广播接收者:

class KeepLiveReceiver : BroadcastReceiver() {

   override fun onReceive(context: Context, intent: Intent) {
       Log.d("KeepLiveReceiver""action = ${intent.action}")

       when(intent.action) {
           // 锁屏
           Intent.ACTION_SCREEN_OFF -> {
               KeepLiveManager.startKeepLiveActivity(context)
           }

           // 解锁
           Intent.ACTION_USER_PRESENT -> {
               KeepLiveManager.finishKeepLiveActivity()
           }
       }
   }
}

管理单例:

object KeepLiveManager {
   var keepLiveActivity: KeepLiveActivity? = null

   fun startKeepLiveActivity(context: Context) 
{
       val intent = Intent(context, KeepLiveActivity::class.java)
       intent.flags 
= Intent.FLAG_ACTIVITY_NEW_TASK
       context.startActivity(intent)
   }

   fun finishKeepLiveActivity() {
       keepLiveActivity?.finish()
   }
}

注:由于锁屏、解锁的动作频率极高,该类广播在清单文件中注册无效,需要启动服务来注册广播,此部分代码和本章主题关系不大,就不贴代码了。思路就是:启动 APP 时,启动一个注册服务,在服务的 onCreate() 方法中注册广播,在 onDestory() 方法中注销广播。

以下是使用该方案保活前和保活后查看 oom_adj 的对比截图:

7a88c1bf9df4c60fda2129a21b6a62ed5b46db01

保活前

051d4f75b75e7e671373cd77276296f55040a295

保活后

可见,保活后在锁屏状态,将进程的 oom_adj 由原来的7提高到了 0 。

附:查看进程 oom_adj 的方法  

在命令行中使用以下两个命令

adb shell ps | grep  packageName
adb shell cat /proc/PID/oom_adj

如:
G:AndroidGithubKeepLive>adb shell
shell@armani:/ $ ps | grep com.cy.keeplive
u0_a3     5991  213   541548 29976 ffffffff 00000000 S com.cy.keeplive
shell@armani:/ $ cat /proc/5991/oom_adj
0

2、利用 Notification 提升权限

通过 setForeground() 方法可以将后台 Service 设置为前台 Service,可以将服务进程优先级提升为与可见进程一致,这将有效提高进程的优先级,从而大大降低进程被kill的概率。

通过 setForeground() 将后台 Service 设置为前台 Service 时,必须在系统的通知栏发送一条通知,也就是说前台 Service 必须绑定一条可见的通知。

在通知栏发送一条通知,是用户可以感知到的,这可能会对用户造成一定的困扰。可以通过实现一个内部 Service,在外部和内部 Service 中同时发送具有相同 ID 的 Notifacation ,然后将内部 Service 结束。随着内部 Service 的结束,Notification 也会消失掉,但系统的优先级仍然提高了。

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
   try {
       Notification notification = new Notification();
       if (Build.VERSION.SDK_INT < 18) {
           startForeground(NOTIFICATION_ID, notification);
       } else {
           startForeground(NOTIFICATION_ID, notification);
           // start InnerService
           startService(new Intent(this, InnerService.class));
       }
   } catch (Throwable e) {
       e.printStackTrace();
   }

   return super.onStartCommand(intent, flags, startId);
}

然后在内部 Service 中也启动一个相同 ID 的 Notifacation ,并调用 stopSelf() 方法,结束内部 Service:

@Override
public void onCreate() {
   super.onCreate();
   try {
       startForeground(NOTIFICATION_ID, new Notification());
   } catch (Throwable throwable) {
       throwable.printStackTrace();
   }
   stopSelf();
}

6、被 kill 后拉活

此类方法暂未实践,后续补充。暂时简单提一下前人研究的可行性方案,不过这类方案都多多少少存在限制条件或者版本兼容性问题。

1、通过系统广播拉活

简单讲就是监听一些特定的系统广播,当系统发出这些广播时,即可相应事件拉活。

2、利用第三方应用广播拉活

此方案和第1中方案类似,不同的时该方案接收第三方 TOP 应用广播。通过反编译第三方 TOP 应用(如 QQ、微信、支付宝等,以及个推、极光推送等推送 SDK ),找出它们外放的广播进行监听,响应相应广播事件拉活。

3、利用系统Service机制拉活

将 Service 设置为 START_STICKY,利用系统机制在 Service 挂掉后拉活。

4、双进程守护

通过双进程的 Service 相互绑定,在一个进程被 kill 时,另一个进程将其拉活。

5、利用 JobScheduler 机制拉活

JobScheduler 允许在特定状态与特定时间间隔周期执行任务。可以利用它的这个特点完成保活的功能,效果类似开启一个定时器,与普通定时器不同的是其调度由系统完成。它是在 Android5.0 之后推出的,在 5.0 之前无法使用。

6、利用 Native 进程拉活

利用 Linux 中的 fork 机制创建 Native 进程,在 Native 进程中监控主进程的存活,当主进程挂掉后,在 Native 进程中立即对主进程进行拉活。


原文发布时间为:2018-11-08

本文作者:cspecialy

本文来自云栖社区合作伙伴“终端研发部”,了解相关信息可以关注“终端研发部”。

原文链接:https://yq.aliyun.com/articles/666425
关注公众号

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。

持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。

转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。

文章评论

共有0条评论来说两句吧...

文章二维码

扫描即可查看该文章

点击排行

推荐阅读

最新文章