相关文章推荐
酷酷的排球  ·  Send-MailMessage ...·  1 月前    · 
暴走的排球  ·  Index of ...·  8 月前    · 
冷静的咖啡  ·  扩展器概述 - WPF .NET ...·  9 月前    · 
含蓄的葡萄酒  ·  python 线程中断-掘金·  1 年前    · 
首发于 CODING笔记

一文详解 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进程这种方式从此不再有效。

Process.killProcessGroup(info.uid, pid)

Android 6.0开始 ,引入了 Doze模式 用户拔下设备的电源插头,并在屏幕关闭后的一段时间后,设备会进入 Doze模式

Android 6.0 Doze模式
  • 在Doze模式下,系统会尝试通过限制应用访问网络和 占用CPU资源的措施来节省电量,阻止应用访问网络,并延迟作业与Alarm闹钟。
  • 系统会定期退出Doze模式一小段时间,让应用完成其延迟的活动。在此维护期内,系统会运行所有待处理的同步操作、Alarm闹钟,并允许应用访问网络。
  • 随着时间的推移,系统进入维护期的次数越来越少,这有助于在设备未连接至充电器的情况下长期处于Doze模式状态降低耗电量。

Android 7.0开始 加强了Doze模式 ,进入Doze模式不再要求设备静止状态。
只要屏幕关闭了一段时间,且设备未插入电源,设备就会进入Doze模。

Android 7.0 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 后台耗电监测 等限制。

这里的限制可能涉及到以下几个方面:

以上几种情况的白名单,也有例外情况,比如: 微信
微信这个体量的APP,终端手机厂商一般会直接将其添加到这个白名单中,甚至在微信出现进程掉线时,可能会被手机厂商直接唤起。
例如:小米手机,打盹模式(Doze)白名单中,默认添加了微信的包名。

例如小米手机:微信的包名会被直接添加到这个白名单中

3.1 打盹模式(Doze)白名单

Android 6.0开始引入了 打盹模式(Doze) ,若想使自己的App不受打盹模式的影响,需终端手机厂商为该App添加这个白名单。

Android developer 针对低电耗模式和应用待机模式进行优化:
developer.android.google.cn

添加 Doze 白名单命令:

// 添加 Doze 白名单
adb shell dumpsys deviceidle whitelist +<package name>
// 显示白名单列表
adb shell dumpsys deviceidle whitelist
添加 Doze 白名单


移除 Doze 白名单命令:


// 移除 Doze 白名单
adb shell dumpsys deviceidle whitelist -<package name>
// 显示白名单列表
adb shell dumpsys deviceidle whitelist
移除 Doze 白名单

四、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配额官方描述:
open.oppomobile.com/wik
OPPO PUSH配额官方描述如下:

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)消息,则终端认为与服务端的连接失败。
rfc5626 TCP ping pong保活

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进制数据 的对应关系,如下图所示:

CRLF为回车换行符

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 pong心跳发送接收流程
  • 终端侧需每隔一段时间(心跳间隔时间)需发送一个“ping”(double CRLF 0xd0xa0xd0xa)消息,到远端服务器侧;
  • 终端发送“ping”消息后,若在10s之内未收到远端服务端的“pong”(CRLF 0xd0xa)消息,则终端认为与服务端的连接失败。

ping pong 心跳WireShark抓包:

ping pong 心跳WireShark抓包如下图所示:

ping pong

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)]