Chapter 1. Android Components and the Need for Multiprocessing
Before we immerse ourselves in the world of threading, we will start with an introduction to the Android platform, the application architecture, and the application’s 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.
Android Software Stack
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 major building blocks of the Android software stack are:
- Applications
- Android applications that are implemented in Java. They utilize both Java and Android framework libraries.
- Core Java
-
The core Java libraries used by applications and the application framework. It is not a fully compliant Java SE or ME implementation, but a subset of the retired Apache Harmony implementation, based on Java 5. It provides the fundamental Java threading mechanisms: the
java.lang.Thread
class andjava.util.concurrent
package. - Application framework
-
The Android classes that handle the window system, UI toolkit, resources, and so on—basically everything that is required to write an Android application in Java. The framework defines and manages the lifecycles of the Android components and their intercommunication. Furthermore, it defines a set of Android-specific asynchronous mechanisms that applications can utilize to simplify the thread management:
HandlerThread
,AsyncTask
,IntentService
,AsyncQueryHandler
, andLoaders
. All these mechanisms will be described in this book. - Native libraries
- C/C++ libraries that handle graphics, media, database, fonts, OpenGL, etc. Java applications normally don’t interact directly with the native libraries because the Application framework provides Java wrappers for the native code.
- Runtime
- Sandboxed runtime environment that executes compiled Android application code in a virtual machine, with an internal byte code representation. Every application executes in its own runtime, either Dalvik or ART (Android Runtime). The latter was added in KitKat (API level 19) as an optional runtime that can be enabled by the user, but Dalvik is the default runtime at the time of writing.
- Linux kernel
- Underlying operating system that allows applications to use the hardware functions of the device: sound, network, camera, etc. It also manages processes and threads. A process is started for every application, and every process holds a runtime with a running application. Within the process, multiple threads can execute the application code. The kernel splits the available CPU execution time for processes and their threads through scheduling.
Application Architecture
The cornerstones of an application are the Application
object and the Android components: Activity
, Service
, BroadcastReceiver
, and ContentProvider
.
Application
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.
Components
The fundamental pieces of an Android application are the components managed by the runtime: Activity
, Service
, BroadcastReceiver
, and 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 is trigged to start with an Intent
, either within the application or between applications. The Intent
specifies actions for the receiver to act upon—for instance, sending an email or taking a photograph—and can also provide data from the sender to the receiver. An Intent
can be explicit or implicit:
-
Explicit
Intent
- Defines the fully classified name of the component, which is known within the application at compile time.
-
Implicit
Intent
-
A runtime binding to a component that has defined a set of characteristics in an
IntentFilter
. If theIntent
matches the characteristics of a component’sIntentFilter
, 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
An Activity
is a screen—almost always taking up the device’s full screen—shown to the user. It displays information, handles user input, and so on. It contains 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 Activity
:
- Active in the foreground: D
- Paused and partly visible: C
- Stopped and invisible: B
- Inactive and destroyed: A
The state of an application’s topmost Activity
has an impact on the application’s system priority—also known as 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—for example, presses the back button—or when the Activity
explicitly calls finish()
.
Service
A 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:
-
Started
Service
-
The
Service
is started with a call toContext.startService(Intent)
with an explicit or implicitIntent
. It terminates whenContext.stopService(Intent)
is called. -
Bound
Service
-
Multiple components can bind to a
Service
throughContext.bindService(Intent, ServiceConnection, int)
with explicit or implicitIntent
parameters. After the binding, a component can interact with theService
through theServiceConnection
interface, and it unbinds from theService
throughContext.unbindService(ServiceConnection)
. When the last component unbinds from theService
, it is destroyed.
ContentProvider
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.
BroadcastReceiver
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
. A 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.
Application Execution
Android is a multiuser, multitasking system that can run multiple applications at the same time and let the user switch between applications without noticing a significant delay. The Linux kernel handles the multitasking, and application execution is based on Linux processes.
Linux Process
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 super user, 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; for example, 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, such as 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.
Lifecycle
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 cannot rely upon this. The underlying Linux process may have been killed before the runtime had a chance to call onTerminate()
. The Application
object is the first component to be instantiated in a process and the last to be destroyed.
Application start
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—leading to the following startup sequence:
- Start Linux process.
- Create runtime.
-
Create
Application
instance. - Create the entry point component for the application.
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.
Application termination
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 ones. With the highest first, the process ranks are:
- Foreground
-
Application has a visible component in front,
Service
is bound to anActivity
in front in a remote process, orBroadcastReceiver
is running. - Visible
- Application has a visible component but is partly obscured.
- Service
- Service is executing in the background and is not tied to a visible component.
- Background
-
A nonvisible
Activity
. This is the process level that contains most applications. - Empty
- A process without active components. Empty processes are kept around to improve startup times, but they are the first to be terminated when the system reclaims resources.
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.
Structuring Applications for Performance
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 doesn’t 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.
Creating Responsive Applications Through Threads
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 update UI components.[1]
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 runtime 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:
- Network communication
- Reading or writing to a file
- Creating, deleting, and updating elements in databases
-
Reading or writing to
SharedPreferences
- Image processing
- Text parsing
Threads in Android applications are as fundamental as any of the component building blocks. All Android components and system callbacks—unless denoted otherwise—run on the UI thread and should use background threads when executing longer tasks.
Summary
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.
[1] Also known as the main thread, but throughout this book we stick to the convention of calling it the “UI thread.”
Get Efficient Android Threading now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.