Apakah AsyncTask benar-benar cacat secara konseptual atau apakah saya hanya melewatkan sesuatu?


264

Saya telah menyelidiki masalah ini selama berbulan-bulan sekarang, muncul dengan solusi yang berbeda untuk itu, yang saya tidak senang karena mereka semua peretasan besar. Saya masih tidak percaya bahwa kelas yang cacat dalam desain membuatnya masuk ke kerangka kerja dan tidak ada yang membicarakannya, jadi saya kira saya pasti kehilangan sesuatu.

Masalahnya dengan AsyncTask. Menurut dokumentasi itu

"memungkinkan untuk melakukan operasi latar belakang dan menerbitkan hasil pada utas UI tanpa harus memanipulasi utas dan / atau penangan."

Contoh kemudian terus menunjukkan bagaimana beberapa showDialog()metode teladan dipanggil onPostExecute(). Ini, bagaimanapun, tampaknya sepenuhnya dibuat - buat untuk saya, karena menunjukkan dialog selalu membutuhkan referensi ke yang valid Context, dan AsyncTask tidak boleh memiliki referensi yang kuat untuk objek konteks .

Alasannya jelas: bagaimana jika aktivitas itu hancur yang memicu tugas? Ini dapat terjadi setiap saat, misalnya karena Anda membalik layar. Jika tugas akan menyimpan referensi ke konteks yang membuatnya, Anda tidak hanya berpegang pada objek konteks yang tidak berguna (jendela akan hancur dan interaksi UI apa pun akan gagal dengan pengecualian!), Anda bahkan berisiko membuat kebocoran memori.

Kecuali jika logika saya cacat di sini, ini berarti: onPostExecute()sama sekali tidak berguna, karena apa gunanya metode ini berjalan di utas UI jika Anda tidak memiliki akses ke konteks apa pun? Anda tidak dapat melakukan sesuatu yang berarti di sini.

Satu solusi adalah dengan tidak mengirimkan instance konteks ke AsyncTask, tetapi sebuah Handlerinstance. Itu bekerja: karena Handler secara longgar mengikat konteks dan tugas, Anda dapat bertukar pesan di antara mereka tanpa risiko kebocoran (kan?). Tetapi itu berarti bahwa premis AsyncTask, yaitu bahwa Anda tidak perlu repot dengan penangan, salah. Sepertinya juga menyalahgunakan Handler, karena Anda mengirim dan menerima pesan pada utas yang sama (Anda membuatnya pada utas UI dan mengirimkannya dalam onPostExecute () yang juga dijalankan pada utas UI).

Untuk melengkapi semuanya, bahkan dengan penyelesaian itu, Anda masih memiliki masalah bahwa ketika konteksnya dihancurkan, Anda tidak memiliki catatan tugas yang dipecatnya. Itu berarti bahwa Anda harus memulai kembali tugas apa pun ketika menciptakan kembali konteks, misalnya setelah perubahan orientasi layar. Ini lambat dan boros.

Solusi saya untuk ini (seperti yang diterapkan di perpustakaan Droid-Fu ) adalah mempertahankan pemetaan WeakReferences dari nama komponen ke instance mereka saat ini pada objek aplikasi yang unik. Setiap kali AsyncTask dimulai, ia merekam konteks panggilan di peta itu, dan pada setiap panggilan balik, ia akan mengambil instance konteks saat ini dari pemetaan itu. Ini memastikan bahwa Anda tidak akan pernah merujuk contoh konteks basi dan Anda selalu memiliki akses ke konteks yang valid dalam panggilan balik sehingga Anda dapat melakukan pekerjaan UI yang bermakna di sana. Itu juga tidak bocor, karena referensi lemah dan dihapus ketika tidak ada contoh komponen yang diberikan ada lagi.

Namun, ini merupakan solusi yang kompleks dan mengharuskan untuk mensubkelas beberapa kelas perpustakaan Droid-Fu, menjadikannya pendekatan yang cukup mengganggu.

Sekarang saya hanya ingin tahu: Apakah saya hanya melewatkan sesuatu secara besar-besaran atau apakah AsyncTask sepenuhnya cacat? Bagaimana pengalaman Anda bekerja dengannya? Bagaimana Anda memecahkan masalah ini?

