Who lives and who dies? Process priorities on Android
Let’s face it: mobile devices do not have unlimited memory, unlimited battery, or unlimited anything else. What that means for you app is that you should consider process death a natural part of your app’s lifecycle. The important part is to make sure that the memory reclamation associated with killing a process does not negatively affect the user. In fact, much of the process architecture in Android is designed specifically around making sure that the ordering is anything but arbitrary and follows a set pattern through an important hierarchy.
Android Process Hierarchy
You’ll find the most important processes are called foreground processes, followed by any visible processes, service processes, background processes, and finally ‘empty’ processes as detailed in the documentation, which we’ll expand on here.
Note that while we’ll talk about specific components (services, activities), Android only kills processes, not components. Of course, that doesn’t preclude the usual garbage collection process (which will reclaim the memory of unreferenced objects), but that’s a topic for another post.
You’d think what the user is currently interacting with would be the most important thing to keep alive and you’d be completely right. But ‘what the user is currently interacting with’ is a bit fuzzy of a definition. The most obvious thing to fall into this definition is the current foreground Activity — the one Activity where onResume() has been called but it has not yet received a call to onPause().
While some activities stand on their own, they can also rely on bound services. Any process hosting a service bound to the foreground activity is given the same foreground priority. This certainly makes intuitive sense — if the foreground activity thinks it is important enough to keep a constant connection to the service, then it is important to both the activity and hence to Android to keep that service alive. The same applies to content providers the foreground service is currently interacting with.
But who says activities are the only things the user would notice disappearing? I’d certainly be mad if my music stopped playing all of a sudden or my navigation directions were suddenly lost. Thankfully, Android allows services to make it obvious to the system when they are a high priority foreground service via startForeground(). This is definitely a best practice for media playback the right way, but the important question to ask here is ‘would the user immediately notice if my service was stopped?’ Foreground services should be used only for critical, immediately noticeable use cases.
Note: Being a foreground service requires that your service include a notification to ensure that the user is completely aware that your service is running. If you don’t feel a notification is needed for your use case, then perhaps a foreground service isn’t right for you (it’s okay: being a foreground service isn’t a requirement to run in the background — see below).
There are a few other cases where a process gets temporarily elevated to be a foreground process around receiving critical lifecycle methods including any of a service’s lifecycle methods (onCreate(), onStartCommand(), and onDestroy()) and any broadcast receiver’s onReceive(). This safeguards those components to make sure that those operations are effectively atomic operations and each component is able to complete them without being killed.
Wait, I thought I already covered the current activity? Through the joys of Android though, you’ll find that there are situations where your activity can be visible but not in the foreground. A simple example is when the foreground activity starts a new activity with Dialog theme or a translucent activity. Another example might when you invoke the runtime permission dialog (which is in fact an Activity!).
You’ll know you’re a visible activity from when you receive onStart() to when you receive onStop(). Between these calls, you should be doing everything expected of a visible activity (updating the screen in real time, etc).
Similar to foreground activities, the same visible process status is also applied to bound services and content providers of visible activities. This again ensures that these dependent processes are not prematurely killed while the activity they are being used in still lives.
Keep in mind though, just because you are visible does not mean you can’t be killed. If there is enough memory pressure from foreground processes, it is still possible that your visible process will be killed. From a user perspective, this means the visible activity behind the current activity is replaced with a black screen. Of course, if you’re properly recreating your activity, your process and Activity will be restored as soon as the foreground Activity is closed without any loss of data.
Note: The fact that your activity and process can be killed even if visible is one of the reasons the startActivityForResult()+onActivityResult() and the requestPermissions()+onRequestPermissionsResult() flows don’t take callback class instances — if your whole process dies, so does every callback class instance. If you see libraries using a callback approach, realize that it will not be resilient to low memory pressure cases.
If your process isn’t in either of the above categories, but you have a started service, then you’re considered a service process. This is typical for many apps that are doing background processing (say, loading data) without the immediacy that comes with being a foreground service.
This isn’t a bad place to be! For the vast majority of cases, this is the perfect place for background processing as it ensures that your process is killed only if there is so much happening in the above visible and foreground process categories that something has got to give.
Take special note of the constant you return from onStartCommand() though as that is what controls what happens if your Service was to be killed due to memory pressure:
- START_STICKY implies that you want the system to automatically restart your Service when it can, but you don’t care whether you get the last Intent back again (i.e., you can recreate your own state or control your own start/stop lifecycle).
- START_REDELIVER_INTENT is intended for services that want to be restarted if killed with the Intent that was received in onStartCommand() until you call stopSelf() with the startId that was passed on onStartCommand() — here you are using the incoming Intents and their startIds as the queue of work to complete.
- START_NOT_STICKY is for the services that are okay to pass silently into the night. This can be appropriate for services managing periodic tasks where waiting until the next time frame isn’t the end of the world.
Let’s say your Activity was the foreground Activity, but the user just hit the home button causing your onStop() to be called. Assuming that was all that was keeping you higher priority categories, your process will fall into the background process category. Here’s where (in normal operating cases) much of the device’s memory is dedicated just in case you decide to go back to one of your previous open activities much later.
Android doesn’t go killing things just for the sake of killing things (remember: starting things from scratch isn’t free!), so these processes can potentially stay around for some time before being reclaimed due to memory needs from anything in a higher category, killed in order of least recent usage (oldest is reclaimed first). However, same as the visible activities when they are killed, you should be able to recreate your activity at any time without losing the user’s state.
As with any hierarchy, there’s a lowest level. If it hasn’t already been covered, it probably resides here. Here, there’s no active components and, as far as Android is concerned these can be killed at any point, although processes can be kept around purely for caching purposes (here we go, using our memory effectively rather than letting it be entirely free).
Caveats and Considerations
While we talked about process priorities in terms of components such as what activities and services you have, keep in mind that these priorities are done at the process level — not the component level. Just one component (say, a foreground service) will push the entire process into the foreground. While the vast majority of apps are a single process, if you have greatly varying lifecycles or an extremely heavyweight portion of your app that is independent of a long running lightweight portion, strongly consider making them separate processes to allow heavyweight processes to be collected sooner rather than later.
Just as importantly though, what process category you fall into is based on what is happening at the component level. That means kicking off that super important long running operation in a separate thread from within your Activity might meet with disaster when you suddenly become a background process. Use the tools available to you (a Service or foreground Service based on priority) to ensure that the system is aware of what you’re doing.
Play nicely with others and keep the user in mind
What makes this whole system work is a strong focus on the user. Be a good citizen and build your app such that you are working at the appropriate priority at all times. Keep in mind that as a developer you may have access to phones that are much, much faster than your worst off user and you may never see a visible process being killed much less a service process, but that does not mean it doesn’t happen!
However, while I’d still recommend buying an extremely low end Android to test on, you can still test how your app responds to being killed on your high end device. To kill your app at the package level, use:
adb shell am force-stop com.example.packagename
And, if you have multiple processes, you can find your process id (PID) by looking at the second column (i.e., the first number) of
adb shell ps | grep com.example.packagename
And then kill it with
adb shell kill PID
Testing how your app responds to being killed is the first step to making sure your app runs well on as many devices as possible, no matter how much memory pressure there is.