Doug Stevenson
デベロッパー アドボケート
こんにちは。Play Services Task API についてのブログシリーズのパート 2 をお届けします。Firebase API では、一部の Firebase 機能の API が非同期に実行する操作に応答する方法として、Play Services Task API を利用しています。
前回 は、Firebase Storage API のタスクを紹介するとともに、タスクが動作する一般的な仕組みの一部を学びました。もし前回の投稿をご覧になっていない場合は、以降を読む前にご覧いただくことをお勧めします。今回の投稿では、結果を取得するためにタスクに追加できるさまざまなリスナーの動作の違いについて見ていきます。
前回は、Firebase Storage API を使う次のようなタスクにリスナーを追加しました。
Task task = forestRef.getMetadata();
task.addOnSuccessListener(new OnSuccessListener() {
@Override
public void onSuccess(StorageMetadata storageMetadata) {
// Metadata now contains the metadata for 'images/forest.jpg'
}
});
このコードでは、完了時に起動される匿名リスナーを単一の引数として addOnSuccessListener() を呼び出しています。この形式を使うと、リスナーはメインスレッドで起動します。つまり、ビューの更新など、メインスレッドでしかできないことを行うことができます。タスクによって制御がメインスレッドに戻されるのはたいへんありがたいことです。ただし、1 点だけ注意事項があります。アクティビティでこのようにしてリスナーを登録した場合、そのアクティビティが破棄されるときにリスナーを削除しないと、アクティビティがリークする可能性があります。
アクティビティのリークを防止する
もちろん、アクティビティのリークを発生させたい人はいません。そのためにまず、アクティビティのリークとはどのようなものか考えてみましょう。簡潔に言えば、アクティビティのリークは、アクティビティの onDestroy() ライフサイクル メソッドが呼び出された後(つまり、アクティビティが利用できる期間を超えた後)もアクティビティ オブジェクトの参照を保持しているオブジェクトがある場合に発生します。アクティビティで onDestroy() が呼び出されると、Android はそのアクティビティのインスタンスをもう利用しないことが確実にわかります。onDestroy() が呼ばれた後、Android ランタイムのガベージ コレクタはそのアクティビティ、付随するすべてのビュー、その他の不要になったオブジェクトを回収します。しかし、別のオブジェクトがアクティビティに対する強い参照を保持している場合、ガベージ コレクタはそのアクティビティやそれに付随するすべてのビューを回収しません。
タスクを使うと、慎重に回避しない限り、アクティビティのリークが問題になる場合があります。(アクティビティの内部にある場合)先ほどのコードの非同期リスナー オブジェクトは、自身を含むアクティビティへの暗黙的な強い参照を保持します。リスナー内部のコードがアクティビティやそのメンバーを変更できるのはそのためですが、このあたりの細かい制御はコンパイラが暗黙的に行ってくれます。アクティビティのリークは、実行中のタスクがアクティビティの onDestroy() が呼び出された後もリスナーを参照している場合に発生します。しかし実際は、タスクにどのくらい時間がかかるかについては何の保証もありません。そのため、リスナーは無限に保持できるようになっています。また、リスナーは暗黙的にアクティビティへの参照を保持しているため、タスクが onDestroy() の前に完了していないとアクティビティがリークする可能性があります。(たとえば、ネットワーク通信が滞っている場合など)たくさんのタスクがアクティビティへの逆参照を保持している場合、アプリのメモリが足りなくなってクラッシュすることもあります。そうなっては一大事です。詳しくは、
こちらの動画 もご覧ください。
手元のタスクに戻る
単一引数版の addOnSuccessListener() を使うと、適切なタイミングでリスナーを削除するよう十分注意しない限り、アクティビティがリークする可能性があります。アクティビティのリークを考慮する場合(もちろん考慮が必要です)、この点を認識しておく必要があります。
Task API を使うと、リスナーの削除を自動的に行ってくれるので便利です。先ほどのコードがアクティビティの中にあるものとして、addOnSuccessListener() を呼び出す部分を少し変更してみましょう。
Task task = forestRef.getMetadata();
task.addOnSuccessListener(this, new OnSuccessListener() {
@Override
public void onSuccess(StorageMetadata storageMetadata) {
// Metadata now contains the metadata for 'images/forest.jpg'
}
});
これは、
addOnSuccessListener() に 2 つの引数があることを除けば、先ほどのものとまったく同じです。最初の引数は「this」です。そのため、このコードがアクティビティの中にある場合、「this」はアクティビティのインスタンスを指します。最初のパラメータにアクティビティへの参照を指定すると、そのアクティビティのライフサイクルのみを「スコープ」としたリスナーであることを Task API に伝えることができます。こうすることによって、アクティビティの onStop() ライフサイクル メソッドが呼び出された際にリスナーが
自動的に タスクから削除されるようになります。アクティビティがアクティブである間に作成したすべてのタスクを自分で削除する必要がなくなるため、これはとても便利な方法です。リスナーを停止する場所が、アクティビティが見えなくなった際に呼び出される onStop() で適切かどうかを確認する必要はありますが、多くの場合は問題ないでしょう。なお、次のアクティビティ(画面の向きが変わって現在のアクティビティが新しいアクティビティに置き換わった場合など)でタスクのトラッキングを継続する場合、次のアクティビティで必要になる情報を保持する方法を検討する必要があります。この点については、
アクティビティの状態を保存する をご覧ください。
メインスレッド以外で処理する
タスクが完了した際の処理をメインスレッド上で行いたくないケースもあるでしょう。リスナーでブロック処理を行いたい場合や、それぞれのタスクの結果を(シーケンシャルではなく)並列に処理したい場合などです。その場合、メインスレッドでの処理ではなく、代わりに自分で制御する別のスレッドで結果を処理したいと思うでしょう。addOnSuccessListener() には、アプリのスレッド化に便利なもう 1 つの形式があります。次のような形式です(リスナーは省略しています)。
Executor executor = ...; // obtain some Executor instance
Task task = RemoteConfig.getInstance().fetch();
task.addOnSuccessListener(executor, new OnSuccessListener() { ... });
ここでは、
Firebase Remote Config API を呼び出して新しい設定値を取得しています。次に、fetch() から返されたタスクに対して
addOnSuccessListener() を呼び出し、最初の引数として
エグゼキュータ を渡します。エグゼキュータは、リスナーを起動するスレッドを決定します。エグゼキュータについて詳しくない方のために説明すると、エグゼキュータは作業単位を受け取り、自らの制御下にあるスレッドで実行するためにルーティングを行うコア Java ユーティリティです。制御下にあるスレッドとは、単一スレッドの場合もありますが、作業を行うべく待ち構えているスレッドプールである場合もあります。アプリで直接エグゼキュータを使うことはあまり多くはなく、この手法はアプリのスレッドの動作を管理する高度な技法と見られることもあります。ここで覚えておくべきことは、状況によっては、リスナーの結果を受信するのはメインスレッドでなくてもよいということです。エグゼキュータを使用する場合は、スレッドがリークしないように共有シングルトンとして管理するか、エグゼキュータのライフサイクルを管理するようにします。
このコードで特筆しておくべきもう 1 つの興味深い点は、Remote Config から返されるタスクのパラメータが Void になっていることです。こうすることによって、タスクがオブジェクトを直接生成しないことを宣言できます。Java では、Void は型データがないことを示すデータ型です。Remote Config API は、このタスクを単なるタスクの完了を示すインジケーターとして使用しています。呼び出し元は、取得された新しい値を検出するために、別の Remote Config API を使用して頂く必要があります。
賢い選択を
まとめると、addOnSuccessListener() には次の 3 つの形式があります。
Task addOnSuccessListener(OnCompleteListener listener)
Task addOnSuccessListener(Activity activity, OnSuccessListener listener)
Task addOnSuccessListener(Executor executor, OnSuccessListener listener)
さらに、失敗時と完了時のリスナーにも同じ形式があります。
Task addOnFailureListener(OnFailureListener listener)
Task addOnFailureListener(Activity activity, OnFailureListener listener)
Task addOnFailureListener(Executor executor, OnFailureListener listener)
Task addOnCompleteListener(OnCompleteListener listener)
Task addOnCompleteListener(Activity activity, OnCompleteListener listener)
Task addOnCompleteListener(Executor executor, OnCompleteListener listener)
OnCompleteListener とは
OnCompleteListener には、特別なことは何もありません。1 つだけで成功と失敗の両方を受信できるリスナーなので、コールバックの内部でステータスを確認する必要があります。前回の投稿のファイル メタデータのコールバックは、成功と失敗でリスナーを分けずに、次のように書き換えることもできます。
Task task = forestRef.getMetadata();
task.addOnCompleteListener(new OnCompleteListener() {
@Override
public void onComplete (Task task) {
if (task.isSuccessful()) {
StorageMetadata meta = task.getResult();
// Do something with metadata...
} else {
Exception e = task.getException();
// Handle the failure...
}
}
});
つまり、OnCompleteListener を使うと 1 つのリスナーで成功と失敗の両方の処理を行うことができます。コールバックに渡される Task オブジェクトに対して isSuccessful() を呼び出すと、成功か失敗かがわかります。実質的には、OnSuccessListener と OnFailureListener の両方を登録するのと同じ機能になります。どちらの方式を使うかは、主に好みの問題です。
このシリーズのパート 2 のまとめ
タスクの結果は成功、失敗、完了という 3 種類のリスナーで受信できます。また、それぞれのリスナーは、メインスレッド、アクティビティをスコープとしたメインスレッド、エグゼキュータが決定するスレッドという 3 つの方法でコールバックを受信することができます。いくつかの選択肢がありますが、どれが一番状況に適しているかを選ぶのは皆さんです。しかし、タスクの結果を処理する方法はこれだけではありません。さらに複雑な処理を行うために、タスク結果のパイプラインを作成することもできます。次回は、その方法を詳しく説明します。Firebase Task マスターへの旅を続けましょう。
質問がありましたら、Twitter でハッシュタグ #AskFirebase を検索するか、
firebase-talk Google Group をご利用ください.専用の Firebase Slack チャネルも用意しています。Twitter で
@CodingDoug のフォローもよろしくお願いします。
ブログシリーズの
パート 1 もお読みください。
Posted by
Khanh LeViet - Developer Relations Team
この記事は Doug Stevenson、デベロッパー アドボケートによる The Firebase Blog の記事 "Become a Firebase Taskmaster! (Part 2: Choosing the Best Options) " を元に翻訳・加筆したものです。詳しくは元記事をご覧ください。
Doug Stevenson
デベロッパー アドボケート
こんにちは。Play Services Task API についてのブログシリーズのパート 2 をお届けします。Firebase API では、一部の Firebase 機能の API が非同期に実行する操作に応答する方法として、Play Services Task API を利用しています。前回 は、Firebase Storage API のタスクを紹介するとともに、タスクが動作する一般的な仕組みの一部を学びました。もし前回の投稿をご覧になっていない場合は、以降を読む前にご覧いただくことをお勧めします。今回の投稿では、結果を取得するためにタスクに追加できるさまざまなリスナーの動作の違いについて見ていきます。
前回は、Firebase Storage API を使う次のようなタスクにリスナーを追加しました。
Task task = forestRef.getMetadata();
task.addOnSuccessListener(new OnSuccessListener() {
@Override
public void onSuccess(StorageMetadata storageMetadata) {
// Metadata now contains the metadata for 'images/forest.jpg'
}
});
このコードでは、完了時に起動される匿名リスナーを単一の引数として addOnSuccessListener() を呼び出しています。この形式を使うと、リスナーはメインスレッドで起動します。つまり、ビューの更新など、メインスレッドでしかできないことを行うことができます。タスクによって制御がメインスレッドに戻されるのはたいへんありがたいことです。ただし、1 点だけ注意事項があります。アクティビティでこのようにしてリスナーを登録した場合、そのアクティビティが破棄されるときにリスナーを削除しないと、アクティビティがリークする可能性があります。
アクティビティのリークを防止する
もちろん、アクティビティのリークを発生させたい人はいません。そのためにまず、アクティビティのリークとはどのようなものか考えてみましょう。簡潔に言えば、アクティビティのリークは、アクティビティの onDestroy() ライフサイクル メソッドが呼び出された後(つまり、アクティビティが利用できる期間を超えた後)もアクティビティ オブジェクトの参照を保持しているオブジェクトがある場合に発生します。アクティビティで onDestroy() が呼び出されると、Android はそのアクティビティのインスタンスをもう利用しないことが確実にわかります。onDestroy() が呼ばれた後、Android ランタイムのガベージ コレクタはそのアクティビティ、付随するすべてのビュー、その他の不要になったオブジェクトを回収します。しかし、別のオブジェクトがアクティビティに対する強い参照を保持している場合、ガベージ コレクタはそのアクティビティやそれに付随するすべてのビューを回収しません。
タスクを使うと、慎重に回避しない限り、アクティビティのリークが問題になる場合があります。(アクティビティの内部にある場合)先ほどのコードの非同期リスナー オブジェクトは、自身を含むアクティビティへの暗黙的な強い参照を保持します。リスナー内部のコードがアクティビティやそのメンバーを変更できるのはそのためですが、このあたりの細かい制御はコンパイラが暗黙的に行ってくれます。アクティビティのリークは、実行中のタスクがアクティビティの onDestroy() が呼び出された後もリスナーを参照している場合に発生します。しかし実際は、タスクにどのくらい時間がかかるかについては何の保証もありません。そのため、リスナーは無限に保持できるようになっています。また、リスナーは暗黙的にアクティビティへの参照を保持しているため、タスクが onDestroy() の前に完了していないとアクティビティがリークする可能性があります。(たとえば、ネットワーク通信が滞っている場合など)たくさんのタスクがアクティビティへの逆参照を保持している場合、アプリのメモリが足りなくなってクラッシュすることもあります。そうなっては一大事です。詳しくは、こちらの動画 もご覧ください。
手元のタスクに戻る
単一引数版の addOnSuccessListener() を使うと、適切なタイミングでリスナーを削除するよう十分注意しない限り、アクティビティがリークする可能性があります。アクティビティのリークを考慮する場合(もちろん考慮が必要です)、この点を認識しておく必要があります。
Task API を使うと、リスナーの削除を自動的に行ってくれるので便利です。先ほどのコードがアクティビティの中にあるものとして、addOnSuccessListener() を呼び出す部分を少し変更してみましょう。
Task task = forestRef.getMetadata();
task.addOnSuccessListener(this, new OnSuccessListener() {
@Override
public void onSuccess(StorageMetadata storageMetadata) {
// Metadata now contains the metadata for 'images/forest.jpg'
}
});
これは、addOnSuccessListener() に 2 つの引数があることを除けば、先ほどのものとまったく同じです。最初の引数は「this」です。そのため、このコードがアクティビティの中にある場合、「this」はアクティビティのインスタンスを指します。最初のパラメータにアクティビティへの参照を指定すると、そのアクティビティのライフサイクルのみを「スコープ」としたリスナーであることを Task API に伝えることができます。こうすることによって、アクティビティの onStop() ライフサイクル メソッドが呼び出された際にリスナーが 自動的に タスクから削除されるようになります。アクティビティがアクティブである間に作成したすべてのタスクを自分で削除する必要がなくなるため、これはとても便利な方法です。リスナーを停止する場所が、アクティビティが見えなくなった際に呼び出される onStop() で適切かどうかを確認する必要はありますが、多くの場合は問題ないでしょう。なお、次のアクティビティ(画面の向きが変わって現在のアクティビティが新しいアクティビティに置き換わった場合など)でタスクのトラッキングを継続する場合、次のアクティビティで必要になる情報を保持する方法を検討する必要があります。この点については、アクティビティの状態を保存する をご覧ください。
メインスレッド以外で処理する
タスクが完了した際の処理をメインスレッド上で行いたくないケースもあるでしょう。リスナーでブロック処理を行いたい場合や、それぞれのタスクの結果を(シーケンシャルではなく)並列に処理したい場合などです。その場合、メインスレッドでの処理ではなく、代わりに自分で制御する別のスレッドで結果を処理したいと思うでしょう。addOnSuccessListener() には、アプリのスレッド化に便利なもう 1 つの形式があります。次のような形式です(リスナーは省略しています)。
Executor executor = ...; // obtain some Executor instance
Task task = RemoteConfig.getInstance().fetch();
task.addOnSuccessListener(executor, new OnSuccessListener() { ... });
ここでは、Firebase Remote Config API を呼び出して新しい設定値を取得しています。次に、fetch() から返されたタスクに対して addOnSuccessListener() を呼び出し、最初の引数としてエグゼキュータ を渡します。エグゼキュータは、リスナーを起動するスレッドを決定します。エグゼキュータについて詳しくない方のために説明すると、エグゼキュータは作業単位を受け取り、自らの制御下にあるスレッドで実行するためにルーティングを行うコア Java ユーティリティです。制御下にあるスレッドとは、単一スレッドの場合もありますが、作業を行うべく待ち構えているスレッドプールである場合もあります。アプリで直接エグゼキュータを使うことはあまり多くはなく、この手法はアプリのスレッドの動作を管理する高度な技法と見られることもあります。ここで覚えておくべきことは、状況によっては、リスナーの結果を受信するのはメインスレッドでなくてもよいということです。エグゼキュータを使用する場合は、スレッドがリークしないように共有シングルトンとして管理するか、エグゼキュータのライフサイクルを管理するようにします。
このコードで特筆しておくべきもう 1 つの興味深い点は、Remote Config から返されるタスクのパラメータが Void になっていることです。こうすることによって、タスクがオブジェクトを直接生成しないことを宣言できます。Java では、Void は型データがないことを示すデータ型です。Remote Config API は、このタスクを単なるタスクの完了を示すインジケーターとして使用しています。呼び出し元は、取得された新しい値を検出するために、別の Remote Config API を使用して頂く必要があります。
賢い選択を
まとめると、addOnSuccessListener() には次の 3 つの形式があります。
Task addOnSuccessListener(OnCompleteListener listener)
Task addOnSuccessListener(Activity activity, OnSuccessListener listener)
Task addOnSuccessListener(Executor executor, OnSuccessListener listener)
さらに、失敗時と完了時のリスナーにも同じ形式があります。
Task addOnFailureListener(OnFailureListener listener)
Task addOnFailureListener(Activity activity, OnFailureListener listener)
Task addOnFailureListener(Executor executor, OnFailureListener listener)
Task addOnCompleteListener(OnCompleteListener listener)
Task addOnCompleteListener(Activity activity, OnCompleteListener listener)
Task addOnCompleteListener(Executor executor, OnCompleteListener listener)
OnCompleteListener とは
OnCompleteListener には、特別なことは何もありません。1 つだけで成功と失敗の両方を受信できるリスナーなので、コールバックの内部でステータスを確認する必要があります。前回の投稿のファイル メタデータのコールバックは、成功と失敗でリスナーを分けずに、次のように書き換えることもできます。
Task task = forestRef.getMetadata();
task.addOnCompleteListener(new OnCompleteListener() {
@Override
public void onComplete (Task task) {
if (task.isSuccessful()) {
StorageMetadata meta = task.getResult();
// Do something with metadata...
} else {
Exception e = task.getException();
// Handle the failure...
}
}
});
つまり、OnCompleteListener を使うと 1 つのリスナーで成功と失敗の両方の処理を行うことができます。コールバックに渡される Task オブジェクトに対して isSuccessful() を呼び出すと、成功か失敗かがわかります。実質的には、OnSuccessListener と OnFailureListener の両方を登録するのと同じ機能になります。どちらの方式を使うかは、主に好みの問題です。
このシリーズのパート 2 のまとめ
タスクの結果は成功、失敗、完了という 3 種類のリスナーで受信できます。また、それぞれのリスナーは、メインスレッド、アクティビティをスコープとしたメインスレッド、エグゼキュータが決定するスレッドという 3 つの方法でコールバックを受信することができます。いくつかの選択肢がありますが、どれが一番状況に適しているかを選ぶのは皆さんです。しかし、タスクの結果を処理する方法はこれだけではありません。さらに複雑な処理を行うために、タスク結果のパイプラインを作成することもできます。次回は、その方法を詳しく説明します。Firebase Task マスターへの旅を続けましょう。
質問がありましたら、Twitter でハッシュタグ #AskFirebase を検索するか、firebase-talk Google Group をご利用ください.専用の Firebase Slack チャネルも用意しています。Twitter で @CodingDoug のフォローもよろしくお願いします。
ブログシリーズの パート 1 もお読みください。
Posted by Khanh LeViet - Developer Relations Team