Low memory android что это

Обзор особенностей ядра Андроида

“А я… карбюратор промываю!”
Анекдот

В детском садике мы с единомышленниками препарировали кузнечиков в надежде разобраться в их строении. В школе распаивали радиоприёмник “Россия”. В институте дошла очередь до автомобилей, гайки которых были многократно переставлены. Интересы поменялись, но желание “разбирать” иногда просыпается, и сегодня оно направлено на Андроид.

Сколько раз вас выручало наличие исходников Андроида? Меня — уже не счесть. Андроид — открытый проект, но, к сожалению, у нас есть возможность только читать; править код Андроида, не будучи сотрудником Google, практически невозможно. Погрустим над этим моментом и загрузим репозиторий. Как это сделать, отлично описано на официальном сайте.

Общая архитектура

Архитектуру Андроида можно схематично изобразить так:

Оригинальная схема не содержит информации об особенностях ядра и не акцентирует внимание на Binder-е и системных сервисах. А ведь Binder является “клеем”, связывающим все компоненты системы.

Как правило, в книгах описывается верхний левый синий прямоугольник, то есть API, которое доступно разработчику прикладных приложений. Нас же интересует всё, что ниже. Сегодня мы рассмотрим только ядро.

Ядро — центральная часть любого дистрибутива, называемого “Линукс”. Несмотря на доступность “чистого” ядра, многие разработчики (Ubuntu, Fedora, SuSe и т.д.) добавляют к нему свои патчи перед включением в дистрибутив. Андроид идёт той же дорогой, только ценой потери прямой совместимости: на “чистом” ядре он не заведётся. В настоящее время есть намерения включить “андроидизмы” в основную версию ядра, в 2011 году Линус Торвальдс давал на этот процесс 4-5 лет. Успех уже достигнут в рамках включения механизма wakelocks в версии ядра 3.5.

Рассмотрим “андроидизмы” более подробно.

История данного механизма эпична, потянет на сборник статей “Путь wakelock-ов в Линукс”: их обсуждение заняло порядка 2000 писем в рассылке LKML.

Настольные компьютеры и ноутбуки имеют устоявшуюся систему энергорежимов (у x86 процессоров таковых несколько): компьютер работает “на полных оборотах”, когда что-то делается, и уходит в энергоэффективный режим, когда система простаивает. Уход в “спящий” режим происходит либо после довольно длительного бездействия, либо вручную, например, при закрытии крышки ноутбука.

На телефонах требовался другой механизм: основное состояние системы — “спячка”, выход из него осуществляется только в случаях необходимости. Таким образом, система может уснуть, даже если какое-то приложение проявляет активность. В Андроиде был реализован механизм wakelock-ов: если приложение (или драйвер) выполняет что-то важное, что должно дойти до логического завершения, оно “захватывает” wakelock, предотвращая засыпание устройства.

Попытки портирования механизма wakelock-ов в ядро вызвали сопротивление многих разработчиков. Программисты Андроида решали конкретную проблему, решением которой стал определённый механизм. Условия задачи были весьма узки. Целевая платформа — ARM, поэтому использовались её особенности: ARM-процессоры изначально предполагают частую смену режимов работы “сна” и “бодрствования”, в отличие от x86. В Андроиде приложения общаются с системой управления питанием через PowerManager, а что делать клиентским Линукс-приложениям?

Разработчики Андроида даже не пытались найти общее решение “на будущее”, которое потом без проблем бы вливалось в основное ядро, не консультировались по этой проблеме с сообществом ядра Линукс. Можно ли их за это винить? Несмотря на все проблемы и обсуждения, как упоминалось выше, в ядре появилось API с идентичной функциональностью autosleep.

Программистам приложений под Андроид довольно редко приходится сталкиваться с wakelock-ами, так как платформа и драйверы обрабатывают возложенные на них обязательства с учётом “спящего” режима. Тем не менее, вмешаться в этот процесс поможет знакомый PowerManager. Кстати, автору приходит в голову только один сценарий: не дать телефону уснуть при запуске сервиса из BroadcastReceiver-а, что решается вспомогательным классом из Android Support Library WakefulBroadcastReceiver.

Low Memory Killer

В стандартном ядре Линукса есть Out of Memory Killer, который на основании параметра badness определяет убиваемый процесс:

badness_for_task = total_vm_for_task / (sqrt(cpu_time_in_seconds) *
sqrt(sqrt(cpu_time_in_minutes)))

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

Все программисты, читавшие документацию или проходившие собеседования, знают, что, во-первых, процесс может быть “убит” и при наличии свободных ресурсов, во-вторых, кандидат на вытеснение выбирается по другим критериям: наличие “живых” Андроид-компонент, видимость пользователю и так далее.

