私はこの問題を数か月間調査しましたが、さまざまな解決策を考え出しましたが、それらはすべて大規模なハッキングであるため満足していません。設計に欠陥のあるクラスがフレームワークに組み込まれ、誰もそれについて話していないとはまだ信じられないので、何かを見逃しているに違いないと思います。
問題はAsyncTask
にあります。ドキュメントによると
「スレッドやハンドラを操作することなく、バックグラウンド操作を実行し、UIスレッドで結果を公開できます。」
この例では、showDialog()
でいくつかの例示的なonPostExecute()
メソッドがどのように呼び出されるかを示しています。ただし、ダイアログを表示するには常に有効なContext
への参照とAsyncTaskが必要であるため、これは完全に考案されたようですコンテキストオブジェクトへの強い参照を保持してはいけません。
理由は明らかです。タスクがトリガーされたアクティビティが破壊された場合はどうなりますか?これは常に発生する可能性があります。あなたが画面を裏返したからです。タスクがそれを作成したコンテキストへの参照を保持する場合、あなたは役に立たないコンテキストオブジェクトを保持しているだけではありません(ウィンドウは破棄され、any UIインタラクションは例外で失敗します! )、メモリリークが発生するリスクさえあります。
ここで私のロジックに欠陥がない限り、これはonPostExecute()
に変換されます。コンテキストにアクセスできない場合、このメソッドがUIスレッドで実行されるのは何のためですか?ここでは意味のあることは何もできません。
1つの回避策は、コンテキストインスタンスをAsyncTaskではなく、Handler
インスタンスに渡すことです。それは機能します:ハンドラーはコンテキストとタスクを緩やかにバインドするので、リークのリスクなしにそれらの間でメッセージを交換できます(右?)。しかし、それは、AsyncTaskの前提、つまり、ハンドラーを気にする必要がないということは間違っていることを意味します。同じスレッドでメッセージを送受信しているため、ハンドラーを乱用しているようにも見えます(UIスレッドでメッセージを作成し、UIスレッドでも実行されるonPostExecute()で送信します)。
さらに、その回避策を使用しても、コンテキストが破棄されたときに、起動されたタスクのno recordが発生するという問題があります。つまり、コンテキストを再作成するときにタスクを再起動する必要があります。画面の向きを変更した後。これは遅くて無駄です。
これに対する私の解決策は( Droid-Fuライブラリで実装されている )、コンポーネント名から一意のアプリケーションオブジェクト上の現在のインスタンスへのWeakReference
sのマッピングを維持することです。 AsyncTaskが開始されるたびに、そのマップに呼び出しコンテキストが記録され、コールバックごとに、そのマッピングから現在のコンテキストインスタンスがフェッチされます。これにより、古いコンテキストインスタンスを参照することはありませんandコールバックで常に有効なコンテキストにアクセスできるため、そこで意味のあるUI作業を行うことができます。また、参照は脆弱であり、特定のコンポーネントのインスタンスが存在しなくなるとクリアされるため、リークも発生しません。
それでも、これは複雑な回避策であり、Droid-Fuライブラリクラスの一部をサブクラス化する必要があるため、これは非常に煩わしいアプローチになります。
今、私は単に知りたいです:私は単に何かが非常に不足しているか、AsyncTaskは本当に完全に欠陥がありますか?あなたの経験はどのように働いていますか?これらの問題をどのように解決しましたか?
ご意見ありがとうございます。
このようなものはどうですか:
class MyActivity extends Activity {
Worker mWorker;
static class Worker extends AsyncTask<URL, Integer, Long> {
MyActivity mActivity;
Worker(MyActivity activity) {
mActivity = activity;
}
@Override
protected Long doInBackground(URL... urls) {
int count = urls.length;
long totalSize = 0;
for (int i = 0; i < count; i++) {
totalSize += Downloader.downloadFile(urls[i]);
publishProgress((int) ((i / (float) count) * 100));
}
return totalSize;
}
@Override
protected void onProgressUpdate(Integer... progress) {
if (mActivity != null) {
mActivity.setProgressPercent(progress[0]);
}
}
@Override
protected void onPostExecute(Long result) {
if (mActivity != null) {
mActivity.showDialog("Downloaded " + result + " bytes");
}
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mWorker = (Worker)getLastNonConfigurationInstance();
if (mWorker != null) {
mWorker.mActivity = this;
}
...
}
@Override
public Object onRetainNonConfigurationInstance() {
return mWorker;
}
@Override
protected void onDestroy() {
super.onDestroy();
if (mWorker != null) {
mWorker.mActivity = null;
}
}
void startWork() {
mWorker = new Worker(this);
mWorker.execute(...);
}
}
その理由は明らかです。タスクがトリガーされたアクティビティが破壊された場合はどうなりますか?
onDestroy()
のAsyncTask
からアクティビティを手動で関連付け解除します。新しいアクティビティをonCreate()
のAsyncTask
に手動で再関連付けします。これには、静的内部クラスまたは標準のJavaクラスに加えて、おそらく10行のコードが必要です。
AsyncTask
は少しだけmoreであるように見えます概念的に欠陥。また、互換性の問題により使用できません。 Android docs read:
最初に導入されたとき、AsyncTasksは単一のバックグラウンドスレッドでシリアルに実行されました。DONUTから、これはスレッドのプールに変更され、複数のタスクが並行して動作できるようになりました。- HONEYCOMBを開始すると、タスクは単一のスレッドで実行されるようになり、並列実行による一般的なアプリケーションエラーを回避できます。本当に並列実行が必要な場合は、executeOnExecutor(Executor, Params...)
このメソッドのバージョン _THREAD_POOL_EXECUTOR
_; ただし、その使用に関する警告については、コメントを参照してください。
executeOnExecutor()
と_THREAD_POOL_EXECUTOR
_は両方ともAPIレベル11で追加(Android 3.0.x、HONEYCOMB)です。
つまり、2つのファイルをダウンロードするために2つのAsyncTask
sを作成すると、最初のファイルが終了するまで2回目のダウンロードは開始されません。 2つのサーバーを介してチャットし、最初のサーバーがダウンした場合、最初のサーバーへの接続がタイムアウトする前に2番目のサーバーに接続しません。 (もちろん、新しいAPI11機能を使用しない限り、これによりコードは2.xと互換性がなくなります)。
また、2.xと3.0+の両方をターゲットにしたい場合は、非常に注意が必要です。
さらに、 docs say:
注意:ワーカースレッドの使用時に発生する可能性のある別の問題は、ランタイム構成の変更(ユーザーが画面の向きを変更したときなど)によるアクティビティの予期しない再起動です。これにより、ワーカースレッド。これらの再起動中にタスクを永続化する方法、およびアクティビティが破棄されたときにタスクを適切にキャンセルする方法については、Shelfsサンプルアプリケーションのソースコードを参照してください。
おそらくGoogleを含む私たち全員が[〜#〜] mvc [〜#〜]の観点からAsyncTask
を誤用しています。
アクティビティはControllerであり、コントローラーはViewよりも長持ちする操作を開始しないでください。つまり、AsyncTasksはModelから、アクティビティライフサイクルにバインドされていないクラスから使用する必要があります-アクティビティはローテーションで破棄されることに注意してください。 (Viewに関しては、通常Android.widget.Buttonなどから派生したクラスをプログラムしませんが、できます。通常、Viewはxmlです。)
つまり、アクティビティのメソッドにAsyncTask派生物を配置するのは間違っています。 OTOH、アクティビティでAsyncTasksを使用してはならない場合、AsyncTaskはその魅力を失います。これは、以前は迅速かつ簡単な修正として宣伝されていました。
AsyncTaskからのコンテキストへの参照によってメモリリークが発生するリスクがあるかどうかはわかりません。
それらを実装する通常の方法は、アクティビティのメソッドの1つのスコープ内で新しいAsyncTaskインスタンスを作成することです。アクティビティが破棄された場合、AsyncTaskが完了すると、到達できなくなり、ガベージコレクションの対象になりませんか?したがって、アクティビティへの参照は重要ではありません。AsyncTask自体がぶらぶらしないからです。
アクティビティに関するWeekReferenceを保持する方がより堅牢です。
public class WeakReferenceAsyncTaskTestActivity extends Activity {
private static final int MAX_COUNT = 100;
private ProgressBar progressBar;
private AsyncTaskCounter mWorker;
@SuppressWarnings("deprecation")
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_async_task_test);
mWorker = (AsyncTaskCounter) getLastNonConfigurationInstance();
if (mWorker != null) {
mWorker.mActivity = new WeakReference<WeakReferenceAsyncTaskTestActivity>(this);
}
progressBar = (ProgressBar) findViewById(R.id.progressBar1);
progressBar.setMax(MAX_COUNT);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.activity_async_task_test, menu);
return true;
}
public void onStartButtonClick(View v) {
startWork();
}
@Override
public Object onRetainNonConfigurationInstance() {
return mWorker;
}
@Override
protected void onDestroy() {
super.onDestroy();
if (mWorker != null) {
mWorker.mActivity = null;
}
}
void startWork() {
mWorker = new AsyncTaskCounter(this);
mWorker.execute();
}
static class AsyncTaskCounter extends AsyncTask<Void, Integer, Void> {
WeakReference<WeakReferenceAsyncTaskTestActivity> mActivity;
AsyncTaskCounter(WeakReferenceAsyncTaskTestActivity activity) {
mActivity = new WeakReference<WeakReferenceAsyncTaskTestActivity>(activity);
}
private static final int SLEEP_TIME = 200;
@Override
protected Void doInBackground(Void... params) {
for (int i = 0; i < MAX_COUNT; i++) {
try {
Thread.sleep(SLEEP_TIME);
} catch (InterruptedException e) {
e.printStackTrace();
}
Log.d(getClass().getSimpleName(), "Progress value is " + i);
Log.d(getClass().getSimpleName(), "getActivity is " + mActivity);
Log.d(getClass().getSimpleName(), "this is " + this);
publishProgress(i);
}
return null;
}
@Override
protected void onProgressUpdate(Integer... values) {
super.onProgressUpdate(values);
if (mActivity != null) {
mActivity.get().progressBar.setProgress(values[0]);
}
}
}
}
なぜ所有アクティビティのonPause()
メソッドをオーバーライドして、そこからAsyncTask
をキャンセルしないのですか?
あなたは絶対に正しいです-それがデータを取得するためにアクティビティで非同期タスク/ローダーを使用することから離れる動きが勢いを得ている理由です。新しい方法の1つは、データの準備ができたら基本的にコールバックを提供する Volley フレームワークを使用することです。これは、MVCモデルとはるかに一貫しています。 VolleyはGoogle I/O 2013で人気を博しました。なぜこれを認識していない人が多いのかはわかりません。
個人的には、スレッドを拡張し、コールバックインターフェイスを使用してUIを更新するだけです。 FCの問題がなければ、AsyncTaskを正しく動作させることはできませんでした。また、非ブロッキングキューを使用して実行プールを管理します。
AsyncTaskは、Activity、Context、ContextWrapperなどとより緊密に結合されたものと考える方がいいでしょう。そのスコープが完全に理解されている場合、より便利です。
最終的にガベージコレクションされ、アクティビティへの参照が保持されなくなり、それもガベージコレクションされるように、ライフサイクルにキャンセルポリシーがあることを確認します。
コンテキストから移動中にAsyncTaskをキャンセルしないと、メモリリークとNullPointerExceptionsが発生します。単純なダイアログでトーストのようなフィードバックを提供する必要がある場合は、アプリケーションコンテキストのシングルトンがNPE問題の回避に役立ちます。
AsyncTaskはすべてが悪いわけではありませんが、予想外の落とし穴につながる可能性のある多くの魔法が確実に進行しています。
キャンセルは機能すると思ったが、機能しない。
ここで彼らはそれについてRTFMingしています:
""タスクが既に開始されている場合、mayInterruptIfRunningパラメーターは、タスクを停止しようとしてこのタスクを実行しているスレッドを中断するかどうかを決定します。
ただし、スレッドが割り込み可能であることを意味するものではありません。それはJavaのことであり、AsyncTaskのことではありません。 "