Одна из естественных и первых задач при разработке под Андроид – организация асинхронного взаимодействия. Например, обращение к серверу из некоторой активности и отображение на ней результата. Трудность состоит в том, что за время обращения к серверу поверх может быть открыта другая активность или другое приложение, исходная активность может быть безвозвратно завершена (пользователь нажал Back) и т. д. Вот получили мы результат от сервера, но активность «неактивна». Под «активна», в зависимости от обстоятельств, можно понимать, например, что находится между onStart и onStop, onResume и onPause (или, как у нас в проекте, между onPostResume и первым из onSaveInstanceState и onStop). Как понять, завершена активность окончательно (и результат нужно отдать сборщику мусора) или лишь временно неактивна (результат нужно хранить, и отобразить, как только активность станет активной)?

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

Уточним понятия активность и activity record. Активность – это экземпляр класса, короткоживущий объект. Activity record – логическое понятие, экран с точки зрения пользователя, более долгоживущий.
Рассмотрим схему Bottom > Middle > Top.
  1. Запускаем активность BottomActivity, поверх неё MiddleActivity. При повороте экрана, временном переключении на другое приложение и т. п. активность (экземпляр класса MiddleActivity) может уничтожаться и создаваться новая, но activity record Middle остаётся неизменным. Запускаем TopActivity поверх MiddleActivity, нажимаем кнопку Back. Активность MiddleActivity снова наверху стека, могла быть пересоздана, но activity record Middle всё так же сохраняется неизменным.
  2. Нажимаем Back – BottomActivity наверху стека. Снова запускаем MiddleActivity. Опять наверху activity record Middle. Но это уже новый activity record, не имеющий отношения к activity record из пункта 1. Тот activity record безвозвратно умер.

Предлагаемое решение основывается на следующем замечательном свойстве android.os.Binder. Если записать Binder в android.os.Parcel, то при чтении в том же процессе (в той же виртуальной машине) гарантированно прочитаем тот же самый экземпляр объекта, который был записан. Соответственно, можно проассоциировать с активностью экземпляр объекта activity record, и сохранять этот объект неизменным с помощью механизма onSaveInstanceState. В асинхронную задачу передаётся объект activity record, в который возвращается результат. Если activity record умирает, то становится доступен сборщику мусора, вместе с результатами работы асинхронных задач.

Для иллюстрации создадим простое приложение «Length». Оно состоит из двух активностей и четырёх инфраструктурных классов.

Project files

MenuActivity состоит из одной кнопки, которая запускает LengthActivity.

Main menu

Работать с Binder напрямую неудобно, так как его нельзя записать в android.os.Bundle. Поэтому обернём Binder в android.os.Parcelable.

public class IdentityParcelable implements Parcelable {

    private final ReferenceBinder referenceBinder = new ReferenceBinder();

    public final Object content;

    public static final Parcelable.Creator<IdentityParcelable> CREATOR = new Creator<IdentityParcelable>() {
        @Override
        public IdentityParcelable createFromParcel(Parcel source) {
            try {
                return ((ReferenceBinder) source.readStrongBinder()).get();
            } catch (ClassCastException e) {
                // It must be application recover from crash.
                return null;
            }
        }

        @Override
        public IdentityParcelable[] newArray(int size) {
            return new IdentityParcelable[size];
        }
    };

    public IdentityParcelable(Object content) {
        this.content = content;
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeStrongBinder(referenceBinder);
    }

    private class ReferenceBinder extends Binder {
        IdentityParcelable get() {
            return IdentityParcelable.this;
        }
    }
}


