Czy AsyncTask jest naprawdę wadliwy koncepcyjnie, czy po prostu czegoś mi brakuje?

Badałem ten problem od miesięcy, wymyśliłem różne rozwiązania, z których nie jestem zadowolony, ponieważ wszystkie są masywnymi hakerami. Nadal nie mogę uwierzyć, że klasa, która ma wady w projektowaniu, weszła do frameworka i nikt o tym nie mówi, więc chyba coś mi umyka.

Problem jest z AsyncTask. Zgodnie z dokumentacją to

" pozwala na wykonanie tła operacje i publikowanie wyników na na Wątek UI bez konieczności manipulowania wątki i / lub uchwyty."

Przykład dalej pokazuje, jak przykładowa metoda showDialog() jest wywoływana w onPostExecute(). Wydaje mi się to jednak całkowicie wymyślone, ponieważ wyświetlanie okna dialogowego zawsze wymaga odniesienia do ważnego Context, A AsyncTask nigdy nie może zawierać silnego odniesienia do obiektu kontekstowego.

Powód jest oczywisty: co jeśli aktywność zostanie zniszczona, co wywołało zadanie? To może dzieje się cały czas, np. dlatego, że przewróciłeś ekran. Jeśli zadanie zawierałoby odniesienie do kontekstu, który je utworzył, nie tylko trzymasz bezużyteczny obiekt kontekstowy (okno zostanie zniszczone i każda interakcja interfejsu użytkownika zakończy się niepowodzeniem z wyjątkiem!), ryzykujesz nawet spowodowanie wycieku pamięci.

O ile moja logika nie jest tutaj wadliwa, to tłumaczy się to na: onPostExecute() jest całkowicie bezużyteczne, bo co dobrego dla tej metody, aby uruchomić na wątku UI, jeśli nie masz dostęp do jakiegokolwiek kontekstu? Nie możesz zrobić tu niczego znaczącego.

Jednym obejściem byłoby nie przekazywanie instancji kontekstu do Asynktasku, ale instancja Handler. To działa: ponieważ Handler luźno wiąże kontekst i zadanie, można wymieniać wiadomości między nimi bez ryzyka wycieku(prawda ?). Ale oznaczałoby to, że założenie AsyncTask, a mianowicie, że nie musisz zawracać sobie głowy obsługującymi, jest błędne. Wydaje się również nadużywać obsługi, ponieważ wysyłasz i odbierasz wiadomości w tym samym wątku (tworzysz je w wątku UI i wysyłasz przez niego w onpostexecute (), która jest również wykonywana w wątku UI).

Na domiar tego wszystkiego, nawet z tym obejściem, nadal masz problem, że gdy kontekst zostanie zniszczony, nie masz żadnego zapisu zadań, które wystrzelił. Oznacza to, że musisz ponownie uruchomić wszystkie zadania podczas ponownego tworzenia kontekstu, np. po zmianie orientacji ekranu. To jest powolne i marnotrawstwo.

Moje rozwiązanie tego (jako zaimplementowane w Bibliotece Droid-Fu) jest zachowanie mapowania WeakReference S z nazw komponentów do ich bieżących wystąpień na unikalnym obiekcie aplikacji. Za każdym razem, gdy uruchamiana jest AsyncTask, zapisuje kontekst wywołania w tej mapie, a przy każdym wywołaniu zwrotnym pobiera bieżącą instancję kontekstu z tego mapowania. Dzięki temu nigdy nie będziesz odwoływać się do przestarzałej instancji kontekstowej i zawsze masz dostęp do prawidłowego kontekstu w wywołaniach zwrotnych, dzięki czemu możesz to zrobić sensownie Interfejs działa tam. Również nie wycieka, ponieważ referencje są słabe i są usuwane, gdy żadna instancja danego komponentu już nie istnieje.

