Дано: 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/...snt-always-work
stackoverflow.com/...l-does-not-work
Модифицируем код, чтобы избавиться от передачи вью в аниматор. Вуаля! Memory leaks gone!
Точная причина, почему аниматор не останавливается, в нашем случае не установлена, и возможно, другие приложения страдать от этого не будут, но факт очень неприятен.
Выводы:
— Не верь глазам своим. Что касается памяти — к сожалению, разные инструменты показывают разные вещи, и неизвестно, чему верить.
— манипулировать явно или неявно объектами UI фреймворка (в нашем случае views) — обычно очень плохая идея.
— При разработке приложений для Android проверяйте вывод:
watch -n 1 adb shell dumpsys meminfo com.app_name
Cудя по всему, это единственный объективный источник метрик памяти и объектов UI фреймворка, на который можно полагаться при разработке.
Удачи!