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
psutility
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_r1as 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
- Watch the reproduction video to find the approximate time.
- Align it with Event Log, then use Event and Main logs to reconstruct the actions.
- In this capture, Launcher was killed after
com.jx.cmcc.ict.ibelievecame to the foreground. Returning home created a new Launcher process. The sequence alone does not yet identify the caller that requested the kill.
1 | EventLog |
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 |
|
Its client entry point is ActivityManager.killBackgroundProcesses():
1 |
|
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.
- In the historical Android Studio setup, select
system_processand followActivityManagerService.killBackgroundProcesses()into its cleanup path.

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

- The historical expression evaluator returned
29771forgetRealCallingPid().

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.
- Correlate the PID, UID, and time with the process list from the same reproduction. The original screenshot uses
adb shell ps | grep 29771:

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():

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.

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.

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


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:

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 | install permissions: |
The permission protection level was normal:

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_SIGNALEDwithSIGKILLdoes not uniquely identify low-memory killing. Not all devices reportREASON_LOW_MEMORY; checkActivityManager.isLowMemoryKillReportSupported().- Interpret reason names in their version and code-path context. The Android 16 background cleanup path above uses
REASON_USER_REQUESTEDwith internalSUBREASON_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:
- Find activity near
killProcessGroupinsystem_serverand inspect the relevant thread’s wakeup source. The screenshot points to thread19688.

- Resolve the thread identity:
19688isBinder:1295_1Ein process1295.

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

- This trace maps
7289to the main thread ofcom.jx.cmcc.ict.ibelieve.


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:
- About me: personal introduction and contact information.
- Blog navigation.
- Android performance resources: recommendations are welcome.
- 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.