Mimo to jest to skomplikowane obejście i wymaga podklasowania niektórych klas Biblioteki Droid-Fu, co czyni to dość inwazyjnym podejściem.

Teraz po prostu chcę wiedzieć: czy po prostu czegoś mi brakuje, czy AsyncTask jest naprawdę całkowicie wadliwy? Jakie są Twoje doświadczenia w pracy z nim? Jak rozwiązałeś te problem?

Dzięki za wkład.

Author: Bobrovsky, 2010-07-28

12 answers

A może coś takiego:

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(...);
    }
}
 86
Author: hackbod,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-05-22 14:05:04

Powód jest oczywisty: co jeśli aktywność zostaje zniszczona, co uruchomił zadanie?

Ręcznie odłącz Aktywność od AsyncTask W onDestroy(). Ręcznie ponownie powiązać nową aktywność z AsyncTask w onCreate(). Wymaga to statycznej klasy wewnętrznej lub standardowej klasy Java, plus może 10 linii kodu.

 20
Author: CommonsWare,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-07-29 00:59:41

Wygląda na to, że AsyncTask jest nieco bardziej niż tylko koncepcyjnie wadliwe. Jest również bezużyteczny ze względu na problemy ze zgodnością. Android Docs czyta:

gdy po raz pierwszy wprowadzono, asynchroniczne zadania były wykonywane seryjnie na jednym wątku tła. począwszy od pączka, została ona zmieniona na pulę wątków pozwalającą na równoległe działanie wielu zadań. rozpoczynając pracę, zadania są z powrotem wykonywane na jednym wątku, aby uniknąć wspólnej aplikacji błędy spowodowane wykonaniem równoległym. Jeśli naprawdę chcesz równoległego wykonania, możesz użyć executeOnExecutor(Executor, Params...) wersja tej metody z THREAD_POOL_EXECUTOR; jednakże, patrz komentarz tam Ostrzeżenia dotyczące jego stosowania.

Zarówno executeOnExecutor() i THREAD_POOL_EXECUTORdodane w API poziom 11 (Android 3.0.x, plaster miodu).

Oznacza to, że jeśli utworzysz dwa AsyncTask s, aby pobrać dwa pliki, drugie pobieranie rozpocznie się dopiero po zakończeniu pierwszego. Jeśli czatujesz przez dwa serwery, a pierwszy serwer jest wyłączony, nie połączysz się z drugim przed połączeniem z pierwszym. (O ile oczywiście nie użyjesz nowych funkcji API11, ale to sprawi, że Twój kod będzie niezgodny z 2.x).

I jeśli chcesz celować w oba 2.x i 3.0+, rzeczy stają się naprawdę trudne.

Dodatkowo, docs mówią:

Uwaga: Innym problemem, który możesz napotkać podczas korzystania z wątku roboczego, jest nieoczekiwane ponowne uruchomienie aktywności z powodu zmiana konfiguracji runtime (np. gdy użytkownik zmieni orientację ekranu), która może zniszczyć Twój wątek roboczy . Aby zobaczyć, jak można utrzymać zadanie podczas jednego z tych restartów i jak prawidłowo anulować zadanie, gdy działanie zostanie zniszczone, zobacz kod źródłowy aplikacji Shelves sample.

 15
Author: 18446744073709551615,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-01-31 04:44:26

Prawdopodobnie wszyscy, w tym Google, nadużywamy AsyncTask z punktu widzenia MVC.

Aktywność jest kontrolerem i kontroler nie powinien uruchamiać operacji, które mogą przetrwać widok . Oznacza to, że asynchroniczne zadania powinny być używane z modelu , z klasy, która nie jest związana z cyklem życia aktywności - pamiętaj, że działania są niszczone podczas rotacji. (Co do widoku , zazwyczaj nie programuje się klas pochodzących z np. android.widget.Button, ale możesz. Zazwyczaj jedyną rzeczą jaką robisz w widoku jest xml.)

