Handler. Відкладені повідомлення, видалення з черги, Handler.Callback

Ми відправляли повідомлення в чергу, а система відразу ж діставала їх і перенаправляла в Handler на обробку. Але ми можемо налаштувати повідомлення так, щоб система відправила його на обробку не відразу, а із затримкою. Для цього використовуються методи sendEmptyMessageDelayed (якщо використовуєте тільки what) і sendMessageDelayed (повне повідомлення). У них ми можемо вказати паузу в мілісекундах. Система вичекає цю паузу і тільки потім відправить повідомлення в Handler.

Якщо раптом помістили таке відкладене повідомлення в чергу, а потім вирішили, що воно не повинно піти на обробку, то його можна з черги видалити. Для цього використовується метод removeMessages.

Ми створювали свій Handler, і в його методі handleMessage кодили свій алгоритм обробки повідомлень. Крім цього способу Handler також може використовувати для обробки повідомлень об'єкт, що реалізує інтерфейс Handler.Callback. В інтерфейсу всього один метод handleMessage - у ньому і прописуємо всю логіку обробки повідомлень.

public class MainActivity extends Activity {
 
  final String LOG_TAG = "myLogs";
 
  Handler h;
 
  Handler.Callback hc = new Handler.Callback() {
    public boolean handleMessage(Message msg) {
      Log.d(LOG_TAG, "what = " + msg.what);
      return false;
    }
  };

  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);
 
    h = new Handler(hc);
    sendMessages();
  }
 
  void sendMessages() {
    Log.d(LOG_TAG, "send messages");
    h.sendEmptyMessageDelayed(1, 1000);
    h.sendEmptyMessageDelayed(2, 2000);
    h.sendEmptyMessageDelayed(3, 3000);
  }
}

Ми створюємо об'єкт hc типу Handler.Callback. У нього є метод handleMessage, в якому ми будемо обробляти повідомлення. У нашому випадку просто читаємо атрибут what і виводимо значення в лог.

В onCreate створюємо handler, використовуючи конструктор Handler(Handler.Callback callback). На вхід передаємо створений раніше hc. І тепер Handler оброблятиме повідомлення не сам, а передоручить це об'єкту hc. Далі ми виконуємо метод sendMessages , який кладе три повідомлення в чергу повідомлень. Для цього використовується метод sendEmptyMessageDelayed. Це аналог знайомого нам методу sendEmptyMessage з минулого уроку. Він теж заповнює в повідомленні тільки атрибут what, але при цьому він дає змогу вказати затримку в обробці повідомлення. Тобто повідомлення буде витягнуто з черги і відправлено на обробку через зазначену кількість мілісекунд.

Отже, ми поміщаємо три повідомлення:

  1. what = 1, обробка через 1000 мс.
  2. what = 2, обробка через 2000 мс.
  3. what = 3, обробка через 3000 мс.

Зауважу, що відлік затримки починається після поміщення в чергу, а не після обробки попереднього повідомлення. Тобто ці повідомлення по відношенню одне до одного спрацюють з інтервалом в одну секунду.

Усе збережемо і запустимо застосунок. У логах один за одним з'являтимуться записи:

10:21:07.759: D/myLogs(332): send messages
10:21:08.786: D/myLogs(332): what = 1
10:21:09.765: D/myLogs(332): what = 2
10:21:10.776: D/myLogs(332): what = 3

Зверніть увагу на час цих записів. Перший спрацьовує через 1000 мс після поміщення в чергу (send messages), другий - через дві секунди, третій - через три.

Тепер спробуємо видалити повідомлення з черги. Перепишемо метод sendMessages:

void sendMessages() {
  h.sendEmptyMessageDelayed(1, 1000);
  h.sendEmptyMessageDelayed(2, 2000);
  h.sendEmptyMessageDelayed(3, 3000);
  h.removeMessages(2);
}

Використовуємо метод removeMessages, у якому вказуємо значення атрибута what. Цей метод знаходить у черзі повідомлення з атрибутом what, що дорівнює 2, і видаляє його з черги.

Усе зберігаємо, запускаємо додаток. Дивимося лог:

10:24:49.916: D/myLogs(434): send messages
10:24:50.927: D/myLogs(434): what = 1
10:24:52.948: D/myLogs(434): what = 3

Як бачимо, повідомлення з what = 2 не спрацювало.

