AsyncTask gerçekten kavramsal olarak kusurlu mu yoksa sadece bir şey mi kaçırıyorum?


264

Bu problemi aylardır araştırdım, hepsi büyük bir hack olduğu için memnun olmadığım farklı çözümler buldum. Tasarımda kusurlu bir sınıfın onu çerçeveye dönüştürdüğüne hala inanamıyorum ve hiç kimse bunun hakkında konuşmuyor, bu yüzden sanırım sadece bir şey eksik olmalıyım.

Sorun şu AsyncTask. Belgelere göre

"iş parçacıklarını ve / veya işleyicileri değiştirmek zorunda kalmadan arka plan işlemleri gerçekleştirmeye ve sonuçları UI iş parçacığında yayınlamaya izin verir."

Örnek daha sonra örnek bir showDialog()yöntemin nasıl çağrıldığını göstermeye devam eder onPostExecute(). Bununla birlikte, bu benim için tamamen çelişkili görünmektedir , çünkü bir diyalogun gösterilmesi her zaman geçerli bir referansa ihtiyaç duyar Contextve AsyncTask hiçbir zaman bir bağlam nesnesine güçlü bir referans içermemelidir .

Sebep açıktır: ya aktiviteyi tahrip eden görevi tahrip ederse? Bu her zaman olabilir, örneğin ekranı çevirdiğiniz için. Görev, onu oluşturan bağlama referans verirse, yalnızca işe yaramaz bir bağlam nesnesine tutunmazsınız (pencere yok edilir ve herhangi bir UI etkileşimi bir istisna dışında başarısız olur!) bellek sızıntısı.

Mantığım burada kusurlu olmadıkça, bu onPostExecute()şu anlama gelir: tamamen işe yaramaz, çünkü herhangi bir bağlama erişiminiz yoksa bu yöntemin UI iş parçacığında çalışması ne kadar iyi? Burada anlamlı bir şey yapamazsınız.

Geçici çözümlerden biri bağlam örneklerini bir AsyncTask'e değil bir Handlerörneğe geçirmektir . Bu işe yarar: Bir İşleyici bağlamı ve görevi gevşek bir şekilde bağladığından, sızıntı riski olmadan aralarında mesaj alışverişi yapabilirsiniz (değil mi?). Ancak bu, AsyncTask'in öncülünün, yani işleyicileri rahatsız etmenize gerek olmadığı anlamına gelir. Aynı iş parçacığında mesaj gönderip aldığınız için de Handler'ı kötüye kullanmak gibi görünüyor (bunu UI iş parçacığında oluşturup UI iş parçacığında da yürütülen onPostExecute () içinde gönderirsiniz).

Her şeyden önemlisi, bu geçici çözümde bile, bağlam yok edildiğinde, tetiklediği görevlerin hiçbir kaydına sahip olmamanız sorununa sahipsiniz . Bu, içeriği yeniden oluştururken, örneğin bir ekran yönlendirme değişikliğinden sonra, tüm görevleri yeniden başlatmanız gerektiği anlamına gelir. Bu yavaş ve savurgan.

Buna benim çözümüm ( Droid-Fu kütüphanesinde uygulandığı gibi ), WeakReferencebileşen adlarından benzersiz uygulama nesnesindeki mevcut örneklerine bir eşleme sağlamaktır . Bir AsyncTask başlatıldığında, çağıran bağlamı bu haritaya kaydeder ve her geri çağrıldığında geçerli bağlam örneğini bu eşlemeden alır. Eğer eski bir bağlam örneğini başvuru asla Bu olmasını sağlar ve orada anlamlı UI işi yapabilir böylece her zaman geri çağrıları geçerli bir içeriğe erişebilir. Ayrıca sızıntı yapmaz, çünkü referanslar zayıftır ve artık belirli bir bileşenin örneği olmadığında temizlenir.

Yine de, karmaşık bir çözümdür ve Droid-Fu kütüphane sınıflarının bazılarını alt sınıflara ayırmayı gerektirir, bu da bunu oldukça müdahaleci bir yaklaşım haline getirir.

