3. The Android Application Model

... the Lord gave, and the Lord has taken away; blessed be the name of the Lord.

JOB 1:21

In the Android environment, the challenge of concurrent programming is compounded by the necessity of dealing with components whose lifecycles are out of the control of the application.

Lifecycles and Components

Android was designed at a time when mobile devices could not reasonably be expected to support paged memory. Whereas most laptops today sport solid-state hard drives, flash memory at the beginning of the century wore out after only approximately 10,000 writes. Using flash memory as swap-space for a general purpose virtual memory system was simply out of the question. In an environment that is limited to only physical memory, an operating system has a hard upper limit on the number of applications that can run at any given time. Android’s solution to this problem is to allow the operating system to terminate application process at any time, to recover its memory for another application.


Note

Android’s architecture, in which applications are abruptly terminated, is not the only possibility. Early versions of Apple’s iOS chose, instead, simply to restrict the number of applications that could be running at any given time.


In an environment in which an app has almost no control over its own lifecycle, the whole idea of an application as an identifiable entity becomes much less meaningful. In the Android world, instead, there are components: Activities, Services, Providers, and Receivers. The operating system assures that these components are around when they are needed, spawning a new application process as a side effect, if necessary, to run them.


Note

This description of the Android application model and component lifecycles, in this chapter, depends on some prior familiarity with Android components and their functions. They are not described here. There are many excellent resources that do describe them in great detail. See, for instance:

Griffiths, Dawn and David: Head First Android Development, 1st Edition

Mednieks, Dornin, Meike, and Nakamura, Programming Android


Whereas the term “application” is roughly synonymous with “process” in many contexts, in Android, an application is simply a collection of coordinating components. A couple of examples will illustrate.

When an Android phone receives a phone call, the system responds, regardless of anything else it might be doing at the time, by showing a page that lets the user pick up the call. To accomplish this, the telephony subsystem sends an Intent. The Intent is a small packet of data that describes a required service.

The operating system matches the Intent requirements to a component that supplies those requirements—in this case, an Activity that displays the “Incoming call” page. Note that the requirement expressed in the Intent is not a request to run a particular application. It is a request for a component, in this case a component that can display a page: an Activity. If displaying the Activity necessitates starting an application process, the system does so as a side effect. The goal is to display a page. Starting the application is a means to that end.

A second example is the reverse of the first. When an Android device is booted, the last thing that the system does as it finishes the boot process is to broadcast an Intent (BOOT_COMPLETED) indicating that it is ready to begin normal operation. Components, called Receivers, can register to receive this special broadcast. Every Receiver that is registered for the broadcast receives it shortly after the system boots.

The interesting thing is that some applications contain Receivers that do nothing at all when they receive the broadcast. The applications that contain such Receivers are depending entirely on the side effect. The system starts those applications at boot time, so that it can deliver the Intent to their Receiver components. The application’s goal, though, is that it is started every time the system is booted.

The previous examples describe the handling of two different Intents, one starting an Activity in response to a hardware event and the other starting a Receiver in response to a system event. Consider, for a moment, the illustrative lifecycle for a single improbable application that contains both of the sample components, the Activity and the Receiver.

When the system boots, Android attempts to deliver the BOOT_COMPLETED Intent to the application’s Receiver. To do that, it must create a process, load the application into the process memory, create an instance of the Receiver object, and then call the Receiver’s onReceive method, passing the Intent. Once the onReceive method returns, the Receiver instance is of no further use, is released, and can be garbage collected.

If a phone call comes in soon after the system boots, Android must deliver the call Intent to the application’s Activity. It discovers that the application is already running (because of the recently delivered BOOT_COMPLETED Intent), so it needs to only create an instance of the required Activity and run it through its lifecycle.

As illustrated in Figure 3.1, there are now three lifecycles to track: the Receiver, the Activity, and the Application Process.

Image

Figure 3.1 Starting an Android process

Process Priority

