Невидимые лики (Android, memory management)

Дано: Android апликейшн с аудиторией 10 млн человек. Crashlytics для трекинга крешей.

Топ 1% крешей выглядят так:

или так:

или еще десятком разных представлений, но все они — OutOfMemory креши.

Была проведена работа по анализу существующих memory leaks в приложении, и все они были устранены. Счастье наступило, но было недолгим. Крешей стало меньше, но они не ушли.

Кардиограмма студии:

Дамп хипа в Memory Analyzer tool (www.eclipse.org/mat/)

Монитор спокоен:

adb shell dumpsys activity activities com.app_name | grep "Running activities" -A 30 | head -30 

говорит: «Узбагойся»:

Task #4 — запущена всего одна активити. Но креши-то не ушли... Заставляет задуматься.

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

Посмотрим, что говорит:

adb shell dumpsys meminfo com.app_name

После минуты работы приложения при одной открытой активити в стеке:

Wow! 560 views и 8 activities... Wow, черт побери... что-то тут не так!

Начинаем бисектить код в живой активити.

Результат:

1. Найден код в стиле:

mHomeView.postDelayed(new Runnable() {
    @Override
    public void run() {
        bla-bla-bla
    }
}, 
время_в будущем_АКТУАЛЬНОЕ_после закрытия_активити);

Казалось бы, всё в порядке — если активити дестроится, его вью, которые не держатся чем-то извне, должны уничтожиться тоже, предварительно почистив колбеки. Но не тут-то было. Данный код приводит к утечке всей активити. Лечится либо выносом runnable в мембер класса с последующим удалением, либо переносом логики в

mHandler = new Handler();
...
    mHandler.postDelayed(new Runnable() {
    @Override
    public void run() {
...
    }
},

то_же_время);

с очисткой в виде

mHandler.removeCallbacksAndMessages(null);

в onDestroy или onStop. Хотя по сути postDelayed на вью и хендлер должны быть эквивалентны, если верить уважаемым людям в мире Android.

2. Некоторое время назад один уважаемый человек индусской национальности вкрутил 3rd-party библиотеку для реализации shimmer эффекта (glow над текстом). Что-то вроде https://github.com/RomainPiel/Shimmer-android

Всё работает отлично. Видимых ликов нет. В коде библитеки найден следующий код:

mAnimator = ObjectAnimator.ofFloat(shimmerView, "gradientX", fromX, toX);
mAnimator.setRepeatCount(mRepeatCount);
mAnimator.setDuration(mDuration);
...
mAnimator.start();

Выглядит подозрительно:
— динамическая установка значений вью через рефлекшн каким-то делегатом;
— передаём вью куда-то в странного вида функцию.

В коде фреймворка в классе ObjectAnimator работа с вью выглядит безопасно:

mTarget = target == null ? null : new WeakReference<Object>(target);
…
   @Nullable
    public Object getTarget() {
        return mTarget == null ? null : mTarget.get();
    }
...
    final Object oldTarget = getTarget();
    if (oldTarget != target) {
...
    }

Утечек быть не должно...

В процессе разрушения исследуемой активити вызывается mAnimator.cancel(), который должен остановить анимацию.

Шок: Анимация НИКОГДА не останавливается. Были опробованы разные методы вида:

mAnimator.cancel();
mAnimator.end();
shimmerView.clearAnimation();

и другие извращения. Не помогло ничего.

Не мы одни такие:
stackoverflow.com/...​cancel-doesnt-always-work
stackoverflow.com/...​tion-cancel-does-not-work

Модифицируем код, чтобы избавиться от передачи вью в аниматор. Вуаля! Memory leaks gone!

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

Выводы:
— Не верь глазам своим. Что касается памяти — к сожалению, разные инструменты показывают разные вещи, и неизвестно, чему верить.
— манипулировать явно или неявно объектами UI фреймворка (в нашем случае views) — обычно очень плохая идея.
— При разработке приложений для Android проверяйте вывод:

watch -n 1 adb shell dumpsys meminfo com.app_name

