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<r.conProviders.size(); i++) {
ContentProviderConnection conn = r.conProviders.get(i);
if (conn.provider == cpr) {
if (stable) {
//The stableCount is increased here!!!
conn.stableCount++;
conn.numStableIncs++;
}
}
}
ContentProviderConnection conn = new ContentProviderConnection(cpr, r);
if (stable) {
//If there is no target ContentProvider found in conProviders, then create a new instance. And initialize the stableCount to 1.
conn.stableCount = 1;
conn.numStableIncs = 1;
}
cpr.connections.add(conn);
r.conProviders.add(conn);
}
}
private ContentProviderHolder getContentProviderImpl(IApplicationThread caller,
String name, IBinder token, boolean stable, int userId) {
synchronized(this) {
boolean providerRunning = cpr != null && cpr.proc != null && !cpr.proc.killed;
if (providerRunning) {
conn = incProviderCountLocked(r, cpr, token, stable);
}
if (!providerRunning) {
conn = incProviderCountLocked(r, cpr, token, stable);
}
checkTime(startTime, "getContentProviderImpl: done!");
}
}
@Override
public final ContentProviderHolder getContentProvider(
IApplicationThread caller, String name, int userId, boolean stable) {
return getContentProviderImpl(caller, name, null, stable, userId);
}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.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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.