Terima kasih atas masukan Anda.


1
Jika Anda penasaran, kami baru saja menambahkan kelas ke pustaka inti pengapian yang disebut IgnitedAsyncTask, yang menambahkan dukungan untuk akses konteks tipe-aman di semua panggilan balik menggunakan pola sambungkan / lepaskan yang dijelaskan oleh Dianne di bawah ini. Ini juga memungkinkan untuk membuang pengecualian dan menanganinya dalam panggilan balik yang terpisah. Lihat github.com/kaeppler/ignition-core/blob/master/src/com/github/…
Matthias


1
Ini pertanyaan terkait juga.
Alex Lockwood

Saya menambahkan tugas-tugas async ke daftar array dan pastikan untuk menutup semuanya pada titik tertentu.
NightSkyCode

Jawaban:


86

Bagaimana dengan sesuatu yang seperti ini:

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
Ya, mActivity akan menjadi! = Null, tetapi jika tidak ada referensi ke instance Pekerja Anda, maka referensi apa pun yang instance tersebut akan dikenakan penghapusan sampah juga. Jika tugas Anda tidak berjalan selamanya, berarti Anda memiliki kebocoran memori (tugas Anda) - belum lagi bahwa Anda menghabiskan baterai telepon. Selain itu, seperti yang disebutkan di tempat lain, Anda dapat mengatur mActivity menjadi nol di onDestroy.
EboMike

13
Metode onDestroy () menetapkan mActivity ke nol. Tidak masalah siapa yang memegang referensi ke aktivitas sebelum itu, karena masih berjalan. Dan jendela aktivitas akan selalu valid hingga onDestroy () dipanggil. Dengan mengatur ke nol di sana, tugas async akan tahu bahwa aktivitas tidak lagi valid. (Dan ketika konfigurasi berubah, aktivitas sebelumnya onDestroy () dipanggil dan yang berikutnya onCreate () dijalankan tanpa ada pesan pada loop utama yang diproses di antara mereka sehingga AsyncTask tidak akan pernah melihat keadaan tidak konsisten.)
hackbod

8
benar, tetapi masih tidak menyelesaikan masalah terakhir yang saya sebutkan: bayangkan tugas mengunduh sesuatu dari internet. Menggunakan pendekatan ini, jika Anda membalik layar 3 kali saat tugas sedang berjalan, itu akan dimulai kembali dengan setiap rotasi layar, dan setiap tugas kecuali yang terakhir membuang hasilnya karena referensi aktivitasnya nol.
Matthias

11
Untuk mengakses di latar belakang, Anda harus melakukan sinkronisasi yang sesuai di sekitar mActivity dan berurusan dengan menjalankan saat-saat null, atau meminta utas latar hanya mengambil Context.getApplicationContext () yang merupakan instance global tunggal untuk aplikasi. Konteks aplikasi dibatasi dalam apa yang dapat Anda lakukan (tidak ada UI seperti Dialog misalnya) dan membutuhkan perhatian (penerima terdaftar dan binding layanan akan dibiarkan selamanya jika Anda tidak membersihkannya), tetapi umumnya sesuai untuk kode yang tidak terkait dengan konteks komponen tertentu.
hackbod

4
Itu sangat membantu, terima kasih Dianne! Saya berharap dokumentasi akan sebaik mungkin.
Matthias

20

Alasannya jelas: bagaimana jika aktivitas itu hancur yang memicu tugas?

Secara manual memisahkan aktivitas dari AsyncTaskdalam onDestroy(). Kaitkan kembali aktivitas baru secara manual ke AsyncTaskdalam onCreate(). Ini membutuhkan kelas dalam statis atau kelas Java standar, ditambah mungkin 10 baris kode.


Hati-hati dengan referensi statis - Saya telah melihat benda-benda dikumpulkan sampah meskipun ada referensi kuat statis untuk mereka. Mungkin efek samping dari pemuat kelas Android, atau bahkan bug, tetapi referensi statis bukanlah cara yang aman untuk bertukar status di seluruh siklus hidup aktivitas. Objek aplikasi adalah, bagaimanapun, itu sebabnya saya menggunakannya.
Matthias

