AsyncTaskは本当に概念的に欠陥があるのですか、それとも何か不足していますか?


264

私は今月この問題を調査してきましたが、さまざまな解決策を考え出しましたが、それらはすべて大規模なハックなので、満足できません。デザインに欠陥のあるクラスがフレームワークに組み込まれ、誰もそれについて話していないとはまだ信じられないので、私は何かを逃しているにちがいないと思います。

問題はにありAsyncTaskます。ドキュメントによると

「スレッドやハンドラーを操作しなくても、UIスレッドでバックグラウンド操作を実行して結果を公開できます。」

次に、例では引き続き、いくつかの例示的なshowDialog()メソッドがで呼び出される方法を示しますonPostExecute()。ただし、ダイアログを表示するには常に有効なへの参照が必要であり、AsyncTask がコンテキストオブジェクトへの強い参照を保持してはならないため、これは私には完全にContext不自然に思われます

その理由は明らかです。タスクをトリガーしたアクティビティが破棄された場合はどうなりますか?これは常に発生する可能性があります。たとえば、画面をめくった場合などです。タスクは、それを作成したコンテキストへの参照を保持したい場合、あなたは(ウィンドウが破壊されているでしょうし、無用なコンテキストオブジェクトへの上に保持しているだけでなく、任意の UIの相互作用が例外で失敗します!)、あなたもAを作成する危険性メモリーリーク。

私のロジックにここで欠陥がない限り、これは次のように変換されます:onPostExecute()コンテキストにアクセスできない場合にこのメソッドをUIスレッドで実行するのはどのようなメリットがあるので、これはまったく役に立たないのですか?ここでは意味のあることは何もできません。

回避策の1つは、コンテキストインスタンスをAsyncTaskに渡すのではなく、Handlerインスタンスを渡すことです。これは機能します。ハンドラーはコンテキストとタスクを緩やかにバインドするため、リークの危険を冒すことなく、それらの間でメッセージを交換できます(正しいですか)。しかし、それはAsyncTaskの前提、つまりハンドラーに煩わされる必要がないということは間違っていることを意味します。同じスレッドでメッセージを送受信しているため、ハンドラーを悪用しているようにも見えます(UIスレッドでメッセージを作成し、UIスレッドで実行されるonPostExecute()で送信します)。

さらに、その回避策を使用しても、コンテキストが破棄されたときに、それが起動したタスクの記録ないという問題があります。つまり、たとえば画面の向きが変わった後など、コンテキストを再作成するときにタスクを再起動する必要があります。これは遅く、無駄です。

これに対する私の解決策(Droid-Fuライブラリで実装されている)は、WeakReferencesのコンポーネント名から、一意のアプリケーションオブジェクトの現在のインスタンスへのマッピングを維持することです。AsyncTaskが起動すると、そのマップに呼び出しコンテキストが記録され、コールバックごとに、そのマッピングから現在のコンテキストインスタンスがフェッチされます。これにより、失効したコンテキストインスタンス参照することがなくなり、コールバック内の有効なコンテキストに常にアクセスできるため、そこで有効なUI作業を実行できます。また、参照が弱く、特定のコンポーネントのインスタンスが存在しなくなったときにクリアされるため、リークも発生しません。

それでも、これは複雑な回避策であり、いくつかのDroid-Fuライブラリクラスをサブクラス化する必要があるため、これはかなり煩わしいアプローチになります。

今私は単に知りたいです:私は何かを大規模に欠落していますか、それともAsyncTaskは本当に完全に欠陥がありますか?あなたの経験はどのように機能していますか?これらの問題をどのように解決しましたか?

ご入力いただきありがとうございます。


1
ご興味があれば、IgnitedAsyncTaskというクラスを点火コアライブラリに最近追加しました。これにより、以下のダイアンによって概説されている接続/切断パターンを使用して、すべてのコールバックにタイプセーフなコンテキストアクセスのサポートが追加されます。また、例外をスローし、別のコールバックで処理することもできます。参照してくださいgithub.com/kaeppler/ignition-core/blob/master/src/com/github/...
マティアス

これを見てください:gist.github.com/1393552
Matthias

1
この質問も関連しています。
Alex Lockwood 2014年

非同期タスクをarraylistに追加し、特定の時点ですべてを確実に閉じます。
NightSkyCode 2014年

回答:


