一文详解 Android进程及TCP动态心跳保活
一直以来,
APP进程保活
都是
各软件提供商
和
个人开发者
头疼的问题。毕竟一切的商业模式都建立在用户对APP的使用上,因此
保证APP进程的唤醒
,
提升用户的使用时间
,便是软件提供商和个人开发者的永恒追求。
面对国内
GCM
(Google Cloud Messaging)推送服务不可用,也
未出现一个统一市场PUSH平台
的现状。早期的第三方软件一般通过维持一个
终端
与
远端服务器
之间的
TCP长连接
,达到
PUSH拉活
和
消息及时送达
的目的。
而为了维持这个
TCP长连接
不断开,前提条件就是保证自己APP的后台服务进程,不会被杀死(因为只有活着的终端进程才能定期与远端服务器通信,保证长连接不断连)。
因此在Android发布的早期,各种技术论坛和GitHub出现了
五花八门、各显神通
的
App进程保活
方案;如今随着Android系统的逐渐完善,各种进程保活方案不断受到限制,想要做到Android进程保活已经不太容易。
一般来说,
Android进程保活
主要有以下两方面工作:
-
进程保活:保持
进程不被系统杀死
或进程被杀后可以重新拉起
。
早期Android,对于后台运行的Service服务进程
限制较小,第三方软件保证自己APP后台服务进程长时间运行
相对容易,但仍然是有被系统杀死的可能。
这个阶段随着APP的不断增多,许多第三方APP开始利用系统漏洞(早期的Android系统尚不完善,可利用的漏洞较多),在APP进入后台
或系统准备休眠时
,通过各种所谓的黑科技
(实际为流氓手段)保证自己的APP进程不被杀死
,甚至阻止系统休眠
。
伴随随着这类软件的不断增多,最终造成的结果是,用户侧手机卡顿
、待机时间短
、耗电量增加
。 -
TCP保活:保证
终端
与远端服务器
之间的TCP长连接
不断连。
在App进程保活
的基础上,一般通过使用Android系统RCT时钟 Alarm
每5~10分钟唤醒一次系统,并发送一条只有几个字节的TCP保活消息
,来维持终端
与远端服务器
之间的TCP长连接
不断开。
一、Android早期进程保活
这里简单回顾一下,Android早期
App进程保活
的各种方案。
-
通过Service的onStartCommand方法中返回
START_STICKY
:
当Service的onStartCommand方法返回START_STICKY
时,若当前Service因内存不足被系统清理掉,待内存再次空闲时,系统将会尝试重新创建此Service,一旦创建成功后将回调onStartCommand方法,但其中的Intent将是NULL。 -
1像素透明Activity进程保活:
监测到Android系统息屏时,启动一个1像素的前台透明Activity
,欺骗系统,使其认为该应用为一个前台应用而不会被清理(据说淘宝早期就使用过这种方案)。 -
开启前台服务:
利用系统漏洞,创建一个不包含系统通知栏
或透明系统通知栏
的前台服务
,提升App进程的系统优先级,使其不被系统清理。 -
Fork Native守护进程:
Android 5.0 以前,App内部 fork 出来的native进程
不受系统管控。系统在杀死 App进程时,只会杀死对应的Java进程
。
因此诞生了一大批流氓软件
,通过fork native进程
,在 App 的 Java 进程被杀死的时候,通过 ActivityManager 重新拉起被杀死的进程,从而达到“进程永生”
的目的(这个方案据说最初由360提出,后来大家纷纷效仿)。 -
通过其他活跃App拉起:
进程保活的后期,又出现一种进程保活的方案。
多个App组成一个联盟,只要有一个App被用户使用,其他所有联盟内的App进程都会被拉起,以此来保证消息的及时到达。
例如:集成个推、极光推送
的App,可以通过前台活跃的App
拉起不活跃的App进程
,提升所有集成个推、极光推送
的第三方App推送消息的到达率。
注:
当前随着Android系统的不断完善,以上方案大多不再有效。
二、Android各版本后台进程限制
Android系统源码的开放,加之早期的系统尚不完善,众多应用软件提供商通过对AOSP各版本源码的深入解读,提出了各种
所谓黑科技的保活方案
,保证自己的应用程序进程不被系统清理。这段时期可谓是魑魅横行、群魔乱舞;Android手机用户则是苦不堪言,Android手机卡顿、待机时间短等问题也为人所诟病。
随着Android系统的不断完善,Google和国内终端手机厂商也对Android系统做了很多的改进,如今已经基本封死了第三方应用各种所谓黑科技流氓保活方案。
- Android后台进程限制;
- 国内手机厂商后台进程限制;
2.1 Android后台进程限制
如今Google每年发布的AOSP(Android Open Source Project)新版本中,每个版本均不断
增加后台服务进程限制的相关条款
,一方面是为了限制第三方App后台服务进程的运行,节省用户手机资源与电量消耗;另一方面,增加后台服务进程限制也是在保护用户的隐私。
Android 5.0 开始
,Android系统开始
以uid为标识,查杀APP进程组
,通过
杀死整个进程组
来杀死App进程,因此通过fork native 进程守护App进程这种方式从此不再有效。
Android 6.0开始
,引入了
Doze模式
用户拔下设备的电源插头,并在屏幕关闭后的一段时间后,设备会进入
Doze模式
。
- 在Doze模式下,系统会尝试通过限制应用访问网络和 占用CPU资源的措施来节省电量,阻止应用访问网络,并延迟作业与Alarm闹钟。
- 系统会定期退出Doze模式一小段时间,让应用完成其延迟的活动。在此维护期内,系统会运行所有待处理的同步操作、Alarm闹钟,并允许应用访问网络。
- 随着时间的推移,系统进入维护期的次数越来越少,这有助于在设备未连接至充电器的情况下长期处于Doze模式状态降低耗电量。
Android 7.0开始
,
加强了Doze模式
,进入Doze模式不再要求设备静止状态。
只要屏幕关闭了一段时间,且设备未插入电源,设备就会进入Doze模。
Android 8.0开始 ,加强了应用后台执行限制:
-
不能再通过
startService
创建后台服务,否则将抛出异常;但可通过Context.startForegroundService()
方法启动一个带通知栏提醒的前台服务; - 应用处于后台时,会对后台应用检索用户当前位置信息的频率进行限制。应用每小时仅接收几次位置信息更新。
-
广播限制:第三方应用将无法通过在
AndroidManifest
注册静态广播来接收大部分的系统隐式广播,以减少App对手机的唤醒,从而节省手机的电量;动态注册不受影响。
Android 9.0开始 ,进一步加强了应用后台执行限制:
- 后台应用不再可以访问麦克风和摄像头,传感器(加速度传感器、陀螺仪);
-
创建前台服务需要申请普通权限:
FOREGROUND_SERVICE
。
Android 10开始 ,再进一步加强了应用后台执行限制:
-
应用在后台运行时,访问手机位置需要动态申请
ACCESS_BACKGROUND_LOCATION
权限,用户则可以选择拒绝; - 应用在后台运行时,对后台应用启动Activity进行限制(运行前台服务的应用仍然会被应用看做“后台”应用)。
Android 11开始 ,再进一步完善了应用后台访问位置限制:
- 在Android11设备上,对于targetSdkVersion=30(Android 11)的应用,需先申请前台位置权限,后申请后台位置权限。
- 在Android11设备上,对于targetSdkVersion=30(Android 11)的应用,同时申请前台、后台位置权限时,系统会忽略该请求,无任何响应(需首先获取前台位置权限,再次申请后台位置权限)。
- 在Android11设备上,对于targetSdkVersion<=29(Android 10)的应用,同时申请前台、后台位置权限时,对话框不再提示始终允许字样,而是提供了位置权限的设置入口,需要用户在设置页面选择始终允许才能获得后台位置权限。
2.2 手机厂商后台进程限制
Google新发布的各版本主要还是限制
第三方App后台进程服务的运行
,而国内各终端厂商则在AOSP的基础上增加了
Alarm
的限制。
国内手机厂商(华米OV等)在手机系统休眠后,第三方App注册的
Alarm定时唤醒闹钟几乎全部无效!!!
“Google的后台进程限制” 与 “国内手机厂商Alarm限制”相叠加的双重影响是:
彻底封死了 “第三方手机软件” 利用 “Alarm闹钟” 定时唤醒手机系统,维持 “终端” 与 “远端服务器” 之间的TCP长连接。达到 远端服务器 可随时拉起 终端App,提升APP用户端使用时长(提升APP的DAU)这一方案。
- 国内手机厂商 Alarm 限制;
- 国内手机厂商 后台进程 限制;
2.2.1 手机厂商 Alarm 限制
国内终端手机厂商,不同厂商对应的Alarm限制方案也不相同,但目前已知的大概有以下两个方向:
-
Alarm 对齐机制:
当手机系统黑屏休眠时
,部分国内的手机品牌,会忽略 “第三方APP” 设置的 “Alarm唤醒周期”,强制将所有注册Alarm闹钟的APP,做Alarm唤醒周期对齐
。
例如:假设有三款App,其设定的Alarm唤醒周期分别为1分钟、3分钟、5分钟。手机会直接忽略以上三款App的Alarm唤醒周期设定,强制将Alarm唤醒对齐为每5分钟唤醒一次。
系统休眠时,将所有App的唤醒时间做对齐,来减少手机的唤醒次数,节省用户的待机电量消耗
。 -
Alarm 无效:
当手机系统黑屏休眠时
,部分国内的手机品牌,忽略 “第三方APP” 注册的全部Alarm,导致第三方应用注册的Alarm无效。
2.2.2 手机厂商 后台进程 限制
除了以上Alarm限制外,对于后台运行进程,不同终端厂商也进行了不同的限制。例如,部分终端厂商增加了后台进程耗电监测机制。
-
后台进程耗电监测机制:
当手机系统黑屏休眠时
,部分国内的手机品牌,会启动一个耗电监测进程
,若发现某个后台进程持续耗电,将直接杀死耗电进程。
三、手机厂商进程白名单
前边说道第三方App进程若在后台运行,会受到
Android系统
、
手机厂商Alarm
、
后台耗电监测
等限制。
这里的限制可能涉及到以下几个方面:
-
Android 系统
打盹模式(Doze)白名单
;
Android 6.0引入了打盹模式(Doze),不在打盹模式白名单中的进程,在系统休眠后其CPU、Alarm使用、网络连接等方面会受到系统的显示。
Android developer 针对低电耗模式和应用待机模式进行优化:
https:// developer.android.google.cn /training/monitoring-device-state/doze-standby?hl=zh-cn -
终端手机厂商
后台进行监测白名单
:
国内终端手机厂商,可能开发一个系统进程,对手机后台进行进行持续的检测,若发现异常在线进程,会直接进行清理,以节省用户的电量。 -
终端手机厂商
系统守护白名单
:
该白名单中的进程,若某些原因后台进程被杀,系统守护进程会在一定时间内迅速拉起该进程,从而保证进程的活跃。
以上几种情况的白名单,也有例外情况,比如:
微信
。
微信这个体量的APP,终端手机厂商一般会直接将其添加到这个白名单中,甚至在微信出现进程掉线时,可能会被手机厂商直接唤起。
例如:小米手机,打盹模式(Doze)白名单中,默认添加了微信的包名。
3.1 打盹模式(Doze)白名单
Android 6.0开始引入了
打盹模式(Doze)
,若想使自己的App不受打盹模式的影响,需终端手机厂商为该App添加这个白名单。
Android developer 针对低电耗模式和应用待机模式进行优化:
https://
developer.android.google.cn
/training/monitoring-device-state/doze-standby?hl=zh-cn
添加 Doze 白名单命令:
// 添加 Doze 白名单
adb shell dumpsys deviceidle whitelist +<package name>
// 显示白名单列表
adb shell dumpsys deviceidle whitelist
移除 Doze 白名单命令:
// 移除 Doze 白名单
adb shell dumpsys deviceidle whitelist -<package name>
// 显示白名单列表
adb shell dumpsys deviceidle whitelist
四、Push推送 建议方案
前边回顾了这几年
在进程保活这个问题上
,
各软件提供商
与
Android系统研发制造商
之间互相博弈过程。
如今在
“Google的后台进程限制” 与 “国内手机厂商Alarm限制”
双重限制下,第三方软件希望做到
App进程常驻后台
已经不太可能。
但每一个第三方App,基本都存在消息Push的需求。对于消息PUSH需求,第三方APP可使用的方案是什么?
- 接入终端手机厂商 Push通道;
- 进程添加到厂商进程保活白名单;
4.1 接入厂商 Push通道
在当前环境下的建议实现方案:接入终端手机厂商Push通道。
接入各终端厂商的Push通道,是最简单的实现方案。
厂商的Push通道常驻内存,所有第三方App接入厂商Push后
即能保证消息及时准确的到达,又可以减少终端用户手机的常驻进程,延长用户手机的待机时长
,可以说是对双方都有利。
- 接入厂商PUSH 终端侧实现方案;
- 接入厂商Push的 到达率上的注意点。
4.1.1 接入厂商PUSH 终端方案
接入厂商PUSH 终端方案,
终端侧可通过创建与维护一个单独的推送
Module 模块
,其中集成
华为、小米、VIVO、OPPO、中兴 5家
厂商的Push SDK;其他终端厂商的Push到达,可通过接入
个推
或
极光推送
来实现。
-
华米OV
等头部手机厂商:
通过接入华米OV
等头部厂商PUSH通道,保证市场上大多数手机用户的PUSH到达率; -
其他非头部手机厂商:
通过极光
或个推
等第三方市场占有率较高的PUSH实现方案,来覆盖除 华米OV 等头部手机厂商外的其他终端手机,保证市场上少部分手机用户的PUSH到达率;
采用这个方案, 研发人员需要开发和维护6个Push SDK组成的Module模块 。因此,接入厂商PUSH,优点和缺点可归结如下。
- 优点 : 可以很好的保证 PUSH到达率;
-
缺点
:
开发与维护成本增加。
国内头部手机终端厂商较多,研发的同学在一个App中需要同时维护 华为、小米、VIVO、OPPO、中兴、魅族 等一系列厂商的PUSH SDK,研发维护成本急剧增加。
4.1.2 接入厂商Push的注意点
- 接入厂商Push的注意点;
- 将来终端手机厂商 PUSH渠道 可能收费。
接入厂商Push的注意点
接入厂商PUSH,在PUSH到达上仍有不同的限制条件,这一点也
需要相关研发人员仔细研究各终端厂商PUSH的接入文档
。
例如OPPO,存在一个
PUSH配额
问题:
OPPO PUSH配额是 OPPO推送消息的数量限制规则。每天的Push量 超过这个PUSH配额,OPPO将不再下发Push。
OPPO PUSH配额官方描述:
https://
open.oppomobile.com/wik
i/doc#id=10200
OPPO PUSH配额官方描述如下:
将来终端手机厂商 PUSH渠道 可能收费。
目前终端手机厂商的PUSH渠道均是免费为开发者使用的,但随着全市场的PUSH通道均为各终端厂商把控后,第三方APP的消息PUSH也许存在收费的可能。
个人认为厂商PUSH渠道
收费
在不久的将来可能性还是很大的。
4.2 添加到厂商进程保活白名单
终端手机厂商追求的还是利益,因此 只要给钱或有钱赚,没什么不可谈的 。
与国内各终端厂商谈合作 添加到对应的进程白名单中。
这个方案,需要与各终端厂商合作 达成利益上的同盟或找到利益契合点,从而使终端厂商为您的App开放进程保活白名单。
若
您的公司与各终端厂商达成了利益同盟
,恭喜您可以考虑采用
维持一个终端与远端服务器的TCP长连接
,实现消息及时到达终端,提升用户端App使用时长的PUSH方案了。
五、TCP 动态心跳方案
前边提到,若
APP用户体量足够大
或
与各终端厂商达成了利益同盟
,可以考虑
维持一个终端与远端服务器的TCP长连接
,实现消息及时到达终端,提升用户端App使用时长。
- TCP心跳 相关标准;
- 维持 TCP心跳 不断连;
- TCP动态心跳 方案。
5.1 TCP心跳 相关标准
在Android下,通过自建 TCP长连接 来进行Push消息推送,TCP长连接若存活,消息Push才能及时送达。
而说到TCP心跳,那什么是TCP心跳,TCP消息的结构又是什么?
rfc5626 4.4.1 Keep-Alive with CRLF
标准中,关于SIP消息的TCP心跳给出了标准。
- Keep-Alive with CRLF;
- CRLF 详细说明;
- ping pong 心跳消息。
5.1.1 Keep-Alive with CRLF
rfc5626 4.4.1 Keep-Alive with CRLF
中,SIP消息TCP心跳标准可以如下描述:
- 终端侧需每隔一段时间(心跳间隔时间)需发送一个“ping”(double CRLF)消息,到远端服务器侧;
- 终端发送“ping”消息后,若在10s之内未收到远端服务端的“pong”(CRLF)消息,则终端认为与服务端的连接失败。
5.1.2 CRLF 详细说明
根据
rfc5626 4.4.1 Keep-Alive with CRLF
标准:
- 终端上行的ping消息为 CRLFCRLF;
- 远端服务器下行的pong消息为 CRLF;
pong消息为
CRLF
,含义是
回车
+
换行
符;
ping消息为
double CRLF
,也就是
CRLFCRLF
含义是
回车换行回车换行
符。
CRLF 在ASCII表中与 16进制数据 的对应关系,如下图所示:
ASCII的对应表中查看:
-
ping心跳消息
CRLFCRLF
,对应的16进制数据为0d0a0d0a
; -
pong心跳消息
CRLF
,对应的16进制数据为0d0a
;
心跳 | 含义 | 缩写 | 十六进制 |
---|---|---|---|
ping | 终端上行心跳数据 | CRLFCRLF | 0d0a0d0a |
pong | 远端服务器下行心跳数据 | CRLF | 0d0a |
5.1.3 ping pong 心跳消息
这一节关于 ping pong心跳消息,从其消息发送接收流程、WireShark现网数据抓包、消息结构举例 三方面进行介绍。
- ping pong 心跳流程;
- ping pong 心跳WireShark抓包;
- ping pong 心跳消息举例。
ping pong 心跳流程:
ping pong 心跳消息的发送/接收流程,如下图所示:
- 终端侧需每隔一段时间(心跳间隔时间)需发送一个“ping”(double CRLF 0xd0xa0xd0xa)消息,到远端服务器侧;
- 终端发送“ping”消息后,若在10s之内未收到远端服务端的“pong”(CRLF 0xd0xa)消息,则终端认为与服务端的连接失败。
ping pong 心跳WireShark抓包:
ping pong 心跳WireShark抓包如下图所示:
ping pong 心跳消息举例:
ping pong 心跳消息的举例如下所示:
// 终端侧发送: TCP Ping:0d 0a 0d 0a
Frame 2427: 60 bytes on wire (480 bits), 60 bytes captured (480 bits)
Linux cooked capture v1
Internet Protocol Version 4, Src: 10.xxx.xxx.xxx, Dst: 183.xxx.xxx.xxx
Transmission Control Protocol, Src Port: 46649, Dst Port: 5460, Seq: 21, Ack: 11, Len: 4
Source Port: 46649
Destination Port: 5460
[Stream index: 3]
[TCP Segment Len: 4]
Sequence Number: 21 (relative sequence number)
Sequence Number (raw): 149531750
[Next Sequence Number: 25 (relative sequence number)]
Acknowledgment Number: 11 (relative ack number)
Acknowledgment number (raw): 3481893389
0101 .... = Header Length: 20 bytes (5)
Flags: 0x018 (PSH, ACK)
Window: 65535
[Calculated window size: 65535]
[Window size scaling factor: -1 (unknown)]
Checksum: 0x727d [unverified]
[Checksum Status: Unverified]
Urgent Pointer: 0
[SEQ/ACK analysis]
[Timestamps]
TCP payload (4 bytes)
Data (4 bytes)
Data: 0d0a0d0a
[Length: 4]
// 终端侧接收:TCP ACK
Frame 2432: 56 bytes on wire (448 bits), 56 bytes captured (448 bits)
Linux cooked capture v1
Internet Protocol Version 4, Src: 183.xxx.xxx.xxx, Dst: 10.xxx.xxx.xxx
Transmission Control Protocol, Src Port: 5460, Dst Port: 46649, Seq: 11, Ack: 25, Len: 0
Source Port: 5460
Destination Port: 46649
[Stream index: 3]
[TCP Segment Len: 0]
Sequence Number: 11 (relative sequence number)
Sequence Number (raw): 3481893389
[Next Sequence Number: 11 (relative sequence number)]
Acknowledgment Number: 25 (relative ack number)
Acknowledgment number (raw): 149531754
0101 .... = Header Length: 20 bytes (5)
Flags: 0x010 (ACK)
Window: 21476
[Calculated window size: 21476]
[Window size scaling factor: -1 (unknown)]
Checksum: 0x7279 [unverified]
[Checksum Status: Unverified]
Urgent Pointer: 0
[SEQ/ACK analysis]
[Timestamps]
// 终端侧接收:TCP Pong:0d 0a
Frame 2433: 58 bytes on wire (464 bits), 58 bytes captured (464 bits)
Linux cooked capture v1
Internet Protocol Version 4, Src: 183.xxx.xxx.xxx, Dst: 10.xxx.xxx.xxx
Transmission Control Protocol, Src Port: 5460, Dst Port: 46649, Seq: 11, Ack: 25, Len: 2
Source Port: 5460
Destination Port: 46649
[Stream index: 3]
[TCP Segment Len: 2]
Sequence Number: 11 (relative sequence number)
Sequence Number (raw): 3481893389
[Next Sequence Number: 13 (relative sequence number)]
Acknowledgment Number: 25 (relative ack number)
Acknowledgment number (raw): 149531754
0101 .... = Header Length: 20 bytes (5)
Flags: 0x018 (PSH, ACK)
Window: 21476
[Calculated window size: 21476]
[Window size scaling factor: -1 (unknown)]
Checksum: 0x727b [unverified]
[Checksum Status: Unverified]
Urgent Pointer: 0
[SEQ/ACK analysis]
[Timestamps]
TCP payload (2 bytes)
Data (2 bytes)
Data: 0d0a
[Length: 2]
// 终端侧发送:TCP ACK
Frame 2434: 56 bytes on wire (448 bits), 56 bytes captured (448 bits)
Linux cooked capture v1
Internet Protocol Version 4, Src: 10.xxx.xxx.xxx, Dst: 183.xxx.xxx.xxx
Transmission Control Protocol, Src Port: 46649, Dst Port: 5460, Seq: 25, Ack: 13, Len: 0
Source Port: 46649
Destination Port: 5460
[Stream index: 3]
[TCP Segment Len: 0]
Sequence Number: 25 (relative sequence number)
Sequence Number (raw): 149531754
[Next Sequence Number: 25 (relative sequence number)]
Acknowledgment Number: 13 (relative ack number)
Acknowledgment number (raw): 3481893391
0101 .... = Header Length: 20 bytes (5)
Flags: 0x010 (ACK)
Window: 65535
[Calculated window size: 65535]
[Window size scaling factor: -1 (unknown)]