Handler. Трохи теорії. Наочний приклад використання
В Android до потоку (thread) може бути прив'язана черга повідомлень. Ми можемо поміщати туди повідомлення, а система буде за чергою стежити і відправляти повідомлення на обробку. При цьому ми можемо вказати, щоб повідомлення пішло на обробку не відразу, а через певну кількість часу.
Handler - це механізм, який дає змогу працювати з чергою повідомлень. Він прив'язаний до конкретного потоку (thread) і працює з його чергою. Handler вміє поміщати повідомлення в чергу. При цьому він ставить самого себе як одержувача цього повідомлення. І коли настає час, система дістає повідомлення з черги і відправляє його адресату (тобто в Handler) на обробку.
Handler дає нам дві цікаві та корисні можливості:
- реалізувати відкладене за часом виконання коду
- виконання коду не у своєму потоці
У цьому уроці зробимо невеликий додаток. Він буде емулювати будь-яку довгу дію, наприклад закачування файлів і в TextView виводитиме кількість закачаних файлів. За допомогою цього прикладу ми побачимо, навіщо може бути потрібен Handler.
strings.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">`Handler`</string>
<string name="start">Start</string>
<string name="test">Test</string>
</resources>
main.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical">
<ProgressBar
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:indeterminate="true">
</ProgressBar>
<TextView
android:id="@+id/tvInfo"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="">
</TextView>
<Button
android:id="@+id/btnStart"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="onclick"
android:text="@string/start">
</Button>
<Button
android:id="@+id/btnTest"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="onclick"
android:text="@string/test">
</Button>
</LinearLayout>
ProgressBar у нас буде крутитися завжди. Пізніше стане зрозуміло, навіщо. TextView - для виведення інформації про закачування файлів. Кнопка Start стартуватиме закачування. Кнопка Test буде просто виводити в лог слово test.
Кодим MainActivity.java:
public class MainActivity extends Activity {
final String LOG_TAG = "myLogs";
Handler h;
TextView tvInfo;
Button btnStart;
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
tvInfo = (TextView) findViewById(R.id.tvInfo);
}
public void onclick(View v) {
switch (v.getId()) {
case R.id.btnStart:
for (int i = 1; i <= 10; i++) {
// довгий процес
downloadFile();
// оновлюємо TextView
tvInfo.setText("Закачано файлов: " + i);
// пишемо лог
Log.d(LOG_TAG, "Закачано файлов: " + i);
}
break;
case R.id.btnTest:
Log.d(LOG_TAG, "test");
break;
default:
break;
}
}
void downloadFile() {
// пауза - 1 секунда
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
В обробнику кнопки Start ми організуємо цикл для закачування файлів. У кожній ітерації циклу виконуємо метод downloadFile (який емулює закачування файлу), оновлюємо TextView і пишемо в лог інформацію про те, що кількість закачаних файлів змінилася. Разом у нас мають закачатися 10 файлів і після закачування кожного з них лог і екран мають показувати, скільки файлів уже закачано.
Після натискання кнопки Test - просто виводимо в лог повідомлення.
downloadFile - емулює закачування файлу, це просто пауза в одну секунду.
Ми бачимо, що ProgressBar крутиться. Натискаємо на кнопку Test, у логах з'являється test. Усе гаразд, додаток відгукується на наші дії.
Якщо ми натиснемо кнопку Start, то маємо спостерігати, як оновлюється TextView і пишеться лог після закачування чергового файлу. Але на ділі буде трохи не так. Наш застосунок просто "зависне" і перестане реагувати на натискання. Зупиниться ProgressBar, не буде оновлюватися TextView, і не буде натискатися кнопка Test. Тобто UI (екран) для нас стане недоступним. І тільки за логами буде зрозуміло, що застосунок насправді працює і файли закачуються. Натисніть Start і переконайтеся.
Екран "висить", а логи йдуть. Щойно всі 10 файлів буде закачано, застосунок оживе і знову почне реагувати на ваші натискання.
А все чому? Тому що робота екрана забезпечується основним потоком програми. А ми зайняли весь цей основний потік під свої потреби. У нашому випадку, начебто під закачування файлів. І щойно ми закінчили закачувати файли - потік звільнився, і екран став знову оновлюватися і реагувати на натискання.
Тут треба зрозуміти одну річ - основний потік програми відповідає за екран. Цей потік у жодному разі не можна вантажити чимось важким - екран просто перестає оновлюватися і реагувати на натискання. Якщо у вас є довгограючі завдання - їх треба винести в окремий потік. Спробуємо це зробити.
Перепишемо onclick:
public void onclick(View v) {
switch (v.getId()) {
case R.id.btnStart:
Thread t = new Thread(new Runnable() {
public void run() {
for (int i = 1; i <= 10; i++) {
// довгий процес
downloadFile();
// оновлюємо TextView
tvInfo.setText("Закачано файлов: " + i);
// пишемо лог
Log.d(LOG_TAG, "i = " + i);
}
}
});
t.start();
break;
case R.id.btnTest:
Log.d(LOG_TAG, "test");
break;
default:
break;
}
}
Тобто ми просто поміщаємо весь цикл у новий потік і запускаємо його. Тепер закачування файлів піде в цьому новому потоці. А основний потік буде не зайнятий і зможе без проблем промальовувати екран і реагувати на натискання. А отже, ми бачитимемо зміну TextView після кожного закачаного файлу і ProgressBar, що крутиться. І, взагалі, зможемо повноцінно взаємодіяти з додатком. Здавалося б, ось воно щастя :)
Все збережемо і запустимо додаток. Тиснемо Start.

