Android 应⽤保活漫谈

1,423 阅读17分钟

前⾔

Android应⽤保活的话题,在android开发领域中始终存在着⼀席之地。话题虽然古⽼(⼤概从android4.4开始就已经诞⽣了),但内容却是随着时间的推移,随着android系统版本的迭代升级和各开源平台的个性化定制,始终在不断丰富着。近年来,随着⾕歌官⽅越来越多的关注后台app可能对⼿机性能、隐私甚⾄是其他前台状态的app产⽣不良影响,从⽽更加严格的限制了app在后台的⾏为,保活的形势也在变得更加严峻,保活的⽅式也在变得越来越有限,曾经有效的保活⼿段,也许经过⼀两年就完全失效了。所以在这⾥,我们就列举当下依然有效的保活⼿段,也希望为读者们提供⼀些思路。

Android⽣态背景

早期的Android系统并没有对处于后台的app做过多的限制,⼤约在5.0版本之前,⼀切似乎都⽐较和谐,只不过那个时候的Android⼿机整体上来看也相对低端,往往会出现卡顿、存储够等问题。我们所不知道的是,那个时候的开发者们就已经在⽣态中不知不觉埋下了祸根,随着时间的推移,越来越多的 app为了保证⾃身的消息触达采⽤形形⾊⾊的⽅式保证⾃⼰被“杀不死”,⼤名鼎鼎的“双进程守护”⽅案就是这个时期的产物。纵观历史,⼀个被杀不死的⽣物,对⽣态的破坏是可怕的。

显然,官⽅和⼚商也都意识到了这⼀点,这些切到后台依然运⾏着的app始终会消耗⼿机的性能,⽐如耗电量、内存占⽤等等,这会影响⼿机的续航能⼒、也可能导致早期的⼿机出现内存不⾜,卡顿等情况,这都是Google官⽅和国内⼚商不希望看到的。因此在android系统不断迭代的过程中,官⽅则不断限制后台app的资源使⽤,国内⼚商更是为了品牌的⾼性能、⾼续航等特性,加紧对后台app的限制,甚⾄会更改系统源码,变更原⽣系统⾏为,直接杀死处于后台的app,防⽌它们对⼿机性能产⽣影响。

我们所使⽤的绝⼤多数应⽤(尤其是2C的产品),都希望在⽤户切到后台的时候,也能及时的拉取到信息,做到有效的提醒。由于国内的Android失去了统一的Google推送这⼀有效的信息触达渠道,能在后台执⾏信息的轮询拉取、或建⽴稳定的⻓连接,就成为了⼀些app的必然选择(相比于更高成本的适配大厂推送来说)。还有⼀些⼯具类的应⽤,更是希望⽤户在不需要关注的情况下,能够⾃动上报⽤户的位置信息等内容。因此,在诸如此类的场景下,这些app是⾮常需要在后台稳定运⾏的。

保活与限活,这两个问题相⽣相克。一方面,处于后台的app总是会因为各种需求场景需要能够正常运行;⽽另一方面,却会增加系统开销,降低系统性能、损耗电量甚至在用户无感知的情况下获取其隐私数据。不加管控的后台app犹如洪⽔猛兽,努⼒榨⼲着有限的资源,威胁用户的信息安全。因此,随着官⽅的升级、⼚商的定制,⽆不加紧限制后台app的⾃由和所能获取的资源权限。⽽为了在不同场景下获取必要的资源,app的开发者们仍在⽆休⽌地与官⽅和⼚商进⾏着博弈。我们在下⽂中提到的保活⽅式,建议⼤家能在合理的场景下使⽤,尽量避免处于后台的应⽤被系统杀死,或是即便被杀死,也能找到有效的⼿段将app拉活。

应⽤保活的⼿段

为了使得app能够达到有效的保活,需要做到内外兼修。外,是指依托系统提供的api、甚⾄可以借助⼀些早期的系统漏洞进⾏守护拉活;内,则是遵循系统的规则,在app的设计实现过程中,合理的分配和利⽤资源,降低⾃身app的资源占有,降低被系统杀死的概率。

总的来看,我们可以从两个⽅⾯⼊⼿app的保活:1、降低死亡率;2、提升拉活率。

降低死亡率

Android app 的死亡场景主要分为应⽤Crash、原⽣系统回收内存、⼚商系统节约性能、和获取了root权限的三⽅app杀死和⽤户⼈为杀死。其中,app Crash和⽤户主动杀死并不是本⽂档 重点,排除在外。剩余三种场景都跟app的保活⼿段息息相关。