А якщо буде кілька повідомлень з однаковим значенням what? Система видалить перше-ліпше чи всі?

Перевіримо. Перепишемо sendMessages:

void sendMessages() {
  Log.d(LOG_TAG, "send messages");
  h.sendEmptyMessageDelayed(1, 1000);
  h.sendEmptyMessageDelayed(2, 2000);
  h.sendEmptyMessageDelayed(3, 3000);
  h.sendEmptyMessageDelayed(2, 4000);
  h.sendEmptyMessageDelayed(5, 5000);
  h.sendEmptyMessageDelayed(2, 6000);
  h.sendEmptyMessageDelayed(7, 7000);
  h.removeMessages(2);
}

Будемо поміщати в чергу купу повідомлень. З них кілька з what = 2. Перевіримо, які видалить система.

Запускаємо додаток і дивимося лог:

10:29:23.297: D/myLogs(467): send messages
10:29:24.372: D/myLogs(467): what = 1
10:29:26.307: D/myLogs(467): what = 3
10:29:28.364: D/myLogs(467): what = 5
10:29:30.332: D/myLogs(467): what = 7

Усі повідомлення з what = 2 було видалено. Не забувайте це. А то захочете видалити одне останнє повідомлення, а система знайде всі відповідні, які очікують на обробку, і знесе їх.

У методу removeMessages є ще реалізація з використанням obj. Тут усе так само, тільки система шукає для видалення з черги повідомлення із зазначеними атрибутами what і obj.

Якщо хочете запланувати повноцінне повідомлення, а не просто what, то використовуйте метод sendMessageDelayed - на вхід даєте повідомлення і вказуєте затримку обробки.

Є ще методи sendEmptyMessageAtTime і sendMessageAtTime. Вони теж дають змогу вказати затримку обробки. Але ця затримка буде відрахована від часу останнього старту системи, а не від часу поміщення в чергу. Якщо повідомлення виявиться простроченим на момент поміщення в чергу, воно виконується відразу.

Обробка Runnable

Крім обробки повідомлень, ми можемо попросити Handler виконати шматок коду - Runnable. Ми працювали з повідомленнями, які містили атрибути. Ми їх обробляли в Handler і залежно від значень атрибутів виконували ті чи інші дії. Runnable же - це шматок коду, який ми надішлемо замість атрибутів повідомлення, і він буде виконаний у потоці, з яким працює Handler. Нам уже нічого не треба обробляти.

Для відправлення коду в роботу використовується метод post. Як і повідомлення, Runnable може бути виконаний із затримкою (postDelayed), і може бути вилучений з черги (removeCallbacks). Напишемо додаток, який продемонструє всі ці можливості.

strings.xml:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="app_name">HandlerRunnable</string>
    <string name="info">Подробно</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:id="@+id/pbCount"
        style="?android:attr/progressBarStyleHorizontal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="20dp">
    </ProgressBar>
    <CheckBox
        android:id="@+id/chbInfo"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/info">
    </CheckBox>
    <TextView
        android:id="@+id/tvInfo"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text=""
        android:visibility="gone">
    </TextView>
</LinearLayout>

ProgressBar, що відображає поточний прогрес. CheckBox, який вмикатиме відображення додаткової інформації в TextView.

MainActivity.java:

public class MainActivity extends Activity {
 
  ProgressBar pbCount;
  TextView tvInfo;
  CheckBox chbInfo;
  int cnt;
 
  final String LOG_TAG = "myLogs";
  final int max = 100;
 
  Handler h;

  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);
 
    h = new Handler();
 
    pbCount = (ProgressBar) findViewById(R.id.pbCount);
    pbCount.setMax(max);
    pbCount.setProgress(0);
 
    tvInfo = (TextView) findViewById(R.id.tvInfo);
 
    chbInfo = (CheckBox) findViewById(R.id.chbInfo);
    chbInfo.setOnCheckedChangeListener(new OnCheckedChangeListener() {
      public void onCheckedChanged(CompoundButton buttonView,
          boolean isChecked) {
        if (isChecked) {
          tvInfo.setVisibility(View.VISIBLE);
          // показуємо інформацію
          h.post(showInfo);
        } else {
          tvInfo.setVisibility(View.GONE);
          // скасовуємо показ інформації
          h.removeCallbacks(showInfo);
        }
      }
    });
 
    Thread t = new Thread(new Runnable() {
      public void run() {
        try {
          for (cnt = 1; cnt < max; cnt++) {
            TimeUnit.MILLISECONDS.sleep(100);
            // оновлюємо ProgressBar
            h.post(updateProgress);
          }
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
      }
    });
    t.start();
 
  }
 
  // оновлення ProgressBar
  Runnable updateProgress = new Runnable() {
    public void run() {
      pbCount.setProgress(cnt);
    }
  };
 
  // показ інформації
  Runnable showInfo = new Runnable() {
    public void run() {
      Log.d(LOG_TAG, "showInfo");
      tvInfo.setText("Count = " + cnt);
      // планує сам себе через 1000 мсек
      h.postDelayed(showInfo, 1000);
    }
  };
}