Cудя по всему, это единственный объективный источник метрик памяти и объектов UI фреймворка, на который можно полагаться при разработке.

Удачи!

Все про українське ІТ в телеграмі — підписуйтеся на канал DOU

👍ПодобаєтьсяСподобалось0
До обраногоВ обраному0
LinkedIn



14 коментарів

Підписатись на коментаріВідписатись від коментарів Коментарі можуть залишати тільки користувачі з підтвердженими акаунтами.

Потребовалось время, чтобы сообразить, что «лики» это не лица, а потери. :)

Для того чтобы понять, что лучше не пользоваться View.post/postDelayed как альтернативой Handler-у, достаточно заглянуть в исходный код класса View (grepcode.com/...unnable,long))

    public boolean postDelayed(Runnable action, long delayMillis) {
        final AttachInfo attachInfo = mAttachInfo;
        if (attachInfo != null) {
            return attachInfo.mHandler.postDelayed(action, delayMillis);
        }
        // Assume that post will succeed later
        ViewRootImpl.getRunQueue().postDelayed(action, delayMillis);
        return true;
    }
В случае если вьюха уничтожается, таски постятся вникуда и это 100% лики.

Мне кажется, эти методы можно использовать только в наследниках класса View, предварительно очищая коллбеки в onDetachedFromWindow. И по хорошему они должны быть protected.

Избегать таким образом создания отдельного экземпляра Handler-а — сомнительная оптимизация, так как этот класс сам по себе — легкая надстройка над существующим в любом Adnroid-приложении Looper-ом.

На хабре недавно была похожая статья (habrahabr.ru/...ia/blog/243537 для связности), обе хороши, вот только обе лишь в кратце упоминают/показывают как пользоваться Memory Analyzer tool, а зря — это основной инструмент для анализа, и очень странно, что вы не нашли ваши утекшие объекты активити через него.

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

Цель статьи была поделиться собственным опытом. И да, мне тоже очень странно почему MAT не показал лики, а они по сути оставались. Мне показалось, что это будет полезно, если не сениорным разработчикам, то по крайней мере начинающим.

Я сначала тоже в общем-то хотел заикнуться про новичков, но потом подумал и понял, что в принципе начинающие не смогут исправить утечки памяти без понимания модели памяти в java и алгоритмов работы сборщика мусора. Утечки опять же часто связаны с нарушениями в архитектуре приложения и непонимании жизненного цикла приложения, особенно там где inversion of control, как например в андроиде — и все это явно указывает, что начинающим сначало нужно многое подчитать и эти статьи для начинающих лишь попытка лечить последствия.
По поводу кучи информации по MAT, на английском да, а вот на русском пару ссылок всего — вот тут можно найти аудиторию, которая почитает с удовольствием. Хоть все и пишут, что знают инглиш, на практике — читая резюме — ухахатываешься.

За статью все равно спасибо — гляди в скором будущем хабр можно будет и не открывать при наличии отечественного производителя© Мне нравится куда эволюционирует доу.

Я думал куда отправить статью. Решил поддержать отечественного производителя. Карма мне все равно ни к чему :)

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

Модифицируем код, чтобы избавиться от передачи вью в аниматор. Вуаля! Memory leaks gone!
остались без глоу эффекта?

Анимация осталась, просто убрали зависимость на непосредственный вью. Вместо этого в аниматор передали пустой объект с функцией которую нужно анимировать, а в нем WeakReference на вью, который реально анимируется.

Замечательная лекция о том как правильно пользоваться асинхроностью в андроиде что бы он не тек памятью, очень много практических примеров:
www.infoq.com/...urrency-Android

Ну я бы сказал, лекция всего лишь о том, почему не стоит пользоваться AsyncTask’ами. Этим concurrency совсем не ограничивается.
И ответом на вопрос последнего слайда могут быть, например:
github.com/...eactiveX/RxJava
github.com/...nrobot/EventBus
github.com/square/otto
code.google.com/.../android-query

и прочие PubSub’ы

было интересно

В мемориз! Спасибо!

Здорово! Спасибо

Підписатись на коментарі