在了解各个保活⼿段之前,⾸先介绍⼀个名词(oom_adj)和⼀个机制(LMK)。LowMemoryKiller(低内存杀⼿)是Andorid基于Linux系统的oomKiller原理所扩展的⼀个多层次oomKiller。原始的OOMkiller(Out Of Memory Killer)会在Linux系统⽆法分配新内存的时候,根据以进程优先级为核⼼的⼀定规则,选择性杀掉进程,但是到oom的时候,系统可能已经不太稳定。⽽LowMemoryKiller是⼀种根据内存阈值级别触发的内存回收的机制,在系统可⽤内存较低时,就会选择性杀死进程,相对OOMKiller,能够做到提前预警,更加灵活。

LMK机制示意图

这套机制中有两个重点需要说明:

  1. 进程优先级的定义
  2. 进程优先级的动态管理

进程优先级的定义

当内存不⾜,⽽其他更急于为⽤户提供服务的进程⼜需要内存时,Android 可能会决定在某⼀时刻关闭某个进程。决定终⽌哪个进程时,Android 系统会权衡其对⽤户的相对重要性。例如,相较于可⻅ Activity 的进程⽽⾔,系统更有可能关闭不再可⻅的 Activity 的进程(⽐如后台进程)为了确定在内存不⾜时应该终⽌哪些进程,Android 会根据每个进程中运⾏的组件以及这些组件的状态,将它们排序。这些进程类型包括(按重要性排序,可参考developer.android.com/guide/compo…

  • 前台进程(Foreground process)-oom_adj= 0
  • 可⻅进程(Visible process)-oom_adj= 1
  • 服务进程(Service process)-oom_adj= 5
  • 后台进程(Background process)-oom_adj= 9 到15之间
  • 空进程(Empty process)-oom_adj= >15

按照上述排序,经过AMS的监管,会在进程运⾏中不断更新处于内核中的⼀张表结构,以pid为key,以oom_adj为值,并进⾏排序,数值越⼤,则进程的重要性越低,当内存不⾜时就越容易被释放。

进程优先级的动态管理

我们熟知的⼀些app操作⾏为,⽐如:app退到后台,移到前台;执⾏⼀个不可⻅service,或者是声明⼀个前台service播放⾳乐等等。AMS会根据app所处的不同状态更新进程列表的oom_adj值,当LMK被触发时指导其挑选进程并释放。基于上述的LMK机制,我们可选的存活⽅式有以下⼏种:

  1. 系统⽩名单 所谓的⽩名单,是通过⼀定⼿段(⽐如跟系统⼚商进⾏合作)将app的oom_adj值降低到系统永远也⽆法杀死的级别或是系统不去⼲涉的级别,⽐如系统进程级别,oomadj值为-16,native进程-17(不被系统管理)。今天的微信、⽀付宝等超级app,就是通过该⽅式实现后台常驻的。当然,⼀些⼚商的⼿机能够允许⽤户⼿动添加⽩名单,普通的app也是可以进⾏引导的。

  2. ForegroundService 前台服务,是⾕歌官⽅所推荐的提升app后台存活率的主要⽅式,以下是官⽅⽂档的说明。“ 前台服务被认为是⽤户主动意识到的⼀种服务,因此在内存不⾜时,系统也不会考虑将其终⽌。前台服务必须为状态栏提供通知,放在“正在进⾏”标题下⽅,这意味着除⾮服务停⽌或从前台移除,否则不能清除通知。例如,应该将通过服务播放⾳乐的⾳乐播放器设置为前台,让⽤户明确意识到其操作。状态栏中的通知可以表示正在播放的歌曲,并允许⽤户启动activity来与⾳乐播放器进⾏交互。”随着android系统版本不断提升,在Android O的版本针对前台Service的创建进⾏了⼀些变更,但并不影响app在前台service的常驻⾏为,总的来看,依照⾕歌官⽅推荐的前台常驻⽅案,是极为靠谱的选择。

  3. 进程拆分 ⼀个简单的app,默认只有⼀个进程,也就是activity所在的主进程。但在⼀些复杂的情况下,app是可以声明多个进程的。不同的进程主要负责不同类型的功能。最常⻅的是在接⼊⼀些三⽅推送的时候,会声明push进程。拿微信举例,它的进程主要有三类:worker、tools和push。worker就是我们的主UI进程,push是推送进程,tools包含了Gallery 和webview。拆分出tools进程也是出于内存原因:⽼版本的webview存在着内存泄漏;Gallery存在着⼤量的缩略图导致内存使⽤⼤。在微信进⼊后台时,会主动把tools进程kill调,所谓“丢卒保⻋”。经历了进程拆分的app,在不同的app状态下,可灵活取舍⼀些进程,其不同的oom_adj能够分担整体被标记回收的⻛险,因此,其存活率更⾼。 进程拆分

  4. 适配各⼤⼚商的配置项 在明确了系统回收内存的机制之后,还是会遇到同⼀app在不同品牌的⼿机上被杀⽅式不⼀的情况。如下的表格记录了部分品牌⼿机的⼀些通⽤设置及引导⾄设置的隐式调⽤⽅式,随着⼿机品牌的不断更新,下⽅所列的调⽤⽅式也会存在⼀定的变化,需要持续不断的更新。虽然不像系统api那样更稳定,但为了适配不同的主流品牌机型进⾏⽤户引导,也不失为⼀种选择,引导效果也还是可以的。

⼿机品牌设置类型隐式调⽤
华为⾃启管理setAction("huawei.intent.action.HSM_BOOTAPP_MANAGER")
锁屏清理setComponent(new ComponentName("com.huawei.systemmanager", "com.huawei.systemmanager.optimize.process.ProtectActivity"))
小米自启动管理setAction("miui.intent.action.OP_AUTO_START")
能源管理ComponentName("com.miui.powerkeeper", "com.miui.powerkeeper.ui.HiddenAppsConfigActivity"))
oppo自启动管理ComponentName("com.coloros.safecenter", "com.coloros.safecenter.permission.startup.StartupAppListActivity")
魅族自启动管理setAction("com.meizu.safe.security.SHOW_APPSEC")
待机耗电管理ComponentName("com.meizu.safe", "com.meizu.safe.powerui.PowerAppPermissionActivity")
vivo后台高耗电ComponentName("com.vivo.abe", "com.vivo.applicationbehaviorengine.ui.ExcessivePowerManagerActivity")
三星⾃启动管理getLaunchIntentForPackage("com.samsung.android.sm")
电源管理ComponentName("com.samsung.android.sm_cn", "com.samsung.android.sm.ui.battery.BatteryActivity")
  1. Service中循环播放静默⾳频 使⽤service⽆限循环播放静默⾳频虽然有效,但不可避免的会带来额外的电量损耗。⽽且其效果并不会⽐前台service更好,因此属于丢了⻄⽠捡芝麻的⼀种⽅案,不推荐使⽤。

  2. 1 像素activity保活⽅案: 同样是基于oom_adj的优先级,就是在当⼿机屏幕关闭时,app开启⼀个1像素的透明activity;当屏幕开启的时候,再关掉这个activity。不过这⾥有个前提,app要处于后台,才能在锁屏的时候使⽤灭屏⼴播启动1像素⽅案。如果app已经处于前台进程了,这么做就没有意义了。该⽅案能使进程的优先级从锁屏时的oom_adj>0(不同⼿机在具体数值上可能有差别),提升⾄0(前台进程)。这样,在锁屏的时候,app就不容易被⼀些三⽅的应⽤给杀死了。这个⽅案据说是qq团队早期采⽤的⼀种⽅式。该⽅案的具体做法⽹络资料也⾮常之多,这⾥就不做 过多的解释了。该⽅案主要针对的是⼀些三⽅应⽤或⼚商系统的管理软件在检测到锁屏之后⼀段时间,会对处于后台的应⽤进⾏清理,以达到省电的⽬的。但在部分⼿机上会同时存在多次锁屏解锁⼴播,导致该⽅案⽆效。同时,如果⼿机存在锁屏限制,该⽅案也是解决不了的。

