AsyncTask действительно концептуально несовершенен или я просто что-то упустил? - PullRequest
257 голосов
/ 29 июля 2010

Я уже несколько месяцев исследую эту проблему, придумаю разные решения, которые меня не устраивают, так как все они массивные хаки.Я до сих пор не могу поверить, что класс с ошибками в дизайне превратил его в фреймворк, и никто не говорит об этом, так что, наверное, я просто что-то упустил.

Проблема в AsyncTask.Согласно документации,

"позволяет выполнять фоновые операции и публиковать результаты в потоке пользовательского интерфейса без необходимости манипулировать потоками и / или обработчиками."

В этом примерепродолжает показывать, как в onPostExecute() вызывается примерный showDialog() метод.Это, однако, мне кажется полностью надуманным , потому что для отображения диалога всегда нужна ссылка на действительный Context, а AsyncTask никогда не должен содержать сильную ссылку на объект контекста .

Причина очевидна: что, если действие будет уничтожено, что вызвало задачу?Это может происходить постоянно, например, потому что вы перевернули экран.Если задача будет содержать ссылку на контекст, который ее создал, вы не только держитесь за бесполезный объект контекста (окно будет уничтожено, и любое взаимодействие с пользовательским интерфейсом завершится неудачей с исключением!)Вы даже рискуете создать утечку памяти.

Если моя логика здесь не лишена недостатков, это приводит к тому, что onPostExecute() совершенно бесполезен, потому что это хорошо для того, чтобы этот метод выполнялся в потоке пользовательского интерфейса, если вы ненет доступа к любому контексту?Вы не можете сделать что-либо значимое здесь.

Один из обходных путей - не передавать экземпляры контекста в AsyncTask, а экземпляр Handler.Это работает: поскольку обработчик свободно связывает контекст и задачу, вы можете обмениваться сообщениями между ними, не рискуя утечкой (верно?).Но это будет означать, что предпосылка AsyncTask, а именно, что вам не нужно беспокоиться о обработчиках, неверна.Это также похоже на злоупотребление обработчиком, поскольку вы отправляете и получаете сообщения в одном потоке (вы создаете его в потоке пользовательского интерфейса и отправляете через него в onPostExecute (), который также выполняется в потоке пользовательского интерфейса).

Чтобы завершить все это, даже с этим обходным приемом, у вас все еще есть проблема, что, когда контекст разрушен, у вас есть нет записей задач, которые он выполнил.Это означает, что вам нужно перезапускать любые задачи при воссоздании контекста, например, после изменения ориентации экрана.Это медленно и расточительно.

Мое решение этого (так как реализовано в библиотеке Droid-Fu ) состоит в том, чтобы поддерживать отображение WeakReference s из имен компонентов в их текущих экземплярах науникальный объект приложения.Всякий раз, когда AsyncTask запускается, он записывает вызывающий контекст в этой карте и при каждом обратном вызове извлекает текущий экземпляр контекста из этого сопоставления.Это гарантирует, что вы никогда не будете ссылаться на устаревший экземпляр контекста и , у вас всегда есть доступ к действительному контексту в обратных вызовах, чтобы вы могли выполнять там значимую работу пользовательского интерфейса.Он также не пропускает, потому что ссылки слабы и очищаются, когда больше не существует экземпляров данного компонента.

Тем не менее, это сложный обходной путь и требует подкласса некоторых Droid-Fuбиблиотечные классы, что делает этот подход довольно навязчивым.

Теперь я просто хочу знать: Я просто что-то упустил или AsyncTask действительно полностью испорчен?Как ваш опыт работы с ним?Как вы решили эту проблему?

Спасибо за ваш вклад.

Ответы [ 12 ]

85 голосов
/ 29 июля 2010

Как насчет этого:

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(...);
    }
}
20 голосов
/ 29 июля 2010

Причина очевидна: что, если действие будет уничтожено, что вызвало выполнение задачи?

Вручную отсоедините действие от AsyncTask в onDestroy().Вручную заново свяжите новое действие с AsyncTask в onCreate().Для этого требуется либо статический внутренний класс, либо стандартный класс Java, а также, возможно, 10 строк кода.

15 голосов
/ 30 января 2013

Похоже, AsyncTask немного больше , чем просто концептуально некорректно . Это также невозможно использовать из-за проблем совместимости. Документы Android гласят:

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

Оба executeOnExecutor() и THREAD_POOL_EXECUTOR Добавлены в API уровень 11 (Android 3.0.x, HONEYCOMB).

Это означает, что если вы создадите два AsyncTask s для загрузки двух файлов, вторая загрузка не начнется, пока не закончится первый. Если вы общаетесь через два сервера, а первый сервер не работает, вы не сможете подключиться ко второму, пока не истечет время подключения к первому. (Если, конечно, вы не используете новые функции API11, но это сделает ваш код несовместимым с 2.x).

И если вы хотите нацелиться на 2.x и 3.0+, материал становится действительно сложным.

Кроме того, документы говорят:

Предупреждение. Другая проблема, с которой вы можете столкнуться при использовании рабочего потока, - непредвиденные перезапуски в вашей активности из-за изменения конфигурации среды выполнения (например, когда пользователь меняет ориентацию экрана), что может привести к разрушению вашего работника. нить . Чтобы увидеть, как вы можете сохранить свою задачу во время одного из этих перезапусков и как правильно отменить задачу, когда действие уничтожено, см. Исходный код примера приложения Shelves.

12 голосов
/ 30 января 2013

Вероятно, мы все, включая Google, неправильно используем AsyncTask с точки зрения MVC .

Деятельность - это Контроллер , и контроллер долженне запускать операции, которые могут пережить просмотр .То есть AsyncTasks следует использовать из Model , из класса, который не связан с жизненным циклом Activity - помните, что Activity уничтожаются при ротации.(Что касается View , вы обычно не программируете классы, производные, например, от android.widget.Button, но можете. Обычно единственное, что вы делаете с View , этоxml.)

Другими словами, неправильно размещать производные AsyncTask в методах Деятельности.OTOH, если мы не должны использовать AsyncTasks в Деятельности, AsyncTask теряет свою привлекательность: раньше его рекламировали как быстрое и простое исправление.

5 голосов
/ 29 июля 2010

Я не уверен, что это правда, что вы рискуете утечкой памяти со ссылкой на контекст из AsyncTask.

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

2 голосов
/ 21 сентября 2012

Было бы надежнее сохранить WeekReference для вашей активности:

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

}
1 голос
/ 15 июля 2014

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

1 голос
/ 08 августа 2012

Почему бы просто не переопределить метод onPause() в действии и отменить AsyncTask оттуда?

0 голосов
/ 07 сентября 2014

Было бы лучше думать об AsyncTask как о чем-то, что более тесно связано с Activity, Context, ContextWrapper и т. Д. Удобнее, когда его область действия полностью понятна.

Убедитесь, что у вас есть политика отмены в вашем жизненном цикле, чтобы в конечном итоге она собирала мусор и больше не хранила ссылку на вашу деятельность, а также она могла собирать мусор.

Не отменяя AsyncTask при обходе вашего контекста, вы столкнетесь с утечками памяти и исключениями NullPointerException, если вам просто нужно будет предоставить отзыв, такой как простое диалоговое окно Toast, тогда единственный контекст вашего Application Context поможет избежать проблемы NPE. 1005 *

AsyncTask не так уж и плох, но определенно происходит много магии, которая может привести к непредвиденным ловушкам.

0 голосов
/ 30 января 2013

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

...