86

このようなものはどうですか:

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(...);
    }
}

5
はい、mActivityは!= nullになりますが、Workerインスタンスへの参照がない場合、そのインスタンスの参照もガベージ削除の対象になります。タスクが永久に実行される場合は、とにかくメモリリークがあります(タスク)-電話のバッテリーを消耗していることは言うまでもありません。さらに、他の場所で述べたように、onDestroyでmActivityをnullに設定できます。
EboMike

13
onDestroy()メソッドはmActivityをnullに設定します。まだ実行されているため、その前にアクティビティへの参照を保持している人は関係ありません。また、アクティビティのウィンドウは、onDestroy()が呼び出されるまで常に有効です。そこでnullに設定すると、非同期タスクはアクティビティが無効になったことを認識します。(。そして、設定の変更、前のアクティビティのonDestroy()が呼び出されると、AsyncTaskように、それらの間に処理メインループ上の任意のメッセージをせずに次の1ののonCreate()の実行が不整合な状態を見ることはありませんとき)
hackbod

8
確かに、しかしそれでも私が言及した最後の問題を解決しません:タスクがインターネットから何かをダウンロードすることを想像してください。この方法を使用すると、タスクの実行中に画面を3回めくると、画面が回転するたびに再起動され、最後のアクティビティを除くすべてのタスクは、アクティビティ参照がnullであるため結果を破棄します。
Matthias

