Tải bản đầy đủ
Chapter 1. Android Components and the Need for Multiprocessing

Chapter 1. Android Components and the Need for Multiprocessing

Tải bản đầy đủ

Applications
Android applications that are implemented in Java. They utilize both Java and An‐
droid 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 and java.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 asyn‐
chronous mechanisms that applications can utilize to simplify the thread manage‐
ment: HandlerThread, AsyncTask, IntentService, AsyncQueryHandler, and Load
ers. 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 appli‐
cation 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 com‐
ponents: Activity, Service, BroadcastReceiver, and ContentProvider.

2

|

Chapter 1: Android Components and the Need for Multiprocessing

Application
The representation of an executing application in Java is the android.app.Applica
tion object, which is instantiated upon application start and destroyed when the ap‐
plication 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 config‐
uration 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 com‐
ponent is trigged to start with an Intent, either within the application or between ap‐
plications. 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 the Intent matches the characteristics of a component’s Intent
Filter, 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 com‐
ponents—buttons, texts, images, and so forth—shown on the screen and holds an object

Application Architecture

|

3

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

Figure 1-2. Activity stack
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 ter‐
minating an application (“Application termination” on page 7) and the scheduled exe‐
cution 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().

4

|

Chapter 1: Android Components and the Need for Multiprocessing

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 to Context.startService(Intent) with an ex‐
plicit or implicit Intent. It terminates when Context.stopService(Intent) is
called.
Bound Service
Multiple components can bind to a Service through Context.bindService(In
tent, ServiceConnection, int) with explicit or implicit Intent parameters. Af‐
ter the binding, a component can interact with the Service through the Service
Connection interface, and it unbinds from the Service through Context.unbind
Service(ServiceConnection). When the last component unbinds from the Ser
vice, it is destroyed.

ContentProvider
An application that wants to share substantial amounts of data within or between ap‐
plications 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 pub‐
lish 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 reg‐
istered 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.

Application Execution

|

5

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 cor‐
responds 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 rela‐
tionship between the Linux process model, the VM, and the application.

Figure 1-3. Applications execute in different processes and VMs
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:
1. Start Linux process.
6

|

Chapter 1: Android Components and the Need for Multiprocessing

2. Create runtime.
3. Create Application instance.
4. 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 preloa‐
ded. 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 auto‐
matically 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 execut‐
ing. 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 an Activity in
front in a remote process, or BroadcastReceiver 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.

Application Execution

|

7

Lifecycles of Two Interacting Applications
This example illustrates the lifecycles of two processes, P1 and P2, that interact in a
typical way (Figure 1-4). P1 is a client application that invokes a Service in P2, a server
application. The client process, P1, starts when it is triggered by a broadcasted Intent.
At startup, the process starts both a BroadcastReceiver and the Application instance.
After a while, an Activity is started, and during all of this time, P1 has the highest
possible process rank: Foreground.

Figure 1-4. Client application starts Service in other process
The Activity offloads work to a Service that runs in process P2, which starts the
Service and the associated Application instance. Therefore, the application has split
the work into two different processes. The P1 Activity can terminate while the P2
Service keeps running.
Once all components have finished—the user has navigated back from the Activity in
P1, and the Service in P2 is asked by some other process or the runtime to stop—both
processes are ranked as empty, making them plausible candidates for termination by
the system when it requires resources.
A detailed list of the process ranks during the execution appears in Table 1-1.

8

|

Chapter 1: Android Components and the Need for Multiprocessing

Table 1-1. Process rank transitions
Application state

P1 process rank

P2 process rank

P1 starts with BroadcastReceiver entry point

Foreground

N/A

P1 starts Activity

Foreground

N/A

P1 starts Service entry point in P2

Foreground

Foreground

P1 Activity is destroyed

Empty

Service

P2 Service is stopped

Empty

Empty

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 ter‐
minated. 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 simulta‐
neously, 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 opera‐
tions must run synchronously, whereas partitioned operations can run asynchronous‐
ly. 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 asyn‐
chronous execution. One approach is to split application execution into several pro‐
cesses, 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 asyn‐
chronous execution. Multiple processes may still be a valid design, but that decision
should be independent of performance. To achieve higher throughput and better per‐
formance, 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
Structuring Applications for Performance

|

9

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

What Is a Long Task?
There is no fixed definition of a long task or a clear indication when a task should execute
on a background thread, but as soon as a user perceives a lagging UI—for example, slow
button feedback and stuttering animations—it is a signal that the task is too long to run
on the UI thread. Typically, animations are a lot more sensitive to competing tasks on
the UI thread than button clicks, because the human brain is a bit vague about when a
screen touch actually happened. Hence, let us do some coarse reasoning with animations
as the most demanding use case.
Animations are updated in an event loop where every event updates the animation with
one frame, i.e., one drawing cycle. The more drawing cycles that can be executed per
time frame, the better the animation is perceived. If the goal is to do 60 drawing cycles
per second—a.k.a. frames per second (fps)—every frame has to render within 16 ms. If

1. Also known as the main thread, but throughout this book we stick to the convention of calling it the “UI
thread.”

10

|

Chapter 1: Android Components and the Need for Multiprocessing

another task is running on the UI thread simultaneously, both the drawing cycle and
the secondary task have to finish within 16 ms to avoid a stuttering animation. Conse‐
quently, a task may require less than 16 ms execution time and still be considered long.
The example and calculations are coarse and meant as an indication of how an appli‐
cation’s responsiveness can be affected not only by network connections that last for
several seconds, but also tasks that at first glance look harmless. Bottlenecks in your
application can hide anywhere.

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 con‐
tained 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 op‐
erations should be kept off the UI thread and executed on other threads.

Summary

|

11