The lifecycle of the application process might well affect the lifecycles of the two components. Suppose that the system determines that there is insufficient memory available for it to perform some required task. This is unlikely, of course, because the system has just booted. Consider the case, though, just for instructive purposes.

Because it needs memory, the system will review running processes to find the best candidates for termination. It does this by examining the oom_adj attribute for each running process. oom_adj is a small integer whose value is dynamically managed by the system. Smaller values indicate higher process priority (and a smaller chance of process termination): -16 is a very high priority, 15 is very low. Figure 3.2 illustrates.

Image

Figure 3.2 OOM-ADJ


Note

It is possible to discover the oom_adj of a process running on a device with root access. Readers comfortable with the command line can use the Android debugger to get access to a shell on a device. The oom_adj associated with a given process is cataloged in the file system as the file /proc/<process-id>/oom-adj.


Negative (highest priority) numbers are reserved for system applications. If Android ever has to kill its own processes to recover memory, the system is probably about to collapse anyway. The process that contains the Activity currently visible on the device screen has an oom_adj of 0. This is the highest priority available for a non-system process. Clearly, it wouldn’t make any sense to terminate the process powering the currently visible screen.

A process with no active components (for instance, a process containing only an Activity that is not currently visible) will be given a very high oom_adj, making it a likely choice for termination. The important consequence of this is that, immediately upon becoming invisible, an application’s chances of being forcefully terminated go from almost nil to very high. Android assumes that the purpose of an Activity component is to power a visible UI page. If the Activity is not displaying a UI page, it is not important and the process powering it can be terminated.


Note

Recent versions of the Linux Kernel, and the versions of Android since KitKat, API level 19 that use those kernels, have replaced oom_adj with oom_score_adj. oom_score_adj is much finer-grained, taking values between 1000 and -1000, lowest priority to highest. Although the numbers have changed, the behavior is essentially the same.


A process with a running Service component has an oom_adj that is smaller than that of a process with no active components but greater than that of the process with the visible Activity. A Service component is never as important as the visible Activity, but if it is running, it is more important than an invisible Activity. It is entirely possible, for instance, that some Service component in an application is doing useful work at a time at which none of the application’s Activity components is visible. This aspect of service scheduling will play an important part in the application strategy developed in later chapters.

Android terminates an application process by sending it an uncatchable, non-ignorable kill signal (-9). Doing this forces the application process to terminate almost immediately, killing all its threads and de-allocating all its memory. In particular, the components that comprise the application are abruptly terminated.

Although the Android system is free to kill processes as soon as their priority permits, it actually does so fairly lazily. Android memory management, like its close relative, virtual machine garbage collection, does as little work as it can. If there is no immediate need for memory, the effort required to free a process is just a waste of battery.


Note

It is interesting to note that, whereas killing an application with a kill -9 is an extraordinary event on most operating systems, on an Android system it is the most common means of terminating an application.


In the example shown in Figure 3.3, for instance, neither the Receiver nor the Activity have completed their lifecycles yet. Android is being lazy. When their process is killed, however, both are ended abruptly, with prejudice, in mid-lifecycle. There is no warning, no invocation of lifecycle methods, and no goodbye-kiss: just lights out.

Image

Figure 3.3 Killing an Android process

The significant consequence of this, of course, is that any state that has not been saved is gone. There is no way to get it back.

Component Lifecycles

In an Android application, separate components are entirely separate entities with entirely separate lifecycles. The Android system controls the lifecycles of the components, starting and stopping them as needed.

The canonical example of the lifecycle of an Android component is what happens when a device is rotated. Android’s extremely versatile UI system supports completely different screen layouts in portrait and landscape mode. So many things can change when the device is rotated that Android throws away the entire Activity powering the UI in its current configuration and creates a new instance, for the new configuration. Figure 3.4 illustrates what happens in response to screen rotation.

Image

Figure 3.4 Device rotation

The lifecycle of the running instance of the visible Activity is completed. Android calls its onPause, onStop, and onDestroy methods, in that order, and then deletes all references to the Activity. It is now eligible for garbage collection.


