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


264

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

Problem tkwi w tym AsyncTask. Zgodnie z dokumentacją to

„umożliwia wykonywanie operacji w tle i publikowanie wyników w wątku interfejsu użytkownika bez konieczności manipulowania wątkami i / lub programami obsługi”.

Przykład dalej pokazuje, jak showDialog()wywoływana jest przykładowa metoda onPostExecute(). Wydaje mi się to jednak całkowicie wymyślone , ponieważ wyświetlenie okna dialogowego zawsze wymaga odwołania do poprawnego Context, a AsyncTask nigdy nie może zawierać silnego odwołania do obiektu kontekstu .

Powód jest oczywisty: co się stanie, jeśli działanie zostanie zniszczone, co uruchomiło zadanie? Może się to zdarzać przez cały czas, np. Dlatego, że odwróciłeś ekran. Jeśli zadanie zawiera odniesienie do kontekstu, który go utworzył, nie tylko trzymasz się bezużytecznego obiektu kontekstu (okno zostanie zniszczone, a każda interakcja interfejsu użytkownika zakończy się niepowodzeniem z wyjątkiem!), Możesz nawet ryzykować utworzenie wyciek pamięci.

O ile moja logika nie jest tutaj wadliwa, przekłada się to na: onPostExecute()jest całkowicie bezużyteczne, ponieważ po co ta metoda działa w wątku interfejsu użytkownika, jeśli nie masz dostępu do żadnego kontekstu? Nie możesz tutaj zrobić nic znaczącego.

Jednym obejściem byłoby nie przekazywanie instancji kontekstu do zadania AsyncTask, ale Handlerinstancja. To działa: ponieważ moduł obsługi luźno wiąże kontekst i zadanie, możesz wymieniać wiadomości między nimi bez ryzyka wycieku (prawda?). Oznaczałoby to jednak, że założenie AsyncTask, a mianowicie, że nie trzeba zawracać sobie głowy obsługą, jest błędne. Wygląda to również na nadużywanie modułu obsługi, ponieważ wysyłasz i odbierasz wiadomości w tym samym wątku (tworzysz go w wątku interfejsu użytkownika i wysyłasz za pośrednictwem onPostExecute (), który jest również wykonywany w wątku interfejsu użytkownika).

Podsumowując, nawet przy takim obejściu nadal masz problem polegający na tym, że gdy kontekst zostanie zniszczony, nie masz zapisanego zadania, które wykonał. Oznacza to, że musisz ponownie uruchomić wszelkie zadania podczas ponownego tworzenia kontekstu, np. Po zmianie orientacji ekranu. Jest to powolne i marnotrawstwo.

Moim rozwiązaniem tego ( zaimplementowanym w bibliotece Droid-Fu ) jest utrzymanie odwzorowania WeakReferences z nazw komponentów na ich bieżące instancje w unikalnym obiekcie aplikacji. Za każdym razem, gdy AsyncTask jest uruchamiany, zapisuje kontekst wywołania na tej mapie i przy każdym wywołaniu zwrotnym pobiera bieżącą instancję kontekstu z tego odwzorowania. Zapewnia to, że nigdy nie będziesz odwoływał się do przestarzałej instancji kontekstu i zawsze będziesz mieć dostęp do poprawnego kontekstu w wywołaniach zwrotnych, dzięki czemu możesz wykonywać znaczącą pracę interfejsu użytkownika. Nie przecieka również, ponieważ odniesienia są słabe i są kasowane, gdy nie istnieje już żadna instancja danego komponentu.

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

Teraz chcę po prostu wiedzieć: czy po prostu coś masowo mi brakuje, czy też AsyncTask jest naprawdę całkowicie wadliwy? Jakie są twoje doświadczenia z tym związane? Jak rozwiązałeś ten problem?

Dzięki za wkład.


1
Jeśli jesteś ciekawy, niedawno dodaliśmy klasę do biblioteki rdzenia zapłonu o nazwie IgnitedAsyncTask, która dodaje obsługę bezpiecznego kontekstowo dostępu do wszystkich wywołań zwrotnych za pomocą wzorca łączenia / rozłączania opisanego przez Dianne poniżej. Pozwala także na zgłaszanie wyjątków i obsługę ich w osobnym wywołaniu zwrotnym. Zobacz github.com/kaeppler/ignition-core/blob/master/src/com/github/…
Matthias,


1
To pytanie jest również powiązane.
Alex Lockwood

Dodaję zadania asynchroniczne do tablicy arraylist i upewniam się, że je wszystkie zamkną w pewnym momencie.
NightSkyCode,

Odpowiedzi:


86

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

