An app that vanishes a minute after the driver presses HOME has not crashed. It has been killed on purpose, by code that is not in AOSP.
Question#
Why does an app on a BYD head unit get killed a minute or so after it goes to the background, while the system reports plenty of free memory?
Environment#
Decompiled read-only with jadx 1.5.5 from /system/framework/services.jar pulled off the unit; the raw resource ids inside PerfMonitor were resolved with aapt against the unit’s own framework-res.apk rather than jadx’s AOSP id table, which maps them to unrelated AOSP names. /vendor/etc/data/proc_mem_info.xml was read from the device. Probed over adb with dumpsys activity processes, dumpsys activity exit-info, dumpsys meminfo and logcat -s PerfMonitor:* PerfMonitorInfo:*. The measured process throughout is our own app, com.ali.sealion7pilot.
Observation#
Our app died repeatedly after being backgrounded during navigation, always the same way, and never as a crash. ApplicationExitInfo recorded REASON_LOW_MEMORY and nothing else. It happened roughly 60 to 90 seconds after HOME, every time, while a foreground service was running — and while the unit had gigabytes of memory free.
The tell was in logcat, and only if you looked within about two minutes: a line tagged PerfMonitor, then thirty seconds later a line tagged PerfMonitorInfo naming a number.
Evidence#
BYD extends ActivityManagerService with its own mPerfMonitor. checkProcessState(proc) runs, one second deferred, on every process-state change.
// ctor
mBackGroundMemThreshold = totalMemKb * 0.04761905f; // 7 791 860 kB → 370 688 kB (≈ 362 MiB)
mProcessWhiteList = res.getStringArray(0x0107009a); // android:array/process_white_list = ["com.gpack.agent"]
mAndroidList = res.getStringArray(0x01070006); // android:array/android_list = []
mProcessBlackList = res.getStringArray(0x01070099); // android:array/process_black_list = 14 BYD apps (hvac, avm, filemanager, …)
mEnableBackgroundCheck = SystemProperties.getBoolean("persist.sys.background_mem_check", false);
mEnableBlackKListCheck = SystemProperties.getBoolean("persist.sys.black_list_check", false);
mBackgroundProcessMaximumMemArray = loadProcMemConfigs(); // /vendor/etc/data/proc_mem_info.xml
// checkProcessPssLeak(proc)
if (!mEnableBackgroundCheck) return false;
if (whitelisted(proc.processName)) return false; // substring match
int cap = getBackgroundProcessMaximumLw(proc.processName) * 1024; // xml value or the default above
return !proc.mHasForegroundActivities
&& proc.curAdj >= 100 // VISIBLE(100) … CACHED — an FGS sits at 200
&& proc.lastPss >= cap
&& !checkAudioFocusApp(proc); // active AudioPlaybackConfiguration for the pid
// checkProcessStateLocked
if (checkProcessPssLeak(proc)) {
Slog.e("PerfMonitor", "checkProcessPssLeak proc: " + proc);
mProcessesForeground.put(proc.processName, now);
if (!proc.mHasForegroundActivities) mHandler.postDelayed(new KillBackgroundProc(proc), 30_000);
}
// KillBackgroundProc.run — re-evaluates checkProcessPssLeak, then
// killAppProcesses(proc): spared only if the TOP running task belongs to the same package
// proc.kill("process : X be killed. Reason: Pss cost too large N kb, MaxMem : cap", 3 /*LOW_MEMORY*/, true)Both switches are on here. Neither is on by default in the code — they read false when the property is absent, and this build sets them.
The terms of that predicate decide everything:
| Fact | Value / source | Consequence |
|---|---|---|
| Cap | proc_mem_info.xml entry, else 370 688 kB |
A process the xml does not name gets 362 MiB. |
curAdj >= 100 |
oom-adj | A foreground service does not help: no visible activity means adj 200. |
mHasForegroundActivities |
OomAdjuster / WindowProcessController.computeOomAdjFromActivities |
The only term in the predicate a normal app controls. |
| Audio focus | active playback for the pid | True only while a prompt is actually playing. |
| Whitelist | process_white_list = ["com.gpack.agent"] |
One entry, and it is not yours. |
| Timing | evaluations on process-state changes and roughly every 60 s, then a 30 s delay | Death 60–90 s after HOME. |
| Evidence trail | ApplicationExitInfo REASON_LOW_MEMORY only; PerfMonitor / PerfMonitorInfo at E level, buffer ≈ 2 min |
Why it looks like a silent disappearance. |
Six kills of our own process are on record, from the app’s ring file and from ApplicationExitInfo:
08-25 14:17 LOW_MEMORY com.ali.sealion7pilot Pss 450137 kb (importance 125 = FGS, no visible activity)
08-25 14:38 LOW_MEMORY com.ali.sealion7pilot Pss 372962 kb
08-25 15:05 LOW_MEMORY com.ali.sealion7pilot Pss 389474 kb
08-25 15:39 LOW_MEMORY com.ali.sealion7pilot Pss 390588 kb
08-25 21:34 LOW_MEMORY com.ali.sealion7pilot Pss 396778 kb <- after the :cluster split; that process stayed healthy
08-26 19:01 LOW_MEMORY com.ali.sealion7pilot Pss 459458 kb <- the control run of the experiment in paper #6The last one is the clearest, because we were watching:
19:00:36 PerfMonitor: checkProcessPssLeak proc: ProcessRecord{6ef3fc9 10651:com.ali.sealion7pilot/u0a46}
19:01:06 PerfMonitorInfo: process : com.ali.sealion7pilot be killed. Reason: Pss cost too large 459458 kb, MaxMem : 370688
ApplicationExitInfo 19:01:06 reason=3 (LOW_MEMORY) pss=449MB "… be killed. Pss : 459458 kb, mMemAvailable : 3221240"Evaluation, thirty seconds, kill — exactly the decompiled sequence. Note the last field: mMemAvailable : 3221240 at the moment of a LOW_MEMORY exit — over 3 GB free, if that field is in kb like the PSS printed beside it.
BYD’s own exemption#
loadProcMemConfigs() reads a vendor file, and that file is the other half of the answer.
900 MB com.byd.vrassistant, com.byd.vrassistant.xf, com.huawei.maps.auto.app, com.waze, com.byd.automap,
com.neusoft.na.navigation, :OneCore, :Telematics, :Service, net.zmap.znav.byd8155,
com.mappls.auto.bydznav8155F, com.tmap.auto.byd
600 MB com.neusoft.na.navigation:sdk, :Dlt, com.byd.mycar
450 MB com.byd.hvac, com.byd.carsettings, com.android.systemui
400 MB com.byd.automultipletheme, com.android.launcher3, com.byd.vrsettings
300 MB com.byd.auto_camera, com.byd.localmusic, com.byd.bluetoothcall
250 MB com.byd.auto_photo, com.byd.filemanager, com.byd.smarttravel, com.byd.trafficmonitor
200 MB com.byd.avmWhy CarPlay and Android Auto never lose the cluster#
Proc # 7: vis F/ /FGS -CM 2378:com.ts.carplay/1000 (service) sharedUserId=android.uid.system, largeHeap
Proc # 6: vis F/ /FGS -CM 2409:com.ts.androidauto/1000 (service)
Proc #51: vis F/ /IMPF --- 1966:com.ts.appservice.cluster/1000 (service)Interpretation#
REASON_LOW_MEMORY is a misnomer here. Nothing about this kill is a response to memory pressure: the kill string’s own mMemAvailable field reported over 3 GB free and the process died anyway, because that one process crossed a fixed ceiling while it was in the background. The AOSP low-memory killer is not involved.
The part most developers will get wrong is curAdj >= 100. A foreground service with an ongoing notification is the standard Android answer to “keep my process alive”, and on this unit it buys nothing: without a visible activity the process sits at adj 200, which satisfies the test. Our own TripRecordingService and ClusterProjectionService run as foreground services, and neither has ever prevented a kill.
Audio focus is a real escape — checkAudioFocusApp does spare a process with active playback for its pid — but it is not a lever. It holds only while sound is actually coming out. A navigation app would be protected during a spoken prompt and unprotected for the two minutes between prompts, which moves the kill rather than preventing it.
The exemptions that work are vendor configuration, not API. process_white_list has a single entry, com.gpack.agent. /vendor/etc/data/proc_mem_info.xml lives under /vendor and is read by system_server; there is no interface through which an app asks to be added to it. The 900 MB tier is how BYD keeps com.byd.automap and com.neusoft.na.navigation alive while backgrounded — and none of those navigation packages is installed on this export unit, which has no on-board navigation engine at all (paper #1). The car ships the rule that kills a large navigation process without the configuration that would exempt one.
CarPlay and Android Auto are immune twice over. They run as system-uid service processes in the vis bucket, so the adj term never trips; and what they push to the cluster is a decoded video surface, not a second map engine, so the PSS term would not trip either. Neither property is available to a normal-signed app.
What this does not mean: it does not mean that any process over 362 MiB dies. All four terms must hold at once — backgrounded, adj at or above 100, last measured PSS at or above the cap, no audio playing — and none of it runs on a build where persist.sys.background_mem_check is 0.
Result#
Confirmed: a backgrounded process on this head unit is SIGKILLed by com.android.server.am.PerfMonitor, a BYD addition to ActivityManagerService, when its last measured PSS reaches a per-process cap — 370 688 kB here unless /vendor/etc/data/proc_mem_info.xml names the process. The kill reason string prints both numbers, and ApplicationExitInfo reports the exit as REASON_LOW_MEMORY regardless of how much memory is free.
Three consequences for anyone writing a memory-hungry app for this unit:
- Budget below 362 MiB, or accept a kill. Shedding on
onTrimMemorybuys headroom, not immunity — a long session drifts back up. - A foreground service is not protection. Neither is a notification, a sticky service or a wake lock. Only the terms in that predicate matter.
- One of those terms is under an ordinary app’s control, and it is not the one anybody reaches for first. That is paper #6.
Limitations#
One car, one market, one head-unit build, Android 11. Both persist.sys switches read 1 on this unit; we have not read them on any other car, and a build with them at 0 would show none of this behaviour. The proc_mem_info.xml here is a ThunderSoft file dated 2023-04-29 — another market or a later build may ship different tiers, or no file at all.
The kills are six exits of a single process, our own app, over two days. We did not test whether an unrelated third-party app of the same size dies the same way. We did not exercise the whitelist or the blacklist path either: process_black_list was counted at 14 BYD entries and persist.sys.black_list_check read as 1, but what the blacklist does with a process was not traced, and android_list was read as empty and never observed to matter.
The roughly-60-second evaluation period comes from the decompiled scheduling, not from instrumenting the timer; what we measured is that death lands 60–90 seconds after HOME on every recorded exit. The kill string and ApplicationExitInfo report the same kill with slightly different PSS figures — 459 458 kb against 449 MB — and we did not establish which sample each one uses.
Splitting the projection into a separate :cluster process changed which process died, not the rule: that process has never been PSS-killed, but its PSS at service stop rose from 175 MB to 261 MB across one day’s sessions, so a long enough drive would take it over the cap too. That is an extrapolation from measurements, not an observed kill.
Nothing here was written to the car. We did not attempt to edit /vendor/etc/data/proc_mem_info.xml, set either property, or hold audio focus as a keep-alive hack.
Reproduction#
With adb reachable on the car (our guide covers getting there). Read the configuration first:
adb connect <car-ip>:5555
adb shell getprop persist.sys.background_mem_check
adb shell getprop persist.sys.black_list_check
adb shell cat /vendor/etc/data/proc_mem_info.xmlThen provoke a kill with any app whose PSS is comfortably over the cap. Start the logcat before you press HOME — the buffer holds about two minutes, and the lines are at E level under two tags:
adb shell dumpsys meminfo your.package.name
adb logcat -s PerfMonitor:* PerfMonitorInfo:*
adb shell input keyevent 3
adb shell dumpsys activity exit-info your.package.nameExpect checkProcessPssLeak proc: ProcessRecord{…} first, then the kill 30 seconds later, naming the PSS and the cap. If nothing appears, check that dumpsys meminfo really puts the process above the cap and that nothing is playing audio.
For the rule itself, pull the framework and read it:
adb pull /system/framework/services.jar
adb pull /system/framework/framework-res.apkOpen services.jar in jadx and look for PerfMonitor in com.android.server.am — it is a BYD addition and does not exist in AOSP. Resolve the resource ids in its constructor against the framework-res.apk you pulled, with aapt, rather than against jadx’s built-in AOSP id table: the same ids map to unrelated AOSP resource names there, which is how process_white_list was first misread as a networking constant.