В onCreate ми прописуємо обробник для CheckBox. У разі ввімкнення прапорця відображається TextView і в роботу відправляється завдання showInfo. У разі вимкнення прапорця - завдання showInfo видаляється з черги.

Далі в новому потоці емулюємо будь-яку дію - запускаємо лічильник із паузами. У кожній ітерації циклу відправляємо в роботу завдання updateProgress, яке оновлює ProgressBar.

  • updateProgress - код, який оновлює значення ProgressBar.
  • showInfo - код, який оновлює TextView і сам себе планує на виконання через 1000 мсек. Тобто ми вмикаємо CheckBox, showInfo спрацьовує перший раз і саме себе планує на наступний раз. Тобто цей код лежить у черзі повідомлень, обробляється і знову кладе себе туди. Так триває, поки ми явно його не видалимо з черги (removeCallbacks), вимкнувши CheckBox.

Будемо виводити що-небудь у лог із showInfo, щоб побачити, коли він працює, а коли ні.

Ще кілька способів виконання коду в UI-потоці

Ми детально розглянули Handler і побачили, що він вміє. Головна його перевага - це вміння виконувати код в UI-потоці. Існує ще пара способів виконувати Runnable в UI-потоці. Це методи:

  • Activity.runOnUiThread(Runnable)
  • View.post(Runnable)
  • View.postDelayed(Runnable, long)

Перші два схожі і відправляють Runnable на негайну обробку. А третій метод дозволяє вказати затримку виконання Runnable.

main.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/llMain"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical">
    <TextView
        android:id="@+id/tvInfo"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="">
    </TextView>
</LinearLayout>

TextView, яке будемо оновлювати з нового потоку.

public class MainActivity extends Activity {
 
  final String LOG_TAG = "myLogs";
 
  TextView tvInfo;
 
  /** Called when the activity is first created. */
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);
 
    tvInfo = (TextView) findViewById(R.id.tvInfo);
 
    Thread t = new Thread(new Runnable() {
      public void run() {
        try {
          TimeUnit.SECONDS.sleep(2);
          runOnUiThread(runn1);
          TimeUnit.SECONDS.sleep(1);
          tvInfo.postDelayed(runn3, 2000);
          tvInfo.post(runn2);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
      }
    });
    t.start();
  }
 
  Runnable runn1 = new Runnable() {
    public void run() {
      tvInfo.setText("runn1");
    }
  };
 
  Runnable runn2 = new Runnable() {
    public void run() {
      tvInfo.setText("runn2");
    }
  };
 
  Runnable runn3 = new Runnable() {
    public void run() {
      tvInfo.setText("runn3");
    }
  };
}

В onCreate створюємо новий потік. У ньому ми через паузи виконуємо runn1 і runn2, і плануємо runn3 із затримкою у 2000 мсек, використовуючи вищезгадані методи.

runn1, runn2 і runn3 - це просто Runnable, які оновлюють текст у TextView. Вони повинні бути виконані в UI-потоці.

Тим самим, якщо ваші алгоритми не особливо складні, можна використовувати ці методи для виконання коду в UI-потоці. Якщо ж потрібні навороти й алгоритм досить складний, то використовуємо Handler.

Доповнення до AsyncTask

Ми вводили їх в окремий потік і використовували Handler для зворотного зв'язку та оновлення екрана. Творці Android вирішили, що ці механізми варто виділити в окремий клас - AsyncTask. Тобто його мета - це виконання важких завдань і передача в UI-потік результатів роботи. Але при цьому нам не треба замислюватися про створення Handler і нового потоку.`