5
Tak, mActivity będzie! = Null, ale jeśli nie będzie żadnych odwołań do instancji klasy Worker, wszelkie odniesienia do tej instancji również zostaną usunięte. Jeśli twoje zadanie działa wiecznie, to i tak masz przeciek pamięci (twoje zadanie) - nie wspominając o tym, że wyczerpujesz baterię telefonu. Poza tym, jak wspomniano w innym miejscu, możesz ustawić mActivity na zero w onDestroy.
EboMike

13
Metoda onDestroy () ustawia mActivity na null. Nie ma znaczenia, kto ma wcześniej odwołanie do działania, ponieważ nadal działa. A okno działania będzie zawsze ważne, dopóki nie zostanie wywołane onDestroy (). Ustawienie wartości null spowoduje, że zadanie asynchroniczne będzie wiedziało, że działanie nie jest już prawidłowe. (. A kiedy zmiany konfiguracyjne, dotychczasowej działalności za onDestroy () jest wywoływana i () uruchamia następną czyjaś onCreate bez żadnych komunikatów na głównej pętli przetworzonej między nimi więc AsyncTask nigdy nie zobaczy niespójnym stanie)
hackbod

8
to prawda, ale wciąż nie rozwiązuje ostatniego problemu, o którym wspomniałem: wyobraź sobie, że zadanie pobiera coś z Internetu. Korzystając z tego podejścia, jeśli ekran zostanie odwrócony 3 razy, gdy zadanie jest uruchomione, zostanie ono zrestartowane przy każdym obrocie ekranu, a każde zadanie oprócz ostatniego wyrzuca jego wynik, ponieważ jego odwołanie do działania jest zerowe.
Matthias

11
Aby uzyskać dostęp w tle, musisz albo ustawić odpowiednią synchronizację wokół mActivity i poradzić sobie z bieganiem w czasie, gdy jest ona zerowa, lub poprosić o wątek w tle, po prostu pobierz Context.getApplicationContext (), która jest pojedynczą globalną instancją dla aplikacji. Kontekst aplikacji jest ograniczony w tym, co możesz zrobić (na przykład brak interfejsu użytkownika, takiego jak Dialog) i wymaga nieco uwagi (zarejestrowani odbiorcy i powiązania usług pozostaną na zawsze, jeśli ich nie wyczyścisz), ale ogólnie jest odpowiedni dla kodu „związany z kontekstem konkretnego komponentu.
hackbod

4
To było niezwykle pomocne, dzięki Dianne! Szkoda, że ​​dokumentacja nie byłaby tak dobra.
Matthias,

20

Powód jest oczywisty: co się stanie, jeśli działanie zostanie zniszczone, co uruchomiło zadanie?

Ręcznie oddzielić działalność od AsyncTaskin onDestroy(). Ręcznie ponownie powiąż nowe działanie z AsyncTaskwejściem onCreate(). Wymaga to albo statycznej klasy wewnętrznej, albo standardowej klasy Java, a także 10 linii kodu.


Uważaj na odniesienia statyczne - widziałem obiekty, które są zbierane w pamięci, mimo że istniały na nich silne odniesienia statyczne. Być może efekt uboczny modułu ładującego Androida, a nawet błąd, ale odniesienia statyczne nie są bezpiecznym sposobem na zmianę stanu w całym cyklu życia aktywności. Jednak celem aplikacji jest to, czego używam.
Matthias

10
@Matthias: Nie mówiłem, że używam odniesień statycznych. Powiedziałem, żeby użyć statycznej klasy wewnętrznej. Istnieje znaczna różnica, mimo że oba mają nazwy „statyczne”.
CommonsWare


5
Rozumiem - kluczem tutaj jest getLastNonConfigurationInstance (), ale nie statyczna klasa wewnętrzna. Statyczna klasa wewnętrzna nie przechowuje żadnych domyślnych odniesień do swojej klasy zewnętrznej, więc jest semantycznie równoważna zwykłej klasie publicznej. Tylko ostrzeżenie: ONRetainNonConfigurationInstance () NIE jest gwarantowane, że zostanie wywołany, gdy działanie zostanie przerwane (przerwanie może być również rozmową telefoniczną), więc musisz spakować swoje zadanie również w onSaveInstanceState (), aby uzyskać naprawdę solidne rozwiązanie. Ale wciąż fajny pomysł.
Matthias

7
Um ... onRetainNonConfigurationInstance () jest zawsze wywoływany, gdy działanie jest w trakcie niszczenia i odtwarzania. Dzwonienie w innym czasie nie ma sensu. Jeśli nastąpi przełączenie na inne działanie, bieżące działanie zostanie wstrzymane / zatrzymane, ale nie zostanie zniszczone, więc zadanie asynchroniczne może kontynuować działanie i korzystać z tej samej instancji działania. Jeśli zakończy się i powiedz, że wyświetla okno dialogowe, okno dialogowe będzie poprawnie wyświetlane jako część tego działania, a tym samym nie będzie wyświetlane użytkownikowi, dopóki nie wróci do działania. Nie możesz umieścić AsyncTask w pakiecie.
hackbod

