Чи AsyncTask насправді концептуально помилковий чи мені просто щось не вистачає?


264

Я досліджував цю проблему вже місяцями, придумав різні її рішення, якими я не задоволений, оскільки всі вони масові хаки. Я досі не можу повірити, що клас, який зіпсувався в дизайні, ввійшов у рамки, і ніхто про це не говорить, тому, мабуть, мені просто щось не вистачає.

Проблема в с AsyncTask. Згідно з документацією, це

"дозволяє виконувати фонові операції та публікувати результати в потоці інтерфейсу користувача, не маніпулюючи потоками та / або обробниками."

Потім приклад продовжує показувати, як showDialog()викликається деякий зразковий метод onPostExecute(). Це, однак, здається мені цілком надуманим , тому що для показу діалогового вікна завжди потрібна посилання на дійсну Context, а AsyncTask ніколи не має чіткого посилання на контекстний об'єкт .

Причина очевидна: що робити, якщо діяльність знищується, що ініціювало завдання? Це може траплятися постійно, наприклад, тому що ви перевернули екран. Якщо завдання міститиме посилання на контекст, який його створив, ви не тільки тримаєтесь за непотрібним контекстним об'єктом (вікно буде зруйновано і будь-яка взаємодія з інтерфейсом вийде з ладу за винятком!), Ви навіть ризикуєте створити витік пам'яті.

Якщо моя логіка тут хибна, це означає: onPostExecute()цілком марний, адже яке добре для цього методу запускати на потоці інтерфейсу, якщо у вас немає доступу до жодного контексту? Тут не можна зробити нічого значимого.

Одним із завдань було б передати екземпляри контексту AsyncTask, але Handlerекземпляр. Це працює: оскільки обробник невміло зв’язує контекст і завдання, ви можете обмінюватися повідомленнями між ними, не ризикуючи протікати (правда?). Але це означатиме, що передумова AsyncTask, а саме, що вам не потрібно турбуватися з обробниками, є неправильним. Це також здається зловживанням Handler, оскільки ви надсилаєте та отримуєте повідомлення в одному потоці (ви створюєте його в потоці інтерфейсу користувача та надсилаєте через нього в onPostExecute (), який також виконується в потоці інтерфейсу).

На додаток до цього, навіть із цим вирішенням, у вас все ще виникає проблема, що коли контекст руйнується, у вас немає запису про виконання завдань, які він виконував. Це означає, що вам потрібно перезапустити будь-які завдання під час створення контексту, наприклад, після зміни орієнтації екрана. Це повільно і марно.

Моє рішення цього (як реалізовано в бібліотеці Droid-Fu ) полягає в підтримці відображення WeakReferences від імен компонентів до їх поточних примірників на унікальному об’єкті програми. Кожного разу, коли запускається AsyncTask, він записує контекст виклику на цій карті, і при кожному зворотному виклику він отримує поточний екземпляр контексту з цього відображення. Це гарантує, що ви ніколи не будете посилатися на нескладний контекстний екземпляр, і ви завжди матимете доступ до дійсного контексту в зворотному звороті, щоб ви могли робити значну роботу інтерфейсу там. Він також не протікає, оскільки посилання слабкі та очищаються, коли жодного примірника певного компонента більше не існує.

Але це складний спосіб вирішення і вимагає підкласи деяких класів бібліотек Droid-Fu, що робить це досить нав'язливим підходом.

Тепер я просто хочу знати: я просто масово щось пропускаю, чи AsyncTask справді повністю помилковий? Як ви працюєте з цим своїм досвідом? Як ви вирішили ці проблеми?

Дякуємо за ваш внесок


1
Якщо вам цікаво, нещодавно ми додали клас до основної бібліотеки запалювання під назвою IgnitedAsyncTask, який додає підтримку безпечного для доступу до контексту у всіх зворотних викликах, використовуючи схему підключення / відключення, викладену Діанна нижче. Це також дозволяє викидати винятки та обробляти їх в окремому зворотному дзвінку. Дивіться github.com/kaeppler/ignition-core/blob/master/src/com/github/…
Маттіас

