AsyncTaskLoaderの動きを確認中 その2

前回の続きでAsyncTaskLoaderを使ったサンプルを作って、Loaderの動きを確認していたのですが、1つの問題点にぶち当たりました。

initLoaderでLoaderを動かす分にはとてもスッキリしたのですが、restartLoaderを使うと非同期処理がイメージ通りに動きませんでした。

それは以前の非同期処理が終わらないと、restartLoaderで新しく動かす非同期処理が始まらないということです。

私が作ったサンプルでは、指定した数字までカウントアップを行う非同期処理をしています。しかし非同期処理中にrestartLoaderを呼び出すと、今動いている非同期処理が終わらないと新しい非同期処理が動いてくれないのです。

restartLoaderを呼んだら今動いている非同期処理には停止してもらい、すぐに新しい非同期処理が始まって欲しいです。使いもしない非同期処理の終了を待つのは時間の無駄ですし、使いもしない処理にリソースを割くのももったいないです。 
## ソースコードを読んで分かったこと
 現在進行形で格闘しているので、まとまっていないですがこんな感じ。

  • onStopLoadingはActivityがonStopになったときに呼ばれる(画面回転時は除く)
  • キャンセルの処理(restartLoader実行時)は、まずonCancelLoadが呼ばれる
  • AsyncTaskLoader.onCancelLoadでLoaderの状態に合わせてキャンセル処理を行う
  • 実際のキャンセル処理はcancelLoadInBackgroundメソッドで行われる
  • しかしAsyncTaskLoader.cancelLoadInBackgroundでは何もしていない
  • すなわち実際にloadInBackgroundの処理を止めるのは自分で実装しなければならない
  • AsyncTaskLoader.onCancelLoadを経ていれば、loadInBackgroundの処理結果は最終的にonCanceledに通知される
  • LoaderManagerがうまいこと管理してくれているので、restartLoader呼んだ数だけ非同期処理が乱立するわけではない(それでもいくつか並行して走るけれども)

そもそもLoaderManagerは何している?

LoaderManagerはrestartLoaderが呼ばれた時に何をしているのかも、同時進行で読み解いています。

LoaderManagerはLoaderをmLoadersとmInactiveLoadersという2つのリストで管理しています。

mLoadersでは現在実行中のLoaderを、mInactiveLoadersでは以前実行されていたLoaderを管理しています。mInactiveLoadersはLoaderを破棄するためのもののようです。おそらく。

restartLoaderをすると、LoaderManagerはLoaderの状態によってあれやこれやしながら新しいLoaderを作成します。現在実行中のLoaderがあればキャンセル処理を行いますが、新しいタスクはmPendingLoaderに登録します。

mPendingLoaderが何者かというと、その名が表すように次に実行される非同期処理のタスク(Loader)です。このmPendingLoaderがいつ実行されるのかというと、今実行されているタスクのloadInBackgroundが終了した時です。 
そのため実行中のタスクが終わらないと、restartLoaderで作られた新しいタスクが始まらないのです。

AsyncTaskLoader上のキャンセル処理

Loaderの非同期処理が実行されているときにキャンセルがかかると、以下の場合にonCanceledが呼ばれます。

  • Loaderが非同期処理実行中の間に、Activity等でinitLoader().forceLoad()をしたとき
  • Activity等でrestartLoaderを呼んだ時(非同期処理が実行中かは問わない)

AsyncTaskLoader.onCancelLoad()でキャンセル関連の処理が行われているためsuper.onCancelLoad()を呼ぶ必要があります。

ただしやってるのはLoaderの管理情報の更新だけで、実行中のloadInBackgroundを止めるような処理は何もしていません。

具体的には、、現在実行中の非同期処理があるか確認(mTask != null)し、タスクがなければキャンセル対象がないので何もしません。

ある場合には、キャンセル処理中の非同期処理があるかを確認します(mCancellingTask != null)。

mCancellingTaskがある場合、onCancelLoadが呼び出されたLoaderがPendingTaskなら破棄します(mTask.waiting == true)。これは実行待ち状態のLoaderをキャンセルすることを意味しています。実行待ちのタスクはまだ開始されてないから破棄するだけでいいわけです。

mCancellingTaskがない場合は、onCancelLoadが呼び出されたLoaderがPendingTaskか確認します。上と同じことをやっていますが、mCancellingTaskがない場合、このLoaderをキャンセルされたタスクとして退避させる必要があるので条件分岐されてます。

で、PendingTaskであればLoaderをそのまま破棄します。まだ非同期処理が始まっていないのでそのまま破棄するだけでいいからです。

PendingTaskでないのであれば、このLoaderは現在稼働中の非同期処理ということになります。そこでこれをキャンセルし、mCancellingTaskへと退避します。その上でcancelLoadInBackgroundを呼び出します。

そのため、Loaderをキャンセルするための処理は、cancelLoadInBackgroundで実装すればいいことになります。

cancelLoadInBackgroundで何をすればいいか

Loaderをキャンセルするための処理を実装するといっても、具体的にどう実装すればいいかというとよく分かりません。

このメソッドの中からloadInBackgroundの処理を停止させることはできないでしょう。むしろこのメソッドは、メインスレッド(Activityとか)からLoaderを停止させるためのメソッドのような気がします。

しかし直接ActivityからこのcancelLoadInBackgroundを呼ぶと、LoaderManagerの管理下から外れた動きをすることになって、変なことになりそうな気がします。

結局のところ、loadInBackgroundの中でisLoadInBackgroundCancelled(Loaderがキャンセルされたらtrueになる)をチェックして非同期処理を途中で止めるように実装するしかなさそうです。

新たな謎

今気づいたんですが、ActivityからLoaderのforceLoadを呼んだ後でrestartLoaderすると、前の非同期処理完了を待つことなくrestartLoaderした処理が走っていることに気づきました。前の処理は走ったままなので、2つの非同期処理が並列で走ってますけども。

この違いはいったいどこからやってくるのか・・・。

ちなみにサンプルはGitHubで公開中です。

Amazonのほしいものリストを公開しています。仕事で欲しいもの、単なる趣味としてほしいもの、リフレッシュのために欲しいものなどを登録しています。 寄贈いただけると泣いて喜びます。大したお礼はできませんが、よりよい情報発信へのモチベーションに繋がりますので、ご検討いただければ幸いです。