10
@ Matthias: Saya tidak mengatakan untuk menggunakan referensi statis. Saya mengatakan untuk menggunakan kelas dalam statis. Ada perbedaan substansial, meskipun keduanya memiliki nama "statis".
CommonsWare


5
Saya melihat - kunci mereka di sini adalah getLastNonConfigurationInstance (), bukan kelas dalam statis sekalipun. Kelas dalam statis tidak menyimpan referensi implisit ke kelas luarnya sehingga secara semantik setara dengan kelas publik biasa. Hanya peringatan: onRetainNonConfigurationInstance () TIDAK dijamin akan dipanggil ketika suatu kegiatan terganggu (gangguan juga bisa berupa panggilan telepon), jadi Anda harus membagi Tugas Anda di onSaveInstanceState (), juga, untuk benar-benar solid larutan. Tapi tetap saja, ide yang bagus.
Matthias

7
Um ... onRetainNonConfigurationInstance () selalu dipanggil ketika aktivitas sedang dalam proses dihancurkan dan diciptakan kembali. Tidak masuk akal untuk menelepon di waktu lain. Jika beralih ke aktivitas lain terjadi, aktivitas saat ini dijeda / dihentikan, tetapi tidak dihancurkan, sehingga tugas async dapat terus berjalan dan menggunakan instance aktivitas yang sama. Jika selesai dan mengatakan menampilkan dialog, dialog akan ditampilkan dengan benar sebagai bagian dari aktivitas itu dan dengan demikian tidak akan ditampilkan kepada pengguna sampai mereka kembali ke aktivitas. Anda tidak dapat memasukkan AsyncTask ke dalam Bundle.
hackbod

15

Sepertinya AsyncTasksedikit lebih dari sekedar cacat secara konseptual . Itu juga tidak dapat digunakan oleh masalah kompatibilitas. Dokumen Android membaca:

Ketika pertama kali diperkenalkan, AsyncTasks dieksekusi secara serial pada utas latar belakang tunggal. Dimulai dengan DONUT, ini diubah menjadi kumpulan untaian yang memungkinkan beberapa tugas beroperasi secara paralel. Mulai HONEYCOMB, tugas kembali dieksekusi pada utas tunggal untuk menghindari kesalahan aplikasi umum yang disebabkan oleh eksekusi paralel. Jika Anda benar-benar ingin eksekusi paralel, Anda dapat menggunakan executeOnExecutor(Executor, Params...) versi metode ini dengan THREAD_POOL_EXECUTOR ; Namun, lihat komentar di sana untuk peringatan tentang penggunaannya.

Kedua executeOnExecutor()dan THREAD_POOL_EXECUTORyang Ditambahkan di tingkat API 11 (3.0.x Android, Honeycomb).

Ini berarti bahwa jika Anda membuat dua AsyncTaskuntuk mengunduh dua file, unduhan ke-2 tidak akan mulai sampai yang pertama selesai. Jika Anda mengobrol melalui dua server, dan server pertama mati, Anda tidak akan terhubung ke yang kedua sebelum koneksi ke yang pertama kali habis. (Kecuali jika Anda menggunakan fitur API11 baru, tentu saja, tetapi ini akan membuat kode Anda tidak kompatibel dengan 2.x).

Dan jika Anda ingin menargetkan 2.x dan 3.0+, semuanya menjadi sangat rumit.

Selain itu, dokumen mengatakan:

Perhatian: Masalah lain yang mungkin Anda temui saat menggunakan utas pekerja adalah restart yang tidak terduga dalam aktivitas Anda karena perubahan konfigurasi runtime (seperti ketika pengguna mengubah orientasi layar), yang dapat menghancurkan utas pekerja Anda . Untuk melihat bagaimana Anda bisa bertahan tugas Anda selama salah satu dari ini restart dan bagaimana cara membatalkan tugas dengan benar ketika aktivitas dihancurkan, lihat kode sumber untuk aplikasi sampel Rak.


12

Mungkin kita semua, termasuk Google, menyalahgunakan AsyncTaskdari sudut pandang MVC .

