CN

Case Study: Debugging the Android Launcher Kill Problem

Word count: 2.4kReading time: 15 min
2019/09/17
loading

This started with a bug I encountered at work. It was not a difficult one, but while writing my diary afterward, I thought the investigation was worth recording and sharing. I also learned some useful analysis and tool tricks from my clever colleague Mei Ming.

I’ve used a variety of tools mentioned in my Android Performance Optimization Methodology, including:

  • Reproduction Video
  • Event Log
  • AS Source/Debug
  • AS Profiler
  • Systrace
  • Dumpsys
  • Unix ps utility

You might wonder whether such a simple bug needs an article. I wanted to keep a record for myself, and the way we used the tools together may be useful in other investigations. If it helps you, all the better. If you have your own debugging tricks, please join our discussion group and share them!

Version note — technical review, 2026-10-13: This is a 2019 case from a specific Flyme/app environment, with AOSP android-9.0.0_r1 as a source reference. The old Studio workflow and cross-package cleanup behavior are not universal today. Updates below cover Android 14 / API 34 restrictions, Android 16 source, and API 30 exit records. The logs and screenshots are historical; the original app has not been retested and no claim is made about its current version.

The Phenomenon

Testing reported a classic performance bug: “Slow loading of icons when returning to the home screen.” They provided a video, logs, and a Systrace. We had the symptom and the records, so let’s start there.

The Analysis

1. Zeroing in on the Time

  1. Watch the reproduction video to find the approximate time.
  2. Align it with Event Log, then use Event and Main logs to reconstruct the actions.
  3. In this capture, Launcher was killed after com.jx.cmcc.ict.ibelieve came to the foreground. Returning home created a new Launcher process. The sequence alone does not yet identify the caller that requested the kill.
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
// Launch com.jx.cmcc.ict.ibelieve
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

// Launcher is killed
09-10 10:14:48.903 1456 2269 I am_kill : [0,13509,com.meizu.flyme.launcher,600,kill background]

// Return from the app to Launcher
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

// Create a new Launcher process
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]

// Launcher resumes and launch timing is reported; not all icons necessarily loaded
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 changed from PID 13509 to 14013, explaining the cold start on return. The 413,413 values are launch timing statistics. In AOSP Android 9’s ActivityRecord.reportLaunchTimeLocked(), they are written as thisTime and totalTime, from launch reporting paths including the window-drawn callback. They are neither CPU time nor a guarantee that all icons and asynchronous content have loaded. Match the device’s system branch when interpreting its fields.

In the original investigation, I observed roughly 2 seconds until the icons were fully visible. That was a video observation, not a value derived from the 413ms log. The complete video is not available here, so this is not a measurement of current Launcher versions or proof that all of that delay was database work.

2. Investigating the Kill Reason

Now we need to find out who killed Launcher. The app is a suspect, but the sequence alone is not enough. Let’s look at this event:

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

The Android 9 event definition lists user, victim PID, process name, OomAdj, and reason. 600 is not a memory size. The 1456/2269 prefix identifies the process/thread writing the log, not the original app caller. kill background points to a framework cleanup path; it does not by itself mean LMK under memory pressure.

Here is ActivityManagerService.killBackgroundProcesses() in android-9.0.0_r1. It checks permission and user scope before selecting processes by package, appId, user, and process-state criteria. It does not unconditionally kill every process belonging to the package.

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);
}
}

Its client entry point is 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();
}
}

This is a source reference for the historical path, not a claim that the Flyme branch was identical to AOSP. Killing background processes is also different from force-stopping an app; the system may restart processes later as needed. Android 14’s restriction on ordinary apps targeting other packages is covered below.

3. Debugging SystemServer

The next step was to identify the service caller, then inspect the app’s arguments and stack. Attaching to system services requires an authorized, debuggable test system and matching sources. Do not assume an ordinary retail device permits attaching to system_server. Pausing critical system threads also changes timing, so this is not a normal startup benchmark.

  1. In the historical Android Studio setup, select system_process and follow ActivityManagerService.killBackgroundProcesses() into its cleanup path.