подивіться на це: gist.github.com/1393552
Маттіас

1
Це питання також пов'язане.
Алекс Локвуд

Я додаю задачі на асинхронізацію до масиву і обов'язково закрию їх у певний момент.
NightSkyCode

Відповіді:


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 екземпляр, то будь-які посилання на цей екземпляр також будуть підлягати видаленню сміття. Якщо ваше завдання працює назавжди, то у вас все одно витік пам'яті (ваше завдання) - не кажучи вже про те, що ви розряджаєте акумулятор телефону. Крім того, як було зазначено в іншому місці, ви можете встановити mActivity на нуль в onDestroy.
EboMike

13
Метод onDestroy () встановлює mActivity на нуль. Не важливо, хто до цього має посилання на діяльність, оскільки вона все ще виконується. І вікно активності завжди буде дійсним, поки не буде викликано onDestroy (). Встановивши для цього значення null, завдання асинхронизації буде знати, що діяльність більше не діє. (І коли конфігурація змінюється, викликається onDestroy () попередньої активності, а наступний onCreate () працює без будь-яких повідомлень у головному циклі, обробленому між ними, щоб AsyncTask ніколи не побачив невідповідний стан.)
hackbod

8
правда, але вона все ще не вирішує останню проблему, яку я згадав: уявіть, що завдання завантажує щось з Інтернету. Використовуючи цей підхід, якщо ви перевернете екран тричі під час виконання завдання, він буде перезапущений з кожним обертанням екрана, і кожне завдання, крім останнього, кидає результат, оскільки його посилання на активність є нульовим.
Маттіас

11
Щоб отримати доступ у фоновому режимі, вам або потрібно буде встановити відповідну синхронізацію навколо mActivity і мати справу з запуском у часи, коли це недійсне, або фоновим потоком просто взяти Context.getApplicationContext (), який є єдиним глобальним примірником для програми. Контекст програми обмежений тим, що ви можете робити (не користувальницький інтерфейс, наприклад, діалог), і вимагає певного догляду (зареєстровані приймачі та прив’язки до служб залишаться назавжди, якщо ви не очистите їх), але, як правило, підходить для коду, який не є не прив'язаний до контексту конкретного компонента.
хакбод

4
Це було неймовірно корисно, дякую Діанне! Я б хотів, щоб документація була в першу чергу такою ж доброю.
Маттіас

20

Причина очевидна: що робити, якщо діяльність знищується, що ініціювало завдання?

Відмежуйте діяльність вручну від AsyncTaskв onDestroy(). Вручну відновіть нову діяльність до входу AsyncTaskв onCreate(). Для цього потрібен або статичний внутрішній клас, або стандартний клас Java, плюс, можливо, 10 рядків коду.


Будьте обережні зі статичними посиланнями - я бачив, як предмети збирають сміття, хоча на них були сильні статичні посилання. Можливо побічний ефект завантажувача класу Android або навіть помилка, але статичні посилання не є безпечним способом обміну станом протягом життєвого циклу діяльності. Об'єкт програми, однак, саме тому я цим і користуюся.
Маттіас

10
@Matthias: Я не сказав використовувати статичні посилання. Я сказав застосувати статичний внутрішній клас. Існує істотна різниця, незважаючи на те, що обидва мають "статичні" у своїх назвах.
CommonsWare


5
Я бачу - тут ключовим є getLastNonConfigurationInstance (), але не статичний внутрішній клас. Статичний внутрішній клас не містить явних посилань на свій зовнішній клас, тому він семантично еквівалентний простому публічному класу. Просто попередження: onRetainNonConfigurationInstance () НЕ гарантовано викликається, коли активність перервана (перерва може бути і телефонним дзвінком), тому вам доведеться розібрати свою задачу в onSaveInstanceState () теж для справді надійного рішення. Але все-таки приємна ідея.
Маттіас

