Является ли AsyncTask действительно концептуально ошибочным или я просто что-то пропустил?

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

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

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

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

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

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

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

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

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

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

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

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

Ответ 1

Как насчет чего-то вроде этого:

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

Ответ 2

Причина очевидна: что, если разрушается деятельность вызвала задачу?

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

Ответ 3

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

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

Оба executeOnExecutor() и THREAD_POOL_EXECUTOR добавляются в уровень API 11 (Android 3.0.x, HONEYCOMB).

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

И если вы хотите настроить таргетинг как на 2.x, так и на 3.0+, материал становится очень сложным.

Кроме того, docs говорят:

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

Ответ 4

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

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

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

Ответ 5

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

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

Ответ 6

Было бы более надежно сохранять 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]);
            }
        }
    }

}

Ответ 7

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

Ответ 8

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

Ответ 9

Я думал, что отмена работает, но это не так.

здесь они RTFMing об этом:

"" Если задача уже началась, то mayInterruptIfRunning параметр определяет, должен ли поток, выполняющий эту задачу, прерван в попытке остановить выполнение задачи.

Это не означает, однако, что поток прерывается. Который Java, а не вещь AsyncTask. "

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

Ответ 10

Вам лучше подумать о AsyncTask как о том, что более тесно связано с Activity, Context, ContextWrapper и т.д. Это более удобно, когда его область полностью понята.

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

Без отмены AsyncTask при переходе от вашего Контекста вы столкнетесь с утечками памяти и NullPointerExceptions, если вам просто нужно предоставить обратную связь, например, Toast, простое диалоговое окно, тогда одноэлемент вашего контекста приложений поможет избежать проблемы с NPE.

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

Ответ 11

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

Ответ 12

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