Класс IdentityParcelable позволяет передавать через parcel-механизм «ссылки» на объекты. Например, передать в качестве extra (Intent#putExtra) объект, который не является ни Serializable, ни Parcelable, и позже получить (getExtra) тот же экземпляр в другой активности.

Классы ActivityRecord и BasicActivity действуют в связке. ActivityRecord умеет исполнять callback-и. Если активность видна (находится в состоянии между onStart и onStop), то callback исполняется сразу, иначе сохраняется для более позднего исполнения. Когда активность становится видимой, исполняются все отложенные callback-и. При создании activity record (первый вызов BasicActivity#onCreate) создаётся новый объект ActivityRecord, и дальше поддерживается в onSaveInstanceState/onCreate.

public class ActivityRecord {

    private static final Handler UI_HANDLER = new Handler(Looper.getMainLooper());

    private Activity visibleActivity;

    private final Collection<Runnable> pendingVisibleActivityCallbacks = new LinkedList<>();

    public void executeOnVisible(final Runnable callback) {
        UI_HANDLER.post(new Runnable() {
            @Override
            public void run() {
                if (visibleActivity == null) {
                    pendingVisibleActivityCallbacks.add(callback);
                } else {
                    callback.run();
                }
            }
        });
    }

    void setVisibleActivity(Activity visibleActivity) {
        this.visibleActivity = visibleActivity;

        if (visibleActivity != null) {
            for (Runnable callback : pendingVisibleActivityCallbacks) {
                callback.run();
            }
            pendingVisibleActivityCallbacks.clear();
        }
    }

    public Activity getVisibleActivity() {
        return visibleActivity;
    }
}


public class BasicActivity extends Activity {

    private static final String ACTIVITY_RECORD_KEY = "com.zzz.ACTIVITY_RECORD_KEY";

    private ActivityRecord activityRecord;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        if (savedInstanceState == null) {
            activityRecord = new ActivityRecord();
        } else {
            activityRecord = (ActivityRecord) ((IdentityParcelable) savedInstanceState.getParcelable(ACTIVITY_RECORD_KEY)).content;
        }
    }

    @Override
    protected void onStart() {
        super.onStart();
        activityRecord.setVisibleActivity(this);
    }

    @Override
    protected void onStop() {
        activityRecord.setVisibleActivity(null);
        super.onStop();
    }

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putParcelable(ACTIVITY_RECORD_KEY, new IdentityParcelable(activityRecord));
    }

    public ActivityRecord getActivityRecord() {
        return activityRecord;
    }
}


На основе ActivityRecord делаем для асинхронных задач базовый класс, похожий контрактом на android.os.AsyncTask.

public class BackgroundTask {

    private final ActivityRecord activityRecord;

    public BackgroundTask(ActivityRecord activityRecord) {
        this.activityRecord = activityRecord;
    }

    public void execute() {
        new Thread() {
            @Override
            public void run() {
                doInBackground();
                activityRecord.executeOnVisible(new Runnable() {
                    @Override
                    public void run() {
                        onPostExecute(activityRecord.getVisibleActivity());
                    }
                });
            }
        }.start();
    }

    protected void publishProgress(final int progress) {
        activityRecord.executeOnVisible(new Runnable() {
            @Override
            public void run() {
                onProgressUpdate(activityRecord.getVisibleActivity(), progress);
            }
        });
    }

    protected void doInBackground() {
    }

    protected void onProgressUpdate(Activity activity, int progress) {
    }

    protected void onPostExecute(Activity activity) {
    }
}


Теперь, наладив инфраструктуру, делаем LengthActivity. При нажатии на кнопку асинхронно вычисляется длина введённой строки. Заметим, что при повороте экрана вычисление не начинается заново, а продолжается.

Length activity

public class LengthActivity extends BasicActivity {

    private TextView statusText;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.length_activity);

        statusText = (TextView) findViewById(R.id.statusText);

        findViewById(R.id.calculateLengthButton).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                new LengthTask(
                        getActivityRecord(),
                        ((TextView) findViewById(R.id.sampleField)).getText().toString()
                ).execute();
            }
        });
    }

    private void setCalculationResult(CharSequence sample, int length) {
        statusText.setText("Length of " + sample + " is " + length);
    }

    private void setCalculationProgress(CharSequence sample, int progress) {
        statusText.setText("Calculating length of " + sample + ". Step " + progress + " of 100.");
    }

    private static class LengthTask extends BackgroundTask {
        final String sample;
        int length;

        LengthTask(ActivityRecord activityRecord, String sample) {
            super(activityRecord);
            this.sample = sample;
        }

        @Override
        protected void doInBackground() {
            for (int i = 0; i < 100; i++) {
                publishProgress(i);
                try {
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    throw new IllegalStateException(e);
                }
            }

            length = sample.length();
        }

        @Override
        protected void onProgressUpdate(Activity activity, int progress) {
            ((LengthActivity) activity).setCalculationProgress(sample, progress);
        }

        @Override
        protected void onPostExecute(Activity activity) {
            ((LengthActivity) activity).setCalculationResult(sample, length);
        }
    }
}