7
Гм ... onRetainNonConfigurationInstance () завжди викликається, коли діяльність знищується і створюється заново. Немає сенсу телефонувати в інший час. Якщо відбувається перехід на іншу діяльність, поточна активність призупиняється / зупиняється, але вона не знищується, тому завдання асинхронізації може продовжувати виконуватись і використовувати той самий екземпляр діяльності. Якщо він закінчить і скаже, що з'явиться діалогове вікно, діалогове вікно буде відображатися правильно як частина цієї діяльності, і, таким чином, не відображатиметься для користувача, поки вони не повернуться до активності. Ви не можете помістити AsyncTask у пакет.
хакбод

15

Схоже AsyncTask, це дещо більше, ніж просто концептуально недолік . Він також непридатний для проблем сумісності. Документи Android читали:

При першому введенні AsyncTasks виконувались послідовно на одному фоновому потоці. Починаючи з DONUT, це було змінено на пул потоків, що дозволяють паралельно працювати безлічі завдань. Починаючи HONEYCOMB, завдання повертаються до виконання в одному потоці, щоб уникнути поширених помилок програми, викликаних паралельним виконанням. Якщо ви справді хочете паралельного виконання, ви можете використовувати executeOnExecutor(Executor, Params...) версію цього методу з THREAD_POOL_EXECUTOR ; однак, дивіться коментар щодо попереджень щодо його використання.

Обидва executeOnExecutor()і THREAD_POOL_EXECUTORє Додано в рівні API 11 (Android 3.0.x, Honeycomb).

Це означає, що якщо ви створите два AsyncTasks для завантаження двох файлів, друге завантаження не розпочнеться, поки перший не закінчиться. Якщо ви спілкуєтесь через два сервери, а перший сервер не працює, ви не підключитесь до другого перед тим, як вимкнено з'єднання з першим. (Якщо, звичайно, ви не використовуєте нові функції API11, але це зробить ваш код несумісним з 2.x).

І якщо ви хочете націлити на 2.x і 3.0+, матеріал стає справді складним.

Крім того, документи говорять:

Увага: Ще одна проблема, з якою ви можете зіткнутися під час використання робочої нитки, - це несподівані перезавантаження вашої діяльності через зміну конфігурації часу виконання (наприклад, коли користувач змінює орієнтацію екрана), що може зруйнувати вашу робочу нитку . Щоб побачити, як ви можете зберегти своє завдання під час одного з цих перезавантажень та як правильно скасувати завдання, коли діяльність знищена, див. Вихідний код для прикладу програми "Шельви".


12

Напевно, ми всі, включаючи Google, зловживаємо AsyncTaskз точки зору MVC .

Діяльність - це контролер , і контролер не повинен починати операції, які можуть переживати погляд . Тобто AsyncTasks слід використовувати з Model , з класу, який не пов'язаний з життєвим циклом діяльності - пам’ятайте, що Діяльність знищується при обертанні. (Щодо перегляду , ви зазвичай не програмуєте класи, похідні, наприклад, від android.widget.Button, але ви можете. Зазвичай, єдине, що ви робите щодо перегляду - це xml.)

Іншими словами, неправильно розміщувати похідні AsyncTask у методах діяльності. ОТОХ, якщо ми не мусимо використовувати AsyncTasks в діяльності, AsyncTask втрачає свою привабливість: його раніше рекламували як швидке та просте виправлення.


5

Я не впевнений, що це правда, що ви ризикуєте витік пам'яті з посиланням на контекст AsyncTask.

Звичайний спосіб їх реалізації - створити новий екземпляр AsyncTask в межах одного з методів діяльності. Отже, якщо активність буде знищена, то після завершення роботи AsyncTask це буде недоступно, а потім придатне для збору сміття? Тому посилання на активність не матиме значення, оскільки сам AsyncTask не буде зависати.


2
правда - але що робити, якщо завдання блокується нескінченно? Завдання призначені для виконання блокуючих операцій, можливо навіть тих, які ніколи не припиняються. Там у вас витік пам'яті.
Маттіас

1
Будь-який працівник, який виконує щось у нескінченному циклі, або що-небудь, що тільки закривається, наприклад, при операції вводу / виводу.
Маттіас

2

