EN

Android App 链式唤醒分析

字数统计: 5.4k阅读时长: 20 min
2020/05/07
loading

MIUI 12 的发布, 将之前一直是应用开发者和 Rom 开发者斗争最激烈的部分展示给了普通消费者, 让普通消费者也知道了这场斗争的细节, 正所谓 “魔高一尺道高一丈”, Rom 开发者由于有更高的代码修改权限, 始终占据着上风 ; App 开发者当然也不甘示弱, 各种保活拉起黑科技层出不穷,甚至 Google 都参与到了这部分斗争中, 居中调和, 制定各种规则来规范双方. 当然斗争对双方来说都算是好事, 毕竟任何一方完全的胜利都会导致 “狡兔死走狗烹,飞鸟尽良弓藏”

不过双方斗争的受害者无疑还是使用手机的消费者, App 如果斗争成功, 那么手机上各种后台进程乱跑, 杀不掉, 占用 CPU 和内存, 这不是消费者想看到的 ; 如果 Rom 开发者斗争成功, App 的体验必定会大打折扣, 各位 App 开发者应该深有体会.

从文章最后一段可以看到, 其实各个手机厂商对付这一套都有自己的策略, 基本上都可以搞定自启动和关联启动. 至于隐私, 李彦宏曾经说过 “中国人对隐私问题的态度更加开放,也相对来说没那么敏感。如果他们可以用隐私换取便利、安全或者效率。在很多情况下,他们就愿意这么做“ . 大家想想在微信里面复制一段话打开到淘宝就可以自动跳转到这个物品, 方不方便? 好不好用? 还想不想用? 剪贴板再借我看一看?

希望大家在隐私问题上不要打哈哈, 技术是把双刃剑, 如果隐私落到别有用心的人手上, 后果是很严重的, 就算不是为了自己, 为了下一代. 欧盟为什么要搞《通用数据保护条例》(General Data Protection Regulation,简称 GDPR), 就是为了隐私. 举个例子, 国内很多厂商的产品现在要区分是否在欧盟买, 如果是在欧盟卖的话, 就得把里面那些收集用户数据的功能都关掉, 否则抓住了就能罚你罚到吐血 . 至于中国和印度, 随便收集.

本篇文章不涉及到隐私部分, 我是对隐私保护无条件支持的 . 这里只从技术的角度, 来讲一下 MIUI 12 爆出来的应用自启动和关联唤醒的问题.

版本说明(2026-10-14 核查):本文保留 2020 年的个人讨论与 Android 10 AOSP 案例。旧应用/SDK 版本未完整记录,不能据此断言这些应用今天仍有相同行为。下文区分“声明了接收器”“进程已启动”“谁触发了启动”;厂商设置和 SDK 配置标注为历史资料,并补充 Android 7~15 的相关限制。原案例不是当前所有系统的复现结果。

技术名词解释

首先区分进程、组件和启动原因,避免把“有组件记录”直接读成“又启动了一个 App”。

进程启动

Android 的四类应用组件是 Activity、Service、BroadcastReceiver 和 ContentProvider;进程是承载它们的运行环境,不是与组件并列的“第五部分”。应用运行时通常使用一个默认进程,也可把组件配置到其他进程;这些进程通常由系统按需创建,不一定是主进程 fork 出来的“子进程”。参见应用基础。

系统在允许的组件请求需要执行时,复用对应进程,或在进程不存在时先创建它。点击桌面图标也可能复用已有进程和 Activity。动态注册的 BroadcastReceiver 依赖已有进程,不能把它等同于可冷启动进程的 Manifest 接收器;文件监听也不能让一个已经不存在的进程自行执行代码。

本文关注没有新界面出现的 Receiver、Service、Provider 路径。不过后台拉起 Activity 不只是用户体验问题:Android 10 起已有后台 Activity 启动限制。进程、组件被启动,也不表示应用界面已展示。

自启动

本文把由系统事件或已安排任务触发、没有用户当场打开界面的进程启动称为“自启动”。它是分析用语,不是一个统一的 Android API;开机广播、任务调度等仍由系统决定是否派发、何时执行。

关联启动

本文把一个应用通过跨应用组件请求,引起另一个应用进程启动称为“关联启动”。同一 SDK 可能参与其中,但“装了相同 SDK”不是充分条件:组件可见性、exported/权限、目标状态及后台限制都可能阻止请求。

启动阻断

ROM 可以在组件解析、请求和进程启动等阶段加入策略;AOSP 本身也有权限、后台执行和进程状态检查。阻断不一定只是“直接返回”,也可能拒绝、延后或抛出异常。