Suatu Kegiatan adalah Pengendali , dan pengontrol tidak boleh memulai operasi yang mungkin lebih lama dari Tampilan . Yaitu, AsyncTasks harus digunakan dari Model , dari kelas yang tidak terikat pada siklus hidup Kegiatan - ingat bahwa Kegiatan dihancurkan secara bergiliran. (Mengenai View , Anda biasanya tidak memprogram kelas yang berasal dari mis. Android.widget.Button, tetapi Anda bisa. Biasanya, satu-satunya hal yang Anda lakukan tentang View adalah xml.)

Dengan kata lain, salah menempatkan derivatif AsyncTask dalam metode Kegiatan. OTOH, jika kita tidak boleh menggunakan AsyncTasks dalam Aktivitas, AsyncTask kehilangan daya tariknya: Dulu diiklankan sebagai perbaikan cepat dan mudah.


5

Saya tidak yakin benar bahwa Anda berisiko kebocoran memori dengan referensi ke konteks dari AsyncTask.

Cara biasa untuk mengimplementasikannya adalah dengan membuat instance AsyncTask baru dalam ruang lingkup salah satu metode Kegiatan. Jadi jika aktivitasnya dihancurkan, maka setelah AsyncTask selesai, bukankah itu tidak dapat dijangkau dan memenuhi syarat untuk pengumpulan sampah? Jadi referensi ke aktivitas itu tidak masalah karena AsyncTask itu sendiri tidak akan bergaul.


2
benar - tetapi bagaimana jika tugas itu memblok tanpa batas? Tugas dimaksudkan untuk melakukan operasi pemblokiran, mungkin bahkan yang tidak pernah berakhir. Di sana Anda memiliki kebocoran memori Anda.
Matthias

1
Setiap pekerja yang melakukan sesuatu dalam satu lingkaran tanpa akhir, atau apapun yang hanya mengunci misalnya pada operasi I / O.
Matthias

2

Akan lebih kuat untuk menyimpan WeekReference pada aktivitas Anda:

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]);
            }
        }
    }

}

Ini mirip dengan apa yang pertama kali kita lakukan dengan Droid-Fu. Kami akan menyimpan peta referensi lemah untuk objek konteks dan akan melakukan pencarian dalam tugas panggilan balik untuk mendapatkan referensi terbaru (jika tersedia) untuk menjalankan panggilan balik terhadap. Namun pendekatan kami berarti ada satu entitas yang mempertahankan pemetaan ini, sedangkan pendekatan Anda tidak, jadi ini memang lebih baik.
Matthias

1
Apakah Anda sudah melihat RoboSpice? github.com/octo-online/robospice . Saya percaya sistem ini bahkan lebih baik.
Snicolas

Kode sampel di halaman depan terlihat seperti membocorkan referensi konteks (kelas dalam menyimpan referensi implisit ke kelas luar.) Tidak yakin !!
Matthias

@ Matthias, Anda benar, itu sebabnya saya mengusulkan kelas batin statis yang akan mengadakan Referensi Lemah pada Kegiatan.
Snicolas

1
@Matithias, saya percaya ini mulai di luar topik. Tapi loader tidak menyediakan caching di luar kotak seperti yang kita lakukan, lebih dari itu, loader cenderung lebih bertele-tele daripada lib kita. Sebenarnya mereka menangani kursor yang cukup baik tetapi untuk jaringan, pendekatan yang berbeda, berdasarkan caching dan layanan lebih cocok. Lihat neilgoodman.net/2011/12/26/… bagian 1 & 2
Snicolas

1

Mengapa tidak mengganti onPause()metode dalam Aktivitas yang dimiliki dan membatalkannya AsyncTaskdari sana?


itu tergantung pada apa tugas itu lakukan. jika hanya memuat / membaca beberapa data, maka itu akan baik-baik saja. tetapi jika itu mengubah keadaan beberapa data pada server jauh maka kami lebih suka memberikan tugas kemampuan untuk menjalankan sampai akhir.
Vit Khudenko

@Arhimed dan saya mengambilnya jika Anda memegang utas UI onPauseyang sama buruknya dengan memegangnya di tempat lain? Yaitu, Anda bisa mendapatkan ANR?
Jeff Axelrod