Şimdi sadece bilmek istiyorum: Bir şeyi büyük ölçüde özlüyor muyum yoksa AsyncTask gerçekten tamamen kusurlu mu? Deneyimleriniz onunla nasıl çalışıyor? Bu sorunu nasıl çözdün?

Girdiniz için teşekkürler.


1
Merak ediyorsanız, yakın zamanda IgnitedAsyncTask adlı ateşleme çekirdeği kitaplığına bir sınıf ekledik. Ayrıca istisnalar atmaya ve bunları ayrı bir geri çağrıda ele almaya izin verir. Bkz. Github.com/kaeppler/ignition-core/blob/master/src/com/github/…
Matthias

şuna bir göz atın: gist.github.com/1393552
Matthias

1
Bu soru da ilişkilidir.
Alex Lockwood

Zaman uyumsuz görevleri bir arrayliste ekliyorum ve hepsini belirli bir noktada kapattığımdan eminim.
NightSkyCode

Yanıtlar:


86

Böyle bir şeye ne dersin:

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
Evet, mActivity! = Null olur, ancak İşçi örneğinize referans yoksa, o zaman bu örnek de çöp kaldırmaya tabi olur. Göreviniz sonsuza dek sürerse, yine de bir telefon sızıntısı var (göreviniz) - telefonun pilini tükettiğinizden bahsetmiyoruz bile. Ayrıca, başka bir yerde belirtildiği gibi, onDestroy'da mActivity'yi null değerine ayarlayabilirsiniz.
EboMike

13
OnDestroy () yöntemi, mActivity'yi null değerine ayarlar. Bundan önce faaliyete kimin referans verdiğinin önemi yok, çünkü hala çalışıyor. Etkinliğin penceresi, onDestroy () çağrılıncaya kadar her zaman geçerli olacaktır. Orada null olarak ayarlandığında, zaman uyumsuz görev etkinliğin artık geçerli olmadığını bilecektir. (Ve bir yapılandırma değiştiğinde, önceki etkinliğin onDestroy () yöntemi çağrılır ve bir sonraki etkinliğin onCreate () yöntemi, aralarında işlenen ana döngüde herhangi bir ileti olmadan çalışır, böylece AsyncTask hiçbir zaman tutarsız bir durum
görmez

8
doğru, ama yine de bahsettiğim son sorunu çözmüyor: görevin internetten bir şey indirdiğini hayal edin. Bu yaklaşımı kullanarak, görev çalışırken ekranı 3 kez çevirirseniz, her ekran döndürme ile yeniden başlatılır ve son referans dışındaki her görev, etkinlik referansı boş olduğu için sonucunu atar.
Matthias

11
Arka planda erişmek için, ya mActivity etrafında uygun senkronizasyonu koymanız ve null olduğunda zamanlarla çalışmayla ilgilenmeniz ya da arka plan iş parçacığının sadece uygulama için tek bir global örnek olan Context.getApplicationContext () yöntemini almanız gerekir. Uygulama bağlamı yapabileceğiniz şeylerle sınırlıdır (örneğin, Dialog gibi kullanıcı arayüzü yoktur) ve biraz dikkat gerektirir (bunları temizlemezseniz kayıtlı alıcılar ve servis bağlamaları sonsuza kadar bırakılır), ancak genellikle olmayan kodlar için uygundur. belirli bir bileşenin bağlamıyla bağlantılı değildir.
hackbod

4
Bu inanılmaz yardımcı oldu, teşekkürler Dianne! İlk etapta dokümantasyonun iyi olmasını diliyorum.
Matthias

20

Nedeni açıktır: ya görevi yok eden ve görevi tetikleyen bir olay olursa?

El ile ilgili etkinliği ilişkisini AsyncTaskin onDestroy(). Yeni etkinliği AsyncTaskin ile manuel olarak yeniden ilişkilendirin onCreate(). Bu, statik bir iç sınıf veya standart bir Java sınıfı ve ayrıca 10 satır kod gerektirir.


