Application Exits
Your process does not live forever. In fact, your process gets terminated a lot, particularly depending on the sort of work that you do. For example, if you are using WorkManager
to get control periodically in the background to do some work, your process will be terminated sometime after each piece of work.
This is nothing new.
What is new is the ability to find out why your process got terminated. You are not told this in real-time as your process is being terminated, but you can find out past reasons for process termination.
Collecting the Data
The ActivityManager
system service now has a getHistoricalProcessExitReasons()
method. This will return a list of ApplicationExitInfo
objects, representing past process termination reasons.
The ForensicPathologist
sample module in the book’s sample project gets those ApplicationExitInfo
objects in its MainMotor
:
class MainMotor(private val context: Context) : ViewModel() {
private val _content = MutableLiveData<List<ExitInfo>>()
val content: LiveData<List<ExitInfo>> = _content
init {
_content.value =
context.getSystemService(ActivityManager::class.java)
?.getHistoricalProcessExitReasons(null, 0, 0).orEmpty()
.map { convert(it) }
}
We get the ActivityManager
, call getHistoricalProcessExitReasons()
, coerce null results into an empty list, and use that to populate a MutableLiveData
.
getHistoricalProcessExitReasons()
takes three parameters:
- The package name of app whose processes you wish to collect, or
null
for your own process - A particular process ID to examine, or
0
to not filter by process ID - The maximum number of items to return, or
0
to return all that are available
Note that if you specify a package name from some other app, you will need to hold the DUMP
permission. This is not available to ordinary third-party apps. And, usually, we will not know any particular process ID to filter upon. So, typically, the call to getHistoricalProcessExitReasons()
will be getHistoricalProcessExitReasons(null, 0, 0)
, to collect all known process exit reasons.
The ApplicationExitInfo
contains several fields that you can use. The big one is the reason
field, which says why the process was terminated. This is an Int
that maps to various REASON_
constants on ApplicationExitInfo
. As part of converting an ApplicationExitInfo
into data to fill into our UI, MainMotor
converts a reason
value into a string resource ID:
@StringRes
private fun convertReason(reason: Int): Int = when (reason) {
ApplicationExitInfo.REASON_ANR -> R.string.reason_anr
ApplicationExitInfo.REASON_CRASH -> R.string.reason_crash
ApplicationExitInfo.REASON_CRASH_NATIVE -> R.string.reason_crash_native
ApplicationExitInfo.REASON_DEPENDENCY_DIED -> R.string.reason_dependency_died
ApplicationExitInfo.REASON_EXCESSIVE_RESOURCE_USAGE -> R.string.reason_excessive_resource_usage
ApplicationExitInfo.REASON_EXIT_SELF -> R.string.reason_exit_self
ApplicationExitInfo.REASON_INITIALIZATION_FAILURE -> R.string.reason_init_failure
ApplicationExitInfo.REASON_LOW_MEMORY -> R.string.reason_low_memory
ApplicationExitInfo.REASON_OTHER -> R.string.reason_other
ApplicationExitInfo.REASON_PERMISSION_CHANGE -> R.string.reason_permission_change
ApplicationExitInfo.REASON_SIGNALED -> R.string.reason_signaled
ApplicationExitInfo.REASON_USER_REQUESTED -> R.string.reason_user_requested
ApplicationExitInfo.REASON_USER_STOPPED -> R.string.reason_user_stopped
else -> R.string.shrug
}
The description
field may contain additional details about the reason for the process to be terminated, depending on what the reason
value is. Similarly, status
may contain an additional number related to the process termination (e.g., if the OS signaled for process termination, status
will contain the signal number)
We also have:
-
pss
andrss
, to tell us about the memory consumption of the process at the time that it was terminated -
timestamp
, indicating when the process was terminated -
importance
, indicating the importance of the process at the time that it was terminated
MainMotor
converts all of that stuff into ExitInfo
model objects:
private fun convert(appExitInfo: ApplicationExitInfo): ExitInfo {
return ExitInfo(
description = appExitInfo.description.orEmpty(),
importance = convertImportance(appExitInfo.importance),
pss = appExitInfo.pss,
rss = appExitInfo.rss,
reason = convertReason(appExitInfo.reason),
status = appExitInfo.status,
timestamp = DateUtils.getRelativeTimeSpanString(
context,
appExitInfo.timestamp
)
)
}
@StringRes
private fun convertImportance(importance: Int): Int = when (importance) {
ActivityManager.RunningAppProcessInfo.IMPORTANCE_CACHED -> R.string.importance_cached
ActivityManager.RunningAppProcessInfo.IMPORTANCE_CANT_SAVE_STATE -> R.string.importance_cant_save_state
ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND -> R.string.importance_foreground
ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND_SERVICE -> R.string.importance_foreground_service
ActivityManager.RunningAppProcessInfo.IMPORTANCE_GONE -> R.string.importance_gone
ActivityManager.RunningAppProcessInfo.IMPORTANCE_PERCEPTIBLE -> R.string.importance_perceptible
ActivityManager.RunningAppProcessInfo.IMPORTANCE_SERVICE -> R.string.importance_service
ActivityManager.RunningAppProcessInfo.IMPORTANCE_TOP_SLEEPING -> R.string.importance_top_sleeping
ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE -> R.string.importance_visible
else -> R.string.shrug
}
The UI then renders the results in a RecyclerView
… once ForensicPathologist
has been run a time or two and actually has results:
What To Do With the Results?
Similar to the data access auditing, the idea is that you might:
- Send aggregated statistics back to your server, such as the total number of process terminations and the count of each
reason
type - Identify unusual cases and report more details on those (e.g.,
REASON_ANR
,REASON_PERMISSION_CHANGE
)
One limiting factor is that you cannot readily identify which ApplicationExitInfo
objects you have seen previously, because there is no unique ID on them. You can attempt to work around this by checking for the last process termination reason immediately upon startup of a fresh process. Otherwise, you might count the same ApplicationExitInfo
results multiple times.
Tracking Application State
You might find it useful to know some details about the nature of your app as part of the application exit reasons.
For example, you might be using “feature flags” to conditionally enable certain features. Depending on how you implemented those, you may not know, for any given situation, which feature flags are enabled and which are disabled. Perhaps they are random for A|B testing, or perhaps you are just worried that the flags might change between when an application exited and when somebody gets a chance to look at a report that you generate using these new APIs.
Apps can add a bit of information to the application exit data. If you call setProcessStateSummary()
on ActivityManager
, you can provide a byte array of data. When the application exits, if you called setProcessStateSummary()
during the life of that process, the byte array gets recorded. Later on, when you retrieve the ApplicationExitInfo
for this exit, you can call getProcessStateSummary()
to retrieve the byte array, to include its contents in whatever your analysis does.
So, going back to the earlier example, once you find out what feature flags are enabled and disabled, you can record those as the process state summary. Later on, if you are trying to diagnose why your app is behaving as it is with respect to exits, you can see what feature flags are being used and perhaps determine if one of those flags is having a particular impact.
However:
- The documentation warns against calling
setProcessStateSummary()
too often - There is no documentation on the size limit for the byte array, but you should not assume it can hold large blobs of data
- This is not a replacement for existing ways of getting data between process invocations of your app (saved instance state, files, preferences, etc.)
ANRs and Traces
Perhaps no single error message caused developers more angst in the early years of Android than did “application not responding”. A dialog with that message would appear if the app tied up the main application thread for a ridiculous amount of time (around 10 seconds). It became so well-known that developers started referring to it by the abbreviation ANR.
The problem with ANRs is that you do not know exactly where you are spending your time. We have had access to ANR trace files showing the state of our threads when an ANR occurs. However, on modern versions of Android, this data is only accessible on certain emulators or non-production builds, where we can achieve root access.
In Android 11, if your app exits with a reason of REASON_ANR
, you can call getTraceInputStream()
on the ApplicationExitInfo
to access the trace data. You can read that in and do something with it (e.g., send it to your server), if desired.
Prev Table of Contents Next
This book is licensed under the Creative Commons Attribution-ShareAlike 4.0 International license.