Июл 13, 2015 - 0 Comments - Без рубрики -

Занимательная история одного Android дебага

Дано:
— Activity — 1 штука.
— ViewPager, который живет в Activity — 1 штука.
— FragmentPagerAdapter для вышеупомянутого ViewPager — 1 штука. Производит на свет 3 фрагмента.

Проблема:
При закрытии Activity в сеть улетают запросы на загрузку контента (изображений), который живет во фрагментах, которые даже не были показаны.

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

Да… о таком стек трейсе можно только мечтать. На него можно смотреть днями, и все равно будет непонятно, что здесь происходит :). Глаз цепляется за doFrame, живущий в классе Choreographer, с которого, похоже, всё начинается.

Если посмотреть на его код,

void doFrame(long frameTimeNanos, int frame) {
   final long startNanos;
   synchronized (mLock) {
       if (!mFrameScheduled) {
           return; // no work to do
       }

...
   doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);
   doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);
doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);
   ...
}

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

Поскольку в нашем стек трейсе видно, что выполняется ViewRootImpl$TraversalRunnable, было бы интересно узнать, кто ставит его в очередь на выполнение.

Легко найти, где происходит регистрация колбеков в том же классе Choreographer:

private void postCallbackDelayedInternal(int callbackType,
       Object action, Object token, long delayMillis) {
   if (DEBUG) {
       Log.d(TAG, "PostCallback: type=" + callbackType
               + ", action=" + action + ", token=" + token
               + ", delayMillis=" + delayMillis);
   }

   synchronized (mLock) {
       final long now = SystemClock.uptimeMillis();
       final long dueTime = now + delayMillis;
mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);
       if (dueTime <= now) {
           scheduleFrameLocked(now);
       } else {
           Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
           msg.arg1 = callbackType;
           msg.setAsynchronous(true);
           mHandler.sendMessageAtTime(msg, dueTime);
       }
   }
}

Поставив брейкпоинт на функцию, получаем:

Ага… в неактивный Fragment ViewPager’a приходят данные через loader’ы. Loader устанавливает курсор в адаптер, который отвечает за отображение картинок, и Glide вытягивает картинки из сети. Но откуда изначальные данные и почему они пришли при закрытии активити?

Loader инициализируется в onActivityCreated, который вызывается только один раз при создании фрагмента. Почему андроид при закрытии активити передоставляет данные для курсора?

Опять же, в стеке есть ModernAsyncTask, при внимательном рассмотрении которого можно найти интересную функцию с сигнатурой:

public final 
ModernAsyncTask<Params, Progress, Result> 
executeOnExecutor(Executor exec,Params… params)

Поставив на нее брейкпоинт, получаем:

На этот скриншот можно и нужно смотреть очень долго. Здесь «блеск и нищета» смешаны в одну кучу и, наверно, можно найти причину некоторых сайд-еффектов в андроид приложениях.

Внимательно читаем стек снизу вверх. OnPause Activity приводит к старту фрагмента? What the f…?

Понять что происходит, можно, просто внимательно прочитав исходный код FragmentManagerImpl.

Краткая выдержка из кода:

...
public void dispatchActivityCreated() {
   mStateSaved = false;
   moveToState(Fragment.ACTIVITY_CREATED, false);
}

public void dispatchStart() {
   mStateSaved = false;
   moveToState(Fragment.STARTED, false);
}

public void dispatchResume() {
   mStateSaved = false;
   moveToState(Fragment.RESUMED, false);
}

public void dispatchPause() {
moveToState(Fragment.STARTED, false);
}
...

Стоп.. мне одному кажется, что последняя функция как-то не вписывается в контекст? Неужели баг в support library? Однако похоже, что нет.

Как все уже знают, фрагменты и активити живут по своему собственному жизненному циклу

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

Если компонент проходит через onPause, но еще не добрался до onStop, то какое состояние долно быть текущим, учитывая, что может быть необходимо перевести компонент в состояние onResume? Судя по диаграмме выше можно предположить, что это пре-onResume и это STARTED.

Н-да… неочевидно. Ну ок. Что дальше?

Дальше все FragmentManager’ы, ассоциированные с текущей Activity, и дочерние FragmentManager’ы непосредственных детей активити рекурсивно переводятся в состояние STARTED, а соответственно отрабатывают их перегруженные функции onStart.

Чуть выше по стеку можно найти обработчик onStart самого класса Fragment из android.support.v4.app.

Его код очень интересен:

public void onStart() {
   mCalled = true;

if (!mLoadersStarted) {
       mLoadersStarted = true;
       if (!mCheckedForLoaderManager) {
           mCheckedForLoaderManager = true;
           mLoaderManager = mActivity.getLoaderManager(mWho, mLoadersStarted, false);
       }
       if (mLoaderManager != null) {
mLoaderManager.doStart();
       }
   }
}

Судя по этому коду, если текущий фрагмент никогда не был STARTED, то loader’ы не будут фактически запущенны. Отсюда два важных вопроса: как так произошло, что живой фрагмент не прошел стадию STARTED, и что делать? 🙂

Ответ на первый вопрос достаточно прост: если в приложении есть ViewPager, в котором живут фрагменты, то несмотря на то, что фрагмент будет сконструирован, когда это требуется (зачастую в фоне для smooth transitioning), он не пройдет состояние STARTED, пока фактически не будет отображен.

Но FragmentActivity из android.support.v4.app ничего об этом не знает 🙂

В итоге закрытие активити приводит к тому, что:

1. Наряду с остальным обработчиками в существующих фрагментах вызывается обработчик onStarted.

2. Все фрагменты, находящиеся в состоянии pre-STARTED, запускают свои лоадеры, доставляют данные и процессят их так, как будто фрагмент только что был запущен.

Поскольку все это происходит на стороне Android SDK, без хаков пофиксить ситуацию довольно трудно. Как минимум можно следить, когда запускаются лоадеры, обязательно — в каком состоянии находится ассоциированная с фрагментом активити в обработчиках onLoadFinished при доставке данных, и, конечно, какой код живет в onStarted, onDestroy и т.д., чтобы не возникало незапланированных побочных эффектов.

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

Have fun!


Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Человек ? *