Selecting system_process in the historical debugging environment

  1. Reproduce candidate app launches. When I launched the suspected app, it hit the breakpoint. The stack shows a Binder transaction; the pictured stop is already inside killPackageProcessesLocked().

Historical Binder stack stopped inside killPackageProcessesLocked

  1. The historical expression evaluator returned 29771 for getRealCallingPid().

The historical getRealCallingPid expression returned 29771

This expression is not a portable AOSP instruction. No method with that name was found in the checked Android 9/16 Binder or AMS sources, and the screenshot does not supply its implementation. Treat it as a facility of that debugging environment.

The standard Binder.getCallingPid() / Binder.getCallingUid() can capture the incoming transaction identity at the service entry point, before Binder.clearCallingIdentity() and before any asynchronous handoff. The Android 9 method above clears identity before its internal cleanup call. Replacing the old expression with standard getCallingPid() at an arbitrary inner breakpoint would therefore not reliably identify the original client. See the Binder identity APIs.

  1. Correlate the PID, UID, and time with the process list from the same reproduction. The original screenshot uses adb shell ps | grep 29771:

PID 29771 belongs to the app; other matches can be in the PPID column

Read the PID column: some rows match because 29771 is their PPID. They are not all the caller, and PIDs can be reused. This identifies a candidate process to check with app-side arguments and stacks.

4. Debugging the App

Matching framework sources can expose an SDK call even without business source code. The target must still be a debuggable build or accessible through the appropriate platform test environment; arbitrary installed release apps are not automatically attachable. profileable does not mean debuggable.

I put a breakpoint in the client android.app.ActivityManager.killBackgroundProcesses():

Attaching to the app and breaking in the ActivityManager client

I reproduced it locally by logging in and then launching the app again. The screenshot shows the target package com.meizu.flyme.launcher and the path MainBasePresenter$2.loadComplete → IconUtil.useIcon → ActivityManager.killBackgroundProcesses. That is more direct evidence than a kill log appearing after an app launch.

App-side stack and target package com.meizu.flyme.launcher

This finding applies to the historical device, app version, and debugging setup. It says nothing about the app’s current behavior. The logs, debugger screenshots, and traces also come from different reproduction runs; their PIDs must not be combined into one execution.

5. Identifying the Root Code using AS Profiler

The following screenshots show the 2019 Android Studio interface: select the process and open CPU profiling.

Process selection in the old Android Studio Profiler

I selected Trace Java Methods, recorded the operation, then stopped to inspect the result.

Historical Trace Java Methods recording interface

Recorded method call chart with a Wall Clock Time axis

Method instrumentation shows captured Java/Kotlin call order, not every process or every native instruction. Instrumentation, debugging, and the test setup affect timing. The chart also contains a VAInstrumentation wrapper whose configuration is not supplied here; it does not establish an untouched release-build benchmark. I use this kind of recording to help follow framework and business code paths.

Searching for killBackgroundProcesses shows loadComplete → useIcon → the API:

loadComplete calls through useIcon; the selected method interval is 3.31ms

The pictured 3.31ms is a wall-clock interval under instrumentation, not production CPU cost or the full reason icons appeared roughly 2 seconds late. The app developer now has a concrete call site to fix.

Current tooling update, checked 2026-10-13: Studio task names and layouts have changed. Follow the official Java/Kotlin method recording guide for instrumentation, and do not mistake sampling for an invocation-by-invocation record. The guide recommends keeping instrumented recordings to about 5 seconds or less around the operation; the old screenshot’s longer capture is not a current recommendation. See the Profiler documentation for debuggable/profileable build capabilities and overhead.

6. Permission Analysis

The historical API requires Manifest.permission.KILL_BACKGROUND_PROCESSES. @RequiresPermission documents that contract; the actual grant and service-side checks still matter. The original adb shell dumpsys package com.jx.cmcc.ict.ibelieve output included:

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
......

The permission protection level was normal:

Historical normal protection level for KILL_BACKGROUND_PROCESSES

A declared normal permission is generally granted at installation without a runtime prompt. This does not mean undeclared apps automatically hold it, or that a grant permanently authorizes killing another package’s processes.

