Lab 4. Threading in Android

Objectives

  • Learn to execute code on multiple threads in an Android app
  • Understand and practice the basic way of multi-threading in Android using handlers, runnables and thread pools.
  • Learn why AsyncTasks are not recommended to be used.

Threading in Android

When an application is launched, the system creates a process for it (or more, if it declares services in separate processes) and a main thread (UI thread). As discussed in the previous labs, any code you write in your components (unless they are Intent Services) is run on the main thread. But an app might need to perform processing in worker threads, for example, in case of networking or database operations. If we block the UI thread while performing computational intensive code or waiting for certain events, the system will display an ANR (Application Not Responding) dialog.

For working with threads and scheduling tasks we can leverage language-specific APIs for threading, Android specific components or many third-party libraries. In this lab we will explore some of the thread management mechanisms from the first two categories and also provide recommendations for third-party libraries.

:!: Calling methods from the Android UI API must be done on the UI thread. For example, if you want to show a toast from a worker-thread, use runOnUiThread.

Regarding the relationship between the threading components and the lifecycle of the UI components (activities and fragments), certain threading objects can be tied to the UI lifecycle and other not. The IntentServices we discussed in Lab3 are not tied to the activities or fragments lifecycle, but the AsyncTasks (Android specific threading components), are terminated when the UI components are destroyed.

In Lab 3 we presented IntentServices which also run on separate threads. The mechanisms we will focus on this lab, some of them you might be familiar with from Java:

  • Runnables and Handlers
  • ThreadPoolExecutor
  • JobScheduler

We will also briefly discuss AsyncTasks, which are not recommended and will be deprecated in the next Android release (R).

Because this course's scope does not include learning a new language, we will not address in this lab asynchronous programming using the coroutines provided in Kotlin. Nonetheless, if you want to learn and code in Kotlin, or are already familiar with it, feel free to use them in your project. Useful video overview: Understand Kotlin Coroutines on Android (Google I/O'19).

Other threading mechanisms you can use and are not covered in this lab:

  • Reactive programming using RxJava or RxKotlin - do not need to explicitly create threads, just choose a scheduler
    • proceed with caution! (completely changes the design of the app, choose it depending on what you need to do, e.g. it's very useful if your app relies heavily on event-driven logic)
  • androidannotations - not to be confused with the annotations offered by the Android API

When the IDE generates override methods it sometimes adds the @WorkerThread annotation. This does not mean that the method will be implicitly run on a worker thread! It is a recommendation or a warning that the developers must explicitly use them on worker thread. They have no effect at runtime, only the linters may check them.

Handlers and Runnables

In Android we can create new threads by creating objects of type Thread and Runnable, as we do in Java programming. Since we also need communication with the Android's UI components, the SDK offers a specific type of objects: Handler.

Using runnables

In the following example we trigger a new thread everytime we press a button:

findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Runnable runnable = new Runnable() {
                    @Override
                    public void run() {
                        Log.d(TAG, "In a different thread " + Thread.currentThread());
                    }
                };
                Thread thread = new Thread(runnable);
                thread.start();
            }
        });
 

What happens if we want to show a Toast from the run() method?

public void run() {
    Log.d(TAG, "In a different thread " + Thread.currentThread());   Toast.makeText(MainActivity.this, "hello", Toast.LENGTH_SHORT).show();
}

If we run the app and click the button we get a crash caused by a fatal exception:

2019-03-17 12:17:37.370 4008-4057/com.smd.lab4app D/MainActivity: In a different thread Thread[Thread-2,5,main]
2019-03-17 12:17:37.378 4008-4057/com.smd.lab4app E/AndroidRuntime: FATAL EXCEPTION: Thread-2
    Process: com.smd.lab4app, PID: 4008
    java.lang.RuntimeException: Can't create handler inside thread that has not called Looper.prepare()

So, what are our options now?

We can post work on the UI thread using the View's post(Runnable) method. In our example, since Button extends View, we can:

button = findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
 
            Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    Log.d(TAG, "In a different thread " + Thread.currentThread());
 
                    button.post(new Runnable() {
                        @Override
                        public void run() {
                            Toast.makeText(MainActivity.this, "hello", Toast.LENGTH_SHORT).show();
                        }
                    });
                }
            };
            Thread thread = new Thread(runnable);
            thread.start();
        }
    });

Using Handlers

We need a messaging mechanism between threads, and Android offers Handlers. These are closely connected to other objects needed for messaging:

  • Looper - provided to a Handler, keeps a thread alive, loops through MessageQueue and sends Messages to the corresponding Handler to process it
  • Message - the message to be sent to a Handler
  • MessageQueue - the messages dispatached to a Looper, the developer doesn't interact directly with it, most interaction is through the Handler class
  • HandlerThread - a thread that has a looper

The Handler is attached to a thread, which means that the code that handles the received messages is run on that thread. If we want to update the UI, we attach the Handler to the main thread obtained with Looper.getMainLooper(), as in the next example:

Creating a Handler
handler = new Handler(Looper.getMainLooper()) {
    @Override
    public void handleMessage(Message inputMessage) {
        String messageText = (String) inputMessage.obj;
        Toast.makeText(MainActivity.this, messageText, Toast.LENGTH_SHORT).show();
    }
};
Sending a message from runnable to handler
button.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
 
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
            Log.d(TAG, "In a different thread " + Thread.currentThread());
 
            Message message = handler.obtainMessage();
            message.obj = "hello";
            message.sendToTarget();
            }
        };
        Thread thread = new Thread(runnable);
                thread.start();
    }
});

