Intents and Extensions

Dave Smith
Dave Smith
Intents and Extensions

I have been focused primarily on the Android platform for about 5 years, so the Intents framework is pretty much a part of my DNA at this point. When Twitter went aflame with "iOS has Intents! #WWDC" tweets during last week's keynote, I sat up and took notice. Could it be? What follows are the things I have observed about what the two frameworks now have in common, and where they divide.

TL;DR

Intents and extensions aren't the same, though they are very similar in many ways. Extensions are less flexible (surprise!), but I don't really care; maybe you shouldn't either. Overall, they are a great first step for Apple.

Diving a bit more into the details, we can see how these two frameworks are distinct from each other.

Processes

In Android, each user application runs in its own unique process. This is a common paradigm (iOS does the same), because it helps sandbox applications and their access to system resources. By default, all the components (activities, services, broadcast receivers, etc.) that an application defines all run within that same process. Strictly speaking, individual components can be explicitly placed into additional processes, but the effect on access and permissions is negligible so we will ignore that case here.

An Android application process is started the first time any of its components need to be activated; activity, service, broadcast receiver...doesn't matter. That process will remain resident until such a time when the system decides it needs to terminate the entire process to reclaim resources for another application in the foreground. That does not mean that the components within that process necessarily remain active (most don't), but the process itself is live in the system. As requests to activate other components come to the system, they will be launched inside of the existing process that is already in memory.

Figure: Image taken from Apple App Extension Programming Guide

While iOS extensions are similarly considered components of a containing application (they are bundled together and cannot be deployed separately), their execution model is different than Android. iOS launches extension components into a separate (i.e. third) process from both the containing application and the calling application making the request (called the "host" by Apple).

Performance

You may hear this touted as a performance advantage since the entire containing application doesn't need to start or be previously running, and on iOS it is true that this is a benefit. As compared to Android, however, this is somewhat of a moot point because in Android not nearly as much is tied to the "application" as in iOS. In Android, the application process is a lightweight container for other components that contain the heavier code; it does not represent a bulky entry-point that we want to avoid launching unless necessary. Starting the application in order to launch a service doesn't represent much overhead.

