Why Android ContentProvider Client Processes May Be Killed by AMS: Deep Dive into StableCount and Process Cleanup
When a client app acquires a stable ContentProvider reference, Android’s ActivityManagerService tracks it with a stableCount; if the provider’s server process dies—due to low‑memory kill, startup failure, or timeout—while stableCount remains above zero, AMS will terminate the dependent client process.
ContentProvider (hereafter CP) is one of Android's four major components, offering database‑like CRUD operations and supporting cross‑process communication. In a cross‑process scenario, the process hosting the CP is the Server process, while the process requesting data is the Client process. While CP simplifies cross‑process data access, the Client process can be unexpectedly killed.
1. Log Analysis
06-06 21:57:52.892 916 2275 I ActivityManager: Start proc 26931:com.example.music/u0a103 for content provider com.example.music/.sharedfileaccessor.ContentProviderImpl
06-06 21:57:53.393 916 941 I ActivityManager: Process com.example.music (pid 26931) has died
06-06 21:57:53.423 916 941 I ActivityManager: Killing 16141:com.example.music:service/u0a103 (adj 2): depends on provider com.example.music/.sharedfileaccessor.ContentProviderImpl in dying proc com.example.musicThese three lines are the key logs showing that the Client process was killed by ActivityManagerService (AMS):
First line – the Server process for the CP was not running, so AMS starts it.
Second line – the Server process dies shortly after start (often due to LowMemoryKill).
Third line – because the Server process died, AMS kills the dependent Client process.
The question is: what mechanism in AMS leads to the innocent Client being terminated? The answer lies in the AMS source code.
2. Cleaning Up a Dead CP Process
Inspecting Android 6.x source, we find the method that logs “has died”:
final void appDiedLocked(ProcessRecord app, int pid, IApplicationThread thread,
boolean fromBinderDied) {
// Clean up already done if the process has been re‑started.
if (app.pid == pid && app.thread != null &&
app.thread.asBinder() == thread.asBinder()) {
boolean doLowMem = app.instrumentationClass == null;
boolean doOomAdj = doLowMem;
if (!app.killedByAm) {
// The "has died" log is printed here!!!
Slog.i(TAG, "Process " + app.processName + " (pid " + pid
+ ") has died");
mAllowLowerMemLevel = true;
} else {
// Note that we always want to do oom adj to update our state with the
// new number of procs.
mAllowLowerMemLevel = false;
doLowMem = false;
}
EventLog.writeEvent(EventLogTags.AM_PROC_DIED, app.userId, app.pid, app.processName);
if (DEBUG_CLEANUP) Slog.v(TAG_CLEANUP,
"Dying app: " + app + ", pid: " + pid + ", thread: " + thread.asBinder());
handleAppDiedLocked(app, false, true);
if (doOomAdj) {
updateOomAdjLocked();
}
if (doLowMem) {
doLowMemReportIfNeededLocked(app);
}
} else if (app.pid != pid) {
// A new process has already been started.
Slog.i(TAG, "Process " + app.processName + " (pid " + pid
+ ") has died and restarted (pid " + app.pid + ").");
EventLog.writeEvent(EventLogTags.AM_PROC_DIED, app.userId, app.pid, app.processName);
} else if (DEBUG_PROCESSES) {
Slog.d(TAG_PROCESSES, "Received spurious death notification for thread "
+ thread.asBinder());
}
}After logging, AMS calls handleAppDiedLocked() , which eventually reaches removeDyingProviderLocked() where the Client process is actually killed.
private final void handleAppDiedLocked(ProcessRecord app,
boolean restarting, boolean allowRestart) {
int pid = app.pid;
boolean kept = cleanUpApplicationRecordLocked(app, restarting, allowRestart, -1);
}
private final boolean cleanUpApplicationRecordLocked(ProcessRecord app,
boolean restarting, boolean allowRestart, int index) {
// Take care of any launching providers waiting for this process.
if (cleanupAppInLaunchingProvidersLocked(app, false)) {
restart = true;
}
}
boolean cleanupAppInLaunchingProvidersLocked(ProcessRecord app, boolean alwaysBad) {
// Look through the content providers we are waiting to have launched,
// and if any run in this process then either schedule a restart of
// the process or kill the client waiting for it if this process has
// gone bad.
boolean restart = false;
for (int i = mLaunchingProviders.size() - 1; i >= 0; i--) {
ContentProviderRecord cpr = mLaunchingProviders.get(i);
if (cpr.launchingApp == app) {
if (!alwaysBad && !app.bad && cpr.hasConnectionOrHandle()) {
restart = true;
} else {
removeDyingProviderLocked(app, cpr, true);
}
}
}
return restart;
}
private final boolean removeDyingProviderLocked(ProcessRecord proc,
ContentProviderRecord cpr, boolean always) {
for (int i = cpr.connections.size() - 1; i >= 0; i--) {
ContentProviderConnection conn = cpr.connections.get(i);
if (conn.waiting) {
// If this connection is waiting for the provider, then we don't
// need to mess with its process unless we are always removing
// or for some reason the provider is not currently launching.
if (inLaunching && !always) {
continue;
}
}
//Got the information of the Client Process of this ContentProvider!!!
ProcessRecord capp = conn.client;
conn.dead = true;
//This is an important checking, stableCount must large than 0.
if (conn.stableCount > 0) {
if (!capp.persistent && capp.thread != null
&& capp.pid != 0
&& capp.pid != MY_PID) {
//This is exactly where the Client Process is killed!!!
capp.kill("depends on provider "
+ cpr.name.flattenToShortString()
+ " in dying proc " + (proc != null ? proc.processName : "??")
+ " (adj " + (proc != null ? proc.setAdj : "??") + ")", true);
}
} else if (capp.thread != null && conn.provider.provider != null) {
}
}
}The call chain is:
handleAppDiedLocked() → cleanUpApplicationRecordLocked() → cleanupAppInLaunchingProvidersLocked() → removeDyingProviderLocked() , where the log output matches the observed “killed because depends on provider … in dying proc …”.
The decisive condition is conn.stableCount > 0 . The next sections explain when stableCount is increased and decreased.
3. When stableCount Increases
The increase happens in AMS.incProviderCountLocked() , which is invoked via the provider acquisition path:
ContentProviderConnection incProviderCountLocked(ProcessRecord r,
final ContentProviderRecord cpr, IBinder externalProcessToken, boolean stable) {
if (r != null) {
for (int i=0; i
The public
Context.getContentResolver()
chain eventually calls
ApplicationContentResolver.acquireProvider()
, which forwards to
ActivityThread.acquireProvider()
, and then to the AMS methods above.
4. When stableCount Decreases
Each CRUD method in
ContentResolver
pairs
acquireProvider()
with
releaseProvider()
. The release path reduces
stableCount
:
public final boolean releaseProvider(IContentProvider provider, boolean stable) {
synchronized (mProviderMap) {
if (stable) {
prc.stableCount -= 1;
}
}
}
Thus, if a Client holds a stable reference and the Server dies before the stable count drops to zero, AMS will kill the Client.
5. Timeout‑Based Killing Scenario
If the Server process has not started,
AMS.getContentProviderImpl()
calls
startProcessLocked()
and then increments
stableCount
. AMS also posts a delayed
CONTENT_PROVIDER_PUBLISH_TIMEOUT_MSG
(10 seconds). If the Server does not publish the provider within this window, the handler invokes
processContentProviderPublishTimedOutLocked()
, which calls
cleanupAppInLaunchingProvidersLocked()
and ultimately kills the Client when
stableCount > 0
.
private final void processContentProviderPublishTimedOutLocked(ProcessRecord app) {
cleanupAppInLaunchingProvidersLocked(app, true);
removeProcessLocked(app, false, true, "timeout publishing content providers");
}
When the Server finally calls
publishContentProviders()
, the timeout message is removed, preventing the kill.
6. Summary
When using
ContentProvider
for cross‑process communication, developers must be aware that the AMS may kill the Client process if the Provider’s Server process dies while the Client holds a stable reference (i.e.,
stableCount > 0
). This can happen during normal low‑memory kills or when the Server fails to start and publish within the 10‑second window.Tencent Music Tech Team
Public account of Tencent Music's development team, focusing on technology sharing and communication.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.