15

Wygląda na to, że AsyncTaskjest nieco więcej niż tylko wadą koncepcyjną . Nie można go również używać z powodu problemów ze zgodnością. Dokumenty Androida brzmią:

Po raz pierwszy AsyncTasks były wykonywane szeregowo w jednym wątku w tle. Począwszy od DONUT, zmieniono to na pulę wątków, umożliwiając równoległe wykonywanie wielu zadań. Po uruchomieniu HONEYCOMB zadania wracają do wykonywania w jednym wątku, aby uniknąć typowych błędów aplikacji spowodowanych równoległym wykonywaniem. Jeśli naprawdę chcesz wykonywać równolegle, możesz użyć executeOnExecutor(Executor, Params...) wersji tej metody z THREAD_POOL_EXECUTOR ; jednak zobacz komentarz tam ostrzeżenia o jego użyciu.

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

Oznacza to, że jeśli utworzysz dwa AsyncTasks, aby pobrać dwa pliki, drugie pobieranie nie rozpocznie się, dopóki pierwszy się nie skończy. Jeśli czatujesz przez dwa serwery, a pierwszy serwer jest wyłączony, nie nawiążesz połączenia z drugim przed upływem limitu czasu połączenia z pierwszym. (Oczywiście, chyba że użyjesz nowych funkcji API11, ale spowoduje to, że Twój kod będzie niezgodny z wersją 2.x).

A jeśli chcesz kierować reklamy zarówno do wersji 2.x, jak i 3.0+, rzeczy stają się naprawdę trudne.

Ponadto doktorzy mówią:

Przestroga: Innym problemem, który może wystąpić podczas korzystania z wątku roboczego, jest nieoczekiwany restart działania w związku ze zmianą konfiguracji środowiska wykonawczego (na przykład gdy użytkownik zmienia orientację ekranu), co może zniszczyć wątek roboczy . Aby zobaczyć, jak możesz zachować swoje zadanie podczas jednego z tych restartów i jak poprawnie anulować zadanie po zniszczeniu działania, zobacz kod źródłowy przykładowej aplikacji Półki.


12

Prawdopodobnie wszyscy, w tym Google, niewłaściwie wykorzystujemy AsyncTaskz punktu widzenia MVC .

Aktywność jest kontrolerem i kontroler nie powinien rozpoczynać operacji, które mogą przeżyć widok . Oznacza to, że AsyncTasks należy stosować z modelu , z klasy, która nie jest związana z cyklem życia działania - pamiętaj, że działania są niszczone podczas rotacji. (Jeśli chodzi o widok , zwykle nie programujesz klas pochodzących z np. Android.widget.Button, ale możesz. Zazwyczaj jedyne, co robisz z widokiem, to xml.)

Innymi słowy, niewłaściwe jest umieszczanie pochodnych AsyncTask w metodach Działania. OTOH, jeśli nie wolno nam używać AsyncTasks w działaniach, AsyncTask traci swoją atrakcyjność: kiedyś reklamowano go jako szybką i łatwą naprawę.


5

Nie jestem pewien, czy to prawda, że ​​ryzykujesz przeciek pamięci w związku z kontekstem z AsyncTask.

Typowym 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 ona nieosiągalna, a następnie kwalifikuje się do odśmiecania? Odniesienie do działania nie będzie miało znaczenia, ponieważ samo AsyncTask nie będzie się zawieszać.


2
prawda - ale co jeśli zadanie zostanie zablokowane na czas nieokreślony? Zadania mają na celu wykonywanie operacji blokujących, może nawet takich, które nigdy się nie kończą. Tam masz wyciek pamięci.
Matthias

1
Każdy pracownik, który wykonuje coś w nieskończonej pętli lub cokolwiek, co po prostu blokuje, np. Podczas operacji we / wy.
Matthias

2

Bardziej solidne byłoby zachowanie WeekReference na temat 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]);
            }
        }
    }

}

Jest to podobne do tego, co po raz pierwszy zrobiliśmy z Droid-Fu. Zachowalibyśmy mapę słabych odniesień do obiektów kontekstowych i sprawdzilibyśmy wywołania zwrotne zadania, aby uzyskać najnowsze odwołanie (jeśli jest dostępne), aby uruchomić wywołanie zwrotne. Nasze podejście oznaczało jednak, że istniał jeden byt, który utrzymywał to mapowanie, podczas gdy twoje podejście nie, więc jest to naprawdę przyjemniejsze.
Matthias,

1
Rzuciłeś okiem na RoboSpice? github.com/octo-online/robospice . Wierzę, że ten system jest jeszcze lepszy.
Snicolas,