Note

Do not confuse process reaping with garbage collection!

Although the two have similar motivations—memory management—process reaping ends the application process, terminating all its threads and de-allocating all its memory. Garbage collection simply recovers unused memory within a process, so that the process can continue running.

The connections between the two kinds of memory management are the onTrimMemory lifecycle events. The Android system uses these events to indicate to an application that there is memory pressure. An application that responds by shrinking is less likely to be killed, to reclaim space.


Component instances come and go even when the task they represent survives. It is easy to imagine that instance #2 of the Activity shown in Figure 3.4 behaves completely identically to its predecessor, instance #1. Nonetheless, instance #1 is gone and forgotten. Instance #2 is the only instance of the Activity in existence. Just as Android creates and destroys processes as it needs them, it creates and destroys instances of components as it needs them.

Again, this has some significant consequences:

Image Components that do not save state lose it.

Image Code cannot rely on the existence of a component object that it saw even just a moment ago.

Android Applications as Web Apps

Figure 3.5 is another, more complete, portrayal of the lifecycle of an Android application. This time, there are a few more components represented, as well as the user’s perception of the application’s lifecycle.

Image

Figure 3.5 Android application lifecycle

It would be intolerable to lose a user’s state simply because Android needs memory. Instead, the user expects a continuous, uninterrupted experience. She expects to return to an application in the same state in which she left it, even if that was several days ago. The top line in Figure 3.5, User Perceived Application Lifecycle, models this expectation.

On the other hand, there is no way that an application’s process will last for several days. Other applications will come and go, and eventually require memory. The only safe place for state is persistent storage: the file system, the network, and so on.

Web app developers, especially those that worked with J2EE, will probably feel a strong sense of déjà vu looking at Figure 3.5. Android applications are, almost literally, web applications.

Although desktop app developers think a lot about starting and stopping their apps, web-app developers do not. The question of when a web app starts running is nearly meaningless, at least as far as it involves users.

The analogy can be extended. The description of an Activity component as an object with its own lifecycle that is responsible for the workflow of a single UI page, is almost exactly the description of a servlet. Android service components are very similar to J2EE session beans. Even the application bundle, the .apk file, with its manifest, is strongly reminiscent of the .war files used to bundle Java web applications. An Android application is a collection of independent components, declared in a manifest, and run in a container.


Note

It is even interesting to compare the evolution of Web frameworks with the evolution of Android frameworks. The separation of view, controller, and eventually presenter, improvements in the mechanisms used for queuing network requests, and advancements in testing strategies all have parallels in web app development.


The Android Process

Unless there are specific arrangements to do otherwise, application code will run on one canonical thread. This thread is called the main, or sometimes the UI, thread. Nearly any nontrivial Android application, however, uses multiple threads.

Application Startup

Every Android application is clone of a proto-application called Zygote. A single instance of Zygote is started as part of bringing up the system. It initializes itself, preloading large portions of the Android framework, and then waits for connections to a socket.

When the system needs to create a new application, it connects to the Zygote socket and sends a small packet describing the application to be started. Zygote clones itself, creating a new kernel-level process.

The new process is interesting in that it shares memory with Zygote, its parent, in a mode called copy-on-write. Because the two processes use the same memory, starting the child process is almost instantaneous. The kernel does not need to allocate much memory for the new process, nor does the startup need to load the Android framework libraries again. They were all already loaded for Zygote.

Copy-on-write sharing, however, means that if the child application ever tries to change a value in the shared space, the kernel allocates new memory and copies the page into which the child is writing, into it. The new child process can never affect Zygote’s memory. Even if the child were to write on every single memory page (and that never happens), the cost of allocating the new memory is amortized over the life of the application, and is not a cost of initialization.

Figure 3.6 shows Zygote’s initial memory allocation. Zygote views its memory through a page table which maps memory references into pages of physical memory.

Image

Figure 3.6 Zygote memory