Statik referanslara dikkat edin - Nesnelere statik güçlü referanslar olmasına rağmen çöp toplandığını gördüm. Belki de Android'in sınıf yükleyicisinin bir yan etkisi veya hatta bir hata, ancak statik referanslar bir etkinlik yaşam döngüsü boyunca durum alışverişi için güvenli bir yol değildir. Ancak uygulama nesnesi bu yüzden bunu kullanıyorum.
Matthias

10
@Matthias: Statik referanslar kullanmayı söylemedim. Statik bir iç sınıf kullanmayı söyledim. Her ikisi de adlarında "statik" olmasına rağmen, önemli bir fark vardır.
CommonsWare


5
Anlıyorum - burada anahtar olsun statik iç sınıf olsa getLastNonConfigurationInstance (). Statik bir iç sınıf, dış sınıfına dolaylı bir referans göstermez, bu nedenle anlamsal olarak düz bir ortak sınıfa eşdeğerdir. Yalnızca bir uyarı: onRetainNonConfigurationInstance () öğesinin bir etkinlik kesildiğinde (kesinti bir telefon görüşmesi de olabilir) çağrılması garanti EDİLMEMEKTEDİR, bu nedenle Görevinizi onSaveInstanceState () öğesinde de tam olarak sağlamlaştırmanız gerekir. çözüm. Ama yine de, güzel fikir.
Matthias

7
Um ... onRetainNonConfigurationInstance () yöntemi, etkinlik yok edilip yeniden oluşturulduğunda her zaman çağrılır. Diğer zamanlarda aramak mantıklı değil. Başka bir etkinliğe geçiş gerçekleşirse, geçerli etkinlik duraklatılır / durdurulur, ancak yok olmaz, böylece zaman uyumsuz görev aynı aktivite örneğini çalıştırmaya ve kullanmaya devam edebilir. Bitirilir ve diyelim ki bir iletişim kutusu görüntülenirse, iletişim kutusu bu etkinliğin bir parçası olarak doğru bir şekilde görüntülenir ve dolayısıyla etkinliğe dönene kadar kullanıcıya gösterilmez. AsyncTask'ı bir Pakete koyamazsınız.
hackbod

15

Sadece kavramsal olarak kusurlu olmaktan AsyncTaskbiraz daha fazlası gibi görünüyor . Uyumluluk sorunları nedeniyle de kullanılamaz. Android dokümanları şunları okur:

İlk tanıtıldığında, AsyncTasks tek bir arka plan iş parçacığında seri olarak yürütüldü. DONUT ile başlayarak, bu, birden fazla görevin paralel olarak çalışmasına izin veren bir iş parçacığı havuzuna değiştirildi. HONEYCOMB başlatıldığında, paralel yürütmenin neden olduğu yaygın uygulama hatalarını önlemek için görevler tek bir iş parçacığında yürütülür. Paralel yürütmeyi gerçekten istiyorsanız executeOnExecutor(Executor, Params...) , bu yöntemin sürümünü THREAD_POOL_EXECUTOR ; ancak, kullanımıyla ilgili uyarılar için oradaki açıklamaya bakınız.

Hem executeOnExecutor()ve THREAD_POOL_EXECUTORedilir API düzeyinde 11'de eklendi (Android 3.0.x, PETEK).

Bu, AsyncTaskiki dosyayı indirmek için iki s oluşturursanız , ikinci indirme ilki bitene kadar başlamayacaktır. İki sunucu üzerinden sohbet ediyorsanız ve ilk sunucu kapalıysa, ilk sunucuya bağlantı zaman aşımına uğramadan ikinci sunucuya bağlanamazsınız. (Tabii ki yeni API11 özelliklerini kullanmadığınız sürece, ancak bu kodunuzu 2.x ile uyumsuz hale getirecektir).

Hem 2.x hem de 3.0+ hedeflemek istiyorsanız, işler gerçekten zor olur.

Ayrıca dokümanlar şunları söylüyor:

Dikkat: bir çalışan iş parçacığı kullanırken karşılaşabileceğiniz başka bir sorun (örneğin, kullanıcı ekran yönünü değiştirdiğinde gibi) nedeniyle çalışma zamanı yapılandırma değişikliği etkinlik beklenmedik yeniden başlatılır olduğunu sizin iş parçacığı yok edebilir . Bu yeniden başlatmalardan birinde görevinize nasıl devam edebileceğinizi ve etkinlik yok edildiğinde görevin nasıl düzgün bir şekilde iptal edileceğini görmek için, Shelves örnek uygulamasının kaynak koduna bakın.


12

Muhtemelen Google dahil tüm, suiistimal AsyncTaskgelen MVC bakış açısından.

Etkinlik bir Denetleyicidir ve denetleyici, Görünümü geçebilecek işlemleri başlatmamalıdır . Yani, AsyncTasks, Modelden , Etkinlik yaşam döngüsüne bağlı olmayan bir sınıftan kullanılmalıdır - Etkinliklerin rotasyon sırasında yok edildiğini unutmayın. ( Görünüm ile ilgili olarak , genellikle android.widget.Button'dan türetilmiş sınıfları programlamazsınız, ancak bunu yapabilirsiniz. Genellikle, Görünüm hakkında yaptığınız tek şey xml'dir.)

Başka bir deyişle, AsyncTask türevlerini Faaliyet yöntemlerine yerleştirmek yanlıştır. OTOH, Faaliyetlerde AsyncTasks kullanmamamız gerekiyorsa, AsyncTask çekiciliğini kaybeder: eskiden hızlı ve kolay bir düzeltme olarak ilan edilirdi.


5

Bir AsyncTask içeriğinden bir referansla bir bellek sızıntısı riskinin olduğundan emin değilim.

Bunları uygulamanın genel yolu, Faaliyetin yöntemlerinden biri kapsamında yeni bir AsyncTask örneği oluşturmaktır. Etkinlik yok edilirse, AsyncTask tamamlandıktan sonra erişilemez ve ardından çöp toplama için uygun olmaz mı? Bu nedenle, aktiviteye referans önemli değildir çünkü AsyncTask'ın kendisi takılmayacaktır.


2
doğru - ama görev süresiz olarak engellenirse? Görevler, belki de asla sonlandırılmamış olanları bile engelleme işlemleri gerçekleştirmek içindir. Orada hafıza sızıntısı var.
Matthias

1
Sonsuz bir döngüde bir şey yapan herhangi bir işçi veya örneğin bir G / Ç işleminde kilitlenen herhangi bir şey.
Matthias

2

Etkinliğiniz için bir WeekReference tutmak daha sağlam olurdu:

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

}

Bu Droid-Fu ile ilk yaptığımıza benzer. Bağlam nesnelerine zayıf referansların bir haritasını tutarız ve geri çağrıyı çalıştırmak için en son referansı (varsa) almak için görev geri çağrılarında arama yaparız. Ancak yaklaşımımız, bu haritayı koruyan tek bir varlık olduğu anlamına gelirken, yaklaşımınız böyle değildi, bu gerçekten daha hoş.
Matthias

1
RoboSpice'e baktınız mı? github.com/octo-online/robospice . Bu sistemin daha iyi olduğuna inanıyorum.
Snicolas

Ön sayfadaki örnek kod, bir bağlam referansı sızdırıyor gibi görünüyor (bir iç sınıf, dış sınıfa dolaylı bir referans tutar.) İkna değil !!
Matthias

@Matthias, haklısın, bu yüzden Faaliyette WeakReference tutacak statik bir iç sınıf öneriyorum.
Snicolas

1
@Matthias, bunun konu dışı olmaya başladığına inanıyorum. Ancak yükleyiciler, yaptığımız gibi önbelleklemeyi sağlamaz, daha fazlası, yükleyiciler lib'imizden daha ayrıntılı olma eğilimindedir. Aslında oldukça iyi imleçleri idare ediyorlar, ancak ağ oluşturma için önbelleğe alma ve hizmete dayanan farklı bir yaklaşım daha uygun. Bkz. Neilgoodman.net/2011/12/26/… bölüm 1 & 2
Snicolas

