В « Понимании параллелизма на Android» Используя HaMeR , мы поговорили об основах инфраструктуры HaMeR ( Handler , Message и Runnable ). Мы рассмотрели его варианты, а также когда и как его использовать.
Сегодня мы создадим простое приложение для изучения изученных концепций. С практическим подходом мы увидим, как применять различные возможности HaMeR в управлении параллелизмом на Android.
1. Образец заявки
Давайте приступим к работе и Runnable несколько Runnable и отправим объекты Message в примере приложения. Чтобы сделать его максимально простым, мы рассмотрим только самые интересные части. Все файлы ресурсов и стандартные вызовы активности здесь будут игнорироваться. Поэтому я настоятельно советую вам проверить исходный код примера приложения с его обширными комментариями.

Приложение будет состоять из:
- Два действия, одно для
Runnableдругое дляMessage - Два объекта
HandlerThread:-
WorkerThreadдля приема и обработки вызовов из пользовательского интерфейса -
CounterThreadдля приемаMessageотWorkerThread
-
- Некоторые служебные классы (для сохранения объектов во время изменений конфигурации и для макета)
2. Проводка и получение Runnables
Давайте начнем экспериментировать с Handler.post(Runnable) и его вариациями, которые добавляют runnable в MessageQueue связанный с потоком. Мы создадим действие с именем RunnableActivity , которое будет взаимодействовать с фоновым потоком с именем WorkerThread .
RunnableActivity создает фоновый поток с именем WorkerThread , передавая Handler и WorkerThread.Callback качестве параметров. Действие может WorkerThread для асинхронной загрузки растрового изображения и показа тоста в определенное время. Результаты задач, выполненных рабочим потоком, передаются в RunnableActivity запускаемыми объектами, размещенными в Handler полученном WorkerThread .
2.1 Подготовка обработчика для RunnableActivity
В RunnableActivity мы создадим Handler для передачи в WorkerThread . uiHandler будет связан с Looper из потока пользовательского интерфейса, так как он uiHandler из этого потока.
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
public class RunnableActivity extends Activity {
// Handler that allows communication between
// the WorkerThread and the Activity
protected Handler uiHandler;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// prepare the UI Handler to send to WorkerThread
uiHandler = new Handler();
}
}
|
2.2 Объявление WorkerThread и его интерфейса обратного вызова
WorkerThread — это фоновый поток, в котором мы будем запускать различные виды задач. Он связывается с пользовательским интерфейсом, используя responseHandler и интерфейс обратного вызова, полученный во время его создания. Ссылки, полученные из действий, имеют WeakReference<> , поскольку действие может быть уничтожено, а ссылка потеряна.
Класс предлагает интерфейс, который может быть реализован с помощью пользовательского интерфейса. Он также расширяет HandlerThread , вспомогательный класс, построенный поверх Thread который уже содержит Looper и MessageQueue . Следовательно, он имеет правильную настройку использовать платформу HaMeR.
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
public class WorkerThread extends HandlerThread {
/**
* Interface to facilitate calls on the UI.
*/
public interface Callback {
void loadImage(Bitmap image);
void showToast(String msg);
}
// This Handler will be responsible only
// for posting Runnables on this Thread
private Handler postHandler;
// Handler is received from the MessageActivity and RunnableActivity
// responsible for receiving Runnable calls that will be processed
// on the UI.
private WeakReference<Handler> responseHandler;
// Callback from the UI
// it is a WeakReference because it can be invalidated
// during «configuration changes» and other events
private WeakReference<Callback> callback;
private final String imageAUrl =
«https://pixabay.com/static/uploads/photo/2016/08/05/18/28/mobile-phone-1572901_960_720.jpg»;
/**
* The constructor receives a Handler and a Callback from the UI
* @param responseHandler in charge of posting the Runnable to the UI
* @param callback works together with the responseHandler
* allowing calls directly on the UI
*/
public WorkerThread(Handler responseHandler, Callback callback) {
super(TAG);
this.responseHandler = new WeakReference<>(responseHandler);
this.callback = new WeakReference<>(callback);
}
}
|
2.3 Инициализация WorkerThread
Нам нужно добавить метод в WorkerThread который будет вызываться действиями, которые подготавливают postHandler для использования. Метод необходимо вызывать только после запуска потока.
|
1
2
3
4
5
6
7
8
9
|
public class WorkerThread extends HandlerThread {
/**
* Prepare the postHandler.
* It must be called after the thread has started
*/
public void prepareHandler() {
postHandler = new Handler(getLooper());
}
}
|
В RunnableActivity мы должны реализовать WorkerThread.Callback и инициализировать поток, чтобы его можно было использовать.
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
public class RunnableActivity extends Activity
implements WorkerThread.Callback {
// BackgroundThread responsible for downloading the image
protected WorkerThread workerThread;
/**
* Initialize the {@link WorkerThread} instance
* only if it hasn’t been initialized yet.
*/
public void initWorkerThread(){
if ( workerThread == null ) {
workerThread = new WorkerThread(uiHandler, this);
workerThread.start();
workerThread.prepareHandler();
}
}
/**
* set the image downloaded on bg thread to the imageView
*/
@Override
public void loadImage(Bitmap image) {
myImage.setImageBitmap(image);
}
@Override
public void showToast(final String msg) {
// to be implemented
}
}
|
2.4 Использование Handler.post() в WorkerThread
Метод WorkerThread.downloadWithRunnable( ) загружает растровое изображение и отправляет его в RunnableActivity для отображения в представлении изображения. Он иллюстрирует два основных использования команды Handler.post(Runnable run) :
- Чтобы разрешить потоку публиковать объект Runnable в MessageQueue, связанном с самим собой, когда вызывается
.post()для обработчика, связанного с лупером потока. - Чтобы разрешить связь с другими потоками, когда
.post()вызывается для обработчика, связанного с Looper другого потока.