比如用户在一个 App 里点了支付宝付款,支付组件却被系统拦了,想想他会是什么心情。判断时要区分这种用户主动发起的支付、分享等跨应用流程与后台维护请求。误拦截会破坏正常使用,放行某次请求也不等于允许永久驻留。

分析手段

Monkey

Monkey 可用于产生应用切换和输入压力,但不是归因的必要步骤。定位单次关联启动时,优先固定设备构建、应用版本/targetSdk、初始进程状态和一次操作,再对照采样。我当时装了不少应用,把下面的 Monkey 命令跑起来就去睡觉了,让它继续制造切换负载。这是当年的长时间压力测试背景,不作为默认复现命令:

1
adb shell monkey --kill-process-after-error --ignore-security-exceptions --ignore-crashes --pct-appswitch 90 --pct-touch 10 --throttle 10000 --ignore-timeouts --ignore-native-crashes 100000000

--pct-appswitch 90 表示 Activity 启动事件的比例,不保证 90% 都切到不同应用;--throttle 10000 是 10 秒级节流,100000000 是事件计数,不是运行毫秒数。按每次 10 秒粗算约 31.7 年,实际时长还受事件组合和退出条件影响;它不应被理解成“一晚跑完”。忽略异常继续运行也不代表没有错误。

按官方 Monkey 参数,可在专用测试环境把包名改成待测应用,从有界采样开始:

1
adb shell monkey -p com.example.app -s 20200507 --pct-appswitch 90 --pct-touch 10 --throttle 500 -v 200

-p 限制 Monkey 可访问的 Activity 包,不隔离系统广播或被应用请求启动的其他进程;seed 也不能固定网络、系统调度和后台任务。本文核查未执行这些命令。

EventLog

am_proc_start、am_proc_died 是定位线索;日志缓冲区会覆盖,采集可能不完整,也不包含所有启动调用方。先保存原始日志,再过滤,避免只剩脱离上下文的几行:

1
adb logcat -b events -v threadtime > events.log

实时快速查看可用:

1
adb logcat -b events -v threadtime | grep -E 'am_proc_died|am_proc_start|am_proc_bound'

Android 10 EventLogTags定义了字段。am_proc_bound 是进程绑定到系统工作的记录,不等于某个 Service 的 bindService 已成功,更不表示 Activity 已显示。PID 会复用,关联时还要看时间、用户和 UID。

Dumpsys

原文用 dumpsys activity 看组件。缩小到广播或目标服务,通常更方便:

1
2
3
4
adb shell dumpsys activity
adb shell dumpsys activity broadcasts
adb shell dumpsys activity services com.qq.reader
adb shell dumpsys package com.qq.reader

这些是状态快照;Connections 可以显示绑定客户端,但不是完整的调用历史。应与同一时段的启动事件、调用方日志或系统 trace 对齐。保存构建号、应用版本、targetSdk、用户 ID 和采样时间,才能比较两个样本。

自启动的技术分析

下面仍沿用开机、网络和媒体扫描三类旧案例。广播存在,不代表任意 targetSdk 的应用都能在 Manifest 注册后被冷启动;还需看接收器状态、权限、过滤条件与系统策略。

开机广播

1
android.intent.action.BOOT_COMPLETED

BOOT_COMPLETED需要 RECEIVE_BOOT_COMPLETED,且不是刚安装从未启动、force-stop 等所有状态都能接收。它与 LOCKED_BOOT_COMPLETED 的 Direct Boot 场景不同,后者还需相应的 directBootAware 配置和设备加密存储处理。

开机广播属于 Android 8.0 隐式广播限制的例外,但收到广播不等于可以无限执行后台工作。onReceive 应尽快结束,备份/同步等可延后任务可交给 WorkManager;WorkManager 同样遵守系统调度,不保证立即运行或进程常驻。

案例: 腾讯新闻监听开机广播拉起后台进程

原记录背景是开机广播,下图裁剪部分显示 com.tencent.news.system.BootBroadcastReceiver,其配置进程是 com.tencent.news:PushService,不是正文原先写的默认进程。

图中 Deliver +7s128ms 不能直接翻译成“onReceive 执行了 7 秒”。在 android-10.0.0_r1 的 BroadcastRecord中它打印 duration[i],由 BroadcastQueue.finishReceiverLocked按 finishTime - receiverTime 记录;Manifest 接收器的计时可覆盖进程启动、分派和等待。这是约 7.128 秒的系统处理区间,不是纯 CPU 时间。裁图没有完整 action 和对应 am_proc_start,因此也不能仅凭这一小段证明当时一定新建了进程。

