Asynchronous programming has become an important topic of discussion in the past few years, especially when using the concurrent processing capabilities available on the most recent mobile hardware.
In recent years, the number of independent processing units (cores) available on the CPU have increased, so to benefit from this new processing power, a new programming model called asynchronous programming has appeared to orchestrate the work between the several independent hardware-processing units available on the device. Asynchronous programming comes to the rescue to solve the problems that could arise from this new processing paradigm.
Android applications, since they mostly run on devices with multiple units of processing, should take advantage of asynchronous programming to scale and improve the application performance when blocking operations, and when CPU-intensive tasks are required.
Android is an open source operating system (OS) based on Linux kernel that was devised in 2003 by Andy Rubin, Nick Sears, Chris White, and Rick Miner, and then acquired by Google in July, 2005.
The Android OS, actually maintained by Google and the Open Handset Alliance, was created to provide an open mobile-device platform for devices with limited resources of computation, memory, and energy.
The platform has been incorporating advanced mobile devices standards, such as NFC and Bluetooth LE, and its scope has grown from a pure smartphone platform to a broader software platform for smart watches, TVs, tablets, and consoles.
The maintainers have been regularly updating the platform with great features and some improvements over minor and major releases since the first release.
The following diagram displays the Android versions over time:
Android software stack (C libraries and Java frameworks), orchestrated by the Android runtime (Dalvik VM, and most recently, ART) was created around the Linux kernel to provide highly interactive user experiences over a well-proven group of technologies.
In each new OS version, a well-defined application interface (API) is provided to the developer in order to create applications around the new features and standards introduced with the release.
The Android application compiled code (bytecode), typically a Java compiled code, runs on a virtual machine based on Dalvik or ART.
The Dalvik VM (DVM) runtime, created by Dan Borstein, was the first runtime for the platform and is a register-based virtual machine that was created to run the Java code efficiently in a constrained runtime with a limited amount of power processing, RAM, and electric power.
Dalvik's creators claim that the DVM is, on an average, around 30% more efficient than the standard Java VM (Oracle). According to Bornstein, it requires 30% less instructions and 35 % less coding units.
Clearly, Google has gone to great lengths to squeeze every drop of performance out of each mobile device to help developers build responsive applications.
The virtual machine, which runs the Java code compiled and transformed to the dex format over the dx tool, runs on a Linux process with its own memory space and file descriptors. It also manages its own group of threads.
In more advanced architectures, an Android application might run a service in a separate process and communicate over the IPC mechanism, but most of the time, it runs on a single self-contained process.
The dex file and application resources are packed in an Android application package (APK) by the AAPT and installed over Google Play in the end user devices.
Note
The application store distribution model has become extremely popular on the mobile platforms since the launch of the Apple iPhone in 2007.
Since Android 2.2 the DVM comes with a trace-based Just-In-Time (JIT) compilation feature that actively optimizes every time the application runs some short segments of frequently used bytecode called traces.
The generated machine code provides significant performance improvements in the application execution and on the time spent on some intensive CPU tasks, and thereafter, decreases the battery power used.
The ART runtime is a new version of the DVM and was introduced to improve the runtime performance and memory consumption. The new runtime was introduced in Android 4.4 KitKat as an experimental runtime, and since the Android 5.0 Lollipop, it has become the main Android runtime.
This new runtime, making use of the ahead-of-time (AOT) compilation, brings new app-performance optimizations on startup time and application execution. The AOT, as opposed to DVM JIT (Just in Time), compiles the dex files during the installation time using the device dex2oat tool. The compiled code generated from the dex2oat tool generates system-dependent code for the target device and removes the delay introduced by the JIT compilation during each application execution.
The AOT compiler also reduces the number of processor cycles used by the application as it removes the time spent by the JIT compiler to convert the code into machine code, and then uses less battery power to run the application.
One of the drawbacks of the AOT compilation is the larger memory footprint in comparison with the JIT used by DVM.
With the new runtime, some improvements were also introduced in the memory allocation and on Garbage Collection (GC), resulting in a more responsive UI and better application experience.
Basically, the platform runs an instance of DVM/ART for each application, but large optimization of the platform is brought about by the way a new DVM instance is created and managed.
A special process called the Zygote (first life cell in an animal's reproduction)—the process that all the Android applications are based on—is launched when an Android device initially boots.
The Zygote starts up a virtual machine, preloads the core libraries, and initializes various shared structures. It then waits for instructions by listening on a socket.
When a new Android application is launched, the Zygote receives a command to create a virtual machine to run the application on. It does this by forking its pre-warmed VM process and creating a new child process that shares some memory portions with the parent, using a technique called copy-on-write (COW).
The COW technique, available on most Unix systems, only allocates new memory on the child process when the process tries to change the memory cloned from the parent process.
This technique has some fantastic benefits, as listed in the following:
First, the virtual machine and core libraries are already loaded into the memory. Not having to read this significant chunk of data from the filesystem to initialize the virtual machine drastically reduces the startup overhead.
Second, the memory in which these core libraries and common structures reside is shared by the Zygote with all other applications, resulting in saving a lot of memory when the user is running multiple apps.
Android is a multiuser, multitasking system that can run multiple applications in parallel, where all the applications attempt to acquire CPU time to execute its job.
Each application runs independently on an isolated Linux process cloned from the Zygote process, and by default, all the Android components run within the same process with the same name as the application package specified in Android Application Manifest (AAM).
The Linux kernel will fairly allocate small amounts of CPU time for application execution called CPU time slices. This time-slicing approach means that even a single-processor device can appear to be actively working in more than one application at the same time, when in fact, each application is taking very short turns on the CPU.
The Android operating system tries to maintain the application running for as long as possible, but when the available memory is low, it will try to free resources in the system by killing the processes with lower importance first.
This is when process ranking comes into the picture; the Android processes are ranked in the next five categories from the higher priority to the lower priorities:
Foreground process: This is a process that hosts an activity or service that the user is currently interacting with: a service started in the foreground or service running its life cycle callbacks
Visible process: This is a process that hosts a paused activity or service bounded to a visible activity
Service process: This is a process that hosts a service not bound to a visible activity
Background process: This is a process that hosts a non-visible activity; all background processes are sorted over a Least-Recently-Used (LRU) list, therefore, the most recently used processes are the last killed processes when they have the same rank
Empty process: This is a process used to cache inactive Android components and to improve any component startup time
When the system reaches a point that it needs to release resources, the processes available to be killed will be sorted, taking into account the process rank, last used processes, and components running.
The Android application always runs under a unique Linux user ID (UID) assigned to the application during the application installation so that the process runs on a sandboxed environment, which by default, isolates your data and code execution from other apps.
In some cases, a user could explicitly be required to share the UID with another application to have access to their data:
USER PID PPID VSIZE RSS PC NAME root 319 1 1537236 31324 S zygote …. u0_a221 5993 319 1731636 41504 S com.whatsapp u0_a96 3018 319 1640252 29540 S com.dropbox.android u0_a255 4892 319 1583828 34552 S com.accuweather.android…
The preceding table that results from running the adb shell ps
command in the computer with Android SDK Table is a list of Android running processes.
The first column shows the user identifier (UID) assigned at the time of installation, the second column is the process ID (PID), the third column shows the parent process ID (PPID) that for Android applications is the Zygote process, and the last column shows the application package.
From this list, we can assure that the WhatsApp application is running under the user ID u0_a221
with the process ID 5993
and the parent process is the Zygote process with the PID 319
.
Within an Android process, there may be many threads of execution. Each thread is a separate sequential flow of control within the overall program—it executes its instructions in order, one after the other, and they also share allocated slices of CPU time managed by the operating system task scheduler.
While the application process is started by the system and prevented from directly interfering with data in the memory address space of other processes, the threads may be started by an application code and can communicate and share data with other threads within the same process. Apart from the shared data that all the threads share in the same process, a thread can use its own memory cache to store its data in its own memory space.
When the application process starts, apart from DVM housekeeping threads, the system creates a thread of execution called main
. This thread, as the name explains, plays a crucial role in the application lifetime as it is the thread that interacts with the Android UI components, updating the state and their look on the device screen.
Moreover, by default, all the Android application components (Activity
, Service
, ContentProvider
, and BroadcastsReceiver
) are also executed over the main thread line of execution. The following image shows the lists of threads running inside an application process with the main thread at the top of the list with a unique thread ID (TID) assigned by the system:
The main thread, also known as UI Thread, is also the thread where your UI event handling occurs, so to keep your application as responsible as possible, you should:
The following diagram displays the main interactions and components involved in the Looper
line of execution thread:
The UI/Main thread, which has a Looper
facility attached to it, holds a queue of messages (MessageQueue
) with some unit of work to be executed sequentially.
When a message is ready to be processed on the queue, the Looper Thread pops the message from the queue and forwards it synchronously to the target handler specified on the message.
When the target Handler
finishes its work with the current message, the Looper
thread will be ready to process the next message available on the queue. Hence, if the Handler
spent a noticeable amount of time processing the message, it will prevent Looper
from processing other pending messages.
For example, when we write the code in an onCreate()
method in the Activity
class, it will be executed on the main thread. Likewise, when we attach listeners to user-interface components to handle taps and other user-input gestures, the listener callback executes on the main thread.
For apps that do little I/O or processing, such as applications that don't do complex math calculations, don't use the network to implement features, or don't use filesystem resources, this single thread model is fine. However, if we need to perform CPU-intensive calculations, read or write files from permanent storage, or talk to a web service, any further events that arrive while we're doing this work will be blocked until we're finished.
As you can imagine, if the main thread is busy with a heavy calculation or reading data from a network socket, it cannot immediately respond to user input such as a tap or swipe.
An application that doesn't respond quickly to user interaction will feel unresponsive—anything more than a couple of hundred milliseconds delay is noticeable. This is such a pernicious problem that the Android platform protects users from applications that do too much on the main thread.
Note
If an app does not respond to user input within five seconds, the user will see the Application Not Responding (ANR) dialog and will be offered the option to quit the application.
The following screenshot shows a typical Android ANR dialog:
Android works hard to synchronize the user interface redraws with the hardware-refresh rate. This means that it aims to redraw at the rate of 60 frames per second—that's just 16.67 ms per frame. If we do work on the main thread that takes anywhere near 16 ms, we risk affecting the frame rate, resulting in jank—stuttering animations, jerky scrolling, and so on.
Ideally, of course, we don't want to drop a single frame. Jank, unresponsiveness, and especially the ANR, offer a very poor user experience, which translates into bad reviews and unpopular applications. A rule to live by when building Android applications is: do not block the main thread!
Note
Android provides a helpful strict mode setting in Developer Options on each device, which will flash on the screen when applications perform long-running operations on the main thread.
Further protection was added to the platform in Honeycomb (API level 11) with the introduction of a new Exception
class, NetworkOnMainThreadException
, a subclass of RuntimeException
that is thrown if the system detects network activity initiated on the main thread.
Ideally then, we may want to offload any long-running operations from the main thread so that they can be handled in the background by another thread, and the main thread can continue to process user-interface updates smoothly and respond in a timely fashion to user interactions.
The typical time-consuming tasks that should be handled on a background thread include the following:
Network communications
Input and output file operations on the local filesystem
Image and video processing
Complex math calculations
Text processing
Data encoding and decoding
For this to be useful, we must be able to coordinate the work and safely pass data between cooperating threads—especially between background threads and the main thread, and it is exactly to solve this problem that asynchronous programming is used.
Let's get started with the synchronous versus asynchronous diagram:
The preceding example graphically shows the main differences between the two models of processing. On the left-hand side, the data download task occurs on the main thread, keeping the thread busy until the download data is finished. So if the user interacts with the UI and generates an event such as a touch event, the application will suffer a lag or will become unresponsive if the download task takes a substantial amount of time to finish.
On the right-hand side, the asynchronous model will hand over the download data task to another background thread, keeping the main thread available to process any event coming from the UI interaction. When the downloaded data is available, the background task could post the result to the main thread if the data handling needs to update any UI state.
When we use an asynchronous model to program our application, the Android OS will also take advantage of additional CPU cores available in the most recent devices to execute multiple background threads at the same time and increase the application's power efficiency.
As explained before, in order to achieve a scalable application in a multicore device environment, the Android developer should be capable of creating concurrent lines of execution that combine and aggregate data from multiple resources.
The Android SDK, as it is based on a subset of Java SDK, derived from the Apache Harmony project, provides access to low-level concurrency constructs such as java.lang.Thread
, java.lang.Runnable
, and the synchronized
and volatile
keywords.
These constructs are the most basic building blocks to achieve concurrency and parallelism, and all the high-level asynchronous constructs are created around these building blocks.
The most basic one, java.lang.Thread
, is the class that is mostly used and is the construct that creates a new independent line of execution in a Java program:
public class MyThread extends Thread { public void run() { Log.d("Generic", "My Android Thread is running ..."); } }
In the preceding code, we subclassed java.lang.Thread
to create our own independent line of execution. When Thread
is started, the run method will be called automatically and it will print the message on the Android log:
MyThread myThread = new MyThread(); myTread.start();
At this time, we will create an instance of our MyThread
, and when we start it in the second line, the system creates a thread inside the process and executes the run()
method.
Other helpful thread-related methods include the following:
Thread.currentThread()
: This retrieves the current running instance of the threadThread.sleep(time)
: This pauses the current thread from execution for the given period of timeThread.getName()
andThread.getId()
: These get the name and TID, respectively so that they can be useful for debugging purposesThread.isAlive()
: This checks whether the thread is currently running or it has already finished its jobThread.join()
: This blocks the current thread and waits until the accessed thread finishes its execution or dies
The Runnable
interface, which is another building block that comes from the Java API, is an interface defined to specify and encapsulate code that is intended to be executed by a Java thread instance or any other class that handles this Runnable
:
package java.lang; public interface Runnable { public abstract void run(); }
In the following code, we basically created the Runnable
subclass so that it implements the run()
method and can be passed and executed by a thread:
public class MyRunnable implements Runnable { public void run(){ Log.d("Generic","Running in the Thread " + Thread.currentThread().getId()); // Do your work here ... } }
Now our Runnable
subclass can be passed to Thread
and is executed independently in the concurrent line of execution:
Thread thread = new Thread(new MyRunnable()); thread.start();
While starting new threads is easy, concurrency is actually a very difficult thing to do. Concurrent software faces many issues that fall into two broad categories: correctness (producing consistent and correct results) and liveness (making progress towards completion). Thread
creation could also cause some performance overhead, and too many threads can reduce the performance, as the OS will have switch between these lines of execution.
A common example of a correctness problem occurs when two threads need to modify the value of the same variable based on its current value. Let's consider that we have a myInt
integer variable with the current value of 2.
In order to increment myInt
, we first need to read its current value and then add 1 to it. In a single-threaded world, the two increments would happen in a strict sequence—we will read the initial value 2, add 1 to it, set the new value back to the variable, and then repeat the sequence. After the two increments, myInt
holds the value 4.
In a multithreaded environment, we will run into potential timing issues. It is possible that two threads trying to increment the variable would both read the same initial value 2, add 1 to it, and set the result (in both cases, 3) back to the variable:
int myInt = 2; ... public class MyThread extends Thread { public void run() { super.run(); myInt++; } } ... Thread t1 = new MyThread(); Thread t2 = new MyThread(); t1.start(); t2.start();
Both threads behaved correctly in their localized view of the world, but in terms of the overall program, we will clearly have a correctness problem; 2 + 2 should not equal 3! This kind of timing issue is known as a race condition.
A common solution to correctness problems, such as race conditions, is mutual exclusion—preventing multiple threads from accessing certain resources at the same time. Typically, this is achieved by ensuring that threads acquire an exclusive lock before reading or updating shared data.
To achieve this correctness, we can make use of the synchronized
construct to solve the correctness issue on the following piece of code:
Object lock = new Object(); public class MyThread extends Thread { public void run() { super.run(); synchronized(lock) { myInt++; } } }
In the preceding code, we used the intrinsic lock available in each Java object to create a mutually exclusive scope of code that will enforce that the increment sentence will work properly and will not suffer from correctness issues as explained previously. When one of the threads gets access to the protected scope, it is said that the thread acquired the lock, and after the thread gets out of the protected scope, it releases the lock that could be acquired by another thread.
Another way to create mutually exclusive scopes is to create a method with a synchronized method:
int myInt = 2;
synchronized void increment(){
myInt++;
}
...
public class IncrementThread extends Thread {
public void run() {
super.run();
increment();
}
}
The synchronized method will use the object-intrinsic lock, where myInt
is defined to create a mutually exclusive zone so IncrementThread
, incrementing myInt
through the increment()
, will prevent any thread interference and memory consistency errors.
Liveness can be thought of as the ability of the application to do useful work and make progress towards goals. Liveness problems tend to be an unfortunate side effect of the solution to the correctness problems.
Both properties should be achieved in a proper concurrent program, notwithstanding the correctness is concerned with making progress in a program preventing a deadlock, livelock, or starvation from happening, and the correctness is concerned with making consistent and correct results.
Note
Deadlock is a situation where two or more threads are unable to proceed because each is waiting for the others to do something. Livelock is a situation where two or more threads continuously change their states in response to the changes in the other threads without doing any useful work.
By locking access to data or system resources, it is possible to create bottlenecks where many threads are contending to access a single lock, leading to potentially significant delays.
Worse, where multiple locks are used, it is possible to create a situation where no thread can make progress because each requires exclusive access to a lock that another thread currently owns—a situation known as a deadlock.
Thread coordination is an important topic in concurrent programming, especially when we want to perform the following tasks:
Synchronize access of threads to shared resources or shared memory:
Shared database, files, system services, instance/class variables, or queues
Coordinate work and execution within a group of threads:
Parallel execution, pipeline executions, inter-dependent tasks, and so on
When we want to coordinate thread efforts to achieve a goal, we should try to avoid waiting or polling mechanisms that keep the CPU busy while we wait for an event in another thread.
The following example shows us a small loop where we will continuously occupy the CPU while we wait for a certain state change to happen:
while(!readyToProcess) { // do nothing .. busy waiting wastes processor time. }
To overcome the coordination issue, and to implement our own constructs, we should use some low-level signals or messaging mechanisms to communicate between threads and coordinate the interaction.
In Java, every object has the wait()
, notify()
, and notifyAll()
methods that provide low-level mechanisms to send thread signals between a group of threads and put a thread in a waiting state until a condition is met.
This mechanism, also known as monitor or guard, is a design pattern commonly used in another languages and it ensures that only one thread can enter a given section of code at any given time with an ability to wait until a condition happens.
This design pattern, in comparison with our previous example, delivers a better and efficient CPU-cycle management while waiting for any particular situation to happen on another thread, and is generally used in situations where we need to coordinate work between different lines of execution.
In the following code example, we are going to explain how to use this construct to create a basic multithreaded Logger
with 10 threads that will wait in the monitor section until a message is pushed (condition) by any other thread in the application.
The Logger
, which is responsible for logging on to the output, has a queue with a maximum of 20 positions to store the new logging text messages:
public class Logger { LinkedList<String> queue = new LinkedList<String>(); private final int MAX_QUEUE_SIZE = 20; private final int MAX_THREAD_COUNT = 10;
In the next code, we will create a Runnable
unit of work that runs indefinitely and retrieves a message from the queue to print the message on the Android log.
After that, we will create and start 10 threads that are going to execute the Runnable
unit of work task
:
public void start() { // Creates the Loop as a Runnable Runnable task = new Runnable() { @Override public void run() { while(true) { String message = pullMessage(); Log.d(Thread.currentThread(). getName(),message); // Do another processing } } }; // Create a Group of Threads for processing for(int i=0; i< MAX_THREAD_COUNT; i++){ new Thread(task).start(); } }
The pullMessage()
, which is a synchorized
method, runs a mutual exclusion and puts the thread in the waiting state when it reaches the wait()
method. All the created threads will stay in this state until another thread calls notifyAll()
:
// Pulls a message from the queue // Only returns when a new message is retrieves // from the queue. private synchronized String pullMessage(){ while (queue.isEmpty()) { try { wait(); } catch (InterruptedException e) { ... } } return queue.pop(); } // Push a new message to the tail of the queue if // the queue has available positions public synchronized void pushMessage(String logMsg) { if ( queue.size()< MAX_QUEUE_SIZE ) { queue.push(logMsg); notifyAll(); } }
When any thread is in the waiting state, it releases the lock temporarily and gives a chance to another thread to enter the mutual exclusion to push new messages or enter into the wait state.
In the following snippet, we will first create the Logger
instance and then we will call the start method to start the working threads and we will push 10 messages into a queue of work to be processed.
When the pushMessage()
method is invoked, a new logging message is inserted at the end of the queue and notifiyAll()
is invoked to notify all the available threads.
As the pullMessage()
method runs in a mutual-exclusion (synchronized) zone, only one thread will wake up and return from the pull
method. Once pullMessage()
returns, the logging message is printed:
Logger logger =new Logger(); logger.start(); for ( int i=0; i< 10 ; i++) { ... logger.pushMessage(date+" : "+"Log Message #"+i); }
In the following console output, we have an example of the output that this code will generate and the logging messages are processed by any available threads in an ordered manner:
D/Thread-108(23915): <Date>: Log Message #0 D/Thread-109(23915): ...: Log Message #1 D/Thread-110(23915): ...: Log Message #2 D/Thread-111(23915): ...: Log Message #3
This kind of low-level construct can also be used to control shared resources (polling) to manage background execution (parallelism) and control thread pools.
Other Java concurrent constructs provided by java.util.concurrent
, which are also available on Android SDK are as follows:
Lock objects (
java.util.concurrent
): They implement locking behaviors with a higher level idiom.Executors: These are high-level APIs to launch and manage a group of thread executions (
ThreadPool
, and so on).Concurrent collections: These are the collections where the methods that change the collection are protected from synchronization issues.
Synchronizers: These are high-level constructs that coordinate and control thread execution (Semaphore, Cyclic Barrier, and so on).
Atomic variables (
java.util.concurrent.atomic
): These are classes that provide thread-safe operations on single variables. One example of it isAtomicInteger
that could be used in our example to solve the correctness issue.
Some Android-specific constructs use these classes as basic building blocks to implement their concurrent behavior, although they could be used by a developer to build custom concurrent constructs to solve a specific use case.
The Executor
framework is another framework available on java.util.concurrent
that provides an interface to submit Runnable
tasks, decoupling the task submission from the way the task will run:
public interface Executor { void execute(Runnable command); }
Each Executor
, which implements the interface that we defined earlier, can manage the asynchronous resources, such as thread creation destruction and caching, and task queueing in a variety of ways to achieve the perfect behavior to a specific use case.
The java.util.concurrent
comes with a group of implementations available out of the box that cover most generic use cases, as follows:
Executors.newCachedThreadPool()
: This is a thread poll that could grow and reuse previously created threadsExecutors.newFixedThreadPool
(nThreads
): This is a thread pool with a fixed number of threads and a message queue for store workExecutors.newSingleThreadPool()
: This is similar to newFixedThreadPool, but with only one working thread
To run a task on Executor
, the developer has to invoke execute()
by passing Runnable
as an argument:
public class MyRunnable implements Runnable { public void run() { Log.d("Generic", "Running From Thread " + Thread.currentThread().getId()); // Your Long Running Computation Task } } public void startWorking(){ Executor executor = Executors.newFixedThreadPool(5); for ( int i=0; i < 20; i++ ) { executor.execute(new MyRunnable()); } }
In the preceding code, we created ThreadPool
over the factory methods with a fixed number of five threads ready to process work.
After the ExecutorService
instance creation, new Runnable
tasks are posted for asynchronous processing.
When a new unit of work is submitted, a thread that is free to work is chosen to handle the task; but when all the threads are occupied, Runnable
will wait in a local queue until a thread is ready to work.
A typical Android application is composed of the following four main building blocks:
android.app.Activity
android.app.Service
android.content.BroadcastReceiver
android.content.ContentProvider
The Activity, Service, and BroadcastReceiver
are activated explicitly or implicitly over an asynchronous message called Intent
.
Each of these building blocks have their own life cycle, so they could be exposed to different concurrency issues if an asynchronous architecture is used to offload work from the main thread.
The Activity building block has a tight connection with a presentation layer because it's the entity that manages the UI view over a defined tree of fragments and views that display information and respond to user interactions.
Android applications are typically composed of one or more subclasses of android.app.Activity
. An Activity instance has a very well-defined lifecycle that the system manages through the execution of lifecycle method callbacks, all of which are executed on the main thread.
To keep the application responsive and reactive, and the activity transition smooth, the developer should understand the nature of each Activity lifecycle callback.
The most important callbacks on the Activity lifecycle are as follows:
onCreate()
: At this state, Activity is not visible, but it is here where all the private Activity resources (views and data) are created. The long and intensive computations should be done asynchronously in order to decrease the time when the users don't get a visual feedback during an Activity transition.onStart()
: This is the callback called when the UI is visible, but not able to interact on the screen. Any lag here could make the user angry as any touch event generated at this stage is going to be missed by the system.onResume()
: This is the callback called when Activity is going to be in the foreground and at an interactable state.onPause()
: This is a callback called when Activity is going to the background and is not visible. Computations should end quickly as the next Activity will not resume until this method ends.onStop()
: This is a callback called when Activity is no longer visible, but can be restarted.onDestroy()
: This is a callback called when the Activity instance is going to be destroyed in the background. All the resources and references that belong to this instance have to be released.
An Activity instance that is completed should be eligible for garbage collection, but background threads that refer to Activity or part of its view hierarchy can prevent garbage collection and create a memory leak.
Similarly, it is easy to waste CPU cycles (and battery life) by continuing to do background work when the result can never be displayed as Activity is completed.
Finally, the Android platform is free at any time to kill processes that are not the user's current focus. This means that if we have long-running operations to complete, we need some way of letting the system know not to kill our process yet.
All of this complicates the do-not-block–the-main-thread rule as we need to worry about canceling background work in a timely fashion or decoupling it from the Activity lifecycle where appropriate.
The other Android-specific problem lies not in what you can do with the UI thread, but in what you cannot do.
This is because the user interface toolkit is not thread-safe, that is, accessing it from multiple threads may cause correctness problems. In fact, the user interface toolkit protects itself from potential problems by actively denying access to user interface components from threads other than the one that originally created these components.
If the system detects this, it will instantly notify the application by throwing CalledFromWrongThreadException
.
The final challenge then lies in safely synchronizing background threads with the main thread so that the main thread can update the user interface with the results of the background work.
If the developer has access to an Activity
instance, the runOnUiThread
instance method can be used to update the UI from a background thread.
The method accepts a Runnable
object like the one used to create an execution task for a thread:
public final void runOnUiThread (Runnable)
In the following example, we are going to use this facility to publish the result from a synonym search that was processed by a background thread.
To accomplish the goal during the OnCreate
activity callback, we will set up onClickListener
to run searchTask
on a created thread:
// Get the Views references Button search = (Button) findViewById(R.id.searchBut); final EditText word = (EditText) findViewById(R.id.wordEt); // When the User clicks on the search button // it searches for a synonym search.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { // Runnable that Searchs for the synonym and // and updates the UI. Runnable searchTask = new Runnable() { @Override public void run() { // Retrieves the synonym for the word String result = searchSynomim( word.getText().toString()); // Runs the Runnable SetSynonymResult // to publish the result on the UI Thread runOnUiThread(new SetSynonymResult(result)); } }; // Executes the search synonym an independent thread Thread thread = new Thread(searchTask); Thread.start(); } });
When the user clicks on the Search button, we will create a Runnable
anonymous class that searches for the word typed in R.id.wordEt EditText
and starts the thread to execute Runnable
.
When the search completes, we will create an instance of Runnable
SetSynonymResult
to publish the result back on the synonym TextView
over the UI thread:
class SetSynonymResult implements Runnable { final String synonym; SetSynonymResult(String synonym){ this.synonym = synonym; } public void run() { TextView tv = (TextView)findViewById(R.id.synonymTv); tv.setText(this.synonym); } };
This technique is sometime not the most convenient one, especially when we don't have access to an Activity instance; therefore, in the following chapters, we are going to discuss simpler and cleaner techniques to update the UI from a background computing task.
These are the Android entities that run in the background, which usually perform tasks in the name
application that does not require any user interaction.
Service
, by default, runs in the main thread of the application process. It does not create its own thread, so if your Service
is going to do any blocking operation, such as downloading an image, play a video, or access a network API, the user should design a strategy to offload the time of the work from the main thread into another thread.
As Service
could have its own concurrent strategy, it should also take into account that, like Activity, it should update the UI over the main thread, so a strategy to post back the results from the background into the main loop is imperative.
In the Android services domain, the way the service is started distinguishes the nature of Service
into the following two groups:
Started services: This is the service that is started by
startService()
that can run definitively even if the component that started it was destroyed. A started service does not interact directly with the component that started it.Bound services: This service exists while at least one Android component is bounded to it by calling
bindService()
. It provides a two-way (client-server) communication channel for communication between components.
When we implement a started service, any application component is able to start it when it invokes the startService(Intent)
method. Once the system receives startService(Intent)
and the service is not yet started, the system calls onCreate()
and then onStartCommand()
with the arguments encapsulated on an Intent object. If the Service
already exists, only onStartCommand()
is invoked.
The callbacks used by a started service are as follows:
// Called every time a component starts the Service // The service arguments are passed over the intent int onStartCommand(Intent intent, int flags, int startId) // Used to initialize your Service resources void onCreate() // Used to release your Service resources void onDestroy()
In the onStartCommand()
callback, once a long computing task is required to handle the service request, a handover to the background threads should be explicitly implemented and coordinated in order to avoid an undesired ANR:
int onStartCommand (Intent intent, int flags, int startId){ // Hand over the request processing to your // background tasks ... }
When the service is done, and it needs to publish results to the UI, a proper technique to communicate with the main thread should be used.
A bound service generally used when a strong interaction between an Android component and a service is required.
When the service runs on the same process, the interaction between the Android component (client) and the bound service (server) is always provided by a Binder
class returned on onBind()
. With the Binder
instance on hand, the client has access to the service's public methods, so when any component invokes the bound service public methods, the component should be aware of the following:
When a long running operation is expected to take place during the method invocation, the invocation must occur in a separate thread
If the method is invoked in a separated thread, and the service wants to update the UI, the service must run the update over the main thread:
public class MyService extends Service { // Binder given to clients private final IBinder mBinder = new MyBinder(); public class MyBinder extends Binder { MyService getService() { // Return this instance of MyService // so clients can call public methods return MyService.this; } } @Override public IBinder onBind(Intent intent) { return mBinder; } /** Method for clients */ public int myPublicMethod() { // } ...
When an Android service runs on its own process, it runs in an independent process, with its own address space, making the communication with the main process UI thread harder to implement the following:
<service android:name="SynonymService" android:process=":my_synonnym_search_proc" android:icon="@drawable/icon" android:label="@string/service_name" > </service>
To implement a service in a different process, we need to use an inter-process communication (IPC) technique to send messages between your application and the service.
Note
IPC is the activity of sharing data across multiple processes, usually using a well-defined communication protocol. It typically has a process that acts as the client and a process that acts as the server.
There are two technologies available on the Android SDK to implement this, as follows:
AIDL (Android Interface Definition Language): This allows you to define an interface over a set of primitive types. It allows you create multithreaded processing services, but it adds other levels of complexity to your implementation. This is only recommended to advanced programmers.
Messenger: This is a simple interface that creates a queue of work for you in the service side. This executes all the tasks sequentially on single thread managed by a
Handler
.
We haven't given more details about these techniques yet; however, an example of this construct is going to be presented later in a more advanced chapter where all the concepts involved are more mature.
This building block is a component that subscribes to system and application events and is notified when these events occur on the system. The broadcast receivers are defined statically in the application manifest or dynamically via the Context.registerReceiver()
.
The broadcast received is activated though the onReceive()
callback and this method runs on the main thread, blocking another Android component from running if we try to execute time-consuming tasks.
Once onReceive()
finishes, the system considers the object inactive and can release the resources attached to this instance and recycle the whole object. This behavior has a tremendous impact on what we can do inside, because if we hand over some processing to a concurrent thread, the resources that belong to BroadcastReceiver
might be recycled and are no longer available, or in an extreme case, the process could be killed if there were no important components running on it.
The good news is that the Android platform provides specific constructs to address the concurrency general issues and to solve the specific problems presented by Android.
There are constructs that allow us to defer tasks to run later on the main thread, communicate easily between cooperating threads, and issue work to the managed pools of worker threads and reintegrate the results back in the main thread.
There are solutions to the constraints of the Activity lifecycle, both for medium-term operations that closely involve the user interface and longer-term work that must be completed even if the user leaves the application.
While some of these constructs were only introduced with newer releases of the Android platform, they are available through the support libraries, and with a few exceptions, the examples in this book target devices that run API level 8 (Android 2.2) and higher versions.
In this chapter, we took a detailed look at the available Android runtimes, Android processes, and thread models.
We then introduced the concurrent issues that we would cope with when we try to implement robust concurrent programs.
Finally, we listed the basic concurrent building blocks available on the SDK to design concurrent programs.
In the next chapter, we'll take a look at some Android-specific low-level building blocks on which the other concurrency mechanisms are built: Handler
, Looper
, and LooperThread
.