The cross-package advice is outdated on Android 14 / API 34 and later. The official changes affecting all apps restrict ordinary third-party apps to killing their own background processes through this API. Passing another package name has no effect and produces an Invalid packageName log. This applies to all apps running on Android 14, regardless of target SDK.

The AOSP android-16.0.0_r1 implementation checks caller and target appIds, with an exception for the platform’s KILL_ALL_BACKGROUND_PROCESSES permission, protected as signature|privileged. Do not generalize ordinary-app restrictions to privileged system callers, or interpret the original permission still being normal as preservation of its historical cross-package behavior.

Android 11 and Later: Inspecting Exit Records

Android 11 / API 30 added ApplicationExitInfo. For an app you develop, a later launch can query retained records through ActivityManager.getHistoricalProcessExitReasons() and correlate time, PID, process name, reason, and status. Only limited recent history is retained. Reading another UID’s package requires additional permission; this is not an unrestricted way for third-party apps to investigate Launcher.

  • No matching record does not prove that a process was never killed.
  • REASON_SIGNALED with SIGKILL does not uniquely identify low-memory killing. Not all devices report REASON_LOW_MEMORY; check ActivityManager.isLowMemoryKillReportSupported().
  • Interpret reason names in their version and code-path context. The Android 16 background cleanup path above uses REASON_USER_REQUESTED with internal SUBREASON_KILL_BACKGROUND. The former alone does not prove a person tapped Force stop. The internal subreason is not presented here as a separate public SDK query interface.

7. Can Systrace find the Culprit?

Wakeup relationships can narrow the investigation, but a wakeup chain alone does not prove which app issued a particular kill request. See Systrace prerequisites for reading wakeups. The original investigation followed these steps:

  1. Find activity near killProcessGroup in system_server and inspect the relevant thread’s wakeup source. The screenshot points to thread 19688.

Wakeup from thread 19688 near killProcessGroup activity

  1. Resolve the thread identity: 19688 is Binder:1295_1E in process 1295.

Binder thread 19688 belongs to system_server process 1295

  1. Around the corresponding time, inspect that Binder thread’s wakeup source: thread 7289.

The Binder thread shows a wakeup from 7289

  1. This trace maps 7289 to the main thread of com.jx.cmcc.ict.ibelieve.

Trace metadata identifies main thread 7289

Main-thread track for app process 7289

That chain points toward the app, but a waker is not automatically the kill caller. Correlate the same Binder transaction, timestamps, and thread handoffs in the complete trace, then use request arguments, service logs, or the client stack where needed. Some pictured selections only show thread identity at a different time; their durations cannot be joined into the kill’s execution time. Here, the client breakpoint and target package provide the more direct evidence. Low-level killProcessGroup is not exclusive to this API.

Conclusion

These observations are retained in the context of the 2019 case; the modern permission boundary is explained above.

In this case, the app had requested a permission and used the API incorrectly, seriously affecting the user’s experience. At this point our investigation was mostly done; the next step was to contact the app store and the app developers to get it fixed.

What surprised me then was how loosely Google allowed KILL_BACKGROUND_PROCESSES. I had assumed it needed special approval to stop apps abusing or misusing it. After all, it could decide whether another app lived or died.


Article on Zhihu

For comments and discussion, see the original Chinese case study on Zhihu.

About Me && Blog

I welcome discussion with fellow developers:

  1. About me: personal introduction and contact information.
  2. Blog navigation.
  3. Android performance resources: recommendations are welcome.
  4. Android performance community: The paid community stopped operating in 2026 and no longer accepts new members; this link preserves its history.

One person can go faster; a group can go further.

WeChat QR code

CATALOG
  1. 1. The Phenomenon
  2. 2. The Analysis
    1. 2.1. 1. Zeroing in on the Time
    2. 2.2. 2. Investigating the Kill Reason
    3. 2.3. 3. Debugging SystemServer
    4. 2.4. 4. Debugging the App
    5. 2.5. 5. Identifying the Root Code using AS Profiler
    6. 2.6. 6. Permission Analysis
    7. 2.7. Android 11 and Later: Inspecting Exit Records
    8. 2.8. 7. Can Systrace find the Culprit?
  3. 3. Conclusion
  4. 4. Article on Zhihu
  5. 5. About Me && Blog