TL;DR
-
PauseHandler#post
メソッドを使う場合はhandleMessage
メソッドではなくdispatchMessage
をoverrideする - 最終的な実装はgistに置いた:PauseHandler for Handler#post method
PauseHandlerとは
FragmentのライフサイクルではonSaveInstanceState
以降にFragmentを触ろうとするとIllegalStateException
が送出されます。
これは、GUIの状態を一時保管するタイミングであるonSaveInstanceState
よりも後にFragmentの状態を変えるために起こります。
アプリがバックグラウンドに回るとUIスレッドは動作を中断しますが、ユーザーが好き勝手に作ったワーカースレッドは特別に操作をしない限り動き続けます。
したがってワーカースレッドからUIスレッドにGUIの更新を要求するアプリだと上記の状況は容易に起こりえます。
これを解決するためには以下のどちらかの処置が必要です。
-
onSaveInstanceState
が呼ばれた後のUIスレッドへの要求は破棄する -
onSaveInstanceState
が呼ばれた後のUIスレッドへの要求はキューに溜めておいて、onRestoreInstanceState
で復帰した後にキューから取り出して受理する
つまり、捨てるか溜めるかですね。
上記の処置はFragment
クラスに実装してもよいですが、UIスレッドへの要求には通常Handler
クラスを使うことを考えるとHandler
クラスを継承した独自クラスに実装する方が筋が良いといえます。
この独自クラスは俗にPauseHandlerと呼ばれています。
android - How to handle Handler messages when activity/fragment is paused - Stack Overflow
このPauseHandlerを使用するFragmentクラスでは、PauseHandler
にonSaveInstanceState
/onRestoreInstanceState
のタイミングを知らせるために、onPause
でPauseHandler#pause
を、onResume
でPauseHandler#resume
を呼び出しておきます1。
この呼び出しさえしておけば、あとはPauseHandler
に対して通常どおりsendMessage
を呼び出すだけでPauseHandler
の中で適切に上述1または2の処置を実行してくれるのです2。
……sendMessage
だと?
オリジナルのPauseHandler
の問題点
Handler
への要求の出し方は大きく2つあります。
Message
インスタンスを渡し、別途設けたコールバックで要求内容の中身を把握しながら要求を実行するHandler#sendMessage
系メソッド3を使う方法と、Runnable
インスタンスを渡して要求内容の中身を見ずに実行するHandler#post
系メソッド3を使う方法です。
StackOverflowのPauseHandler実装はHandler#sendMessage
系メソッドには対応しているものの、Handler#post
系メソッドには対応していません。
実際にPauseHandler#post
を実行してみると、FragmentのonPause
が呼び出された後であってもアクション(引数に指定したRunnableインスタンス)は破棄されずキューにも溜められず、通常どおりUIスレッドで実行されてしまいます。
それもそのはず。
StackOverflowのPauseHandler実装はHandler#handleMessage
をoverrideすることで破棄またはキューへの積み込みを実現しているのですが、このメソッドが呼ばれるのはHandler#sendMessage
系メソッドだけでHandler#post
系メソッドでは呼ばれません。
解決方法
解決方法は単純明快。
Handler#handleMessage
ではなくHandler#dispatchMessage
をoverrideすればよいのです。
Handler
クラスの実装を見てみるとわかりますが、Handler#dispatchMessage
はHandler#sendMessage
とHandler#post
のどちらの場合でも呼び出されます。Handler#dispatchMessage
はsendMessage経由から呼ばれたかpost経由で呼ばれたかの振り分けを行っており、呼び出し元がHandler#sendMessage
だった場合はHandler#handleMessage
を呼び、呼び出し元がHandler#post
だった場合はRunnable
インスタンスのRunnable#run
メソッドを呼ぶ仕組みになっています。
よってHandler#dispatchMessage
に処理を介入させられればどちらの場合でも対応できるのです。
修正した私家版PauseHandlerのコードを以下のとおりです。
ただし、簡単のため、1, 2の処置の切り換えを実現していた抽象メソッドstoreMessage
は廃し、単純にコンストラクタに引数で与える形にしてあります。
public class PauseHandler extends Handler {
private boolean mIsPause;
private boolean mStoresMessages;
private final Queue<Message> mQueue = new LinkedList<>();
public PauseHandler(boolean storesMessages) {
super();
mStoresMessages = storesMessages
}
public PauseHandler(Callback callback, boolean storesMessages) {
super(callback);
mStoresMessages = storesMessages
}
public void resume() {
mIsPause = false;
while (!mQueue.isEmpty()) {
Message msg = mQueue.poll();
if (msg != null) {
sendMessage(msg);
}
}
}
public void pause() {
mIsPause = true;
}
@Override
public void dispatchMessage(Message msg) {
if (mIsPause) {
if (mStoresMessages) {
Message copied = Message.obtain(msg);
mQueue.offer(copied);
}
} else {
super.dispatchMessage(msg);
}
}
}
本当に単純です。
違いはoverrideの対象がhandleMessage
メソッドからdispatchMessage
メソッドになったことと、processMessage
がなくなった代わりに親クラスのdispatchMessage
メソッドを呼ぶようになったことだけです。
このクラスを使用すればpost
系メソッドであっても上述の1または2の処置どおり期待どおりに動作します。
public class AFragment extends Fragment {
// ...
private PauseHandler handler = new PauseHandler(true);
// ...
public void aMethod() {
// ...
handler.post(new Runnable() {
@Override
public void run() {
// UIスレッドで実行したい処理。
// 普通のHandler#postを使う場合と使い方に違いはない。
}
});
// ...
}
}
もちろん、sendMessage
系メソッドもオリジナルのPauseHandler
同等に動作します。オリジナルのPauseHandler
では抽象メソッドprocessMessage
がコールバックインタフェースになっていましたが、修正版のPauseHandler
ではhandleMessage
を直接、コールバックにしてしまえばよいです。
public class AFragment extends Fragment {
// ...
private PauseHandler handler = new PauseHandler(true) {
@Override
public void handleMessage(Message msg) {
// 以前はprocessMessageに書いていた処理をここに書く。
}
};
}
もしくはCallback
インタフェースを継承したインスタンスを与えてもよいでしょう4。
public class AFragment extends Fragment {
// ...
private PauseHandler handler = new PauseHandler(new Callback() {
@Override
public boolean handleMessage(Message msg) {
// 以前はprocessMessageに書いていた処理をここに書く。
return true;
}
}, true);
}
上記を使いやすいようにリファクタした最終形はgistに置いたコードを参照してください。Android Studioが自動変換で作ってくれたKotlin版のコードもあります。
PauseHandler for Handler#post method
おわりに、あるいは記事を書いた動機
Handlerを使うときはsendMessageよりpostを使うことの方が多いんじゃないかなって思うんですけど、PauseHandlerでググってもdispatchMessageをoverrideするやり方は見つからなかったです。
ので、この記事が誰かの参考になれば嬉しいです。
-
「おいおい、
onSaveInstanceState
/onRestoreInstanceState
じゃなくてonPause/onResume
で呼ぶってどういうことだよ? 誤記か?」と思われるかもしれませんが、誤記にあらず。onRestoreInstanceState
はライフサイクルの中で常に呼ばれるコールバックではありません。pauseの設定/解除の確実な状態遷移とタイミングの対称性を保つために、onSaveInstanceState
/onRestoreInstanceState
の前後にあり、かつライフサイクルの中で確実に呼び出されるonPause/onResume
を使用しているのです。 ↩ -
PauseHandler#storeMessage
の戻り値がfalse
であれば1、true
であれば2を実行します。 ↩ -
どちらのメソッドにも指定した時刻に実行する
*AtTime
メソッドと、指定した時間だけ遅延させてから実行する*delayed
メソッドがある。 ↩ ↩2 -
というよりこれがHandlerでMessageを取り扱う際の推奨されるやり方だ。 ↩