В « Понимании параллелизма на 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
связанного сLooper
WorkThread
. - Когда исполняемый файл обрабатывается, он загружает
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
, в зависимости от ваших потребностей. Изучите больше фреймворка и прочитайте документацию, и вы создадите замечательные вещи с ее помощью.
До скорого!