提升拉活率

当app被杀死之后,⽤⼀定的⼿段让app重新启动运⾏,或者是部分重要的功能能够在后台恢复运⾏。

  1. ⼚商推送

国内的app之所以“求活若渴”,其中⼀个很重要的原因就在于要保证消息触达率。与国外app不同,由于google官⽅的推送⽆法翻越GFW,导致国内app均不能使⽤系统级推送保障消息触达。加之android源码的开源性质,国内各⼤⼚商纷纷定制⾃⼰品牌的系统级推送服务。当然,如果要适配⼀些主流品牌的android机,接⼊⼚商推送确实是⼀个⾮常稳妥的⽅式,但花在push功能的适配成本就会⽐较⼤。

  1. JobScheduler

android系统在5.0之前,对于处于后台的app都还没有限定像今天这么严格的执⾏条件。那时的app为了在后台能够持续执⾏,能够使⽤service常驻后台,甚⾄是使⽤“双进程守护”这样的神⽅法保证不被杀死。但严肃的考虑⼀下这种现象,你会发现,照此以往下去,后台的app若不加节制的使⽤系统资源,整部⼿机的性能都会受到严重的影响,耗电量、内存占⽤,都会严重影响前台使⽤的app。因此Google在Android 5.0之后,为了优化Android系统,提⾼使⽤流畅度以及延⻓电池续航,加⼊了在应⽤后台/锁屏时,系统会回收应⽤,同时⾃动销毁应⽤关联的Service的机制。