Przykładowy kod na pierwszej stronie wygląda tak, jakby przeciekał odwołanie kontekstowe (klasa wewnętrzna zachowuje niejawne odniesienie do klasy zewnętrznej). Nie przekonany !!
Matthias

@Matthias, masz rację, dlatego proponuję statyczną klasę wewnętrzną, która będzie utrzymywać Słabe odniesienie do działania.
Snicolas,

1
@Matthias, uważam, że to zaczyna być nie na temat. Ale programy ładujące nie zapewniają buforowania od razu po wyjęciu z pudełka, podobnie jak programy ładujące, są bardziej szczegółowe niż nasza biblioteka. W rzeczywistości radzą sobie całkiem dobrze z kursorami, ale w przypadku sieci lepiej jest zastosować inne podejście, oparte na buforowaniu i usłudze. Widzieć neilgoodman.net/2011/12/26/... część 1 i 2
Snicolas

1

Dlaczego nie zastąpić onPause()metody w działaniu będącym właścicielem i anulować AsyncTaskstamtąd?


zależy od tego, co robi to zadanie. jeśli po prostu ładuje / odczytuje niektóre dane, byłoby OK. ale jeśli zmieni to stan niektórych danych na zdalnym serwerze, wolelibyśmy dać temu zadaniu możliwość uruchomienia do końca.
Vit Khudenko,

@Arhimed i wezmę to, jeśli podtrzymasz wątek interfejsu użytkownika onPause, ponieważ jest tak samo zły, jak trzymanie go gdziekolwiek indziej? Czyli możesz dostać ANR?
Jeff Axelrod

dokładnie. nie możemy zablokować wątku interfejsu użytkownika (czy to innego, onPauseczy innego), ponieważ ryzykujemy uzyskanie ANR.
Vit Khudenko,

1

Masz całkowitą rację - dlatego odejście od używania zadań asynchronicznych / modułów ładujących w działaniach do pobierania danych nabiera tempa. Jednym z nowych sposobów jest użycie struktury Volley , która zasadniczo zapewnia oddzwonienie, gdy dane są gotowe - znacznie bardziej spójne z modelem MVC. Siatkówka została zaludniona w Google I / O 2013. Nie jestem pewien, dlaczego więcej osób nie jest tego świadomych.


dzięki za to ... przyjrzę się temu ... moim powodem, dla którego nie lubię AsyncTask, jest to, że utknąłem w jednym zestawie instrukcji na PostExecute ... chyba że włamię się do niego za pomocą interfejsów lub zastąpię go za każdym razem Potrzebuję tego.
carinlynchin

0

Osobiście po prostu rozszerzam wątek i używam interfejsu zwrotnego do aktualizacji interfejsu użytkownika. Nigdy nie mogłem zmusić AsyncTask do prawidłowej pracy bez problemów z FC. Korzystam również z nieblokującej kolejki do zarządzania pulą wykonawczą.


1
Cóż, twoja siła zamknięcia była prawdopodobnie z powodu problemu, o którym wspomniałem: próbowałeś odwołać się do kontekstu, który wyszedł poza zakres (tj. Jego okno zostało zniszczone), co spowoduje wyjątek ramowy.
Matthias

Nie ... właściwie to dlatego, że kolejka jest do bani wbudowana w AsyncTask. Zawsze używam getApplicationContext (). Nie mam problemów z AsyncTask, jeśli to tylko kilka operacji ... ale piszę odtwarzacz multimedialny, który aktualizuje okładki albumów w tle ... w moim teście mam 120 albumów bez grafiki ... więc, podczas gdy moja aplikacja nie zamknęła się całkowicie, asynctask zgłaszał błędy ... więc zamiast tego zbudowałem klasę singleton z kolejką, która zarządza procesami i do tej pory działa świetnie.
androidworkz


0

Lepiej byłoby myśleć o AsyncTask jako o czymś ściślej powiązanym z Aktywnością, Kontekstem, ContextWrapper itp. Jest to wygodniejsze, gdy jego zakres jest w pełni zrozumiały.

Upewnij się, że masz zasady anulowania w swoim cyklu życia, aby w końcu zostały one wyrzucone i nie będą już zawierały odniesienia do Twojej aktywności, a także mogą być wyrzucane.

Bez anulowania AsyncTask podczas przechodzenia od kontekstu napotkasz wycieki pamięci i wyjątki NullPointerException, jeśli po prostu potrzebujesz przekazać opinię, np. Toast, proste okno dialogowe, wówczas singleton kontekstu aplikacji pomógłby uniknąć problemu NPE.

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


-1

Jeśli chodzi o „doświadczenia w pracy z nim”: możliwe jest zabicie procesu wraz ze wszystkimi AsyncTasks, Android odtworzy stos aktywności, aby użytkownik nic nie wspominał.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.