EN

Android 桌面被杀问题分析案例

字数统计: 4k阅读时长: 15 min
2019/09/17
loading

写这篇文章的契机是因为一个实际遇到的问题, 这个问题其实不难, 不过在分析了这个问题然后写日记的时候, 我突然觉得这个问题分析的过程有必要记录一下, 分享给大家 . 分析过程中有用到一些工具, 一些方法, 也从另外一个聪明的小伙伴梅明那里学到了一些分析技巧和工具的使用技巧 .

这篇文章中分析过程包括我之前在Android 中的卡顿丢帧原因概述 - 方法论 里面提到的一些工具, 包括 : 复现视频 \ Event Log \ Android Studio 源码和 App Debug \ Android Studio Profile \ Systrace \ Dumpsys \ PS 等 . 大多数工具大家都在开发过程中使用过, 这次分析正是使用了这些工具相互配合, 最终找到的问题的原因.

大家看下来可能会觉得, 这么简单一个问题还需要写一篇文章 ? 我写这篇文章的目的一是为了记录给自己, 二是觉得分析过程比较有普遍性, 包括分析思路和工具的使用, 如果可以帮助到大家, 那么最好不过了, 如果你也有好的思路或者独家调试技巧, 欢迎大家扫描关于我 里面的讨论群二维码加入群聊, 共同进步!

版本说明(2026-10-13 技术复核): 本文保留 2019 年特定 Flyme/应用环境的调试案例,源码参照为 AOSP android-9.0.0_r1。旧版 Studio 操作和跨包清理能力不再适用于所有设备;下文补充 Android 14 / API 34 的行为限制、Android 16 源码及 API 30 起的退出记录。日志和截图是历史证据,没有重新复现原应用,也不评价其当前版本。

现象

这个问题是测试直接报过来的, Bug 描述是典型的按现象描述 : “从应用返回桌面, 桌面图标加载慢“. 测试这边提供了录制的视频和抓取的 Log, 以及对应的 Systrace 等. 既然现象和 Log 都在, 那么就开始分析吧.

分析过程

确定问题发生的时间点

  1. 先看复现视频,确定问题出现的大致时间。
  2. 用视频和 Event Log 对时,再结合 Main Log 还原用户操作。
  3. 这段日志里,和我信(com.jx.cmcc.ict.ibelieve)恢复到前台后,Launcher 被杀;返回桌面时,系统重新创建了 Launcher 进程。此时还不能仅凭先后关系确认谁发起了清理。
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
EventLog
// 启动 com.jx.cmcc.ict.ibelieve 这个 App
09-10 10:14:48.877 1456 2269 I am_set_resumed_activity: [0,com.jx.cmcc.ict.ibelieve/.ui.MainTabActivity,resumeTopActivityInnerLocked]
09-10 10:14:48.886 1456 2269 I am_resume_activity: [0,80317506,54938,com.jx.cmcc.ict.ibelieve/.ui.MainTabActivity]
09-10 10:14:48.891 1456 1485 I sysui_count: [window_time_0,0]
09-10 10:14:48.891 1456 1485 I sysui_multi_action: [757,803,799,window_time_0,802,0]
09-10 10:14:48.902 1456 2269 I am_uid_stopped: 10021

// 这里桌面被杀
09-10 10:14:48.903 1456 2269 I am_kill : [0,13509,com.meizu.flyme.launcher,600,kill background]

// 这里开始从 App 返回桌面
09-10 10:14:51.990 1456 1791 I am_pause_activity: [0,80317506,com.jx.cmcc.ict.ibelieve/.ui.MainTabActivity]
09-10 10:14:51.994 1456 1791 I am_task_to_front: [0,54923]
09-10 10:14:51.996 13674 13674 I am_on_paused_called: [0,com.jx.cmcc.ict.ibelieve.ui.MainTabActivity,handlePauseActivity]
09-10 10:14:52.013 1456 2270 I am_uid_running: 10021

// 重新创建桌面进程
09-10 10:14:52.025 1456 2270 I am_proc_start: [0,14013,10021,com.meizu.flyme.launcher,activity,com.meizu.flyme.launcher/.Launcher]
09-10 10:14:52.045 1456 2270 I am_proc_bound: [0,14013,com.meizu.flyme.launcher]
09-10 10:14:52.069 1456 2270 I am_uid_active: 10021
09-10 10:14:52.069 1456 2270 I am_restart_activity: [0,238217861,54923,com.meizu.flyme.launcher/.Launcher]

// 桌面恢复与启动耗时记录(不等于全部图标加载完成)
09-10 10:14:52.071 1456 2270 I am_set_resumed_activity: [0,com.meizu.flyme.launcher/.Launcher,minimalResumeActivityLocked]
09-10 10:14:52.335 14013 14013 I am_on_resume_called: [0,com.meizu.flyme.launcher.Launcher,LAUNCH_ACTIVITY]
09-10 10:14:52.437 1456 1504 I am_activity_launch_time: [0,238217861,com.meizu.flyme.launcher/.Launcher,413,413]