Механизм довольно простой: каждому процессу присваивается приоритет от -17 до 16, при этом чем выше приоритет, тем выше вероятность убивания процесса, и, в зависимости от количества свободной памяти, выбирается приоритет, начиная с которого процессы будут завершены. Приоритеты описаны в ProcessList.java. Занимательно, что приоритет приложения домашнего экрана HOME_APP_ADJ довольно высок, а я-то думал: почему он постоянно перезапускается?

Читайте также:  Android google play не загружает

Массивы mOomAdj и mOomMinFreeLow/mOomMinFreeHigh как раз задают правила “когда что очистить”:

Таким образом, приложение домашнего экрана вытесняется при остатке свободной памяти в 73728 КБ на телефоне с экраном 1280×800 и ОЗУ в 700 МБ.
ProcessList передаёт соответствующие значения в ядро, что можно видеть в его методе updateOomLevels.

Приоритеты процессам выставляет Activity Manager Service, один из многих системных сервисов, общаться с которым можно через Activity Manager.

Binder, наряду с другими решениями (Files, Sigmals, Sockets, Pipes, Semaphores, Shared Memory и т.д.), решает задачу межпроцессного взаимодействия. Ноги у данного решения растут из проекта OpenBinder, разработчики которого в своё время перешли в команду Андроида.

Bionic (реализация libc) не использует System V IPC, так как в андроидовском окружении стандартные средства приведут к утечкам ресурсов.

Особенности:

  1. Управление потоками (мы все помним, что сервис, поддерживающий AIDL, должен работать в многопоточном окружении). Максимальное число потоков — 15 (ProcessState.c, метод open_driver), поэтому не стоит блокировать Binder-потоки в большом количестве без лишней необходимости.
  2. Механизм информирования о смерти процесса, держащего объект Binder “Link to Death”. Например, через него Window Manager узнаёт о смерти приложения и удаляет связанные с ним окна. Также LocationManager при смерти всех своих слушателей перестаёт опрашивать GPS-приёмник. Lowmemorykiller доволен. 🙂
  3. 2 режима вызова: блокирующий и неблокирующий (oneway). В первом случае вызывающий поток блокируется и ждёт отработки метода в потоке процесса-обработчика. Программисты просто вызывают методы через точку, взаимодействие потоков берёт на себя платформа.
  4. Передача UID и PID для безопасности. Через них системные сервисы определяют, есть ли у вызывающего процесса права совершать запрашиваемые действия.
  5. Для Java-программистов — средства создания Proxy и Stub-ов для конвертирования вызовов Java-методов в транзакции Binder-а.

Рассмотрим как это работает на примере LocationManager-а.

Когда мы хотим получить информацию о GPS, происходит следующее:

  1. Наше приложение вызывает соответствующий метод у LocationManager-а.
  2. LocationManager делегирует вызов прокси-объекту, преобразующему Java-методы и объекты в Binder-транзакцию (прокси-объектом у LocationManager-а является mService).
  3. Транзакция посылается драйверу ядра, который перенаправляет её LocationManagerService-у, отнаследованному от .LocationManager.Stub.
  4. .LocationManager.Stub делает обратные действия: разворачивает транзакцию в вызов Java-метода.
  5. .LocationManagerService обрабатывает запрос (используя, например, GPS-драйвер).
  6. Stub-объект пакует ответ в транзакцию, и процесс идёт в обратном направлении.
  7. Драйвер пересылает ответ обратно.
  8. Прокси-объект распаковывает результат вызова метода в Java-объекты.

Как видим, за вызовом методов системных сервисов скрывается довольно большая логика.

Anonymous Shared Memory (ashmem) — механизм разделяемой памяти. В Линуксе, как правило, данный механизм реализован через POSIX SHM. Разработчики Андроида сочли его недостаточно защищённым, что могло сыграть на руку вредоносному ПО. Особенностями ashmem-а являются счётчик ссылок, при обнулении которого разделяемая память может быть освобождена (например, память освобождается при завершении всех процессов, использующих её), и сокращение разделяемого региона при нехватке памяти в системе.

Ярким примером использования ashmem-а является процесс zygote, в котором загружается стартовая версия Dalvik VM с загруженными базовыми классами и ресурсами, а остальные приложения просто ссылаются на эту память.

Binder имеет ограничение на размер транзакции в 1МБ (иначе будет выброшено исключение TransactionTooLargeException). Если нам надо передать из одного процесса в другой большой объём данных, можно как раз воспользоваться Ashmem-ом: создать MemoryFile и передать дескриптор файла в другой процесс.

Обычные дистрибутивы, как правило, используют две системы логирования: лог ядра, доступный через команду dmesg, и системные логи, располагающиеся обычно в директории /var/log.