Додаток вилетів із помилкою. Дивимося лог помилок у LogCat. Там є рядки:
android.view.ViewRoot$CalledFromWrongThreadException: Лише оригінальний потік, який створив ієрархію подання, може торкатися її подань.
і
at com.arakviel.develop.p0801Handler.MainActivity$1.run(MainActivity.java:37)
Дивимося, що за код у нас у MainActivity.java в 37-му рядку:
tvInfo.setText("Закачано файлів: " + i);
При спробі виконати цей код (не в основному потоці) ми отримали помилку "Only the original thread that created a view hierarchy can touch its views" ("Тільки оригінальний потік, який створив view-компоненти, може взаємодіяти з ними"). Тобто робота з view-компонентами доступна тільки з основного потоку. А нові потоки, які ми створюємо, не мають доступу до елементів екрана.
Тобто з одного боку, не можна завантажувати основний потік важкими завданнями, щоб не "вішався" екран. З іншого боку - нові потоки, створені для виконання важких завдань, не мають доступу до екрана, і ми не зможемо з них показати користувачеві, що наше важке завдання якось рухається.
Тут нам допоможе Handler. План такий:
- ми створюємо в основному потоці
Handler - у потоці закачування файлів звертаємося до
Handlerі з його допомогою поміщаємо в чергу повідомлення для нього ж самого - система бере це повідомлення, бачить, що адресат -
Handler, і відправляє повідомлення на опрацювання вHandler Handler, отримавши повідомлення, оновитьTextView
Чим це відрізняється від нашої попередньої спроби оновити TextView з іншого потоку? Тим, що Handler був створений в основному потоці, і обробляти повідомлення, що надходять до нього, він буде в основному потоці, а отже, матиме доступ до екранних компонентів і зможе змінити текст у TextView. Отримати доступ до Handler з будь-якого іншого потоку ми зможемо без проблем, оскільки основний потік монополізує тільки доступ до UI. А елементи класів (у нашому випадку це Handler у MainActivity.java) доступні в будь-яких потоках. Таким чином Handler виступить як «міст» між потоками.
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
tvInfo = (TextView) findViewById(R.id.tvInfo);
btnStart = (Button) findViewById(R.id.btnStart);
h = new Handler() {
public void handleMessage(android.os.Message msg) {
// оновлюємо TextView
tvInfo.setText("Закачано файлов: " + msg.what);
if (msg.what == 10) btnStart.setEnabled(true);
};
};
}
Тут ми створюємо Handler і в ньому реалізуємо метод обробки повідомлень handleMessage. Ми витягуємо з повідомлення атрибут what - це кількість закачаних файлів. Якщо вона дорівнює 10, тобто всі файли закачані, ми активуємо кнопку Start. (кількість закачаних файлів ми самі кладемо в повідомлення - зараз побачите, як)
Метод onclick перепишемо так:
public void onclick(View v) {
switch (v.getId()) {
case R.id.btnStart:
btnStart.setEnabled(false);
Thread t = new Thread(new Runnable() {
public void run() {
for (int i = 1; i <= 10; i++) {
// довгий процес
downloadFile();
h.sendEmptyMessage(i);
// пишемо лог
Log.d(LOG_TAG, "i = " + i);
}
}
});
t.start();
break;
case R.id.btnTest:
Log.d(LOG_TAG, "test");
break;
default:
break;
}
}
Ми деактивуємо кнопку Start перед запуском закачування файлів. Це просто захист, щоб не можна було запустити кілька закачувань одночасно. А в процесі закачування, після кожного закачаного файлу, відправляємо (sendEmptyMessage) для Handler повідомлення з кількістю вже закачаних файлів. Handler це повідомлення прийме, витягне з нього кількість файлів і оновить TextView.
Усе зберігаємо і запускаємо додаток. Тиснемо кнопку Start.
Кнопка Start стала неактивною, оскільки ми її самі вимкнули. А TextView оновлюється, ProgressBar крутиться і кнопка Test натискається. Тобто і закачування файлів триває, і застосунок продовжує працювати без проблем, відображаючи статус закачування.
Коли всі файли закачаються, кнопка Start знову стане активною.
Підсумуємо все вищесказане.
- Спочатку ми спробували вантажити додаток важким завданням в основному потоці. Це призвело до того, що ми втратили екран - він перестав оновлюватися і відповідати на натискання. Сталося це тому, що за екран відповідає основний потік програми, а він був сильно завантажений.
- Ми створили окремий потік і виконали весь важкий код там. І це б спрацювало, але нам треба було оновлювати екран у процесі роботи. А з не основного потоку доступу до екрану немає. Екран доступний тільки з основного потоку.
- Ми створили
Handlerв основному потоці. А з нового потоку відправляли дляHandlerповідомлення, щоб він нам оновлював екран. У підсумкуHandlerдопоміг нам оновлювати екран не з основного потоку.
Простий приклад
Як ми пам'ятаємо, Handler дає змогу класти в чергу повідомлення і сам же вміє їх обробляти. Фішка тут у тому, що покласти повідомлення він може з одного потоку, а прочитати з іншого.
Повідомлення може містити в собі атрибути. Розглянемо найпростіший варіант, атрибут what.
Напишемо простий додаток-клієнт. Він, начебто, буде підключатися до сервера, виконувати якусь роботу і відключатися. На екрані ми спостерігатимемо, як змінюється статус підключення і як крутиться ProgressBar під час підключення.
При змінах стану підключення ми будемо відправляти повідомлення для Handler. А в атрибут what будемо класти поточний статус. Handler під час обробки повідомлення прочитає з нього what і виконає будь-які дії.
strings.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">HandlerSimpleMessage</string>
<string name="connect">Connect</string>
</resources>
main.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical">
<Button
android:id="@+id/btnConnect"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="onclick"
android:text="@string/connect">
</Button>
<TextView
android:id="@+id/tvStatus"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="">
</TextView>
<ProgressBar
android:id="@+id/pbConnect"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:indeterminate="true"
android:visibility="gone">
</ProgressBar>
</LinearLayout>
Кнопка для старту підключення, TextView для виведення інформації про статус підключення і ProgressBar, що працює в процесі підключення.
MainActivity.java:
public class MainActivity extends Activity {
final String LOG_TAG = "myLogs";
final int STATUS_NONE = 0;
final int STATUS_CONNECTING = 1;
final int STATUS_CONNECTED = 2;
Handler h;
TextView tvStatus;
ProgressBar pbConnect;
Button btnConnect;
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
tvStatus = (TextView) findViewById(R.id.tvStatus);
pbConnect = (ProgressBar) findViewById(R.id.pbConnect);
btnConnect = (Button) findViewById(R.id.btnConnect);
h = new Handler() {
public void handleMessage(android.os.Message msg) {
switch (msg.what) {
case STATUS_NONE:
btnConnect.setEnabled(true);
tvStatus.setText("Not connected");
break;
case STATUS_CONNECTING:
btnConnect.setEnabled(false);
pbConnect.setVisibility(View.VISIBLE);
tvStatus.setText("Connecting");
break;
case STATUS_CONNECTED:
pbConnect.setVisibility(View.GONE);
tvStatus.setText("Connected");
break;
}
};
};
h.sendEmptyMessage(STATUS_NONE);
}
public void onclick(View v) {
Thread t = new Thread(new Runnable() {
public void run() {
try {
// встановлюємо підключення
h.sendEmptyMessage(STATUS_CONNECTING);
TimeUnit.SECONDS.sleep(2);
// встановлено
h.sendEmptyMessage(STATUS_CONNECTED);
// виконується якась робота
TimeUnit.SECONDS.sleep(3);
// розриваємо підключення
h.sendEmptyMessage(STATUS_NONE);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
}
}
STATUS_NONE, STATUS_CONNECTING, STATUS_CONNECTED - це константи статусу. Їх будемо передавати в повідомленні, в атрибуті what. Зрозуміло, назви і значення цих констант довільні і взяті з голови. Ви можете придумати і використовувати свої.
В onCreate ми створюємо Handler і реалізуємо його метод handleMessage. Цей метод відповідає за обробку повідомлень, які призначені для цього Handler. Відповідно на вхід методу йде повідомлення - Message. Ми читаємо атрибут what і залежно від статусу підключення змінюємо екран:
STATUS_NONE- немає підключення. Кнопка підключення активна,TextViewвідображає статус підключення.STATUS_CONNECTING- у процесі підключення. Кнопка підключення неактивна, показуємоProgressBar,TextViewвідображає статус підключення.STATUS_CONNECTED- підключено. ПриховуємоProgressBar,TextViewвідображає статус підключення.
В onCreate після створення Handler ми відразу відправляємо йому повідомлення зі статусом STATUS_NONE. Для цього ми використовуємо метод sendEmptyMessage. У цьому методі створюється повідомлення, заповнюється його атрибут what (значенням, яке ми передаємо в sendEmptyMessage), встановлюється Handler як адресат і повідомлення надсилається в чергу.
У методі onclick ми створюємо і запускаємо новий потік. У ньому ми, за допомогою sleep, емулюємо процес під'єднання до сервера, виконання роботи і вимкнення. І, в міру виконання дій, відправляємо повідомлення зі статусами для Handler. Тобто виходить, що після натискання на кнопку Connect статус змінюється на STATUS_CONNECTING, дві секунди йде підключення, статус змінюється на STATUS_CONNECTED, 3 секунди виконуються дії і статус змінюється на STATUS_NONE. Давайте перевіримо.
Усе збережемо і запустимо застосунок.
Тобто для простого оновлення статусу з нового потоку нам вистачило атрибута what. Але крім what повідомлення може мати ще кілька атрибутів.
Приклад з більш змістовними повідомленнями
Ми використовували метод sendEmptyMessage. Цей метод сам створював повідомлення Message, заповнював його атрибут what і відправляв у чергу. Крім what у повідомлення є ще атрибути arg1 і arg2 типу int, і obj типу Object. У цьому уроці ми самі створюватимемо повідомлення, заповнюватимемо атрибути та відправлятимемо.
Створимо застосунок, який буде підключатися до сервера, запитувати кількість файлів, готових для завантаження, емулювати завантаження і відображати на екрані перебіг дій, використовуючи горизонтальний ProgressBar і TextView.
strings.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">HandlerAdvMessage</string>
<string name="connect">Connect</string>
</resources>
main.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical">
<Button
android:id="@+id/btnConnect"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="onclick"
android:text="@string/connect">
</Button>
<TextView
android:id="@+id/tvStatus"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="">
</TextView>
<ProgressBar
android:id="@+id/pbDownload"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone">
</ProgressBar>
</LinearLayout>
MainActivity.java:
public class MainActivity extends Activity {
final String LOG_TAG = "myLogs";
final int STATUS_NONE = 0; // нет подключения
final int STATUS_CONNECTING = 1; // подключаемся
final int STATUS_CONNECTED = 2; // подключено
final int STATUS_DOWNLOAD_START = 3; // загрузка началась
final int STATUS_DOWNLOAD_FILE = 4; // файл загружен
final int STATUS_DOWNLOAD_END = 5; // загрузка закончена
final int STATUS_DOWNLOAD_NONE = 6; // нет файлов для загрузки
Handler h;
TextView tvStatus;
ProgressBar pbDownload;
Button btnConnect;
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
tvStatus = (TextView) findViewById(R.id.tvStatus);
pbDownload = (ProgressBar) findViewById(R.id.pbDownload);
btnConnect = (Button) findViewById(R.id.btnConnect);
h = new Handler() {
public void handleMessage(android.os.Message msg) {
switch (msg.what) {
case STATUS_NONE:
btnConnect.setEnabled(true);
tvStatus.setText("Not connected");
pbDownload.setVisibility(View.GONE);
break;
case STATUS_CONNECTING:
btnConnect.setEnabled(false);
tvStatus.setText("Connecting");
break;
case STATUS_CONNECTED:
tvStatus.setText("Connected");
break;
case STATUS_DOWNLOAD_START:
tvStatus.setText("Start download " + msg.arg1 + " files");
pbDownload.setMax(msg.arg1);
pbDownload.setProgress(0);
pbDownload.setVisibility(View.VISIBLE);
break;
case STATUS_DOWNLOAD_FILE:
tvStatus.setText("Downloading. Left " + msg.arg2 + " files");
pbDownload.setProgress(msg.arg1);
saveFile((byte[]) msg.obj);
break;
case STATUS_DOWNLOAD_END:
tvStatus.setText("Download complete!");
break;
case STATUS_DOWNLOAD_NONE:
tvStatus.setText("No files for download");
break;
}
};
};
h.sendEmptyMessage(STATUS_NONE);
}
public void onclick(View v) {
Thread t = new Thread(new Runnable() {
Message msg;
byte[] file;
Random rand = new Random();
public void run() {
try {
// встановлюємо підключення
h.sendEmptyMessage(STATUS_CONNECTING);
TimeUnit.SECONDS.sleep(1);
// підключення встановлено
h.sendEmptyMessage(STATUS_CONNECTED);
// визначаємо кількість файлів
TimeUnit.SECONDS.sleep(1);
int filesCount = rand.nextInt(5);
if (filesCount == 0) {
// повідомляємо, що файлів для завантаження немає
h.sendEmptyMessage(STATUS_DOWNLOAD_NONE);
// і відключаємося
TimeUnit.MILLISECONDS.sleep(1500);
h.sendEmptyMessage(STATUS_NONE);
return;
}
// завантаження починається
// створюємо повідомлення, з інформацією про кількість файлів
msg = h.obtainMessage(STATUS_DOWNLOAD_START, filesCount, 0);
// відправляємо
h.sendMessage(msg);
for (int i = 1; i <= filesCount; i++) {
// завантажується файл
file = downloadFile();
// створюємо повідомлення з інформацією про порядковий номер
// файла,
// кількістю тих, що залишилися, і самим файлом
msg = h.obtainMessage(STATUS_DOWNLOAD_FILE, i,
filesCount - i, file);
// відправляємо
h.sendMessage(msg);
}
// завантаження завершено
h.sendEmptyMessage(STATUS_DOWNLOAD_END);
// відключаємося
TimeUnit.MILLISECONDS.sleep(1500);
h.sendEmptyMessage(STATUS_NONE);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
}
byte[] downloadFile() throws InterruptedException {
TimeUnit.SECONDS.sleep(2);
return new byte[1024];
}
void saveFile(byte[] file) {
}
}
В onCreate ми створюємо Handler і в його методі обробки (handleMessage) прописуємо всю логіку зміни екрана залежно від повідомлень, які надходять. Не буду детально це розписувати, там усе просто - змінюємо текст, вмикаємо/вимикаємо кнопку, показуємо/прикриваємо ProgressBar, змінюємо значення ProgressBar. З цікавого тут варто зазначити, що читаємо ми цього разу не тільки what, а й інші атрибути повідомлення - arg1, arg2, obj. А як вони заповнюються, побачимо далі.
В onclick створюємо новий потік для завантаження файлів. Встановлюємо підключення, отримуємо кількість готових для завантаження файлів. Якщо файлів для завантаження немає, надсилаємо відповідне повідомлення в Handler і відключаємося. Якщо ж файли є, ми створюємо повідомлення Message за допомогою методу getMessage(int what, int arg1, int arg2). Він приймає на вхід атрибути what, arg1 і arg2. У what ми кладемо статус, в arg1 - кількість файлів, arg2 - не потрібен, там просто нуль.
Далі починаємо завантаження. Після завантаження кожного файлу ми створюємо повідомлення Message за допомогою методу getMessage(int what, int arg1, int arg2, Object obj), заповнюємо його атрибути: what - статус, arg1 - порядковий номер файлу, arg2 - к-ть файлів, що залишилися, obj - файл. І відправляємо.
По завершенню завантаження відправляємо відповідне повідомлення і відключаємося.
downloadFile- емулює завантаження файлу. чекає дві секунди і повертає масив із 1024 байтів.saveFile- метод збереження файлу на диск. Просто заглушка. Нічого не робить.
Все зберігаємо і запускаємо. Тиснемо Connect.
Використовуючи різні атрибути крім what, ми змогли передати в основний потік і використовувати там більш різноманітні дані.
Ми створюємо повідомлення за допомогою різних реалізацій методу getMessage. А чому б не створювати безпосередньо об'єкт Message за допомогою його конструкторів? У принципі можна, але офіційний хелп рекомендує користуватися методами getMessage, тому що це ефективніше і швидше. У цьому випадку повідомлення дістається з глобального пулу повідомлень, а не створюється з нуля.
Тут ви можете подивитися всі реалізації методу getMessage для формування повідомлень і використовувати той, який підходить для ситуації. Вони різняться різними комбінаціями вхідних параметрів.