Innymi słowy, błędem jest umieszczanie pochodnych Asynktasku w metodach działań. OTOH, jeśli nie musimy używać AsyncTasks w działaniach, AsyncTask traci swoją atrakcyjność: kiedyś był reklamowany jako szybki i łatwy fix.
 12
Author: 18446744073709551615,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-01-31 05:37:48

Nie jestem pewien, czy to prawda, że ryzykujesz wyciek pamięci z odniesieniem do kontekstu z Asynktasku.

Zwyczajowym sposobem ich implementacji jest utworzenie nowej instancji AsyncTask w ramach jednej z metod działania. Więc jeśli aktywność zostanie zniszczona, to po zakończeniu AsyncTask nie będzie nieosiągalna, a następnie kwalifikuje się do zbierania śmieci? Więc odniesienie do aktywności nie będzie miało znaczenia, ponieważ sama AsyncTask nie będzie się kręcić.

 5
Author: oli,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-07-28 23:52:57

Byłoby bardziej wytrzymałe, aby utrzymać tydzień na swojej aktywności:

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

}
 2
Author: Snicolas,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-09-20 22:56:33

Dlaczego po prostu nie nadpisać metody onPause() w aktywności właściciela i anulować AsyncTask stamtąd?

 1
Author: Jeff Axelrod,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-08-08 05:23:27

Masz absolutną rację - dlatego nabierają rozpędu odejście od używania zadań/ładowarek asynchronicznych w ćwiczeniach do pobierania danych. Jednym z nowych sposobów jest użycie frameworka Volley, który zasadniczo zapewnia wywołanie zwrotne po przygotowaniu danych - znacznie bardziej zgodne z modelem MVC. Volley został spopularyzowany w Google I/O 2013. Nie wiem, dlaczego więcej ludzi nie jest tego świadomych.

 1
Author: C0D3LIC1OU5,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-07-15 14:25:15

Osobiście po prostu przedłużam wątek i używam interfejsu zwrotnego do aktualizacji interfejsu użytkownika. Nigdy nie mogłem uzyskać AsyncTask do pracy prawo bez problemów FC. Używam również kolejki Nie blokującej do zarządzania pulą realizacji.

 0
Author: androidworkz,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-07-29 00:03:22

Myślałem, że cancel działa, ale nie działa.

Tutaj o tym mówią:

"" jeśli zadanie już się rozpoczęło, to może parametr określa, czy wątek wykonujący to zadanie powinien być przerwany w próbie zatrzymania zadania."

Nie oznacza to jednak, że wątek jest przerywany. To jest Java thing, not an AsyncTask rzecz."

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

 0
Author: nir,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-12-14 02:20:55

Lepiej byłoby myśleć o AsyncTask jako o czymś, co jest ściślej powiązane z aktywnością, kontekstem, Contextwrapperem itp. Jest to bardziej wygodne, gdy jego zakres jest w pełni zrozumiały.

Upewnij się, że masz Zasady Anulowania w swoim cyklu życia, aby ostatecznie były zbierane śmieci i nie przechowują odniesienia do Twojej aktywności, a także mogą być zbierane śmieci.

Bez anulowania Asynctasku podczas przechodzenia od kontekstu napotkasz wycieki pamięci i NullPointerExceptions, jeśli po prostu potrzebujesz przekazać opinię jak Toast proste okno dialogowe, a następnie singleton kontekstu aplikacji pomoże uniknąć problemu z NPE.

AsyncTask nie jest zły, ale na pewno dzieje się wiele magii, która może prowadzić do nieprzewidzianych pułapek.

 0
Author: jtuchek,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-09-07 17:29:49

Co do "doświadczenia pracy z nim": jest możliwe Aby Zabij proces wraz ze wszystkimi zadaniami asynchronicznymi Android odtworzy stos aktywności, aby użytkownik o niczym nie wspominał.

 -1
Author: 18446744073709551615,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:10:37