网络变化

1
android.net.conn.CONNECTIVITY_CHANGE

比如直播看得好好的,Wi-Fi 突然断了,切到移动网络,应用就可以提醒用户是否继续看,毕竟直播还是很耗流量的。网络连接、断开或默认网络变化可以通知应用调整业务。但 CONNECTIVITY_ACTION从 API 28 起已弃用;在 Android 7.0(API 24)起,targetSdk ≥24 的应用不能靠 Manifest 接收它,仍可在有效 Context 中动态注册。动态接收不是让已死亡进程重新运行的替代手段。

现在观察网络状态通常使用 registerDefaultNetworkCallback/registerNetworkCallback;需要满足网络条件后再同步的任务可使用 WorkManager 约束。网络已连接也不保证目标服务器可达,不能只把 Wi-Fi/移动网络切换当作联网成功。

案例

我当时在笔记里列出的接收器包括:

  1. com.alibaba.android.rimet(钉钉):com.xiaomi.push.service.receivers.NetworkStatusReceiver
  2. cn.xuexi.android:com.xiaomi.push.service.receivers.NetworkStatusReceiver
  3. com.sina.weibo:com.xiaomi.push.service.receivers.NetworkStatusReceiver
  4. com.sdu.didi.psnger:com.didi.sdk.push.PushNetReceiver
  5. com.meelive.ingkee:com.network_optimization.NetWorkStateReceiver

这张裁图实际只显示学习强国与钉钉,状态为 Pending,同时带有 maxManifestReceiverApiLevel=23。Android 10 广播派发代码还会据此检查 targetSdk 并决定是否跳过,不能把候选列表当作“全部已被拉起”。原文所说“200 多个”缺少完整原始统计,不能由这张图核验。

1
android.net.wifi.STATE_CHANGE

第二张图也是 Pending 列表,包含腾讯手机管家和 UC 的接收器。Wi-Fi 状态广播与 CONNECTIVITY_CHANGE 不是同一个 action;targetSdk ≥26 的普通应用还受 Android 8.0 Manifest 隐式广播限制约束,不能把两张图理解为当前通用自启方案。

媒体库扫描

1
2
3
android.intent.action.MEDIA_SCANNER_STARTED
android.intent.action.MEDIA_SCANNER_FINISHED
android.intent.action.MEDIA_EJECT

前两个 action 表示媒体扫描器开始/完成目录扫描,第三个是外部存储移除请求相关事件;它们不是“每新增一个文件就会发给全部 App”的统一通知。隐式广播例外列表包含 MEDIA_EJECT 等存储状态广播,但不能把该例外扩展到所有扫描广播。读取媒体还需满足相应权限和存储访问规则。

案例: jd 监听 MEDIA_SCANNER_STARTED 广播自启

原文把下图作为京东金融自启案例。实际可见的是 com.jd.jrapp.library.longconnection.receiver.BootReceiver 被列为 Pending #0,action 为 MEDIA_SCANNER_STARTED,data 为 file:///storage/emulated/0:

它能证明这次广播记录包含该接收器,不能单独证明派发成功或已创建进程。确认自启还需要后续投递结果和同时间段的 am_proc_start。

三方 SDK - 个推

推送 SDK 可能声明后台组件,但不能只靠进程名后缀鉴定 SDK。:pushservice 是应用内的进程命名约定,多种库都可能使用;组件类名也需结合实际 APK 依赖、合并后的 Manifest 和运行证据。

下面保留原文 2020 年、未记录具体 SDK 版本的配置片段,用于识别历史组件,不作为当前集成模板:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<service
android:name="com.igexin.sdk.PushService"
android:permission="android.permission.BIND_JOB_SERVICE"
android:exported="false"
android:label="NotificationCenter"
android:process=":pushservice"/>
<receiver android:name="com.igexin.sdk.PushReceiver" >
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
<action android:name="android.intent.action.USER_PRESENT" />
<!-- 以下三项为可选的 action 声明,有助于提高 service 存活率 -->
<action android:name="android.intent.action.MEDIA_MOUNTED" />
<action android:name="android.intent.action.ACTION_POWER_CONNECTED" />
<action android:name="android.intent.action.ACTION_POWER_DISCONNECTED" />
</intent-filter>
</receiver>
<activity
android:name="com.igexin.sdk.PushActivity"
android:excludeFromRecents="true"
android:exported="false"
android:process=":pushservice"
android:taskAffinity="com.igexin.sdk.PushActivityTask"
android:theme="@android:style/Theme.Translucent.NoTitleBar" >
</activity>
<activity
android:name="com.igexin.sdk.GActivity"
android:excludeFromRecents="true"
android:exported="true"
android:process=":pushservice"
android:taskAffinity="com.igexin.sdk.PushActivityTask"
android:theme="@android:style/Theme.Translucent.NoTitleBar"/>