Stated more simply, the setup work done in a typical iOS AppDelegate is significantly more than an Android Application class (many applications don't even provide an Application class).

Every request to launch an iOS extension will result in a new process with a new extension instance running inside it. Some extension types (like Today widgets) only have one possible host. However, with action extensions or keyboards, for example, this can result in multiple instances of the same extension active simultaneously if different applications send a request.

Android also allows multiple instances when the component is an activity; though they all still live within the same process. Other components (services, broadcast receivers, and content providers) are bound to a single active instance through which all Intents are routed.

Most extensions have a view component, so they can be considered comparable to a launched activity. In these cases, the behavior is nearly the same. When a user is done with the component, Android will destroy the activity and iOS will terminate the extension's process. Different methods of accomplishing the same end.

Container Sharing

Since iOS runs the extension in a separate process with a separate bundle identifier, the extension and the container application don't have access to the same file system resources. Similarly, the two components cannot directly share common code that may exist between them without copying due to the process boundary.

Figure: Image taken from Apple App Extension Programming Guide

Apple has provided sufficient solutions for both of these cases. Using entitlements and app groups, applications can create shared containers on the file system for data sharing. For instance, with NSUserDefaults it's pretty straightforward to create an instance in a shared container as follows (apologies for using archaic Objective-C code) in both the container and the extension:

NSUserDefaults *defaults = [[NSUserDefaults alloc] initWithSuiteName:@"app-group-id"];

With the advent of embedded frameworks, the two components can also be efficient and share common code despite the process boundary between them. So while this is less convenient for the developer than the Android model (in which neither of these concerns really apply), the functionality to handle most use cases effectively still exists.

Host/Client Communication

Intents are essentially fire and forget data structures from the calling application's perspective. The request is made of the system to perform the action in the appropriate application, and the caller can optionally register to receive a result action in a later callback. Intents are meant to activate activity (user interface) components, service (non-user interface) components, or broadcast an event to registered listeners across multiple applications. Only in the case of launching an activity can an application register to receive a result directly via the Intents framework. In other cases, results must be handled another way.

In this way, Intents and extensions are notably similar. Rather than a continuous communication channel, a receiver is passed a command with some data attached, and then optionally returns back some additional data as a result at the completion of the task. In fact, extensions make this return path a bit more straightforward as the data items can be returned in the same manner, regardless of whether the activated extension component has a user interface.

Intent Security

It has been noted that extensions are more secure than other platforms because the system frameworks are in the middle, but this is true of Intents as well.

Intent transactions are always routed through ActivityManagerService; neither forward Intent requests nor returned results are ever sent directly from one application process to another. Even in cases where an explicit Intent is sent to launch a component within the same application, that request is still routed through the framework.

Android does provide application developers direct app-to-app IPC using bound services with either AIDL or the Messenger/Handler paradigm, but even this method requires the use of Intents routed through the framework to get started.

User Interactivity

When an Intent request is made of the system, and Android can determine that a single destination exists for that request, no additional system UI will be displayed to the user; routing can happen automatically and the request is delivered to its destination. In cases where an Intent is implicit and multiple destinations are possible, the system will display chooser UI to the user in order to determine the request's appropriate destination.

Figure: Example Intent Chooser UI in KitKat

In iOS, an application cannot launch an extension without at least some system UI. In most cases, the system's UIActivityViewController is the gate through which the user must pass to select the extension they would like to launch. In principle, this is very similar to the chooser UI presented by an Intent request with multiple destinations. However, the iOS system UI must always display, even if the action is defined narrowly enough to go to exactly one destination in a receiving application (which is not easy to do, either...more below). Again, in most use cases this is not an issue and even desired. Nevertheless, it is a distinction worth keeping in mind.

Figure: UIActivityViewController on iOS 7.

Action extensions can be constructed either with or without a view. "No View" action extensions do allow a service-like implementation to be written in iOS, where an action is silently performed and returns to the calling application sometime in the future (with optional result data). The user, though, must still initiate that action from UIActivityViewController.

Packaging

A significant difference between the two models comes in their packaging requirements. iOS extensions are packaged inside an otherwise functional application and, according to Apple, the containing application must provide useful additional functionality to the user. In most cases, this is not even a concern. Even on Android, custom keyboard applications that have no other purpose usually also contain some UI for tutorial or configuration of the service. However, Android does not require that an application provide a component that lives in the Launcher's main app group.

This is a double-edged sword; on the one hand, enforcing that a purely service-based application (like a keyboard extension, or possibly a widget) have a visible app icon makes it easier to find if you want to uninstall it later. I have fielded many "how do I remove this?" questions from Android users when no main app icon exists. However, the downside here is that iOS is getting another dose of Springboard pollution (i.e. icons that serve no useful purpose but you can't remove) and more items for most people to put in that "Unused" folder. In the standard Android Launcher paradigm app icons are tucked away by default, except for those the user has explicitly chosen to promote as important.

Note: That may sound like an argument for customization of the home screen, but it's simply an observation that if you don't give users control over their home screen (which is a valid choice), you ought to be very careful about what you force to remain on it.

Data Transfer

Note: The behaviors described here are notably in flux during the beta phase. As more information is discovered, I will update this section until the behavior is final.

Alongside an Intent's destination definition (via an implicit action, explicit class name, etc.), the filter scope can further narrow by referencing a custom data type that is only known to a private suite of applications. Currently in iOS, NSExtensionItem data returned from the extension can provide unique type identifiers, but the same is not true for data passed forward to an extension; identifiers are inferred from the type of data passed in (rdar://17217209) [This issue has been fixed in Beta 2].

This is important because iOS, via the NSExtensionActivationRule in the Info.plist, determines when and where to show a custom action extension by comparing the input items passed to the UIActivityViewController with an NSPredicate. By limiting the scope of input items to standard data types, it becomes difficult to create more targeted actions that only respond to special data.

Android Intents may attach any amount of arbitrary "extra" data passed along with it for the receiver to read, interpret, and act upon. Any data structure that can be safely "flattened" into a string of bytes may be attached as an extra and sent to the receiving application. iOS is documented to support any object conforming to NSSecureCoding via an NSItemProvider, but is currently limiting transfer in both directions to anything beyond strings, URLs, images, and data (rdar://17236971) [Update: As of Beta 2, this now works in the forward direction; still crashes in the extension return values].

A functional workaround for NSCoding objects is to encode the object into NSData before passing it, and then decode it on the other side. As mentioned above, though, you won't be able to identify the NSData attached using anything other than the public.data UTI; at least for now. [As of Beta 2, even when objects can't pass, custom UTIs are functional]

So What?

So, they aren't the same; maybe half-brothers or cousins? Raise your hand if you could have guessed that at the beginning. The next time you see someone state their equality, now you know better.

Nevertheless, this is exactly what I would have expected Apple to do as a first step in this arena, and I like the changes they've made in iOS 8. I fully expect this functionality to grow as the feature matures and Apple truly learns what developers want these things to do. As a developer, they could have made life easier for me; and it didn't take me long to think up a simple use case that wasn't so straightforward to do with the framework. However, I can dream up plenty of scenarios now (especially on the accessory side) where this framework does provide exactly what iOS needs.