An Introduction to Android Interprocess Communication and Common Pitfalls

Vardaan Sharma
10 min readMar 29, 2019

--

Please be forewarned, this article only covers IPC within a single application in detail and just brushes over inter application IPC. If you are looking for inter application IPC this article is NOT FOR YOU.

Still here? Alright, let’s get started then!

Multiprocess Android Applications

In android every application that is started runs in its own process (with a unique Process ID or PID).

This allows the app to live in an isolated environment, where it cannot be hindered by other applications/processes. Typically, when you want to start an application, Android creates a process, spawns the main thread and starts running the main Activity.

Android also gives us the capability to specify some components of your app to run on a different process than the one that was used to start the app. This can be achieved by using the process tag.

The process attribute can be applied to activities, services, content providers and broadcast receivers and specifies what process that particular component should be executed in.

An example of what the manifest would look like when we want to start an activity in a new process:

<activity
android:name=”com.abc.application.SomeActivity”
android:configChanges=”keyboardHidden|orientation|keyboard|screenSize”
android:process=”:myProcess”
</activity>

Here “SomeActivity” will run in its own process called “myProcess”.

Advantages of Multiprocessing

Memory Management

For an apps memory budgets, Android has heavy limitations that go from 24/32/48 MB for the most recent devices down to 16 MB for older devices.

That RAM budget is everything the app has got to work with. Meaning, it must be sufficient for loading classes, threads, services, UI resources AND the content your app wants to display.

The above mentioned limit is enforced in a per-process basis. In other words, each process of an application will have have a dedicated memory budget

Security

Additionally, processes are meant to be isolated by design (as a security feature), which means that every process will have its own instance of Dalvik VM. In turn, this means that you won’t be able to share data across those instances, at least not in the traditional sense. For example, static fields will have their own value for each process, and not a single one common across all processes. And this extends to every state of the application.

Mechanisms for Inter Process Communication (IPC)

As mentioned in the official android documentation:

Some apps attempt to implement IPC using traditional Linux techniques such as network sockets and shared files. However, you should instead use Android system functionality for IPC such as Intent, Binder or Messenger with a Service, and BroadcastReceiver. The Android IPC mechanisms allow you to verify the identity of the application connecting to your IPC and set security policy for each IPC mechanism.

Hence we will limit our discussions the mechanisms evangelised in the official android documentation. We shall also only discuss IPC within the same application and not IPC amongst different applications.

Using Intents

Using intents, it is possible for us to pass data from one activity to another by setting extras in an intent and calling the startActivity method.

If the activity that is being initialised has the “process” attribute in its manifest, the activity will start in its own process but will have access to the data that was passed to it in the intent’s extra.

Intent intent = new Intent(this, SomeActivity.class);
intent.putExtra("key", "some serialisable data");
startActivity(intent); //or startActivityForResult(intent,Code) if we expect the invoked activity to response with some data

The above is slightly limited in the fact that we would have data passing between processes either at the start of an activity or at the end (in case of startActivityForResult). The android app must be build in such a way that all possible data exchanges only happen as the start/end of an activity.

We can also use intent-filters/ Broadcast receivers to intercept intents, but these options can potentially trigger external applications as well, hence we shall not discuss them further. Please refer to this link if you want to know more about intent filters and broadcast receivers.

Using AIDL

This is one of the most popular mechanism of IPC, but as mentioned in the android doc

AIDL is necessary only if you allow clients from different applications to access your service for IPC and want to handle multithreading in your service. If you do not need to perform concurrent IPC across different applications, you should create your interface by implementing a Binder or, if you want to perform IPC, but do not need to handle multithreading, implement your interface using a Messenger

Hence AIDL would be out of the scope of this article. If you would still like to learn more about AIDL, please refer to this link.

Using Bound Services

A bound service is an implementation of the Service class that allows other applications to bind to it and interact with it. To provide binding for a service, you must implement the onBind() callback method. This method returns an IBinder object that defines the programming interface that clients can use to interact with the service.

This is the preferred mechanism for IPC within the same application because this allows us to create well defined contracts in the service. Whenever any communication must go between processes it MUST go through the service and only the data defined in the contract can be exchanged.

Memory lifecycle of a bound service

A picture is worth a thousand words…

But, as with all things, there are risks that one should be aware of while using IPC

To be able to understand potential failure point for Bound Services we need to talk a bit about how android deals with memory management.

Android Memory Management

Android Process Hierarchy

the most important processes are called foreground processes, followed by any visible processes, service processes, background processes, and finally ‘empty’ processes,

Android only kills processes, not components. Of course, that doesn’t preclude the usual garbage collection process.

Foreground Processes

The process that the user is currently interacting is the most important thing to keep alive. 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()

Any process hosting a service bound to the foreground activity is given the same foreground priority. The same applies to content providers the foreground service is currently interacting with.

Android also allows services to make it obvious to the system when they are a high priority foreground service via startForeground(). 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.

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.

Visible Processes

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).

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.

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, realise that it will not be resilient to low memory pressure cases.

Service Processes

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 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. This can be appropriate for services managing periodic tasks where waiting until the next time frame isn’t the end of the world.

Background Processes

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.

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).

Empty Processes

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.

Caveats

In heterogenous markets like India and Chine, there are a large number of devices running their own custom flavours of android which often also have their own custom thresholds (and potentially mechanisms) for memory management. While most of these flavours use the above mentioned rules which deciding which process to kill, they do tend to be a LOT more aggressive in how often they kill processes. This is coming from experience where we faced particular problems with memory in Redmi phones. The only surefire way to deal with this is to make sure the application is designed to recover from such low memory conditions.

Challenges with Multiprocess Communication

Memory Management

Each process must be designed in such a manner that it is able to recover on its own if android kills it to reclaim memory. Not doing so can lead to app crashes or in some cases some very strange unexpected behaviour (such as the user getting stuck on a blank screen.)

The challenge with recovering state after being killed is that only Serialisable or Parcelable elements can be recovered. If your app uses any non serialisable elements (such as Callbacks or Listeners) there is no trivial way of recovering them.

One common method is that when an activity is being killed it invokes “onSaveIntanceState()” method where we can store all our serialisable data in a bundle. When the activity is recovered it passes that bundle to the onCreate method.

In some cases, we might not have a handle on OnSaveInstanceState in which case we would have to write the data ourselves to internal memory and recover it subsequently (we cannot write to shared preferences as they are less reliable than internal memory especially during low memory conditions).

Depending on how complicated this recovery mechanism is, we may end up adding latency to the app while recovering state. Also in cases where we have to write to internal memory ourselves, we have to build around the consideration that the file write may fail.

Implementation Complexity

With memory management in mind (especially working with a large application where major redesign might not be possible) the entire IPC mechanism needs to be implemented with great care especially considering it will not be possible to alter the existing design of the app.

The implementation details of a bound service: https://developer.android.com/guide/components/bound-services.html#Creating

When a Service is started by an application component like an Activity it runs in the background and keeps running even if the user switches to another application or the starting component is itself destroyed. Services are given higher priority than other Background processes and hence, it’s less likely that Android will terminate it.

References

This article uses a LOT of references from other great articles (including the diagrams). It would be really shitty of my to not give credit to the OPs, and do encourage you to read their articles to gain an even better understanding.

https://medium.com/rotxed/going-multiprocess-on-android-52975ed8863c by Sebastiano Gottardo

https://medium.com/google-developers/who-lives-and-who-dies-process-priorities-on-android-cb151f39044f by Ian Lake

--

--