片段中的 Receiver 有 intent-filter,却没有显式 exported;targetSdk ≥31 的相应组件必须显式声明 exported,值需按组件用途和权限决定,不能统一改成 true。当前个推集成指南标明适用于 SDK 3.1.2.0 及以后,采用 Maven/AAR 配置和版本配套要求;迁移应对照所用版本,不能叠加这份旧 XML。

图里虽然有 com.ss.android.ugc.aweme:pushservice,实际接收器类名是 com.taobao.accs.EventReceiver,而且仍为 Pending。它不足以证明“抖音因个推被唤醒”,也不能据此推断该 APK 是否还包含其他 SDK。

关联启动的技术分析

多个应用共用 SDK 时,可能通过可访问的 Service 或 Provider 建立联系;是否成功启动目标,仍取决于平台检查。原文的实验观察不能支持“多数关联启动都由极光造成”这种总体比例判断。下面只分析留下的这组 JPush 组件记录。

1
[0,19428,10195,com.qq.reader,service,{com.qq.reader/cn.jpush.android.service.DaemonService}]

按 Android 10 字段定义,六项依次是:用户 0、新进程 PID 19428、UID 10195、进程名 com.qq.reader、启动类型 service、关联组件 cn.jpush.android.service.DaemonService。UID 不是调用者 PID;这条记录没有给出是谁发起了服务请求。

案例:起点 App 通过极光推送拉起 QQ 阅读

当时我先执行 adb shell am force-stop com.qq.reader,再观察 EventLog 中的启动记录。force-stop 会改变包的 stopped 状态,不等同于 LMK 回收、普通进程退出或划掉最近任务。 单凭下图不能复原强停和下一次请求之间发生的全部操作。

截图中的事件是 05-06 08:48:35.337,PID 4302;下一行是同 PID 的 am_proc_bound。它与上面文字例子的 PID 19428 不是同一条记录,不能拼成同一次启动。

EventLog 还没告诉我们是谁发起了请求,我接着看 QQ 阅读的 ServiceRecord。这里对应 PID 4302,Connections 客户端为 2808:com.qidian.QDReader:pushcore/u0a174。这证明采样时存在绑定关系,是追查调用方的线索;并不单独证明这条连接就是进程创建的首次触发者。started=false 表示当时 startRequested 为 false,不表示服务从未运行过。

这里还有一个容易读错的字段:ActiveServices 的简略 dump先打印绑定 Intent,再打印客户端,所以 flg=0x20 是 Intent.FLAG_INCLUDE_STOPPED_PACKAGES,不是 bindService 的 flags,不能据此判断是否带 BIND_AUTO_CREATE。Android 10 的服务启动路径也有清除包 stopped 状态的逻辑;这有助于理解旧系统行为,但不证明截图的完整调用顺序。

要确认因果,应在同一时间线记录服务请求的 caller UID/PID、组件、bind/start 标志和进程创建事件,并做只改变触发操作的对照。本文没有 MIUI 12 真机截图,不能声称其管理页面已经验证显示了“起点在 8:48 拉起 QQ 阅读”。

新版边界:Android 15 的 stopped-state 变更要求通过直接或间接用户操作解除停止状态,并在 force-stop 时取消 PendingIntent。不要把 Android 10 旧实验当作绕过当前强停的通用方法。

极光官方文档唤醒配置

下图是原文保存的历史回调说明,列出 startService、bindService、ContentProvider 三种类型。回调类型是 SDK 的报告,不替代系统日志对调用方和时序的核查。

极光关联启动文档

两张历史说明分别提到 JPush 1.8.0 的服务互拉和 3.1.6 的通道共享;当年的组件开关及 VIP 说明不能作为当前产品规则。

本次核查的官方 Android API 文档已分别列出 JCollectionAuth.enableAutoWakeup(应用自启动)和 JPushInterface.setLinkMergeEnable(链路调节器),参数 false 用于关闭对应功能。这两段没有标明各自最早支持版本,接入前应核对所用 JPush/JCore 版本;不能假定旧 APK 可直接调用,也不能把一个开关理解为关闭所有系统后台入口。厂商对省电或到达率的描述也不是本文重新测得的结果。