Прикладываю архив со всеми исходниками и собранным APK.

Спасибо за внимание! Буду рад услышать комментарии и поучаствовать в обсуждении. Буду счастлив узнать более простое решение, без заморочек с Binder.

UPD: deej подсказал класс android.support.v4.app.BundleCompat, который умеет записывать IBinder в Bundle. Когда разрабатывали решение, этого класса ещё не было. BundleCompat немного упрощает код, позволяя обойтись без IdentityParcelable, одним Binder-ом наподобие
public class ValueBinder extends Binder {

    public Object value;

    public ValueBinder() {
    }

    public ValueBinder(Object value) {
        this.value = value;
    }

    public <V> V value() {
        //noinspection unchecked
        return (V) value;
    }
}

Возможно, IdentityParcelable всё равно может быть полезен, например, для передачи произвольных объектов в Intent в качестве extra, хотя можно обойтись ValueBinder-ом, передавая через Bundle.

Комментарии (31)


  1. agent10
    08.01.2016 13:45

    Честно, не очень понял проблему, для которой нужно именно такое решение.
    В чём отличие, от скажем: есть сущность Cache в которой хранится результат асинхронной задачи. Этот результат можно получать спрашивая его у кэша в onResume() в активити, или через подписку/отписку в onResume()/onPause(). Также можно сделать аналог sticky broadcast, т.е. если результат есть, то сообщить его при подписке на событие.


    1. mychka
      08.01.2016 14:01

      Из активности MiddleActivity посылаем запрос на сервер, получаем ответ, кладём ответ (который занимает в памяти 100 КБ) в Cache. В этот момент MiddleActivity может быть окончательно завершена (пользователь ушёл на BottomActivity) или лишь временно закрыта активностью TopActivity. В какой момент удалять ответ из Cache? Если держать его там вечно в надежде, что когда-нибудь понадобится, то получится утечка памяти.


      1. agent10
        08.01.2016 14:06

        Если результат нужен только в рамках жизни MiddleActivity можно удалять ответ/чистить кэш в onDestroy()


        1. mychka
          08.01.2016 14:33

          Да, что-то такое типа onFinalDestroy() вполне подошло бы. Но пара onDestroy()+isFinishing() слишком ненадёжна, во многих случаях не работает. Например, запускаем Bottom > Middle > Top. MiddleActivity#onDestroy() отрабатывает с isFinishing()==false. Из TopActivity запускаем BottomActivity с флагом Intent#FLAG_ACTIVITY_CLEAR_TOP (вылогиниваем пользователя). При этом метод MiddleActivity#onDestroy() повторно вызван не будет (соответственно, произойдёт утечка).


      1. agent10
        08.01.2016 14:25

        Кстати, может вашу задачу как раз решает AsyncTaskLoader? Я сам не пользовался, могу ошибаться, но это вроде что-то типа AsyncTask, но которая ещё следит за жизненным циклом активити.


        1. mychka
          09.01.2016 02:24

          Не, AsyncTaskLoader ничем не поможет. Отрабатывает AsyncTaskLoader#loadInBackground(), получаем результат, но активность не активна (в случае Loader — не видна). И встаёт тот же вопрос: «Доколе хранить результат?»


          1. SamSol
            09.01.2016 12:15

            Loader-ы (и в частности AsyncTaskLoader) очень хорошо справляются с задачей асинхронной загрузки и повторной асинхронной загрузки данных. Только их нужно использовать совместно с LoaderManager.


            1. mychka
              10.01.2016 02:04

              Конечно, я и имел ввиду, в частности, LoaderManager#initLoader(). Если объясните, чем AsyncTaskLoader может помочь в деле определения статуса активности-получателя результата асинхронной задачи, то буду очень благодарен.


      1. withoutuniverse
        09.01.2016 21:36

        А чем плох вариант с ограничением размера кэша? Скажем 30 записей в кэше максимум, более старые удаляются при добавлении новых. Ну или 5 мегабайт, вариантов много.


        1. mychka
          10.01.2016 01:54

          Очевидные недостатки варианта с ограниченным кэшем:

          • может быть удалён элемент кэша, который ещё нужен,
          • расходуется память на элементы, которые уже не нужны.

          Но для большинства реальных задач, думаю, вполне приемлемое решение. Другое дело, что всё равно инфраструктуру нужно городить: вводить кэш, назначать активностям UID. Получится не намного проще, чем добавить несколько строк в BasicActivity#onCreate() и BasicActivity#onSaveInstanceState(). Класс IdentityParcelable в любом случае может пригодиться, чтобы какой-нибудь объект по ссылке через parcel пробросить. Остальной код не является специфичным для решения с Binder-ом.

          Можно всё хранить, но не в памяти, а где-нибудь в файле или БД. Очищать при старте процесса, или, в зависимости от требований, результат асинхронной задачи может пережить даже смерть процесса. Но с сериализацией/десериализацией не всегда удобно работать. Иногда хочется какой-нибудь Runnable передать.

          Можно вручную кэш чистить. Допустим, если переходим на «корневые» активности (LoginActivity, MainMenuActivity), то очищаем кэш, так как гарантированно все вложенные activity record-ы умерли. Но вручную не хочется управлять, особенно когда активностей много, workflow сложный, включая случаи, когда одна активность может встречаться в back stack-е несколько раз.

          Есть вариант забывать про асинхронную задачу, например, в Activity#onStop(). Пользователь поворачивает экран до того, как запрос завершился, — ответ игнорируется, и в следующий раз отправляется новый запрос.


  1. agent10
    08.01.2016 14:06

    Промахнулся


  1. SamSol
    09.01.2016 01:00

    Функциональный аналог IdentityParcelable может быть очень простым: статический HashMap<Serializable, Object> и несложный код доступа.
    Например так:

    class IdentityParcelable implements Serializable {
        private static HashMap<UUID, Object> cache = new HashMap<>();
        private static HashMap<Object, UUID> reverseCache = new HashMap<>();
    
        private UUID id;
    
        public IdentityParcelable() {
        }
    
        public IdentityParcelable(Object o) {
            if (reverseCache.containsKey(o)) {
                id = reverseCache.get(o);
            } else {
                id = UUID.randomUUID();
                cache.put(id, o);
                reverseCache.put(o, id);
            }
        }
    
        public Object getObject() {
            return cache.get(id);
        }
    }
    

    В чём, по-вашему, этот простой аналог уступает предложенному вами коду?


    1. mychka
      09.01.2016 03:03

      Уступает тем, что объекты накапливаются в памяти (в cache и reverseCache), происходит утечка.
      Рассмотрим пример

      public class MiddleActivity extends Activity {
      
          private Object object;
      
          @Override
          protected void onCreate(Bundle savedInstanceState) {
              super.onCreate(savedInstanceState);
      
              if (savedInstanceState == null) {
                  object = new Object();
              } else {
                  object = ((IdentitySerializable) savedInstanceState.getSerializable(OBJECT_KEY)).getObject();
              }
          }
      
          @Override
          protected void onSaveInstanceState(Bundle outState) {
              super.onSaveInstanceState(outState);
              outState.putSerializable(OBJECT_KEY, new IdentitySerializable(object));
          }
      }
      

      Если использовать не IdentitySerializable, а IdentityParcelable из статьи, то как только activity record Middle умирает (пользователь нажимает Back, вызывается Activity#finish(), используется флаг Intent#FLAG_ACTIVITY_CLEAR_TOP и т. д.), объект «object» становится доступным сборщику мусора. (Но пока activity record жив — поворот экрана, переключение на другие приложения — object не доступен для сборки).


      1. SamSol
        09.01.2016 11:03

        Уверены, что в вашем коде нет утечки которая очевидна в моем коде?


        1. mychka
          09.01.2016 21:56

          В Андроиде вообще мало, в чём можно быть стопроцентно уверенным.
          Допустим, что в Activity#onSaveInstanceState() мы сохранили какой-то экземпляр Binder. Идея в том, что пока activity record жив и, соответственно, может быть вызван метод Activity#onCreate(), сохранённый в onSaveInstanceState() объект Binder не может быть уничтожен, так как в параметре savedInstanceState метода onCreate() должен быть передан в точности тот экземпляр Binder, который был сохранён. Если же activity record умирает, то onCreate() больше не может быть вызван, и нет причин держать (от сборщика мусора) Binder. То есть, такой Binder по сути представляет собой activity record, имеет идентичный жизненный срок. Дальше можно к Binder-у подцеплять любые данные, например, через статический WeakHashMap, или как в статье, сделать отдельный класс ActivityRecord, который держится IdentityParcelable-ом, который держится ReferenceBinder-ом. Обёртка не принципиальна.
          Конечно, мы проверяли и в DDMS, и программно (Runtime#totalMemory() — freeMemory(), отсутствие OOM, переопределение Object#finalize()). Утечек не обнаружили. Решение с Binder-ом используется два с половиной года на большом разнообразии устройств. С утечками также не сталкивались.


          1. SamSol
            10.01.2016 14:19
            +1

            Мне удалось проверить, что:
            Ссылка на ActivityRecord удерживается пока существует Bundle в который записали instance state.
            Ссылка на ActivityRecord удаляется когда удаляется Bundle в который ее записали.

            Но не удалось заставить систему восстановить ReferenceBinder из Parcel!
            IdentityParcelable createFromParcel(Parcel source) — не вызывается чтобы я ни делал. Такое ощущение, что Bundle хранит ссылку на Parcelable, и игнорирует все что записали в Parcel.

            Для проверки гипотезы я удалил из IdentityParcelable всё вплоть до ReferenceBinder и не нашел никаких изменений в работе приложения.

            public class IdentityParcelable implements Parcelable {
            
                public static final Parcelable.Creator<IdentityParcelable> CREATOR = new Creator<IdentityParcelable>() {
                    @Override
                    public IdentityParcelable createFromParcel(Parcel source) {
                        throw new RuntimeException("Not implemented");
                    }
            
                    @Override
                    public IdentityParcelable[] newArray(int size) {
                        throw new RuntimeException("Not implemented");
                    }
                };
            
                public final Object content;
            
                public IdentityParcelable(Object content) {
                    this.content = content;
                }
            
                @Override
                public int describeContents() {
                    return 0;
                }
            
                @Override
                public void writeToParcel(Parcel dest, int flags) {
                }
            }
            

            Эта реализация работает так же как ваша (и не использует никаких Binder/IBinder).


            1. mychka
              10.01.2016 18:40

              Дело в какой-то недокументированной оптимизации в Андроиде. В некоторых случаях сериализация/десериализация объекта Parcelable может не проводиться. При повороте в onCreate() может передаваться тот же экземпляр Parcelable, который был записан в onSaveInstanceState(). При этом методы writeToParcel/createFromParcel не вызываются.
              Сериализацию можно пронаблюдать так.

              1. Выставляем в Developer options опцию «Don't keep activities».
              2. Переходим на экран «Length».
              3. Нажимаем кнопку Home. LengthActivity благодаря опции гарантированно уничтожается.
              4. Возвращаемся в приложение (на экран «Length»).


              1. Dimezis
                11.01.2016 14:48

                В некоторых случаях сериализация/десериализация объекта Parcelable может не проводиться

                Например, когда передача идет во фрагмент, а не Activity. Может, в этом дело?


                1. mychka
                  11.01.2016 15:36

                  В примере «Length» из статьи фрагменты не используются.
                  Возможно, что при повороте сериализация Parcelable не производится никогда. Возможно, производится в некоторых случаях, на некоторых устройствах, некоторых версиях операционной системы, или станет производиться в будущем. Это поведение ведь не специфицировано.


  1. agent10
    09.01.2016 10:41

    Вспомнил одну задачу. Как бы вы её разрулили в вашей системе:
    1) Есть две активити A и B.
    2) Внутри активити A запускается фоновая задача и в этот момент переходим в активити B.
    3) Внутри активити B юзер делает что-то из-за чего результат фоновой задачи становится не актуальным.
    В вашем случае по возвращении через Back в активити A у вас «выстрелит» результат уже не актуальный.
    А прямого доступа к «хранилищу» чтобы инвалидировать результат у вас нет…


    1. Dimezis
      09.01.2016 14:31

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

      В целом, подход интересный, можно подтюнить под свои нужды


    1. mychka
      10.01.2016 04:16
      +1

      По нашему опыту могу сказать, что идея введения сущности для activity record оказалась плодотворной. Класс ActivityRecord — очень удобный, гибкий и мощный. Рекомендую. Конечно, в реальном проекте он немного сложнее, чем в статье. Кстати, в андроидных внутренностях есть нечто подобное: com.android.server.am.ActivityRecord, android.app.ActivityThread$ActivityClientRecord, android.app.BackStackRecord.

      Затрудняюсь дать наилучшее решение задачи, так как оно зависит от существующей архитектуры и от деталей требований. Например, активность A может в качестве extra передавать активности B weak reference на свой activity record.

      Intent intent = new Intent(this, BActivity.class);
      intent.putExtra("AActivityRecord", new IdentityParcelable(
              new WeakReference<?>(getActivityRecord())
      ));
      

      Теперь у активности B есть «прямой доступ к хранилищу».
      WeakReference<AActivityRecord> aActivityRecordRef = (WeakReference<AActivityRecord>) ((IdentityParcelable) getIntent().getParcelableExtra("AActivityRecord")).content;
      
      . . .
      
      AActivityRecord aActivityRecord = aActivityRecordRef.get();
      if (aActivityRecord != null) {
          aActivityRecord.invalidateTaskResult();
      }
      


      1. agent10
        10.01.2016 09:35

        Ну в целом это решение. Хотя можно прикопаться:)
        — активити B может запускаться из разных активити, т.е. за этим надо следить и знать откуда были запущены
        — мы привязываем B к неким сущностям A и/или предыдущих активити. Вместо того, чтобы знать о сущности общего кэша. Т.е. удаление/изменение A, потребует изменения в B.

        Но тем не менее, как многие пишут — вам надо всё таки показать/рассказать о ваших реальных задачах, может быть составить таблицу сравнения(по кол-ву коду, удобство) вашего решения, со многими другими, тем же RoboSpice/Кэшем. Даже Гугл вроде нигде не рассказывает о таком способе хранения «состояний».


  1. rfq
    09.01.2016 11:48

    а способ, описанный в github.com/stephanenicolas/robospice — он что, некорректен или неуниверсален?


    1. mychka
      09.01.2016 23:40

      Неуниверсален.
      Можно как угодно запускать асинхронные задачи: через Thread, Executor, AsyncTask, AsyncTaskLoader, сервисы и т. д. Можно сколь угодно удобно/безопасно организовывать взаимодействие с жизненным циклом активностей, и вообще со всей андроидной кухней. Вопрос в другом: «Что делать с асинхронно полученным результатом?»
      RoboSpice ничем не помогает. Предлагается метод SpiceManager#shouldStop(). А в какой момент его дёргать? В картинках и презентациях на сайте правильно критикуются AsyncTask и Loader-ы. Но сам RoboSpice, мне кажется, от них недалеко ушёл.


  1. HighFlyer
    09.01.2016 14:53

    Присоединяюсь к вопросам о том, какая именно проблема решается :)

    Похоже на то, что здесь повторён LoaderManager. В SDK он передаётся через метод onRetainNonConfigurationInstance, а здесь — через Binder в onSaveInstanceState.

    Проблема стандартная, предложено ещё одно решение, за это спасибо.


    1. mychka
      10.01.2016 03:19

      В статье озвучена проблема: «Что делать с результатом работы асинхронной задачи в случае, если активность не готова в данный момент его принять?» Не хочется терять результат. Но и хранить без нужды тоже не хочется.
      LoaderManager никак не помогает решить проблему. (Пожалуйста, поправьте, если я не прав.)

      В том-то и дело, что проблема абсолютно стандартная, а вот предложенное в статье решение не «ещё одно», а единственное мне известное. (Несколько вариантов для неперфекционистов перечислены тут.) То есть в большинстве серьёзных андроидных приложений что-то такое binder-образное должно использоваться. Но оно не то, что не используется, а я вообще ни такой идеи, ни даже схожей постановки проблемы не встречал. Надеюсь получить от хабрасообщества помощь в разрешении парадокса :)


  1. SamSol
    10.01.2016 14:21
    +1

    Пока анализировал нашел багу ;-(.
    1. Ориентация портрет
    2. Запуск приложения
    3. Тап на кнопку Length
    4. Тап на кнопку Calculate Length
    5. Ориантация альбом
    6. Нажать аппаратную кнопку Home
    7. Ориентация портрет
    8. Запуск приложения
    Ожидаемое состояние: Текст «Lengh of is 0»
    Действительное состояние: Текст «Calculating length of. Step 60 of 100.» (и не меняется).


    1. mychka
      10.01.2016 19:43

      Спасибо! Да, немного напортачил. Если выставлена опция «Don't keep activities», то вертеть экран не обязательно. Достаточно уйти в Home, и вернуться после того, как асинхронная задача завершится.

      Бага чисто UI-ная, не связана с обсуждаемым вопросом. Для того, чтобы максимально упростить код, я выставил TextView полю атрибут freezesText, чтобы текст не исчезал при повороте.

      <!-- res/layout/length_activity.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/statusText"
                  android:layout_width="wrap_content"
                  android:layout_height="wrap_content"
                  android:freezesText="true"/>
          <EditText
                  android:id="@+id/sampleField"
                  android:layout_width="match_parent"
                  android:layout_height="wrap_content"/>
          <Button android:id="@+id/calculateLengthButton"
                  android:layout_width="match_parent"
                  android:layout_height="wrap_content"
                  android:text="Calculate Length"/>
      </LinearLayout>
      

      Можно этот атрибут убрать, и баги не будет. Или, например, перенести activityRecord.setVisibleActivity(this) из BasicActivity#onStart() в onResume() или onPostResume(), как у нас в проекте. Кстати, в реальном проекте callback-и не вызываются в методах жизненного цикла активности, а делается Handler#post (в ActivityRecord#setVisibleActivity()). Можно более аккуратно вручную поддерживать значение TextView (в onSaveInstanceState()/onCreate()). В реальном проекте, возможно, лучше иметь два разных TextView: для прогресса и для последнего результата.

      А сейчас получается, что после возврата в приложение в методе onStart() полю выставляется правильный текст, но затем в реализации по умолчанию onRestoreInstanceState() значение поля перетирается тем, что было сохранено до ухода в Home.


  1. kemsky
    15.01.2016 11:08

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


    1. SamSol
      15.01.2016 19:39

      Ой, извиняюсь.
      При выключенном «Don't keep Activities» всё работает
      — во время пересоздания Garbage Collector не уничтожает IdentityParcelable (на него ссылается Bundle)
      — после создания и последующего удаления Activity ссылка на IdentityParcelable освобождается и утечка памяти не происходит (Bundle уничтожается)

      Если включить «Don't keep Activities» тоже всё работает. Но я не проверил удерживает ли Parcel ссылку на ReferenceBinder во время пересоздания Activity. Или может быть Bundle как и раньше удерживает ссылку на IdentityParcelable…
      Я только проверил, что после пересоздания и удаления Activity нет утечки памяти.