为了满⾜在特定条件下需要执⾏某些任务的需求,google在全新⼀代操作系统上,采取了Job (jobservice & JobInfo)的⽅式,即每个需要后台的业务处理为⼀个job,通过系统管理job,来提⾼资源的利⽤率,从⽽提⾼性能,节省电源。这样⼜能满⾜APP开发商的要求,⼜能满⾜系统性能的要求。这就是JobScheduler的由来。 当然,以上介绍这些都是在原⽣android系统上的官⽅特性,按照官⽅⽂档的定义,在原⽣的Android系统上,当设定了⼀个Job之后,哪怕该App的进程已经结束或者被杀掉,对应的 JobService也是可以启动的,这恰好能够应对我们今天的诉求,即便app被杀死,⼀些情况下的必须的功能还能够依托于job机制得以执⾏。但当经历过国内⼚商的⼆次定制后,问题就复杂了。⾸先导致的⼀个问题就是当前App的进程被杀掉之后,JobService⽆法启动。⽐如在MIUI系统中,第三⽅App如果没有被⽤户设置到允许⾃启动的名单中,在启动Service的时候会被拦截掉;如果在job正在执⾏过程中,使⽤⼀键清理等⽅式杀死进程,那么JobService将会被强制停⽌,连onStopJob的回 调也不会被执⾏。正如低版本的AlarmManager的命运⼀样,如此优秀的原⽣功能也变得不靠谱了。

  1. 三⽅推送

三⽅推送的拉活核⼼在于“共享推送通道”,举个例⼦说明它的原理:假设你接⼊了友盟,⽽恰好今⽇头条也接⼊了友盟,那么当你的app被杀死之后,如果⼿机上启动了今⽇头条(凭借着今⽇头条的装机量,这个场景是很有可能的),推送系统也就会通过共享的推送通道顺便把你的消息推送到⼿机上,然后还可能把你的进程也唤醒,这就是被拉活了。由此看来,⼀个“共享通道”接⼊的app种类越多,在同⼀时刻被不同app拉活的概率也就越⼤。这也就是为什么有时候你打开⼀个app的时候,另⼀个不相关的app就会刚好来个推送,其实这并不是⼀个巧合。并⽬前来看,专业的三⽅推送存在着两派阵营:中⽴阵营(如友盟、极光等)和⼤⼚阵营(如百度推送、信鸽推送、阿⾥云移动推送等)。中⽴阵营能够凭借上述原理形成app之间的“守护联盟”,⽽⼤⼚阵营则暴露出了“全家桶效应”,做到⼰⽅的app互相拉活。因此,三⽅平台的规模效应在拉活成功率上起到了⾮常重要的作⽤。 当然,我们并不能天真的认为只要接⼊了三⽅推送,凭借着守护联盟或是全家桶效应就能够轻⽽易举的拉活。在调研中发现,有些三⽅推送,宣称他们SDK⾥的“看护功能”,其实是付费功能,如果是⽤免费版本,则不会享有这个服务。所以,读者在选择推送平台的时候,还是需要擦亮眼睛仔细调查的。

应⽤保活的未来

我相信⽂中提到的方案在未来也会逐渐被替代,正如曾经的c层fork双进程互守经典⽅案,也随着Android 5.0版本的系统升级,退出了历史舞台。Google官⽅也明确了将对android系统后台app的限制更加规范和严格,或许今后各个⼚商对android系统的定制化也会与google系统更加趋于⼀致,降低因⼆次定制改造带来的原⽣系统功能不稳定不可靠。但由于天⽣失去了系统推送这⼀有效的信息触达⽅式,保活的话题也将始终存在,并随着android的发展逐步改变其内容。作为开发者,我们应该始终铭记的是:应⽤保活是⼀把双刃剑。合理地使⽤保活⼿段,能够有效利⽤系统资源,提⾼信息触达率,提⾼后台功能的稳定运⾏能⼒;⽽不加节制的滥⽤,则会导致系统资源的浪费、⼿机性能变差、内存存储的泄露、⽤户的使⽤体验变差,最终会破坏整个android开发⽣态。

近期,随着华为鸿蒙系统的开源和国内App开发生态的逐步建立,也希望华为能够汲取Android在设计方面的缺陷,避免不可收拾的版本兼容问题,同时能够建立完善的国内推送体系(虽然几年前有听说过工信部在筹备相关的事宜)和App的国内开发标准,最终,我们可能不用再去为了一个后台App为什么会被断网甚至被杀死而彻夜思索,但这都是以后的事儿了。在此之前,保活的话题会依然存在,开发者们也会依然为之奋斗。