Multicore processors are standard in the handheld devices of today and applications should utilize the opportunity to process data in parallel. With parallel data processing
comes—in most cases—better performance and responsiveness as more processors can actively take part in running the application. But the other side of the coin is that execution becomes more complex, which can induce indeterministic behavior and timing related bugs that can be hard to reproduce. Every application developer need to manage a multi-threaded programming model and how to steer clear of the problem it induces.
This book focuses on application threading on the Android platform, and how multi-threading cooperates with the Android programming model. It provides an understanding of underlying execution mechanisms and how to utilize the asynchronous techniques efficiently. Multiple-CPU hardware holds many benefits for applications and developers: the application code can be executed on multiple CPUs concurrently to gain performance. Concurrent execution and responsiveness is a key factor for an application to succeed; a snappy application is crucial to preserve the user experience of an otherwise great application. But this book does not stop at making the application fast—it should be fast with minimal implementation effort, a good code design, and minimal risk for unexpected errors (not too uncommon in the world of concurrency…). Choosing the right asynchronous mechanism for the right use case is crucial to achieve simple and robust code execution with coherent code design.
But before we immerse ourselves in the world of threading, we will start with an introduction to the Android platform, the application architecture, and its execution. This chapter provides a baseline of knowledge required for an effective discussion of threading in the rest of the book, but a complete information on the Android platform can be found in the official documentation or in most of the numerous Android programming books on the market.
Applications run on top of a software stack that is based on a Linux kernel, native C/C++ libraries, and a runtime that executes the application code (Figure 1-1). The elements are:
Loaders. All these mechanisms will be described in this book.
The cornerstones of an application are the
Application object and the Android components:
The representation of an executing application in Java is the
android.app.Application object, which is instantiated upon application start and destroyed when the application stops–i.e., an instance of the
Application class lasts for the lifetime of the Linux process of the application. When the process is terminated and restarted, a new
Application instance is created.
The fundamental pieces of an Android application are the components managed by the Dalvik runtime:
ContentProvider. The configuration and interaction of these components define the application’s behavior. These entities have different responsibilities and lifecycles, but they all represent application entry points, where the application can be started. Once a component is started, it can trigger another component, and so on, throughout the application’s lifecycle.
A component triggers the start of another component through an
Intent, either within the application or between applications. The
Intent specifies actions for the receiver to act upon—for instance, sending an email message or taking a photograph—and can also provide data from the sender to the receiver. An
Intent can be explicit or implicit:
IntentFilter. If the
Intentmatches the characteristics of a component’s
IntentFilter, the component can be started.
Components and their lifecycles are Android-specific terminologies, and they are not directly matched by the underlying Java objects. A Java object can outlive its component, and the runtime can contain multiple Java objects related to the same live component. This is a source of confusion, and as we will see in Chapter 6, it poses a risk for memory leaks.
An application implements a component by subclassing it, and all components in an application must be registered in the AndroidManifest.xml file.
Activity is a screen—almost always taking up the device’s full screen—shown to the user. It displays information, handles user input, etc. It contains all the UI components–buttons, texts, images, and so forth–shown on the screen and holds an object reference to the view hierarchy with all the
View instances. Hence, the memory footprint of an
Activity can grow large.
When the user navigates between screens,
Activity instances form a stack. Navigation to a new screen pushes an
Activity to the stack, whereas backward navigation causes a corresponding pop.
In Figure 1-2, the user has started an initial
Activity A and navigated to B while A was finished, then on to C and D. A, B and C are full-screen, but D covers only a part of the display. Thus, A is destroyed, B is totally obscured, C is partly shown, and D is fully shown at the top of the stack. Hence, D has focus and receives user input. The position in the stack determines the state of each
The state of an application’s topmost
Activity has an impact on the application’s system priority—a.k.a. process rank—which in turn affects both the chances of terminating an application (Application Termination) and the scheduled execution time of the application threads (Chapter 3).
An Activity lifecycle ends either when the user navigates back—i.e. presses the back button–or when the
Activity explicitly calls
Service can execute invisibly in the background without direct user interaction. It is typically used to offload execution from other components, when the operations can outlive their lifetime. A
Service can be executed in either a started or a bound mode:
Serviceis started with a call to
Context.startService(Intent)with an explicit or implicit
Intent. It terminates when
Context.bindService(Intent, ServiceConnection, int)with explicit or implicit
Intentparameters. After the binding, a component can interact with the
ServiceConnectioninterface, and it unbinds from the
Context.unbindService(ServiceConnection). When the last component unbinds from the
Service, it is destroyed.
An application that wants to share substantial amounts of data within or between applications can utilize a
ContentProvider. It can provide access to any data source, but it is most commonly used in collaboration with SQLite databases, which are always private to an application. With the help of a
ContentProvider, an application can publish that data to applications that execute in remote processes.
This component has a very restricted function: it listens for Intents sent from within the application, remote applications, or the platform. It filters incoming Intents to determine which ones are sent to the
BroadcastReceiver should be registered dynamically when you want to start listening for Intents, and unregistered when it stops listening. If it is statically registered in the
AndroidManifest, it listens for Intents while the application is installed. Thus, the
BroadcastReceiver can start its associated application if an Intent matches the filter.
Android is a multi-user, multi-tasking system that can run multiple applications at the same time and let the user switch between applications without noticing a significant delay. The multi-tasking is handled by the Linux kernel, and application execution is based on Linux processes.
Linux assigns every user a unique user ID, basically a number tracked by the OS to keep the users apart. Every user has access to private resources protected by permissions, and no user (except root, the superuser, which does not concern us here) can access another user’s private resources. Thus, sandboxes are created to isolate users. In Android, every application package has a unique user ID; i.e., an application in Android corresponds to a unique user in Linux and cannot access other applications’ resources.
What Android adds to each process is a runtime execution environment, the Dalvik virtual machine, for each instance of an application. Figure 1-3 shows the relationship between the Linux process model, the VM, and the application.
By default, applications and processes have a one-to-one relationship, but if required, it is possible for an application to run in several processes, or for several applications to run in the same process.
The application lifecycle is encapsulated within its Linux process, which–in Java—maps to the
android.app.Application class. The
Application object for each app starts when the runtime calls its
onCreate() method. Ideally, the app terminates with a call by the runtime to its
onTerminate(), but an application can not rely upon this. The underlying Linux process may have been killed before the runtime had a chance to call
Application object is the first component to be instantiated in a process and the last to be destroyed.
An application is started when one of its components is initiated for execution. Any component can be the entry point for the application and once the first component is triggered to start a Linux process is started—unless it is already running—which leads to the following start up sequence:
Setting up a new Linux process and the runtime is not an instantaneous operation. It can degrade performance and have a noticeable impact on the user experience. Thus, the system tries to shorten the startup time for Android applications by starting a special process called Zygote on system boot. Zygote has the entire set of core libraries preloaded. New application processes are forked from the Zygote process without copying the core libraries, which are shared across all applications.
A process is created at the start of the application and finishes when the system wants to free up resources. Because a user may request an application at any later time, the runtime avoids destroying all its resources until the number of live applications leads to an actual shortage of resources across the system. Hence, an application isn’t automatically terminated even when all of its components have been destroyed.
When the system is low on resources, it’s up to the runtime to decide which process should be killed. To make this decision, the system imposes a ranking on each process depending on the application’s visibility and the components that are currently executing. In the following ranking, the bottom-ranked processes are forced to quit before the higher-ranked. The process ranks are (highest first):
Serviceis bound to an
Activityin front in a remote process or
Activity. This is the process level that contains most applications.
In practice, the ranking system ensures that no visible applications will be terminated by the platform when it runs out of resources.
It should be noted that there is a difference between the actual application lifecycle—defined by the Linux process–and the perceived application lifecycle. The system can have multiple application processes running even while the user perceives them as terminated. The empty processes are lingering–if system resources permit it—to shorten the startup time on restarts.
Android devices are multiprocessor systems that can run multiple operations simultaneously, but it is up to each application to ensure that operations can be partitioned and executed concurrently to optimize application performance. If the application does not enable partitioned operations, but prefers to run everything as one long operation, it can exploit only one CPU, leading to suboptimal performance. Unpartitioned operations must run synchronously, whereas partitioned operations can run asynchronously. With asynchronous operations, the system can share the execution among multiple CPUs and therefore increase throughput.
An application with multiple independent tasks should be structured to utilize asynchronous execution. One approach is to split application execution into several processes, because those can run concurrently. However, every process allocates memory for its own substantial resources, so the execution of an application in multiple processes will use more memory than an application in one process. Furthermore, starting and communicating between processes is slow, and not an efficient way of achieving asynchronous execution. Multiple processes may still be a valid design, but that decision should be independent of performance. To achieve higher throughput and better performance, an application should utilize multiple threads within each process.
An application can utilize asynchronous execution on multiple CPU’s with high throughput, but that doesn’t guarantee a responsive application. Responsiveness is the way the user perceives the application during interaction: that the UI responds quickly to button clicks, smooth animations, etc. Basically, performance from the perspective of the user experienced is determined by how fast the application can update the UI components. The responsibility for updating the UI components lies with the UI thread, which is the only thread the system allows to handle UI updates.
To make the application responsive, it should ensure that no long-running tasks are executed on the UI thread. If they do, all the other execution on that thread will be delayed. Typically, the first symptom of executing long running tasks on the UI thread is that the UI becomes unresponsive because it is not allowed to update the screen or accept user button presses properly. If the application delays the UI thread too long, typically 5-10 seconds, the runtime displays an “Application Not Responding” (ANR) dialog to the user, giving her an option to close the application. Clearly, you want to avoid this. In fact, the run-time prohibits certain time-consuming operations, such as network downloads, from running on the UI thread.
So long operations should be handled on a background thread. Long-running tasks typically include:
Threads in Android applications are as fundamental as any of the component building blocks. All Android components and system callbacks—unless denoted otherwise—runs on the UI thread, and should use background threads when executing longer tasks.
An Android application runs on top of a Linux OS in a Dalvik runtime, which is contained in a Linux process. Android applies a process ranking system that priorities the importance of each running application to ensure that it is only the least prioritized applications that are terminated. To increase performance, an application should split operations among several threads so that the code is executed concurrently. Every Linux process contains a specific thread that is responsible for updating the UI. All long operations should be kept off the UI thread and executed on other threads.
 Also known as the main thread, but throughout this book we stick to the convention of calling it the “UI thread.”