1

Neden yalnızca onPause()sahiplik etkinliğindeki yöntemi geçersiz kılmayabilir ve AsyncTaskoradan iptal edebilirsiniz ?


bu görevin ne yaptığına bağlıdır. sadece bazı verileri yükler / okursa Tamam olur. ancak uzak sunucudaki bazı verilerin durumunu değiştirirse, göreve sonuna kadar çalışma olanağı vermeyi tercih ederiz.
Vit Khudenko

@Arhimed ve UI iş parçacığını onPausebaşka bir yerde tutmak kadar kötü olduğu takdirde alır mıyım? Yani, bir ANR alabilir misin?
Jeff Axelrod

kesinlikle. onPauseANR elde etme riski taşıdığımız için UI iş parçacığını (a veya başka anithing olsun) engelleyemeyiz .
Vit Khudenko

1

Kesinlikle haklısınız - işte bu nedenle, veri getirmek için aktivitelerde zaman uyumsuz görevler / yükleyiciler kullanmaktan uzak bir hareket ivme kazanıyor. Yeni yollardan biri , veriler hazır olduğunda esasen bir geri arama sağlayan bir Volley çerçevesi kullanmaktır - MVC modeliyle çok daha tutarlı. Volley Google I / O 2013'te popülize edildi. Bunun neden daha fazla insanın farkında olmadığından emin değilim.


bunun için teşekkürler ... içine bakacağım ... AsyncTask'ı beğenmeme nedenim, arayüzleri kullanmak veya her seferinde geçersiz kılmak gibi kesmek istemediğim için beni bir dizi talimatla sıkıştırabilmem ... Ona ihtiyacım var.
carinlynchin

0

Şahsen ben sadece Thread uzatmak ve kullanıcı arayüzünü güncellemek için bir geri arama arayüzü kullanın. AsyncTask'ın FC sorunları olmadan düzgün çalışmasını asla sağlayamadım. Ayrıca yürütme havuzunu yönetmek için engelleme olmayan bir kuyruk kullanın.


1
Kapanış gücünüz muhtemelen bahsettiğim sorundan kaynaklanıyordu: kapsam dışında kalan bir çerçeveye (yani penceresi yok edilmişti) başvurmaya çalıştınız, bu da bir çerçeve istisnasına neden olacaktır.
Matthias

Hayır. Her zaman getApplicationContext () kullanıyorum. AsyncTask ile sadece birkaç işlem varsa sorunum yok ... ama albüm sanatını arka planda güncelleyen bir medya oynatıcı yazıyorum ... testimde sanatsız 120 albümüm var ... yani, benim app tüm yolu kapatmadı, asynctask hatalar atıyordu ... bu yüzden süreçleri yöneten bir kuyruk ile tek bir sınıf inşa ve şimdiye kadar harika çalışıyor.
androidworkz


0

AsyncTask'ı bir Etkinlik, Bağlam, ContextWrapper, vb. İle daha sıkı bir şekilde bağlanmış bir şey olarak düşünmek daha iyi olurdu.

Yaşam döngünüzde bir iptal politikasının bulunduğundan emin olun, böylece sonunda çöp toplanır ve etkinliğinize artık referans olmaz ve çöp de toplanabilir.

Bağlamınızdan uzaklaşırken AsyncTask'ınızı iptal etmeden, bellek sızıntılarına ve NullPointerExceptions ile karşılaşırsınız, eğer basit bir Tost gibi bir geri bildirim sağlamanız gerekiyorsa, Uygulama Bağlamınızın bir tonu NPE sorununu önlemeye yardımcı olacaktır.

AsyncTask hiç de fena değil ama kesinlikle beklenmedik tuzaklara yol açabilecek çok fazla büyü var.


-1

"Onunla çalışan deneyimleri" gelince: o mümkün için öldürme işlemi tüm AsyncTasks birlikte kullanıcı şey söz olmaz ki, Android etkinlik yığınını yeniden yaratacaktır.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.