Launcher 的 PID 从 13509 变成 14013,解释了为什么返回时要走冷启动。am_activity_launch_time 中的 413,413 是启动耗时统计:在 AOSP Android 9 的 ActivityRecord.reportLaunchTimeLocked() 中,分别写入 thisTime 和 totalTime,由窗口绘制等启动报告路径触发。它们既不是 CPU 运行时间,也不保证所有图标和异步内容加载完毕;具体设备仍要对照自己的系统分支。

原始记录中,我观察到图标完整显示大约还要 2 秒。这个观察来自当时的视频,不能由上面的 413ms 推算出来;本文没有提供完整视频,不能据此给当前 Launcher 的加载时间下结论。

分析被杀原因

现在要查的是谁杀了 Launcher。和我信虽然可疑,但只有先后关系还不够,先看这条 Event Log:

1
am_kill : [0,13509,com.meizu.flyme.launcher,600,kill background]

按 Android 9 的事件字段定义,这里依次是用户、被杀进程 PID、进程名、OomAdj 和原因。600 不是内存大小;日志前缀中的 1456/2269 是记录日志的进程/线程,也不是原始应用调用者。kill background 指向框架清理路径,不能直接归为内存压力下的 LMK。

AOSP android-9.0.0_r1 的 ActivityManagerService.killBackgroundProcesses() 如下。它先检查权限和用户范围,再按包名、appId、用户及进程状态条件处理,不是无条件杀掉目标包的一切进程。

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
33
34
35
36
37
38
39
40
41
42
@Override
public void killBackgroundProcesses(final String packageName, int userId) {
if (checkCallingPermission(android.Manifest.permission.KILL_BACKGROUND_PROCESSES)
!= PackageManager.PERMISSION_GRANTED &&
checkCallingPermission(android.Manifest.permission.RESTART_PACKAGES)
!= PackageManager.PERMISSION_GRANTED) {
String msg = "Permission Denial: killBackgroundProcesses() from pid="
+ Binder.getCallingPid()
+ ", uid=" + Binder.getCallingUid()
+ " requires " + android.Manifest.permission.KILL_BACKGROUND_PROCESSES;
Slog.w(TAG, msg);
throw new SecurityException(msg);
}

userId = mUserController.handleIncomingUser(Binder.getCallingPid(), Binder.getCallingUid(),
userId, true, ALLOW_FULL_ONLY, "killBackgroundProcesses", null);
final int[] userIds = mUserController.expandUserId(userId);

long callingId = Binder.clearCallingIdentity();
try {
IPackageManager pm = AppGlobals.getPackageManager();
for (int targetUserId : userIds) {
int appId = -1;
try {
appId = UserHandle.getAppId(
pm.getPackageUid(packageName, MATCH_DEBUG_TRIAGED_MISSING,
targetUserId));
} catch (RemoteException e) {
}
if (appId == -1) {
Slog.w(TAG, "Invalid packageName: " + packageName);
return;
}
synchronized (this) {
killPackageProcessesLocked(packageName, appId, targetUserId,
ProcessList.SERVICE_ADJ, false, true, true, false, "kill background");
}
}
} finally {
Binder.restoreCallingIdentity(callingId);
}
}

客户端是 ActivityManager.killBackgroundProcesses():

