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:

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:

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:

ForensicPathologist, Showing Results After Previous Process Termination
ForensicPathologist, Showing Results After Previous Process Termination

What To Do With the Results?

Similar to the data access auditing, the idea is that you might:

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:

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.