понедельник, 11 июля 2011 г.

Производительность Android приложения.

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

Традиционно в низкой производительности Android приложений винят Java. Но только ли Java виноват? Не только. Во многих случаях виноват вовсе не он.

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

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

Общие рекомендации Google

Первое, что следует прочитать - раздел Designing for Performance (перевод) в Android developers guide. В нем перечислены основные правила, которые следует соблюдать при кодировании на Java под Android.

Ключевая рекомендация - избегайте создание ненужных объектов. Чем меньше объектов создается, тем реже вызывается сборщик мусора, тем меньше используется память. Идея очевидна, но вот избегать создания лишних объектов ой как не просто...

Отмечу рекомендацию касающуюся циклов "for-each": для ArrayList классический цикл с счетчиком примерно в 3 раза быстрее, чем "for-each", а для прочих коллекций, "for-each" предпочтительнее. Лично я использовал "for-each" везде, опрометчиво считая, что "компилятор сам разберется". Оказывается это не так. Так что для ArrayList следует писать:
int size = list.size();
for (int i = 0; i < size; ++i) {
  ItemType item = list.get(i);
....
}
а для прочих коллекций
for (ItemType item : list) {
....
}

Грамотная работа с UI

В использовании Android UI есть определенные тонкости, о которых желательно знать. Подробно о них рассказывается в докладе Google I/O 2009 - ...Make your Android UI Fast and Efficient.

Затрагиваются следующие вопросы:
  • Эффективная реализация адаптеров для ListView, использующая ViewHolder.
  • Масштабирование картинок - дорогая операция, в реальном времени лучше его избегать - "runtime scaling is very expensive"
  • Если бакграунт не нужен, его прорисовку следует исключить (по умолчанию бакраунд рисуется, даже если он полностью закрыт контролами; отключается через стиль "Theme.NoBackground").
  • Invalidate очень дорог, если возможно, лучше использовать Invalidate(Rect)
  • Чем больше видов (view) в активити, тем медленнее работа. Существует ряд приемов, позволяющих снизить количество используемых view: compound drawable, ViewStub, тег merge.
  • Корректная реализация custom views и custom layouts
  • Список функций, в которых крайне не рекомендуется выделять память.
  • Использование SoftReferences для кешей и WeakReferences для исключения утечек памяти.
(Update: см. так же раздел Improving Layout Performance в Android Training).
Отмечу несколько моментов. Во первых, в докладе представлен список функций, которые желательно делать максимально легковесными и в которых, соответственно, желательно не выделять память:
  • Measurement: onMeasure()
  • Layout: onLayout()
  • Drawing: draw(); dispatchDraw(); onDraw();
  • Events handling: dispatchTouchEvent(); onTouchEvent();
  • Adapters: getView(); bindView();

Во-вторых, на одном из слайдов (48:15) показан интересный прием. Предположим, мы пишем супер производительную функцию, в которой нигде не должна выделяться память. Как убедиться в том, что память действительно не выделяется? Вот так:
int prevLimit = -1;
try {
  //Limit the number allocated objects
  prevLimit = Debug.setAllocationLimit(0);

  //Execute code that shouldn't perform
  //any allocations
} finally {
 Debug.setAllocationLimit(prevLimit);
}
При попытке выделить память будет сгенерировано исключение.

В третьих, на одном из листингов (53:25) приведен пример использования SoftReference для организации кеша битмапок. Более полные варианты реализации представлены здесь и здесь.

Наконец, обратите внимание на слайд (30:43). Здесь рассказывается про "Compound drawable" - возможность заменить layout, содержащий TextView и ImageView, на единственный TextView. Чем меньше view, тем быстрее приложение. Лично я не знал про такую возможность и использовал более громоздкий код, чем требовалось.

Снижение времени отклика приложения

Пользователь считает, что приложение "тормозит", если время отклика приложения превышает 100-200 мс. Еще хуже, когда задержка становится слишком большой, Android решает, что приложение зависло и появляется диалог ANR с предложением закрыть приложение.

Как уменьшить время отклика? Кое-какие рекомендации есть в руководстве разработчика, в разделе Designing for Responsiveness. Основная идея - длительные операции нужно выносить из главного потока во второстепенные.

Несколько больше информации в докладе
Google I/O 2010 - Writing zippy Android apps. Здесь затрагиваются вопросы производительности sqllite (в ряде случаев обычный файл предпочтительнее), рассматриваются варианты использования AsyncTask и intentService, демонстрируются возможности профайлера TraceView.

Корректное использование памяти

Правильно ли ваше приложение использует память? Нет ли утечек? Работа с памятью рассмотрена в докладе Google I/O 2011: Memory management for Android Apps.

В частности, объясняется механизм возникновения утечек памяти. Типичная причина - создание долгоживущих ссылок на Context (в руководстве разработчика этот момент так же детально рассмотрен).

Здесь же приведена интересная информация про Bitmap. До Honeycomb битмапки создавались в неуправляемой памяти. Для освобождения памяти, занимаемой битмапкой, необходимо было ждать вызова финалайзера или вручную вызывать метод recycle (вы его вызваете?). Начиная с Honeycomb битмапки создаются в управляемой памяти.

Для поиска утечек памяти автор доклада рекомендует использовать Eclipse Memory Analyzer (MAT). Подробности в блоге Java Performance blog.

Производительность в играх

Вопросам производительности игр посвящены доклады
Google I/O 2010 - Writing real-time games for Android redux и Google I/O 2009 - Writing Real-Time Games for Android.

SparseArray вместо HashMap

Вместо HashMap<Integer, E> можно использовать android.utils.SparseArray<E>. У этого класса почти такой же интерфейс, как у HashMap, но с важным отличием: методы get и put работают с int, а не с Integer. Таким образом, при использовании SparseArray исключаются лишние boxing операции. В многих случаях, исключение создания лишних Integer-объектов более важно, чем небольшое (потенциальное) снижение в скоростях доступа.

В android.utils есть еще пара аналогичных классов - android.utils.SparseIntArray и SparseBooleanArray - замена для HashMap<Integer, Integer> и HashMap<Integer, Boolean> соответственно.

Выводы

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

Update Имеет смысл периодически проверять приложение на предмет неиспользуемых ресурсов... и удалять их. Проверять можно, например, с помощью утилиты Android Unused Resource Detector (начиная с Android SDK версии r16 для этих целей можно использовать встроенный статический анализатор Lint. Так же как Android Unused Resource Detector, Lint порой выдает ложные сообщения о неиспользуемых ресурсах, так что нужно быть очень внимательным, чтобы не удалить лишнего).

Полезная статья Speed up your Android UI про отключение ненужной прорисовки фона.

Update Многие статические анализаторы умеют отлавливать неоптимальный код, у них есть чему поучиться. FindBugs, CodePro, PMD, Lint - у всех у них есть правила типа Performance или Optimization.

Update, январь 2012 Скоро выйдет интересная книжка по теме: Hervé Guihot. Pro Android Apps Performance Optimization

4 комментария:

  1. спасибо! прекрасный обзор основных приемов!

    ОтветитьУдалить
  2. "В частности, объясняется механизм возникновения утечек памяти. Типичная причина - создание долгоживущих ссылок на Context (в руководстве разработчика этот момент так же детально рассмотрен)."
    Верная ссылка:
    http://android-developers.blogspot.ru/2009/01/avoiding-memory-leaks.html

    ОтветитьУдалить
  3. А книжка 2012 г - тут http://it-ebooks.info/book/646/

    ОтветитьУдалить