手机厂商应对

下面是我在 2020 年那台测试机上的使用感受,保留当时的语境:

最前面的有说到, 进程管理是应用开发者和 Rom 开发者斗争最激烈的部分, MIUI 选择了将斗争的过程展示给了普通消费者, 让普通消费者也知道了这场斗争的细节 . 其他的厂商也做了相同的事情, 否则整个系统基本上是没法用的, 就像我手上现在这台测试用的 pixel, 不断有进程因为整机内存太小被 LMK 杀掉, 然后马上被各种手段重新启动, 耗电极快, 卡的连娘都不认识了.

旧设置清单已过期,以下仅留作历史定位。 厂商、系统版本、地区和机型都会改变入口与效果;没有逐台新设备复测,不能提供统一的今日菜单路径。关闭通知、限制后台、强行停止是不同操作,也没有“加入白名单就一定存活”的保证。

极光推送白名单配置

原文保存的厂商说明包含 MIUI 7 神隐模式等旧入口,不能直接指导当前设备:

个推白名单配置

EMUI OS(华为)

旧资料列出“自启动管理”“后台应用保护”和“通知管理”。它们控制的对象不同;不再沿用“未加入就只能手动恢复”这种跨版本保证。

Flyme OS(魅族)

旧资料列出“自启动管理”和通知开关。禁止通知展示不等于停止进程,后台调度还需看当前系统策略。

Funtouch OS(VIVO)

旧资料使用 i 管家的自启动入口。普通清理、force-stop 和系统回收不能混称为“杀进程”,也不能只凭白名单推断后续是否启动。

Color OS(OPPO)

旧资料提到“纯净后台”和锁定进程,菜单已不能作为通用路径。最近任务锁定不是 Android 的永久进程保活合同。

MIUI OS (小米)

旧资料包含“自启动管理”“省电策略”和 MIUI 7 神隐模式,保留名称仅供查旧记录。是否后台联网、何时收到消息,应按当前系统和业务场景测试,不再使用“后台几分钟必定断网”的统一结论。

跨 ROM 排查时先检查平台限制:targetSdk ≥26 的后台服务/隐式广播限制;targetSdk ≥31 的从后台启动前台服务的限制及例外;Android 13 起 targetSdk ≥33 且处于 restricted 状态时的开机广播延后。Android 14/15 对开机接收器启动部分 FGS 类型还有额外约束。通知权限和后台执行权限也应分开检查。

关于我 && 博客

下面是个人的介绍和相关的链接,期望与同行的各位多多交流,三人行,则必有我师!

  1. 博主个人介绍 :里面有个人的微信和微信群链接。
  2. 本博客内容导航 :个人博客内容的一个导航。
  3. 个人整理和搜集的优秀博客文章 - Android 性能优化必知必会 :欢迎大家自荐和推荐 (微信私聊即可)
  4. Android性能优化知识星球:付费社群已于 2026 年停止运营,不再招收新成员;此处保留历史介绍。

一个人可以走的更快 , 一群人可以走的更远

微信扫一扫

CATALOG
  1. 1. 技术名词解释
    1. 1.1. 进程启动
    2. 1.2. 自启动
    3. 1.3. 关联启动
    4. 1.4. 启动阻断
  2. 2. 分析手段
    1. 2.1. Monkey
    2. 2.2. EventLog
    3. 2.3. Dumpsys
  3. 3. 自启动的技术分析
    1. 3.1. 开机广播
      1. 3.1.1. 案例: 腾讯新闻监听开机广播拉起后台进程
    2. 3.2. 网络变化
      1. 3.2.1. 案例
    3. 3.3. 媒体库扫描
      1. 3.3.1. 案例: jd 监听 MEDIA_SCANNER_STARTED 广播自启
    4. 3.4. 三方 SDK - 个推
  4. 4. 关联启动的技术分析
    1. 4.0.1. 案例:起点 App 通过极光推送拉起 QQ 阅读
    2. 4.0.2. 极光官方文档唤醒配置
    3. 4.0.3. 极光关联启动文档
  • 5. 手机厂商应对
    1. 5.1. 极光推送白名单配置
    2. 5.2. 个推白名单配置
      1. 5.2.1. EMUI OS(华为)
      2. 5.2.2. Flyme OS(魅族)
      3. 5.2.3. Funtouch OS(VIVO)
      4. 5.2.4. Color OS(OPPO)
      5. 5.2.5. MIUI OS (小米)
  • 6. 关于我 && 博客