From the Handler docs: There are two main uses for a Handler:

  1. to schedule messages and runnables to be executed at some point in the future
  2. to enqueue an action to be performed on a different thread than your own.

Schedule tasks on a Handler

Using the Handler's postDelayed method we can schedule runnables to be executed in the future, on that thread. In this way we can also implement timers in Android.

One of the common scenarios is to handle timeouts for the tasks or operations necessary in the app. For instance, if we initiate a connect to a remote device, we can schedule a postDelayed runnable that will inform us when it timeouts. If the task ends in time, it also has to remove the timeout runnable it scheduled. In the next example we show how to create and schedule a runnable.

HandlerThread initialization
private HandlerThread timeoutHandlerThread;
private Handler timeoutHandler;
 
private void postDelayedExample() {
    timeoutHandlerThread = new HandlerThread(TAG);
    timeoutHandlerThread.start();
    timeoutHandler = new Handler(timeoutHandlerThread.getLooper());
}
Scheduling a runnable
public void startTask() {
    //create the task we need to track the timeout for
    //....
    scheduleTimeout(getTimeoutRunnable(), TIMEOUT_MS);
}
 
public Runnable getTimeoutRunnable() {
    return new Runnable() {
        @Override
        public void run() {
            Log.d(TAG, "Timeout - task too more than " + TIMEOUT_MS/1000 + "seconds");
        }
    };
}
 
// called when the task starts
public void scheduleTimeout(Runnable timeoutRunnable, long millis) {
    removeTimeout(); //remove previous timeouts
    timeoutHandler.postDelayed(timeoutRunnable, millis);
}
 
// this is called when the tasks ends either in success or in error
public void removeTimeout() {
    timeoutHandler.removeCallbacksAndMessages(null);
}

Scheduling work can be done also using Android’s JobScheduler. In this case we can trigger tasks not only based on a specific time, but also by certain conditions (battery, network) defined when creating the job (see JobInfo.Builder). The scheduled job is handled by a JobService, which, being a service, runs on the UI main thread. Therefore we need to explicitly start a thread if we want that task to be executed on a worked thread, asynchronously, like in this example

If we need to only schedule a task to happen at a specific time, regardless of other conditions we can also use AlarmManager.

ThreadPoolExecutor

In the previous examples we executed the runnables on a thread that we created and started explicitly. One other way of running them is to use the Executor objects provided by the java concurrent package. We submit runnables to Executors, which, depending on their implementation, execute them on worker threads. One type of executor of interest in this lab is ThreadPoolExecutor. If we need to parallelize several operations (E.g. download tens of photos, decode them etc) we can use ThreadPoolExecutor as in this tutorial.

When using a ThreadPoolExecutor, we need to specify not only its size parameters but also a BlockingQueue in which we add the runnables that need to be executed (our tasks).

AsyncTask

Using AsyncTasks we can run logic in background on a thread and pass results back to the UI (main thread).

They have been widely used in the Android world for more than 10 years. When searching for tutorials on asynchronous programming/threading in Android you might encounter a lot of examples with AsyncTask, but bear in mind their deprecation and if you really want to use them, use them only for very short operations.

The AsyncTask is an abstract task and developers need to extend it and implement at least its only abstract method, doInBackground, which is always run on the dedicated thread. Inside it we can call publishProgress if we want to send updates to the UI while the task is still running. The rest of its methods are run on the main UI thread and usually called implicitly: onPreExecute, onProgressUpdate, onPostExecute. You should not call them from your UI components, just implement the ones you need, they will be called by the system. The official documentation recommends the AsyncTasks only for short operations (a few seconds).

AsyncTasks offer the advantage of easily running code on a different thread and also send the results to the UI, but they are not recommended for operations longer than a few seconds, because of their dependence on UI.

  • The UI can be blocked in some cases because pre and post execute methods are run on the UI thread: e.g. when one task takes more than expected, and other tasks are triggered from the UI, the second task’s preExecute method is blocked until the first task’s postExecute method is called.
  • Can lead to memory leaks if the activity that started the task was destroyed in the meantime.

Official reason for deprecation of the AsyncTask: AsyncTask was intended to enable proper and easy use of the UI thread. However, the most common use case was for integrating into UI, and that would cause Context leaks, missed callbacks, or crashes on configuration changes. It also has inconsistent behavior on different versions of the platform, swallows exceptions from doInBackground, and does not provide much utility over using Executors directly. (snippet from AsyncTask javadoc)

Synchronization mechanisms

  • Synchronized - java keyword
    • Can be used for methods or specific blocks of code
    • Implemented as a monitor
    • Only one thread can execute the synchronized region, the others are suspended until they can enter the locked region.
  • Synchronized collections: e.g. ConcurrentHashMap
  • Atomic variables: e.g. AtomicInteger

Tasks

Task 0

Create an Android Studio project and name it `Lab 4`.

Task 1 (6p)

Create an application that mocks the download of a file from an URL and will inform the user about the progress and final result. All of this will be done on a background thread using an a runnable and handler.

  • the mock download is performed in a runnable
  • the main activity is informed by the runnable about the mock progress using a handler

Hint: Check the code examples from the lab.

Task 2 (4p)

Mock the download of several files (at least 10) in parallel on multiple threads using Runnables and ThreadPoolExecutor (documentation, example).

  • For the “mock” download, each thread should sleep a random number between 100 and 200 ms.
  • The UI should update in real time a text view with the following content:
     Image-3: done
     Image-2: done
     Image-5: done
      …
      
  • To show multiple line you can add the android:inputType=“text” to the TextView
smd/laboratoare/04.txt · Last modified: 2021/05/12 17:46 by adriana.draghici
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0