В этом уроке:
- сохраняем связь с AsyncTask при повороте экрана
Для полного понимания урока желательно знать, что такое внутренние классы и static объекты.
В прошлых уроках мы в Activity создавали внутренний класс, наследующий AsyncTask. Далее мы по нажатию кнопки создавали экземпляр этого класса и работали с ним. Все бы хорошо … но, если мы повернем экран, Activity будет создано заново, все прошлые объекты будут потеряны. В том числе мы потеряем и ссылку на наш созданный AsyncTask. А сам AsyncTask будет работать со старым Activity и держать его в памяти, т.к. объект внутреннего класса (AsyncTask) содержит скрытую ссылку на объект внешнего класса (Activity).
Давайте в этом убедимся и разберемся, как это пофиксить.
Т.к. будем работать с поворотом экрана, создавайте проект для Android 2.2 и используйте AVD на базе Android 2.2, потому что 2.3 криво поворачивается.
Создадим проект:
Project name: P0911_AsyncTaskRotate
Build Target: Android 2.2
Application name: AsyncTaskRotate
Package name: ru.startandroid.develop.p0911asynctaskrotate
Create Activity: MainActivity
main.xml:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:orientation="vertical"> <TextView android:id="@+id/tv" android:layout_width="fill_parent" android:layout_height="wrap_content" android:text=""> </TextView> </LinearLayout>
MainActivity.java:
import java.util.concurrent.TimeUnit; import android.app.Activity; import android.os.AsyncTask; import android.os.Bundle; import android.util.Log; import android.widget.TextView; public class MainActivity extends Activity { MyTask mt; TextView tv; public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); Log.d("qwe", "create MainActivity: " + this.hashCode()); tv = (TextView) findViewById(R.id.tv); mt = new MyTask(); Log.d("qwe", "create MyTask: " + mt.hashCode()); mt.execute(); } class MyTask extends AsyncTask<String, Integer, Void> { @Override protected Void doInBackground(String... params) { try { for (int i = 1; i <= 10; i++) { TimeUnit.SECONDS.sleep(1); publishProgress(i); Log.d("qwe", "i = " + i + ", MyTask: " + this.hashCode() + ", MainActivity: " + MainActivity.this.hashCode()); } } catch (InterruptedException e) { e.printStackTrace(); } return null; } @Override protected void onProgressUpdate(Integer... values) { super.onProgressUpdate(values); tv.setText("i = " + values[0]); } } }
Обычный AsyncTask, который в цикле выполняет паузы (1 сек.) и в TextView на экране пишет номер (i) итерации цикла.
Из необычного здесь можно отметить то, что мы используем метод hashCode. Этот метод возвращает хэш-код объекта. Сейчас не будем вникать что это и зачем нужно. Главное, надо знать, что разным объектам соответствует разный хэш-код. Т.е. по хэш-коду мы будем отличать объекты друг от друга (можно воспринимать хэш-код, как некий ID).
Мы при создании MainActivity и MyTask пишем в лог их хэш-коды. Затем при выполнении задачи, мы также будем писать в лог хэш-коды этих объектов. Сейчас станет понятно, зачем это нужно.
Все сохраним и запускаем приложение. Оно сразу запускает задачу, которая раз в секунду выдает на экран номер итерации цикла. Дождемся пока на экране появится, например, 5
и повернем экран (CTRL+F12 или CTRL+F11)
Отсчет снова пошел с единицы.
Дождемся конца отсчета и посмотрим в логи. Буду выдавать их частями и каментить:
create MainActivity: 1156854488
create MyTask: 1156875480
Создались объекты и мы видим их хэш-коды.
Далее начинает работу MyTask
i = 1, MyTask: 1156875480, MainActivity: 1156854488
i = 2, MyTask: 1156875480, MainActivity: 1156854488
i = 3, MyTask: 1156875480, MainActivity: 1156854488
i = 4, MyTask: 1156875480, MainActivity: 1156854488
i = 5, MyTask: 1156875480, MainActivity: 1156854488
Выводит в лог номер итерации и хэш-коды – свой и MainActivity, с которым он работает. Хэш-коды совпадают с теми, что ранее вывелись в лог при создании. Тут все ясно.
Теперь мы поворачиваем экран.
create MainActivity: 1156904328
create MyTask: 1156916144
Создается новое MainActivity и в нем создается новый MyTask. Их хэш-коды (1156904328 и 1156916144) отличаются от хэш-кодов старых MainActivity и MyTask (1156854488 и 1156875480). Т.е. это совершенно другие, новые объекты.
i = 6, MyTask: 1156875480, MainActivity: 1156854488
i = 7, MyTask: 1156875480, MainActivity: 1156854488
i = 1, MyTask: 1156916144, MainActivity: 1156904328
i = 8, MyTask: 1156875480, MainActivity: 1156854488
i = 2, MyTask: 1156916144, MainActivity: 1156904328
i = 9, MyTask: 1156875480, MainActivity: 1156854488
i = 3, MyTask: 1156916144, MainActivity: 1156904328
i = 10, MyTask: 1156875480, MainActivity: 1156854488
i = 4, MyTask: 1156916144, MainActivity: 1156904328
i = 5, MyTask: 1156916144, MainActivity: 1156904328
i = 6, MyTask: 1156916144, MainActivity: 1156904328
i = 7, MyTask: 1156916144, MainActivity: 1156904328
i = 8, MyTask: 1156916144, MainActivity: 1156904328
i = 9, MyTask: 1156916144, MainActivity: 1156904328
i = 10, MyTask: 1156916144, MainActivity: 1156904328
Мы видим, как продолжает работать старый MyTask (1156875480), и работает он со старым MainActivity (1156854488), продолжая отсчет от 6 до 10.
А параллельно с ним работает новый MyTask (1156916144) с новым MainActivity (1156904328), он начал с 1. На экране мы видим именно работу этих новых объектов. Поэтому цифры в TextView снова пошли с единицы. А старые объекты продолжают существовать где-то в памяти и работать. Но главное то, что мы потеряли связь со старым MyTask, создалась новая задача и работа пошла сначала.
Каждый раз начинать задачу заново при повороте экрана – это получится кривое приложение. Будем фиксить. Нам надо при создании нового Activity как-то получать ссылку на старый MyTask и не создавать новый, чтобы не начинать работу с начала, а продолжать ее. В этом нам помогут методы onRetainNonConfigurationInstance и getLastNonConfigurationInstance. О них можно прочесть в уроке 70.
Добавим в класс MainActivity реализацию метода onRetainNonConfigurationInstance:
public Object onRetainNonConfigurationInstance() { return mt; }
При повороте экрана, система сохранит для нас ссылку на объект mt.
И перепишем onCreate:
public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); Log.d("qwe", "create MainActivity: " + this.hashCode()); tv = (TextView) findViewById(R.id.tv); mt = (MyTask) getLastNonConfigurationInstance(); if (mt == null) { mt = new MyTask(); mt.execute(); } Log.d("qwe", "create MyTask: " + mt.hashCode()); }
При создании Activity мы просим систему вернуть (getLastNonConfigurationInstance) нам сохраненный в методе onRetainNonConfigurationInstance объект и приводим его к MyTask. Если Activity создается не после поворота экрана, то мы получим null, а значит, создаем сами MyTask.
Таким образом, при повороте экрана мы возвращаем себе старый MyTask. Посмотрим, что получится.
Верните эмулятор в вертикальную ориентацию. Сохраняем и запускаем приложение.
Ждем до 5
и поворачиваем экран:
А на экране ничего не происходит, хотя логи продолжают идти. Давайте посмотрим, что в логах:
create MainActivity: 1156854504
create MyTask: 1156875408
i = 1, MyTask: 1156875408, MainActivity: 1156854504
i = 2, MyTask: 1156875408, MainActivity: 1156854504
i = 3, MyTask: 1156875408, MainActivity: 1156854504
i = 4, MyTask: 1156875408, MainActivity: 1156854504
i = 5, MyTask: 1156875408, MainActivity: 1156854504
Тут все понятно, создались объекты, начала работать задача
create MainActivity: 1156904256
create MyTask: 1156875408
Создается новое MainActivity с новым хэш-кодом (1156904256). А вот MyTask мы добыли старый (хэш-код тот же - 1156875408), у нас получилось вернуть доступ к старому MyTask и не создавать новый. А значит работа продолжится и не будет начинаться заново. Это хорошо. Но есть и плохая новость.
i = 6, MyTask: 1156875408, MainActivity: 1156854504
i = 7, MyTask: 1156875408, MainActivity: 1156854504
i = 8, MyTask: 1156875408, MainActivity: 1156854504
i = 9, MyTask: 1156875408, MainActivity: 1156854504
i = 10, MyTask: 1156875408, MainActivity: 1156854504
Старый MyTask продолжает работать со старым MainActivity (1156854504), а новое (1156904256) в упор не видит.
Так происходит, потому что объект внутреннего класса (MyTask) содержит скрытую ссылку на объект внешнего класса (MainActivity). Обратите внимание, что мы в методах MyTask работаем с объектом tv. А ведь такого объекта в MyTask нет, он есть только в MainActivity. Тут используется скрытая ссылка – это позволяет MyTask работать с объектами MainActivity.
Поэтому наш старый MyTask связан со своим объектом внешнего класса MainActivity и видит только его. И меняет текст в TextView старого MainActivity, которое висит где-то в памяти. А на экране мы видим новое MainActivity. И оно не меняется.
То, что MyTask содержит ссылку на старое MainActivity плохо еще тем, что MainActivity не может быть уничтожено и висит в памяти.
Значит, нам надо избавиться от связки MainActivity и MyTask. Для этого применим static к внутреннему классу MyTask. Внутренний static класс никак не связан с объектом внешнего класса и не содержит скрытую ссылку на него. Но нам надо получить доступ к объектам (tv) MainActivity. Если не будет ссылки, не будет и доступа. Значит, сами создадим такую ссылку. В MyTask опишем объект, он и будет ссылаться на MainActivity. А мы будем этой ссылкой управлять – когда создается новое MainActivity, мы будем давать ссылку на него в MyTask.
Перепишем MainActivity.java:
package ru.startandroid.develop.p0911asynctaskrotate; import java.util.concurrent.TimeUnit; import android.app.Activity; import android.os.AsyncTask; import android.os.Bundle; import android.util.Log; import android.widget.TextView; public class MainActivity extends Activity { MyTask mt; TextView tv; public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); Log.d("qwe", "create MainActivity: " + this.hashCode()); tv = (TextView) findViewById(R.id.tv); mt = (MyTask) getLastNonConfigurationInstance(); if (mt == null) { mt = new MyTask(); mt.execute(); } // передаем в MyTask ссылку на текущее MainActivity mt.link(this); Log.d("qwe", "create MyTask: " + mt.hashCode()); } public Object onRetainNonConfigurationInstance() { // удаляем из MyTask ссылку на старое MainActivity mt.unLink(); return mt; } static class MyTask extends AsyncTask<String, Integer, Void> { MainActivity activity; // получаем ссылку на MainActivity void link(MainActivity act) { activity = act; } // обнуляем ссылку void unLink() { activity = null; } @Override protected Void doInBackground(String... params) { try { for (int i = 1; i <= 10; i++) { TimeUnit.SECONDS.sleep(1); publishProgress(i); Log.d("qwe", "i = " + i + ", MyTask: " + this.hashCode() + ", MainActivity: " + activity.hashCode()); } } catch (InterruptedException e) { e.printStackTrace(); } return null; } @Override protected void onProgressUpdate(Integer... values) { super.onProgressUpdate(values); activity.tv.setText("i = " + values[0]); } } }
Мы добавили static к описанию класса MyTask. Также описали в нем объект activity класса MainActivity и два метода:
link – с его помощью MyTask будет получать ссылку на MainActivity, с которой будет работать
unlink – обнуление ссылки
И теперь в классе MyTask мы уже не можем просто так работать с объектами MainActivity, т.к. MyTask у нас static, и не содержит скрытой ссылки на MainActivity. Мы должны явно указывать, что обращаемся к MainActivity, например activity.tv.
В методе onRetainNonConfigurationInstance перед тем, как сохранить MyTask для передачи новому Activity, мы обнуляем ссылку на старое MainActivity. MyTask больше не будет держать старое MainActivity и система сможет его (MainActivity) уничтожить.
А в onCreate мы после создания/получения объекта MyTask вызываем метод link и передаем туда ссылку на текущее новое MainActivity. С ним и продолжит работу MyTask.
Давайте проверим. Верните эмулятор в вертикальную ориентацию. Все сохраним и запустим приложение. Пошел отсчет. Дожидаемся 5
и поворачиваем экран
Отсчет продолжился, что и требовалось получить. Смотрим логи:
create MainActivity: 1156967624
create MyTask: 1156978504
i = 1, MyTask: 1156978504, MainActivity: 1156967624
i = 2, MyTask: 1156978504, MainActivity: 1156967624
i = 3, MyTask: 1156978504, MainActivity: 1156967624
i = 4, MyTask: 1156978504, MainActivity: 1156967624
i = 5, MyTask: 1156978504, MainActivity: 1156967624
Объекты создались, работа пошла
Поворот экрана
create MainActivity: 1156991528
create MyTask: 1156978504
MainActivity новое (1156991528), MyTask старый (1156978504).
i = 6, MyTask: 1156978504, MainActivity: 1156991528
i = 7, MyTask: 1156978504, MainActivity: 1156991528
i = 8, MyTask: 1156978504, MainActivity: 1156991528
i = 9, MyTask: 1156978504, MainActivity: 1156991528
i = 10, MyTask: 1156978504, MainActivity: 1156991528
Старый MyTask получил ссылку на новый MainActivity и продолжает работу уже с ним. А старое MainActivity кануло в небытие.
Уф! Хотел поверхностно показать механизм, но полез в объяснения «что да как» и получился достаточно непростой для понимания урок. Если остались непонятные моменты – велкам в форум, в ветку этого урока. Будем разбираться :)
Есть еще один способ (кроме static) избежать связки AsyncTask и Activity - просто сделайте ваш класс, наследующий AsyncTask, не внутренним, а отдельно от MainActivity.
Просьба к вам - откройте снова урок 86 и прочтите 4 правила использования AsyncTask. Я думаю, теперь они будут для вас гораздо информативнее, чем при первом прочтении.
P.S. В форуме верно заметили, что есть небольшой изъян в этом всем. Будет плохо, если onProgressUpdate выполнится между моментом, когда старое Activity выполнит метод unLink, и моментом, когда новое Activity выполнит метод link. В этом случае у нас activity будет равен null и мы получим NullPointerException. Вероятность это всего, конечно, мала, но решать проблему как-то надо.
Напишу здесь свой вариант решения. В методе onProgressUpdate мы ставим проверку activity == null. Если activity - не null, то без проблем меняем textView. Если же activity - null, то текст, который мы хотели прописать в TextView, мы сохраняем в какую-нить свою переменную класса MyTask. А новое Activity, когда получает MyTask, достает данные из этой переменной и сама помещает их в TextView.
Ваши предложения по решению проблемы пишите на в ветке этого урока.
На следующем уроке:
- создаем, запускаем и останавливаем простой сервис
Присоединяйтесь к нам в Telegram:
- в канале StartAndroid публикуются ссылки на новые статьи с сайта startandroid.ru и интересные материалы с хабра, medium.com и т.п.
- в чатах решаем возникающие вопросы и проблемы по различным темам: Android, Compose, Kotlin, RxJava, Dagger, Тестирование, Performance
- ну и если просто хочется поговорить с коллегами по разработке, то есть чат Флудильня