Figure 3.7 shows the creation of a new process for a new application. The new application has its own page table. Most of the new page table is simply a copy of Zygote’s page table. It points to the exact same pages of physical memory. Only the pages the new application uses for its own purposes are not shared.

Image

Figure 3.7 A new application shares most of Zygote’s memory

Figure 3.8 illustrates what happens when the new application tries to change a memory value. Because the pages are tagged as copy-on-write in the page table, the write attempt triggers a copy. The page is copied over, and the page table is adjusted to point at the new copy. When the write goes through, it modifies only the new application’s memory, not Zygote’s.

Image

Figure 3.8 Copy-on-write

The Android Main Thread

After a few more initializations—mostly loading the new application into memory—the main thread of the new process initializes itself as a looper. A Looper is, essentially, an implementation of the safe publication idiom introduced in Chapter 2, “Java Concurrency.”

A thread initialized as a Looper enters a tight loop, servicing a queue. The thread removes the next item from the queue and executes the task it represents. Loopers are discussed in detail in Chapter 5, “Looper/Handler.” For now, it is sufficient to understand that the heart of a new Android process is a single thread in a tight loop, processing tasks from a queue.

This thread powers the entire application UI (thus its common name, UI-thread). Many UI methods verify that they are run from the main thread and throw an exception if they are not.

In the early days of the adoption of the model-view-controller (MVC) pattern, developers discovered that it was difficult to build a multithreaded UI that didn’t deadlock. The reasons for this are complex, but not all that difficult to grasp, intuitively.

In an MVC architecture, as illustrated in Figure 3.9, one can think of events as flowing in two different directions: from the screen/keyboard, through the Controller, in toward the Model, and then from an updated Model, outward, through the View, to the screen. If there are resources that need to be seized to process in- and outbound events, those resources are likely to be seized in different orders during processing in different directions, making deadlocks nearly inevitable.

Image

Figure 3.9 Deadlock in the MVC pattern

There are other reasons for making a UI single-threaded. Two important reasons are simplicity and atomicity.

Single-threaded UIs are simple because they run on a single thread. There is no reason to introduce locks, semaphores, synchronization, or thread safety concerns into code that is only run on a single thread. If no more than one thread can ever access object state, then the rule has been satisfied.

A second convenient aspect of a single-threaded UI is that events are atomic. When the main thread removes a task from its queue and begins to execute it, that task has complete control of the UI. If the task involves, for instance, enabling a button on the lower left of the screen, replacing an icon in some view, and repopulating the menu with some new items, there is no possibility that the UI will, because of some kind of race condition, display the new icon and enabled button, but not the menu items. Until the current task is complete, it is in control of all changes made in the UI. Everything that happens between the time it is removed from the queue and the time it releases the thread to process the next task is, from the point of view of the UI, atomic.

Of course, this is also the problem with a single-threaded UI. Android targets a minimum frame rate of 60fps. That means that the main thread needs to process redraw tasks at least every 17ms. That’s a lot of CPU time, but not a lot of time for IO, even to the local file system.

So, a major difficulty of building solid, delightful Android programs becomes apparent:

Image Application state, displayed through the UI, must be visible to the main thread.

Image Application state displayed through the UI must be kept in a persistent store (I/O).

Image I/O should not be performed on the main thread. This includes reading and writing files, databases and, of course, the network.

Summary

An Android application is not at all a normal, desktop application. It is much more like a web application: it is built of components that are analogous to the servlets, controllers, and data access layers, familiar to web-devs. The user’s perception of an application’s lifecycle is related neither to that of the processes that power it nor of the components that comprise it. Android creates and destroys both processes and components when it needs the resources for other purposes.

Android applications are powered, unless the developer makes specific arrangements otherwise, by a single thread, the main thread. This thread is a Looper, a tight loop processing tasks from a queue. It processes each task to completion, before starting the next. This is convenient in that it makes tasks atomic. It can be problematic, though. When a task involves I/O or, for some other reason takes a long time to complete, it stalls the user-visible UI.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.179.225