11
背景にアクセスするには、いずれかの適切なmActivity周り同期置く必要があるでしょうし、それがnullであるとき時代に実行しているとの契約を、またはアプリのための単一のグローバルインスタンスである)(バックグラウンドスレッドがちょうどContext.getApplicationContextを取る持っています。アプリケーションコンテキストは、何ができるか(たとえば、DialogのようなUIはありません)に制限されており、注意が必要です(登録されたレシーバーとサービスバインディングは、クリーンアップしないと永久に残されます)が、通常はそうでないコードに適しています特定のコンポーネントのコンテキストに関連付けられていません。
10

4
それは信じられないほど役に立ちました、ダイアンに感謝します!そもそもドキュメントが良ければいいのに。
Matthias

20

理由は明らかです。タスクをトリガーしたアクティビティが破棄された場合はどうなりますか?

のアクティビティとの関連付けを手動で解除AsyncTaskonDestroy()ます。手動に新しいアクティビティを再度関連付けるAsyncTaskの中でonCreate()。これには、静的内部クラスまたは標準Javaクラスのいずれかと、おそらく10行のコードが必要です。


静的参照に注意してください-静的な強い参照があったとしても、オブジェクトがガベージコレクションされているのを見てきました。おそらくAndroidのクラスローダーの副作用やバグですが、静的参照はアクティビティのライフサイクル全体で状態を安全に交換する方法ではありません。アプリオブジェクトは、しかし、それが私がそれを使用する理由です。
Matthias

10
@Matthias:静的参照を使用することはしませんでした。静的内部クラスを使用すると言いました。どちらも名前に「静的」があるにもかかわらず、かなりの違いがあります。
CommonsWare 2010


5
わかりました-ここで重要なのは、静的な内部クラスではなく、getLastNonConfigurationInstance()です。静的内部クラスは外部クラスへの暗黙的な参照を保持しないため、意味的には単純なパブリッククラスと同等です。単なる警告:アクティビティが中断されたときにonRetainNonConfigurationInstance()が呼び出されることは保証されていません(中断も電話である可能性があります)。解決。しかし、それでも素晴らしいアイデアです。
Matthias

7
ええと... onRetainNonConfigurationInstance()は、アクティビティが破棄および再作成されているときに常に呼び出されます。他の時間に呼び出すことは意味がありません。別のアクティビティへの切り替えが発生した場合、現在のアクティビティは一時停止/停止されますが、破棄されないため、非同期タスクは実行を続け、同じアクティビティインスタンスを使用できます。終了してダイアログが表示されると、ダイアログはそのアクティビティの一部として正しく表示され、ユーザーがアクティビティに戻るまでユーザーに表示されません。AsyncTaskをバンドルに含めることはできません。
10

15

以下のように見えますAsyncTaskビットですよりちょうどより概念的に不備。また、互換性の問題により使用できません。Androidのドキュメントは次のとおりです。

最初に導入されたとき、AsyncTasksは単一のバックグラウンドスレッドで順次実行されました。 DONUTから、これはスレッドのプールに変更され、複数のタスクが並行して動作できるようになりました。 HONEYCOMBを開始すると、タスクは単一のスレッドで実行されるように戻り、並列実行によって引き起こされる一般的なアプリケーションエラーを回避します。 本当に並列実行が必要な場合は executeOnExecutor(Executor, Params...) 、このメソッドのバージョンをで 使用できますTHREAD_POOL_EXECUTORただし、その使用に関する警告については、解説を参照してください。

executeOnExecutor()THREAD_POOL_EXECUTORはどちらもAPIレベル11で追加されています(Android 3.0.x、HONEYCOMB)。

つまり、2つAsyncTaskのを作成して2つのファイルをダウンロードした場合、2番目のダウンロードは最初のダウンロードが完了するまで開始されません。2つのサーバーを介してチャットし、最初のサーバーがダウンしている場合、最初のサーバーへの接続がタイムアウトするまで、2番目のサーバーに接続しません。(もちろん、API11の新機能を使用しない限り、これによりコードが2.xと互換性がなくなります)。

また、2.xと3.0+の両方をターゲットにしたい場合は、かなり注意が必要です。

さらに、ドキュメントは言う:

注意:ワーカースレッドの使用時に発生する可能性があるもう1つの問題は、ランタイム構成の変更(ユーザーが画面の向きを変更した場合など)によるアクティビティの予期しない再起動であり、ワーカースレッドが破壊される可能性があります。これらの再起動中にタスクを永続化する方法と、アクティビティが破棄されたときにタスクを適切にキャンセルする方法については、Shelvesサンプルアプリケーションのソースコードを参照してください。


12

おそらく、Googleを含め、私たち全員AsyncTaskMVCの観点から誤用しています。

アクティビティはコントローラーであり、コントローラーはビューよりも長く存続する可能性のある操作を開始しないでください。つまり、AsyncTasksは、Modelから、Activityライフサイクルにバインドされていないクラスから使用する必要があります。Activityはローテーションで破棄されることに注意してください。(ビューに関しては、通常android.widget.Buttonなどから派生したクラスをプログラムすることはありませんが、プログラムすることはできます。通常、ビューに対して行うことはxml のみです。)

つまり、アクティビティのメソッドにAsyncTask派生物を配置するのは間違っています。OTOH、アクティビティでAsyncTasksを使用してはならない場合、AsyncTaskはその魅力を失います。以前は迅速で簡単な修正として宣伝されていました。


5

AsyncTaskからのコンテキストへの参照でメモリリークが発生する危険性があるかどうかはわかりません。

それらを実装する通常の方法は、アクティビティのいずれかのメソッドのスコープ内に新しいAsyncTaskインスタンスを作成することです。したがって、アクティビティが破棄された場合、AsyncTaskが完了すると、到達できなくなり、ガベージコレクションの対象になりますか?したがって、AsyncTask自体はハングアップしないため、アクティビティへの参照は重要ではありません。


2
true-しかし、タスクが無期限にブロックされた場合はどうなりますか?タスクは、ブロッキング操作を実行することを意図しています。そこにあなたのメモリリークがあります。
Matthias

1
エンドレスループで何かを実行するワーカー、またはたとえばI / O操作でロックするだけのワーカー。
Matthias

2

あなたの活動に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]);
            }
        }
    }

}

これは、Droid-Fuで最初に行ったものと似ています。コンテキストオブジェクトへの弱い参照のマップを保持し、コールバックを実行するための最新の参照(利用可能な場合)を取得するためにタスクコールバックでルックアップを行います。しかし、私たちのアプローチは、このマッピングを維持する単一のエンティティが存在することを意味しましたが、あなたのアプローチはそうではないので、これは確かにより優れています。
Matthias

1
ロボスパイスをご覧になりましたか?github.com/octo-online/robospice。私はこのシステムがさらに良いと信じています。
Snicolas 2012

フロントページのサンプルコードは、コンテキスト参照をリークしているように見えます(内部クラスは外部クラスへの暗黙的な参照を保持します)。
Matthias

@Matthias、その通りです。そのため、ActivityにWeakReferenceを保持する静的内部クラスを提案します。
Snicolas 2012