persis. kita tidak dapat memblokir utas UI (baik itu onPauseanithing maupun lainnya) karena kita berisiko terkena ANR.
Vit Khudenko

1

Anda benar sekali - itulah sebabnya mengapa gerakan yang jauh dari menggunakan tugas / pemuat async dalam aktivitas untuk mengambil data mendapatkan momentum. Salah satu cara baru adalah dengan menggunakan kerangka kerja Volley yang pada dasarnya menyediakan panggilan balik setelah data siap - jauh lebih konsisten dengan model MVC. Volley dipopulerkan di Google I / O 2013. Tidak yakin mengapa lebih banyak orang tidak menyadari hal ini.


terima kasih untuk itu ... saya akan memeriksanya ... alasan saya untuk tidak menyukai AsyncTask adalah karena itu membuat saya terjebak dengan satu set instruksi padaPostExecute ... kecuali saya meretasnya seperti menggunakan antarmuka atau menimpanya setiap kali Saya membutuhkannya.
carinlynchin

0

Secara pribadi, saya hanya memperluas Utas dan menggunakan antarmuka panggilan balik untuk memperbarui UI. Saya tidak pernah bisa membuat AsyncTask bekerja dengan benar tanpa masalah FC. Saya juga menggunakan antrian yang tidak menghalangi untuk mengelola kumpulan eksekusi.


1
Nah, kekuatan Anda ditutup mungkin karena masalah yang saya sebutkan: Anda mencoba referensi konteks yang keluar dari ruang lingkup (yaitu jendelanya telah dihancurkan), yang akan menghasilkan kerangka pengecualian.
Matthias

Tidak ... sebenarnya itu karena antrian menyebalkan yang dibangun ke AsyncTask. Saya selalu menggunakan getApplicationContext (). Saya tidak memiliki masalah dengan AsyncTask jika hanya beberapa operasi ... tapi saya menulis media player yang memperbarui seni album di latar belakang ... dalam pengujian saya, saya memiliki 120 album tanpa seni ... jadi, sementara aplikasi saya tidak menutup semua jalan, asynctask melempar kesalahan ... jadi alih-alih saya membangun kelas tunggal dengan antrian yang mengelola proses dan sejauh ini berfungsi dengan baik.
androidworkz

0

Saya pikir batal bekerja tetapi tidak.

di sini mereka RTFMing tentang hal itu:

"" Jika tugas sudah dimulai, maka parameter mayInterruptIfRunning menentukan apakah utas yang menjalankan tugas ini harus diinterupsi dalam upaya untuk menghentikan tugas. "

Itu tidak berarti, bagaimanapun, bahwa utas itu interruptible. Itu hal Java, bukan hal AsyncTask. "

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


0

Anda akan lebih baik memikirkan AsyncTask sebagai sesuatu yang lebih erat digabungkan dengan Activity, Context, ContextWrapper, dll. Lebih nyaman ketika ruang lingkupnya dipahami sepenuhnya.

Pastikan bahwa Anda memiliki kebijakan pembatalan dalam siklus hidup Anda sehingga pada akhirnya akan menjadi sampah yang dikumpulkan dan tidak lagi menyimpan referensi ke aktivitas Anda dan itu juga bisa menjadi sampah yang dikumpulkan.

Tanpa membatalkan AsyncTask Anda saat melintasi jauh dari Konteks Anda, Anda akan mengalami kebocoran memori dan NullPointerExceptions, jika Anda hanya perlu memberikan umpan balik seperti Toast dialog sederhana, maka tunggal Konteks Aplikasi Anda akan membantu menghindari masalah NPE.

AsyncTask tidak semuanya buruk tetapi pasti ada banyak keajaiban yang terjadi yang dapat menyebabkan beberapa jebakan yang tidak terduga.


-1

Mengenai "pengalaman bekerja dengannya": adalah mungkin untuk menghentikan proses bersama dengan semua AsyncTasks, Android akan menciptakan kembali tumpukan aktivitas sehingga pengguna tidak akan menyebutkan apa pun.

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.