Система Андроида включает несколько циклических буферов для хранения сообщений пользовательских программ (что продлевает время жизни карт памяти, так как циклы чтения-записи не расходуются впустую) и не имеет дополнительных задержек от работы с сокетами, которые применяются в стандартном syslog-е.

На диаграмме представлена общая система логирования Андроида. Драйвер логирования предоставляет доступ к каждому буферу через /dev/log/*. Приложения имеют доступ к ним не напрямую, а через библиотеку liblog. С библиотекой liblog общаются классы Log, Slog и EventLog. Команда adb logcat показывает содержимое буфера “main”.

В данной заметке мы кратко рассмотрели некоторые особенности Андроида как Линукс-системы. За скобками остались некоторые другие части (pmem, RAM console и т.д.), а также такие важные аспекты платформы в целом, как System Service, процесс запуска системы и другие. Если данная тема будет интересна, в следующих статьях мы рассмотрим и их.

Источник

Большие требования к памяти в Android – что делать?

Здравствуйте, уважаемые читатели.

Сегодня предлагаем вашему вниманию небольшой материал о грамотном использовании памяти в Android.

Приятного чтения!

Эта статья посвящена базовым приемам управления использованием памяти в приложениях – например, в браузерах, фоторедакторах и программах для просмотра PDF – в которых предъявляются большие запросы к памяти.

Читайте также:  Vpn аккаунт для андроид

Для начала немного теории

Большинство приложений для Android работают поверх среды исполнения (ART), заменившей ныне устаревающую виртуальную машину Dalvik. ART и Dalvik похожи на виртуальную машину Java (JVM), с которой их роднят схожие принципы проектирования. Они используют для хранения данных приложений два отдельных пространства: стек и кучу.

Стек-память в Java используется для хранения локальных переменных (примитивных типов и ссылок на объекты). Каждый поток Java имеет собственный отдельный стек. Стек-память относительно невелика по сравнению с памятью кучи. Размер стека Java в Dalvik обычно составляет 32 KB для кода Java и 1 MB для нативного кода (C++/JNI). В ART появился унифицированный стек для Java и C++, размер которого составляет около 1 MB.

Когда приложение выбирает всю стек-память до предела, выдается ошибка StackOverflowError . Наиболее вероятные причины, по которым может быть достигнут предел стека – либо бесконечная рекурсия, либо чрезмерно глубокий вызов метода. Ссылки на стек-память всегда делаются в очередности LIFO (последним пришел – первым обслужен). Всякий раз при вызове метода в стек проталкивается новый фрейм с локальными переменными этого метода. Когда метод завершит работу, его фрейм выталкивается из стека, и любое возможное результирующее значение отправляется обратно в стек. Итак, первая проблема (бесконечная рекурсия) – это баг, который легко исправить, но вторая требует некоторого рефакторинга, заключающегося в разворачивании рекурсивных вызовов метода и преобразования их в цикл.

Память кучи в Java используется виртуальной машиной для выделения объектов. Когда бы ни создавался объект, это происходит в куче. Виртуальные машины, например, JVM или ART, выполняют регулярную сборку мусора, убирают все объекты, на которые больше не осталось ссылок, и таким образом высвобождают память для выделения новых объектов.
Для обеспечения удобства использования Android жестко ограничивает размеры кучи для каждого работающего приложения. Предельный размер кучи варьируется от устройства к устройству и зависит от того, сколько RAM на этом устройстве. Если ваше приложение достигает предельного размера кучи и пытается выделить еще памяти, то выдается ошибка OutOfMemoryError , и приложение завершается. Давайте рассмотрим некоторые примеры, помогающие избежать такой ситуации.

Анализ памяти кучи

Самый важный инструмент, позволяющий разобраться с проблемами памяти в ваших приложениях и понять, как используется память – это профилировщик памяти, доступный в Android Studio.

Этот инструмент визуализирует, сколько памяти потребляет ваше приложение с течением времени. Можно делать мгновенные снимки кучи Java в работающем приложении, записывать операции выделения памяти и следить за кучей или этой хронологией выделений памяти в мощном UI.

Типичный сеанс работы с профилировщиком памяти должен выглядеть так:

  • Отсматриваем наиболее частые выделения памяти и проходы сборщика мусора для выявления возможных проблем с производительностью.
  • Отсматриваем, как использовалась память с течением времени, в особенности такие операции, на которые, как известно, требуется выделять много памяти. Убедитесь, что после завершения этих операций использование памяти снижается. Например, ниже показано, как воздействует на память активность PdfActivity из PSPDFKit после загрузки документа.
  • Делаем дамп кучи в разные моменты времени исполнения вашего приложения и проверяем, как используется память. Ищем большие объекты, которые хранятся в памяти и не подпадают под сборку мусора. Дампы кучи также помогают выявить утечки памяти – например, можно поискать в дампе кучи ваши активности и посмотреть, были ли собраны их старые экземпляры.

Утечки памяти

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

Образцовое решение для выявления утечек в памяти – библиотека LeakCanary. Она автоматически выдает уведомления, когда в вашей тестовой сборке (development build), выдавая вам стектрейс утечки в UI этой программы. Можно (и следует) интегрировать ее уже сегодня, тем более, что это не сложно!

Особенно легко спровоцировать утечки памяти, работая со сложными жизненными циклами активностей или фрагментов Android. Такое часто случается в тех точках, где разработчики удерживают сильные ссылки на контексты UI или другие UI-специфичные объекты в фоновой задаче или в статических переменных. Один из способов спровоцировать такие задержки – активно покрутить устройство, когда тестируете ваше приложение.

Высвобождайте память в ответ на события

Android может затребовать у приложения выделенную память или просто принудительно завершить его, когда память необходимо высвободить для выполнения более критичных задач. Прежде, чем это произойдет, система позволит вам отдать всю память, которая вам не нужна. В вашей активности понадобится реализовать интерфейс ComponentCallbacks2 . В таком случае, всякий раз, когда ваша система будет испытывать дефицит памяти, поступит вызов к вашему методу onTrimMemory() , и вы сможете высвободить память или отключить те возможности, которые не будут работать в таких условиях дефицита памяти.

Читайте также:  Свап фейс для андроид

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

Большая куча

Одно из лобовых решений, позволяющих справиться с высокими требованиями к памяти – запросить большую кучу Dalvik для вашего приложения. Для этого можно добавить android:largeHeap=»true» к тегу в файле AndroidManifest.xml .

Если для свойства largeHeap задано значение true , Android будет создавать все процессы для вашего приложения с большой кучей. Эта настройка предназначена только для тех приложений, которые по природе своей без нее работать не смогут, то есть, они используют объемные ресурсы, которые должны одновременно умещаться в памяти.

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

Проверка, сколько памяти сможет использовать ваше приложение

Никогда не помешает проверить, насколько велика куча вашего приложения и динамически адаптировать ваш код и доступные возможности под эти пределы памяти. Можно прямо во время исполнения проверить максимальный размер кучи при помощи методов getMemoryClass() или getLargeMemoryClass() (когда включена большая куча).

Android поддерживает даже такие устройства, на которых всего 512 MB RAM. Убедитесь, что не обошли вниманием и низкотехнологичные устройства! При помощи метода isLowRamDevice() можно проверить, не запущено ли ваше приложение на таком устройстве, где мало доступной памяти. Точное поведение этого метода зависит от устройства, но обычно он возвращает true на тех устройствах, где меньше 1 GB RAM. Нужно убедиться, что ваше приложение корректно работает и на этих устройствах, и на них отключать все возможности, использующие большой объем памяти.

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

Используйте оптимизированные структуры данных

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

Коллекции Java не могут хранить эффективные примитивные типы и требуют упаковки их ключей и значений. Например, HashMap с целочисленными ключами следует заменять оптимизированным SparseArray . В конечном итоге, вместо коллекций всегда можно использовать сырые массивы, и это отличная идея, если ваша коллекция не поддается изменению размера.

К другим структурам данных, неэффективным с точки зрения использования памяти, относятся различные сериализации. Да, действительно, форматы XML или JSON удобны в использовании, можно сократить использование памяти, если работать с более эффективным двоичным форматом, например, буферами протоколов.

Все эти примеры с упоминанием структур данных, оптимизированных для экономии памяти – просто подсказки. Как и в случае с рефакторингом, нужно сначала найти источник проблем, а затем переходить к таким оптимизациям производительности.

Предотвращайте перемешивание памяти

Виртуальные машины Java/Android выделяют объекты очень быстро. Сборка мусора также идет весьма быстро. Однако при выделении большого количества объектов за короткий промежуток времени можно столкнуться с проблемой под названием «перемешивание памяти» (memory churn). В таком случае виртуальная машина не будет успевать выделять объекты в таком темпе, а сборщик мусора – их утилизировать, и приложение начнет притормаживать, а в экстремальных случаях даже израсходует всю память.

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

Пример ситуации, приводящей к перемешиванию памяти – выделение больших объектов, например, Paint внутри метода onDraw() представления. В таком случае быстро создается много объектов, и может начаться сборка мусора, которая может негативно повлиять на работу этого представления. Как указывалось выше, всегда нужно отслеживать использование памяти, чтобы избегать таких ситуаций.

Источник

Оцените статью