- Метод
WorkerThread.downloadWithRunnable()RunnableвWorkerThreadс помощьюpostHandler,Handlerсвязанного сLooperWorkThread. - Когда исполняемый файл обрабатывается, он загружает
BitmapвWorkerThread. - После загрузки растрового изображения
responseHandler, обработчик, связанный с потоком пользовательского интерфейса, используется для публикации запускаемого объекта вRunnableActivityсодержащем растровое изображение. - Обрабатываемый объект обрабатывается, а
WorkerThread.Callback.loadImageиспользуется для отображения загруженного изображения вImageView.
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
|
public class WorkerThread extends HandlerThread {
/**
* post a Runnable to the WorkerThread
* Download a bitmap and sends the image
* to the UI {@link RunnableActivity}
* using the {@link #responseHandler} with
* help from the {@link #callback}
*/
public void downloadWithRunnable() {
// post Runnable to WorkerThread
postHandler.post(new Runnable() {
@Override
public void run() {
try {
// sleeps for 2 seconds to emulate long running operation
TimeUnit.SECONDS.sleep(2);
// Download image and sends to UI
downloadImage(imageAUrl);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
/**
* Download a bitmap using its url and
* send to the UI the image downloaded
*/
private void downloadImage(String urlStr){
// Create a connection
HttpURLConnection connection = null;
try {
URL url = new URL(urlStr);
connection = (HttpURLConnection) url.openConnection();
// get the stream from the url
InputStream in = new BufferedInputStream(connection.getInputStream());
final Bitmap bitmap = BitmapFactory.decodeStream(in);
if ( bitmap != null ) {
// send the bitmap downloaded and a feedback to the UI
loadImageOnUI( bitmap );
} else {
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if ( connection != null )
connection.disconnect();
}
}
/**
* sends a Bitmap to the ui
* posting a Runnable to the {@link #responseHandler}
* and using the {@link Callback}
*/
private void loadImageOnUI(final Bitmap image){
Log.d(TAG, «loadImageOnUI(«+image+»)»);
if (checkResponse() ) {
responseHandler.get().post(
new Runnable() {
@Override
public void run() {
callback.get().loadImage(image);
}
}
);
}
}
// verify if responseHandler is available
// if not the Activity is passing by some destruction event
private boolean checkResponse(){
return responseHandler.get() != null;
}
}
|
2.5 Использование Handler.postAtTime() и Activity.runOnUiThread()
The WorkerThread.toastAtTime() планирует задачу, которая будет выполнена в определенное время, выставляя пользователю Toast . Метод иллюстрирует использование Handler.postAtTime() и Activity.runOnUiThread() .
-
Handler.postAtTime(Runnable run, long uptimeMillis)публикует runnable в определенный момент времени. -
Activity.runOnUiThread(Runnable run)использует обработчик пользовательского интерфейса по умолчанию для публикации runnable в главном потоке.
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
|
public class WorkerThread extends HandlerThread {
/**
* show a Toast on the UI.
* schedules the task considering the current time.
* It could be scheduled at any time, we’re
* using 5 seconds to facilitates the debugging
*/
public void toastAtTime(){
Log.d(TAG, «toastAtTime(): current — » + Calendar.getInstance().toString());
// seconds to add on current time
int delaySeconds = 5;
// testing using a real date
Calendar scheduledDate = Calendar.getInstance();
// setting a future date considering the delay in seconds define
// we’re using this approach just to facilitate the testing.
// it could be done using a user defined date also
scheduledDate.set(
scheduledDate.get(Calendar.YEAR),
scheduledDate.get(Calendar.MONTH),
scheduledDate.get(Calendar.DAY_OF_MONTH),
scheduledDate.get(Calendar.HOUR_OF_DAY),
scheduledDate.get(Calendar.MINUTE),
scheduledDate.get(Calendar.SECOND) + delaySeconds
);
Log.d(TAG, «toastAtTime(): scheduling at — » + scheduledDate.toString());
long scheduled = calculateUptimeMillis(scheduledDate);
// posting Runnable at specific time
postHandler.postAtTime(
new Runnable() {
@Override
public void run() {
if ( callback != null ) {
callback.get().showToast(
«Toast called using ‘postAtTime()’.»
);
}
}
}, scheduled);
}
/**
* Calculates the {@link SystemClock#uptimeMillis()} to
* a given Calendar date.
*/
private long calculateUptimeMillis(Calendar calendar){
long time = calendar.getTimeInMillis();
long currentTime = Calendar.getInstance().getTimeInMillis();
long diff = time — currentTime;
return SystemClock.uptimeMillis() + diff;
}
}
|
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
|
public class RunnableActivity extends Activity
implements WorkerThread.Callback {
/**
* Callback from {@link WorkerThread}
* Uses {@link #runOnUiThread(Runnable)} to illustrate
* such method
*/
@Override
public void showToast(final String msg) {
Log.d(TAG, «showToast(«+msg+»)»);
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(getApplicationContext(), msg, Toast.LENGTH_LONG).show();
}
});
}
}
|
3. Отправка сообщений с помощью MessageActivity & WorkerThread
Далее давайте рассмотрим несколько различных способов использования MessageActivity для отправки и обработки объектов Message . MessageActivity создает экземпляр WorkerThread , передавая Handler в качестве параметра. WorkerThread имеет несколько открытых методов с задачами, которые должны вызываться действием для загрузки растрового изображения, загрузки случайного растрового изображения или демонстрации Toast через некоторое время с задержкой. Результаты всех этих операций отправляются обратно в MessageActivity с использованием объектов Message отправляемых responseHandler .
3.1 Подготовка обработчика ответа из MessageActivity
Как и в RunnableActivity , в MessageActivity нам нужно будет создать и инициализировать WorkerThread отправляющий Handler для получения данных из фонового потока. Однако на этот раз мы не будем реализовывать WorkerThread.Callback ; вместо этого мы получим ответы от WorkerThread исключительно от объектов Message .
Поскольку большая часть MessageActivity и RunnableActivity в основном одинакова, мы сосредоточимся только на подготовке uiHandler , которая будет отправлена в WorkerThread для получения сообщений от него.
Сначала давайте предоставим некоторые int ключи, которые будут использоваться в качестве идентификаторов для объектов Message.
|
1
2
3
4
5
6
|
public class MessageActivity extends Activity {
// Message identifier used on Message.what() field
public static final int KEY_MSG_IMAGE = 2;
public static final int KEY_MSG_PROGRESS = 3;
public static final int KEY_MSG_TOAST = 4;
}
|
В реализации MessageHandler нам нужно будет расширить Handler и реализовать метод handleMessage(Message) , где будут обрабатываться все сообщения. Обратите внимание, что мы Message.what для идентификации сообщения, и мы также получаем различные виды данных из Message.obj . Давайте быстро рассмотрим наиболее важные свойства Message прежде чем углубляться в код.
-
Message.what:intидентифицирующийMessage -
Message.arg1: произвольный аргументint -
Message.arg2: произвольный аргументint -
Message.obj:Objectдля хранения различных видов данных
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
public class MessageActivity extends Activity {
/**
* Handler responsible to manage communication
* from the {@link WorkerThread}.
* back to the {@link MessageActivity} and handle
* those Messages
*/
public class MessageHandler extends Handler {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
// handle image
case KEY_MSG_IMAGE:{
Bitmap bmp = (Bitmap) msg.obj;
myImage.setImageBitmap(bmp);
break;
}
// handle progressBar calls
case KEY_MSG_PROGRESS: {
if ( (boolean) msg.obj )
progressBar.setVisibility(View.VISIBLE);
else
progressBar.setVisibility(View.GONE);
break;
}
// handle toast sent with a Message delay
case KEY_MSG_TOAST:{
String msgText = (String)msg.obj;
Toast.makeText(getApplicationContext(), msgText, Toast.LENGTH_LONG ).show();
break;
}
}
}
}
// Handler that allows communication between
// the WorkerThread and the Activity
protected MessageHandler uiHandler;
}
|
3.2 Отправка сообщений с WorkerThread
Теперь вернемся к классу WorkerThread . Мы добавим некоторый код для загрузки определенного растрового изображения, а также код для загрузки случайного. Чтобы выполнить эти задачи, мы отправим объекты Message из WorkerThread себе и отправим результаты обратно в MessageActivity используя точно такую же логику, которая применялась ранее для RunnableActivity .
Сначала нам нужно расширить Handler для обработки загруженных сообщений.
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
|
public class WorkerThread extends HandlerThread {
// send and processes download Messages on the WorkerThread
private HandlerMsgImgDownloader handlerMsgImgDownloader;
/**
* Keys to identify the keys of {@link Message#what}
* from Messages sent by the {@link #handlerMsgImgDownloader}
*/
private final int MSG_DOWNLOAD_IMG = 0;
private final int MSG_DOWNLOAD_RANDOM_IMG = 1;
/**
* Handler responsible for managing the image download
* It send and handle Messages identifying then using
* the {@link Message#what}
* {@link #MSG_DOWNLOAD_IMG} : single image
* {@link #MSG_DOWNLOAD_RANDOM_IMG} : random image
*/
private class HandlerMsgImgDownloader extends Handler {
private HandlerMsgImgDownloader(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message msg) {
showProgressMSG(true);
switch ( msg.what ) {
case MSG_DOWNLOAD_IMG: {
// receives a single url and downloads it
String url = (String) msg.obj;
downloadImageMSG(url);
break;
}
case MSG_DOWNLOAD_RANDOM_IMG: {
// receives a String[] with multiple urls
// downloads a image randomly
String[] urls = (String[]) msg.obj;
Random random = new Random();
String url = urls[random.nextInt(urls.length)];
downloadImageMSG(url);
}
}
showProgressMSG(false);
}
}
}
|
Метод downloadImageMSG(String url) в основном совпадает с методом downloadImage(String url) . Единственное отличие состоит в том, что первый отправляет загруженный растровый рисунок обратно в пользовательский интерфейс, отправляя сообщение с использованием responseHandler .
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
public class WorkerThread extends HandlerThread {
/**
* Download a bitmap using its url and
* display it to the UI.
* The only difference with {@link #downloadImage(String)}
* is that it sends the image back to the UI
* using a Message
*/
private void downloadImageMSG(String urlStr){
// Create a connection
HttpURLConnection connection = null;
try {
URL url = new URL(urlStr);
connection = (HttpURLConnection) url.openConnection();
// get the stream from the url
InputStream in = new BufferedInputStream(connection.getInputStream());
final Bitmap bitmap = BitmapFactory.decodeStream(in);
if ( bitmap != null ) {
// send the bitmap downloaded and a feedback to the UI
loadImageOnUIMSG( bitmap );
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if ( connection != null )
connection.disconnect();
}
}
}
|
loadImageOnUIMSG(Bitmap image) отвечает за отправку сообщения с загруженным растровым изображением в MessageActivity .
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
/**
* sends a Bitmap to the ui
* sending a Message to the {@link #responseHandler}
*/
private void loadImageOnUIMSG(final Bitmap image){
if (checkResponse() ) {
sendMsgToUI(
responseHandler.get().obtainMessage(MessageActivity.KEY_MSG_IMAGE, image)
);
}
}
/**
* Show/Hide progressBar on the UI.
* It uses the {@link #responseHandler} to
* send a Message on the UI
*/
private void showProgressMSG(boolean show){
Log.d(TAG, «showProgressMSG()»);
if ( checkResponse() ) {
sendMsgToUI(
responseHandler.get().obtainMessage(MessageActivity.KEY_MSG_PROGRESS, show)
);
}
}
|
Обратите внимание, что вместо создания объекта Message с нуля, мы используем метод Handler.obtainMessage(int what, Object obj) для извлечения Message из глобального пула, сохраняя некоторые ресурсы. Также важно отметить, что мы вызываем obtainMessage() для responseHandler , получая Message связанное с Looper MessageActivity . Есть два способа получить Message из глобального пула: Message.obtain() и Handler.obtainMessage() .
Единственное, что остается сделать в задаче загрузки образа, — это предоставить методы для отправки Message WorkerThread чтобы начать процесс загрузки. Обратите внимание, что на этот раз мы вызовем Message.obtain(Handler handler, int what, Object obj) для handlerMsgImgDownloader , связывая сообщение с WorkerThread .
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
/**
* sends a Message to the current Thread
* using the {@link #handlerMsgImgDownloader}
* to download a single image.
*/
public void downloadWithMessage(){
Log.d(TAG, «downloadWithMessage()»);
showOperationOnUIMSG(«Sending Message…»);
if ( handlerMsgImgDownloader == null )
handlerMsgImgDownloader = new HandlerMsgImgDownloader(getLooper());
Message message = Message.obtain(handlerMsgImgDownloader, MSG_DOWNLOAD_IMG,imageBUrl);
handlerMsgImgDownloader.sendMessage(message);
}
/**
* sends a Message to the current Thread
* using the {@link #handlerMsgImgDownloader}
* to download a random image.
*/
public void downloadRandomWithMessage(){
Log.d(TAG, «downloadRandomWithMessage()»);
showOperationOnUIMSG(«Sending Message…»);
if ( handlerMsgImgDownloader == null )
handlerMsgImgDownloader = new HandlerMsgImgDownloader(getLooper());
Message message = Message.obtain(handlerMsgImgDownloader, MSG_DOWNLOAD_RANDOM_IMG, imagesUrls);
handlerMsgImgDownloader.sendMessage(message);
}
|
Другая интересная возможность — отправка объектов Message для последующей обработки командой Message.sendMessageDelayed(Message msg, long timeMillis) .
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
/**
* Show a Toast after a delayed time.
*
* send a Message with delayed time on the WorkerThread
* and sends a new Message to {@link MessageActivity}
* with a text after the message is processed
*/
public void startMessageDelay(){
// message delay
long delay = 5000;
String msgText = «Hello from WorkerThread!»;
// Handler responsible for sending Message to WorkerThread
// using Handler.Callback() to avoid the need to extend the Handler class
Handler handler = new Handler(new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
responseHandler.get().sendMessage(
responseHandler.get().obtainMessage(MessageActivity.KEY_MSG_TOAST, msg.obj)
);
return true;
}
});
// sending message
handler.sendMessageDelayed(
handler.obtainMessage(0,msgText),
delay
);
}
|
Мы создали Handler специально для отправки отложенного сообщения. Вместо того чтобы расширять класс Handler , мы пошли по пути создания экземпляра Handler с помощью интерфейса Handler.Callback , для которого мы реализовали метод handleMessage(Message msg) для обработки отложенного Message .
4. Заключение
Вы уже видели достаточно кода, чтобы понять, как применять базовые концепции инфраструктуры HaMeR для управления параллелизмом на Android. Есть некоторые другие интересные особенности финального проекта, хранящиеся на GitHub , и я настоятельно рекомендую вам проверить его.
Наконец, у меня есть несколько последних соображений, которые вы должны иметь в виду:
- Не забывайте учитывать жизненный цикл Android-активности при работе с HaMeR и Threads в целом. В противном случае ваше приложение может завершиться ошибкой, когда поток попытается получить доступ к действиям, которые были уничтожены из-за изменений конфигурации или по другим причинам. Распространенным решением является использование
RetainedFragmentдля хранения потока и заполнения фонового потока ссылкой на действие каждый раз, когда действие уничтожается. Взгляните на решение в финальном проекте на GitHub . - Задачи, выполняемые из-за объектов
RunnableиMessageобрабатываемых вHandlers, не работают асинхронно. Они будут работать синхронно в потоке, связанном с обработчиком. Чтобы сделать его асинхронным, вам нужно создать другой поток, отправить / опубликовать на нем объектMessage/Runnableи получить результаты в соответствующее время.
Как видите, инфраструктура HaMeR имеет множество различных возможностей, и это довольно открытое решение с множеством вариантов управления параллелизмом на Android. Эти характеристики могут быть преимуществами по сравнению с AsyncTask , в зависимости от ваших потребностей. Изучите больше фреймворка и прочитайте документацию, и вы создадите замечательные вещи с ее помощью.
До скорого!