1
@マティアス、これは話題から外れ始めていると思います。しかし、ローダーはそのままではキャッシュを提供しません。さらに、ローダーはlibよりも冗長になる傾向があります。実際にはかなりうまくカーソルを処理しますが、ネットワーキングには、キャッシングとサービスに基づく別のアプローチの方が適しています。neilgoodman.net/2011/12/26/を参照してくださいパート1および2
Snicolas

1

onPause()所有するアクティビティのメソッドをオーバーライドしてAsyncTask、そこからをキャンセルしないのはなぜですか?


それはそのタスクが何をしているかに依存します。単にデータをロードまたは読み取るだけの場合、それは問題ありません。ただし、リモートサーバー上の一部のデータの状態が変化する場合は、タスクを最後まで実行できるようにすることをお勧めします。
Vit Khudenko、2012

@Arhimedと私は、UIスレッドを保持している場合、onPauseそれを他の場所に保持しているのと同じくらい悪いと思いますか?つまり、ANRを取得できますか?
ジェフアクセルロッド

丁度。onPauseANRを取得するリスクがあるため、UIスレッドをブロックすることはできません。
Vit Khudenko、2012

1

あなたは絶対的に正しいです-それが、データをフェッチするためのアクティビティで非同期タスク/ローダーを使用することから離れる動きが勢いを増している理由です。新しい方法の1つは、データの準備ができたら基本的にコールバックを提供するVolleyフレームワークを使用することです。MVCモデルとの整合性が大幅に向上します。VolleyはGoogle I / O 2013で人気を博しました。なぜこれを知らない人が増えているのかわかりません。


おかげで...調べてみます... AsyncTaskが嫌いな私の理由は、PostExecuteで1組の命令で立ち往生するためです...インターフェイスを使用したり、毎回オーバーライドしたりしない限り、それが必要。
カリンリンチン

0

個人的には、Threadを拡張し、コールバックインターフェイスを使用してUIを更新します。FCの問題がなければ、AsyncTaskを正しく機能させることはできませんでした。実行プールの管理には、非ブロッキングキューも使用します。


1
さて、あなたの強制終了はおそらく私が言及した問題によるものでした:スコープ外に出た(つまり、ウィンドウが破壊された)コンテキストを参照しようとしたため、フレームワーク例外が発生しました。
Matthias

いいえ...実際は、AsyncTaskに組み込まれているのがキューの問題でした。私は常にgetApplicationContext()を使用しています。数回の操作であればAsyncTaskに問題はありません...しかし、バックグラウンドでアルバムアートを更新するメディアプレーヤーを書いています...私のテストでは、アートのない120枚のアルバムを持っています...アプリが完全に閉じず、asynctaskがエラーをスローしていたので、代わりに、プロセスを管理するキューを備えたシングルトンクラスを作成しました。
androidworkz 2010

0

キャンセルは機能すると思いましたが、機能しません。

ここで彼らはそれについてRTFMingしています:

""タスクが既に開始されている場合、mayInterruptIfRunningパラメータは、このタスクを実行しているスレッドがタスクを停止するために中断されるべきかどうかを決定します。 "

ただし、スレッドが割り込み可能であることを意味するものではありません。これはJavaの問題であり、AsyncTaskの問題ではありません。」

http://groups.google.com/group/android-developers/browse_thread/thread/dcadb1bc7705f1bb/add136eb4949359d?show_docid=add136eb4949359d


0

AsyncTaskは、Activity、Context、ContextWrapperなどとより密接に結合しているものと考えるのがよいでしょう。スコープを完全に理解すると、より便利になります。

ライフサイクルにキャンセルポリシーがあり、最終的にはガベージコレクションされ、アクティビティへの参照が保持されなくなり、ガベージコレクションもできるようにしてください。

コンテキストから離れるときにAsyncTaskをキャンセルしないと、メモリリークとNullPointerExceptionsが発生します。単純なダイアログのトーストのようなフィードバックを提供するだけの場合は、アプリケーションコンテキストのシングルトンがNPEの問題を回避するのに役立ちます。

AsyncTaskはすべてが悪いわけではありませんが、予期しない落とし穴につながる可能性のある多くの魔法が確実に進行しています。


-1

「操作の経験」について:すべてのAsyncTaskとともにプロセスを強制終了することが可能で、Androidはユーザーが何も言及しないようにアクティビティスタックを再作成します。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.