1
2
3
4
5
6
7
8
9
@RequiresPermission(Manifest.permission.KILL_BACKGROUND_PROCESSES)
public void killBackgroundProcesses(String packageName) {
try {
getService().killBackgroundProcesses(packageName,
mContext.getUserId());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}

这是 2019 年案例所用路径的源码参照,不代表该 Flyme 分支与 AOSP 逐行一致。杀后台进程也不等于“强行停止”应用;系统之后仍可能按需重启进程。后面的 Android 14 更新说明了普通应用的跨包调用限制。

对 SystemServer 进程进行断点 Debug

先在系统服务端确认调用,再去应用端看参数和业务栈。这一操作需要具备系统调试权限、可调试的测试系统和匹配的源码;普通零售设备不能默认按图附加 system_server。暂停系统关键线程也会改变时序,不适合用来测正常启动耗时。

  1. 在当时的 Android Studio 调试环境中选择 system_process,沿 ActivityManagerService.killBackgroundProcesses() 进入清理路径。

历史调试环境中选择 system_process

  1. 依次复现候选应用的启动操作。我当时启动和我信后命中断点,调用栈显示来自 Binder 事务;截图中的停点已在内部 killPackageProcessesLocked()。

Binder 调用进入 killPackageProcessesLocked 的历史断点

  1. 当时在表达式求值窗口使用 getRealCallingPid(),结果为 29771。

历史环境的 getRealCallingPid 求值结果为 29771

不要把这个表达式直接照搬到其他 AOSP 工程。 核对 Android 9 和 Android 16 的 Binder/AMS 源码,没有找到这个同名方法;截图没有给出其实现,本文只能将其视为当时调试环境提供的方法。

标准 Binder 调用身份可在服务端事务入口通过 Binder.getCallingPid() / Binder.getCallingUid() 读取,但必须在 Binder.clearCallingIdentity() 之前保存,并在异步转交前关联到这次请求。上面的 Android 9 代码在内部清理前已清除身份;因此,不能在截图所示的任意内部断点上用标准 getCallingPid() 替换旧表达式,再认定返回值就是客户端。参见 Binder 的身份接口。

  1. 同一轮复现里,再将 PID、UID、时间与进程列表对应。原图执行的是 adb shell ps | grep 29771:

ps 输出中 PID 29771 对应和我信,其他行可能只是在 PPID 列匹配

要读 PID 列。这张图还匹配了以 29771 为 PPID 的其他进程,不能把每一行都当成调用者;PID 也会复用。该结果给出了候选调用进程,下面用应用侧参数和栈继续确认。

对 App 进程进行断点 Debug

没有业务源码时,也可以利用匹配的 framework 源码查看 SDK 调用。但前提仍是可以合法附加的可调试构建或相应平台测试环境,并非任意已安装的发布版应用都能这样操作。profileable 也不等于 debuggable。

当时把断点放在客户端 android.app.ActivityManager.killBackgroundProcesses():

附加应用进程,在 ActivityManager 客户端入口设置断点

我当时在本地登录后,再次启动应用,就复现了这个问题。截图显示传入的包名为 com.meizu.flyme.launcher,业务路径包含 MainBasePresenter$2.loadComplete → IconUtil.useIcon → ActivityManager.killBackgroundProcesses。这比“启动后碰巧出现 kill 日志”提供了更直接的调用证据。

应用侧调用栈与目标包名 com.meizu.flyme.launcher

这个结论限定在当时的设备、应用版本和调试环境,不能据此断言该应用当前版本仍有相同行为。本文的日志、断点和 Trace 截图还来自不同复现轮次,PID 不应跨图直接拼成同一次执行。

使用 Android Studio Profiler 工具

以下是 2019 年 Android Studio 的操作界面:选择目标进程,再进入 CPU 分析。

旧版 Android Studio Profiler 的进程选择界面

当时选择 Trace Java Methods 开始记录,复现后停止并查看结果。

旧版 Trace Java Methods 录制界面

方法录制的 Call Chart,时间轴为 Wall Clock Time

方法插桩可以看到捕获范围内的 Java/Kotlin 调用顺序,但它不是所有进程、所有 native 指令的完整执行记录。插桩、调试和测试环境都会扰动时间;图中还出现了 VAInstrumentation 包装层,本文没有给出它的配置;不能据此声称测量了未修改发布版的性能。平时我会用这个工具辅助阅读源码和业务路径。

在录制结果中搜索 killBackgroundProcesses,可以找到 loadComplete → useIcon → API 的调用关系:

loadComplete 经 useIcon 调用 killBackgroundProcesses,选中区间为 3.31ms

图中的 3.31ms 是本次方法录制下的墙钟区间,不等于生产环境的 CPU 开销,更不是桌面图标晚出现约 2 秒的全部原因。到了这一步,开发者已经有具体业务调用点可以修正。

当前工具入口补充(2026-10-13 核查): Android Studio 的任务名称和布局已有变化;按官方 Java/Kotlin 方法录制文档 选择方法插桩任务,不要把采样当成逐次调用记录。官方建议将插桩录制控制在约 5 秒以内,围绕待查操作采集;原截图的录制长度不作为当前推荐。可调试与可分析构建的能力和开销也不同,见 Profiler 文档。

权限问题分析

旧路径要求 Manifest.permission.KILL_BACKGROUND_PROCESSES。@RequiresPermission 是 API 合同提示,实际是否获权仍需看安装状态和服务端检查。原案例使用 adb shell dumpsys package com.jx.cmcc.ict.ibelieve,得到以下历史输出:

1
2
3
4
5
6
install permissions:
......
android.permission.ACCESS_NETWORK_STATE: granted=true
android.permission.KILL_BACKGROUND_PROCESSES: granted=true
android.permission.WRITE_USER_DICTIONARY: granted=true
......

权限级别是 normal:

KILL_BACKGROUND_PROCESSES 的历史 normal 权限定义

声明这个普通权限通常在安装时获授,不需要运行时弹窗;这不等于未声明的应用自动拥有权限,也不意味着获授后永远可以清理其他包。

Android 14 / API 34 起,这部分旧教程已经过期: 面向所有应用的行为变更 明确,普通第三方应用只能通过这个 API 清理自己的后台进程;传入其他包名不会生效,系统会记录 Invalid packageName。该限制适用于运行在 Android 14 上的所有应用,不以 targetSdkVersion 是否升级为前提。

AOSP android-16.0.0_r1 的实现 继续检查调用方与目标 appId;平台另有 KILL_ALL_BACKGROUND_PROCESSES 特权权限例外,其保护级别是 signature|privileged。所以不能把普通应用规则泛化到系统特权调用方,也不能把 KILL_BACKGROUND_PROCESSES 仍为 normal 解读成旧的跨包能力仍然存在。

Android 11 及之后:补查进程退出记录

Android 11 / API 30 加入 ApplicationExitInfo。对自己开发的应用,可以在后续启动时通过 ActivityManager.getHistoricalProcessExitReasons() 查询保留的退出记录,结合时间、PID、进程名、原因和状态码分析。它只保留有限的近期历史,不是完整审计日志;查询其他 UID 的包需要额外权限,不能把它当成第三方应用任意调查 Launcher 的入口。

几个容易误判的地方:

  • 没有查到记录不代表进程从未被杀。
  • REASON_SIGNALED 加 SIGKILL 不能单独证明低内存回收;并非所有设备支持报告 REASON_LOW_MEMORY,可检查 ActivityManager.isLowMemoryKillReportSupported()。
  • 原因名称也要按版本和调用路径解释。例如 Android 16 的上述后台清理路径使用 REASON_USER_REQUESTED 和内部 SUBREASON_KILL_BACKGROUND,不能仅凭前者就认定用户亲手点击了“强行停止”。内部 subreason 不是这里承诺给普通 SDK 调用者的独立查询接口。

Systrace 工具可以找出来 Kill 桌面的元凶么?

Systrace 的唤醒关系可以帮助缩小范围,但单看唤醒链不能证明哪个应用发出了特定 kill 请求。查看方法可参考分析 Systrace 预备知识。以下保留当时的追踪过程:

  1. 在 system_server 中找到 killProcessGroup 附近的活动,再看相关线程的唤醒来源。图中显示来自线程 19688。

killProcessGroup 附近活动及来自 19688 的唤醒信息

  1. 查线程标识,19688 对应进程 1295 中的 Binder:1295_1E。

Binder 线程 19688 所属 system_server 进程为 1295

  1. 在对应时间附近查看该 Binder 线程的唤醒来源,图中是线程 7289。

Binder 线程的唤醒来源显示为 7289

  1. 根据这份 Trace 的线程和进程信息,7289 是 com.jx.cmcc.ict.ibelieve 的主线程。

Trace 中主线程 7289 的进程归属

和我信进程 7289 的主线程轨道

这条链把调查方向指向了该应用,但唤醒者不自动等于 kill 调用者。还要在完整 Trace 中核对同次 Binder 事务、对应时间与线程交接,必要时结合请求参数、服务端日志或应用侧栈。用于显示线程身份的截图选区也不全是 kill 发生的同一时刻,不能拿它们的时长拼接成调用耗时。本文给出的更直接证据是前面的客户端断点和目标包名;不把低层 killProcessGroup 当成只会由这一 API 触发。

总结

以下感受保留在 2019 年案例背景中;现代系统的权限边界见上文更新。

从上面的分析来看, 这个问题是由于应用申请了不恰当的权限并错误使用对应的函数导致的一个严重影响用户使用的问题. 一般分析到这一步, 我们的工作就基本上结束了, 后续只需要和商店沟通, 跟 App 开发者联系进行修改即可.

不过令我感到意外的是 android.permission.KILL_BACKGROUND_PROCESSES 这个权限 Google 居然放的这么松, 我一直以为这个权限是要专门申请以防止 App 滥用或者卵用的(毕竟涉及到其他 App 的生死存亡).

本文知乎地址

由于博客留言交流不方便,点赞或者交流,可以移步本文的知乎界面
知乎 - Android 框架问题分析案例 - 谁杀了桌面?

关于我 && 博客

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

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

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

微信扫一扫

CATALOG
  1. 1. 现象
  2. 2. 分析过程
    1. 2.1. 确定问题发生的时间点
    2. 2.2. 分析被杀原因
    3. 2.3. 对 SystemServer 进程进行断点 Debug
    4. 2.4. 对 App 进程进行断点 Debug
    5. 2.5. 使用 Android Studio Profiler 工具
    6. 2.6. 权限问题分析
    7. 2.7. Android 11 及之后:补查进程退出记录
    8. 2.8. Systrace 工具可以找出来 Kill 桌面的元凶么?
  3. 3. 总结
  4. 4. 本文知乎地址
  5. 5. 关于我 && 博客