1001 способ потерять загруженную картинку

Обратимся к материалу статьи Отзывчивое Android-приложение или 1001 способ загрузить картинку и рассмотрим представленные примеры. Код скопирован из статьи-оригинала и подчищен для ускорения восприятия.

1. Thread и Runnable

class WorkingThread extends Thread {
    @Override
    public void run() {
        // Фоновая работа
    }
}

Конечно, это самый простой и очевидный для любого джависта способ. new WorkingThread().start(); — что может быть проще? Эквивалент, только с Runnable:

new Thread(() -> {
    // Фоновая работа
}).start();

Проблемы

а) по умолчанию приоритет нового потока равен приоритету того потока, из которого он был запущен. То есть поток, запущенный из UI-потока, имеет максимальный приоритет, как и сам UI-поток. Не забудьте понизить приоритет своего потока;

б) это низкоуровневое решение. Например, пулы потоков, в отличие от одиночных потоков, могут принимать блоки кода на выполнение, ставить его в очередь, возвращать результаты — с Thread это нужно делать ручками;

в) можно оставить ссылку на поток в той Activity или том Fragment, в котором он был создан и запущен. Но мы-то знаем, что пользователь повернёт экран, так что для обновления UI имеет смысл слать Local Broadcast (ой, а Fragment#getActivity вернёт null, какая боль, мы оторваны от контекста, нужно заранее сохранить application context).

(далее в статье рассматриваются View#post, Activity#runOnUiThread и Handler для обновления UI после выполнения задачи, но см. пункт 1-в), о ужас, пользователь повернул экран, мы потеряли UI)

2. AsyncTask

class DownloadTask extends AsyncTask<String, Integer, Drawable> {

    @Override
    protected void onPreExecute() {
	// Действия в UI-потоке перед началом выполнения
    }

    @Override
    protected Drawable doInBackground(String... params) {
        // Действия в фоне
        // для обновления прогресса можно вызвать publishProgress…
    }

    @Override
    protected void onProgressUpdate(Integer... progress) {
        // …получить аргументы сюда и обновить прогресс — этот метод будет вызван в UI-потоке
    }
        

    @Override
    protected void onPostExecute(Drawable result) {
        // действия после завершения фоновой задачи
    }
}

Тут всё гораздо удобнее.

В статье написано, что приоритет потока менять нельзя. Чтение кода показало мне, что перед запуском doInBackground выполняется код Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);, то есть приоритет потока понижается. Но это не значит, что изменить его нельзя. Хотя повышать его, конечно, не нужно.

В любом случае, проблема 1-а с приоритетом решена, он понижен; проблема 1-б с возвратом результата решена с помощью методов onProgressUpdate и onPostExecute.

Можно было бы сказать, что проблема 1-в с потерей экземпляра Activity или Fragment осталась нерешённой, но на самом деле AsyncTask предназначен никак не для скачивания картинок из интернета. Он нужен, чтобы выполнять очень кратковременные задачи, скажем, продолжительностью до 100 мс. Например, загрузить картинку с карты памяти за 20-50 мс.

В приложениях, которые неудачно используют AsyncTask, можно наблюдать, как задача запускается на выполнение и ждёт своей очереди несколько минут. Причём создаётся впечатление, что однопоточный Executor там не один на процесс, а один на всю ОС, т. к. тупить начинают несколько приложений. Так что стоит использовать собственный Executor.

В общем, для загрузки картинки из интернета AsyncTask не подойдёт, потому что за это время может смениться конфигурация (поворот экрана), и картинка, загруженная чуть менее, чем полностью, будет висеть в памяти вместе с уже закрытой Activity. Также стоит запускать задачу не методом execute, а методом executeOnExecutor, передавая туда свой ExecutorService, иначе можно хлебнуть тормозной жидкости.

3. Loader

Код большой, приводить не буду. Вероятно, удобен для работы с SQLite. Отличительная черта — возможность получить нужный экземпляр Loader'а с помощью LoaderManager'а, допустим, из пересозданной Activity.

Но, как я понимаю, onFinishLoad не будет вызвано в новом экземпляре Activity, если она была пересоздана.

4. Service

Сервис в Android — это контроллер с положенным ему длительным жизненным циклом и опосредованным доступом к UI. Собственно, это и есть то, что подходит для загрузки картинки.

public class DownloadService extends IntentService {

    public DownloadService() {
        super("DownloadService"); // имя сервиса
    }

    public static final String CHANNEL = DownloadService.class.getSimpleName()+".broadcast";

    // Этот метод вызывается автоматически и в отдельном потоке
    @Override
    protected void onHandleIntent(Intent intent) {
        // фоновая операция

        // отправка сообщения о завершении операции
        sendResult();
    }

    private void sendResult() {
        Intent intent = new Intent(CHANNEL);
        LocalBroadcastManager
            .getInstance(this)
            .sendBroadcast(intent);
    }
}
public class MainActivity extends Activity {

    TextView textView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        textView = (TextView) findViewById(R.id.hello);

        // Подписываемся на события нашего сервиса
        registerReceiver(receiver, new IntentFilter(DownloadService.CHANNEL));

        // Запускаем сервис, передавая ему новый Intent
        Intent intent = new Intent(this, DownloadService.class);
        startService(intent);
    }

    @Override
    protected void onStop() {
        // Отписываемся от событий сервиса
        unregisterReceiver(receiver);
        super.onStop();
    }


    private BroadcastReceiver receiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            textView.setText("Message from Service");
        }
    };
}

Здесь всё в порядке. Если пользователь повернёт экран, ответ придёт в новую Activity, потому что Service передаёт результат не конкретному экземпляру Activity / Fragment, а опосредованно, через BroadcastReceiver.

Новая проблема заключается в том, что объекты не передаются в Service непосредственно. Они задаются через Intent#putStringExtra, putIntExtra и им подобные методы либо пачкой — методом Intent#putExtras. Хранение и транспортировка происходит в Bundle — реализации карты (словаря, key-value-хранилища), которая является Parcelable. При отправке Intent все Extras с помощью механизма Parcel упаковываются в массив байт, а затем распаковываются из него при достижении цели.

Шанс совершить ошибку здесь заключается в том, что на выходе из Bundle можно забрать то, что положил, по имени. Нет типобезопасности и гарантии наличия нужных аргументов. Найти, в каком месте сервис позвали с недостаточным количеством аргументов, крайне сложно.

5. DownloadManager

Настолько многословен, что проще установить соединение через Retrofit или даже HttpUrlConnection, скачать картинку самостоятельно и обработать результат. Получать курсор и спрашивать у него индексы колонок не только неудобно, но и малонадёжно. (Надеюсь, вы не используете курсоры и SQLite в серьёзных приложениях?)

Статья о решении проблемы.


← клик, если это интересно   |   ↓ место для вопросов и идей