Було б надійніше зберегти тижневу референцію про свою діяльність:

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. Ми збережемо карту слабких посилань на контекстні об'єкти і зробимо пошук у зворотних викликах завдань, щоб отримати останню посилання (за наявності) для запуску зворотного виклику. Однак наш підхід означав, що існує одна сутність, яка підтримує це відображення, тоді як ваш підхід - ні, це справді приємніше.
Маттіас

1
Ви подивилися на RoboSpice? github.com/octo-online/robospice . Я вірю, що ця система ще краща.
Snicolas

Зразок коду на першій сторінці виглядає так, що він просочується контекстною посиланням (внутрішній клас зберігає неявну посилання на зовнішній клас.) Не переконаний !!
Маттіас

@Matthias, ти маєш рацію, тому я пропоную статичний внутрішній клас, який матиме слабкі референції щодо діяльності.
Snicolas

1
@Matthias, я вважаю, що це починає поза темою. Але навантажувачі не забезпечують кешування поза коробкою, як і ми. Більше того, навантажувачі, як правило, більш багатослівні, ніж у нас. Насправді вони добре справляються з курсорами, але для створення мереж краще підходить інший підхід, заснований на кешуванні та службі. Дивіться neilgoodman.net/2011/12/26/… частина 1 та 2
Snicolas

1

Чому б просто не змінити onPause()метод у власній діяльності та скасувати AsyncTaskзвідти?


це залежить від того, що це завдання робить. якщо він просто завантажує / читає деякі дані, то це було б нормально. але якщо це змінить стан деяких даних на віддаленому сервері, тоді ми вважаємо за краще надати завдання виконати до кінця.
Віт Худенко

@Arhimed, і я вважаю, якщо ви тримаєте потік інтерфейсу в onPauseтому, що це так само погано, як тримати його деінде? Тобто, ви могли отримати ANR?
Джефф Аксельрод

точно. ми не можемо заблокувати потік інтерфейсу користувача (будь то onPauseаніт або інше), оскільки ми ризикуємо отримати ANR.
Віт Худенко

1

Ви абсолютно праві - ось чому відхилення від використання асинхронних завдань / завантажувачів у заходах для отримання даних набирає обертів. Одним із нових способів є використання фреймворку Volley, який по суті забезпечує зворотний виклик, коли дані будуть готові - набагато більше відповідає моделі MVC. Воллі популяризувався в Google I / O 2013. Не впевнений, чому більше людей не знають про це.


дякую за це ... я збираюся заглянути в це ... моя причина не подобається AsyncTask в тому, що це змушує мене застрягти з одним набором інструкцій onPostExecute ... якщо я не зламаю його, як користуватися інтерфейсами або переосмислювати його кожен раз Мені це потрібно.
carinlynchin

0

Особисто я просто розширюю Thread і використовую інтерфейс зворотного дзвінка для оновлення інтерфейсу користувача. Я ніколи не міг змусити AsyncTask працювати правильно без проблем з FC. Я також використовую неблокуючу чергу для управління пулом виконання.


1
Що ж, ваше закриття було, мабуть, через проблему, про яку я згадав: ви спробували посилатися на контекст, який вийшов за рамки (тобто його вікно було знищено), що призведе до виключення з фреймворку.
Маттіас

Ні ... насправді це було тому, що черга смокче, що вбудовано в AsyncTask. Я завжди використовую getApplicationContext (). У мене немає проблем з AsyncTask, якщо це лише кілька операцій ... але я пишу медіаплеєр, який оновлює зображення альбому на задньому плані ... у моєму тесті у мене 120 альбомів без мистецтва ... так, поки мій додаток не закривався повністю, асинтактаск кидав помилки ... тому замість цього я створив однотонний клас із чергою, яка керує процесами, і поки він прекрасно працює.
androidworkz

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, якщо вам просто потрібно надати зворотний зв'язок, як простий діалог Toast, тоді однократне повідомлення вашого контексту додатків допоможе уникнути проблеми NPE.

AsyncTask - це не все погано, але, безумовно, відбувається багато магії, яка може призвести до деяких непередбачених підводних каменів.


-1

Що стосується «досвіду роботи з ним»: це можливо , щоб вбити процес разом з усіма AsyncTasks, Android буде заново створити стек активності , так що користувач не буде нічого згадувати.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.