Что такое android архитектура ос. Как устроена операционная система андроид. Что подразумевается под Native user space

ОС Android - операционная система для мобильных телефонов, планшетных компьютеров и нетбуков, основанная на ядре Linux. Изначально разрабатывалась компанией Android Inc., которую затем купила компания Google. Первая версия ОС Google Android вышла в сентябре 2008 года. В конце 2010 года платформа Android стала самой продаваемой ОС для смартфонов.

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

Архитектура операционной системы Android

Если представить компонентную модель Android в виде иерархии (Рис.2) , то можно выделить 4 основных уровня:

Уровень ядра Linux;

Уровень библиотек (Libraries);

Уровень среды исполнения (Android Runtime);

Уровень каркаса приложений (ApplicationFramework);

Уровень приложений (Applications).

Уровень ядра Linux. В самом низу, в основе будет располагаться ядро операционной системы.ОС Android основана на ОС Linux версии 2.6, тем самым платформе доступны системные службы ядра, такие как управление памятью и процессами, обеспечение безопасности, работа с сетью и драйверами. Также ядро служит слоем абстракции между аппаратным и программным обеспечением.

Рис.2

Уровень библиотек (Libraries)."Выше" ядра, как программное обеспечение промежуточного слоя, лежит набор библиотек, предназначенный для решения типовых задач, требующих высокой эффективности. То есть, именно этот уровень отвечает за предоставление реализованных алгоритмов для вышележащих уровней, поддержку файловых форматов, осуществление кодирования и декодирования информации, отрисовку графики и многое другое. Библиотеки реализованы на C/C++ и скомпилированы под конкретное аппаратное обеспечение устройства, вместе с которым они и поставляются производителем в предустановленном виде.

Перечислим некоторые из низкоуровневых библиотек:

SurfaceManager - диспетчер поверхностей управляет доступом к подсистеме отображения 2D- и 3D- графических слоев.

Media Framework - библиотеки, c помощью которых система может осуществлять запись и воспроизведение аудио и видео данных, а также вывод статических изображений.

SQLite - легковесная и производительная реляционная СУБД, используемая в Android в качестве основного движка для работы с базами данных.

3D libraries - библиотеки для работы с 3D-графикой.

FreeType - библиотека, предназначенная для работы со шрифтами.

LibWebCore - ядро встроенного web-браузера.

SGL (ScalableGraphicsLibrary) - библиотека для работы с 2D-графикой.

SSL - библиотеки для поддержки одноименного криптографического протокола.

libc - библиотека стандартных вызовов языка C.

Уровень среды исполнения (Android Runtime). На этом же уровне располагается Android Runtime - среда выполнения прикладных программ. Ключевыми её составляющими являются набор стандартных библиотек и виртуальная машина Dalvik. Каждое приложение в ОС Android запускается в собственном экземпляре виртуальной машины Dalvik. Таким образом, все работающие процессы изолированы от операционной системы и друг от друга. Архитектура Android Runtime такова, что работа программ осуществляется строго в рамках окружения виртуальной машины. Благодаря этому осуществляется защита ядра операционной системы от возможного вреда со стороны других её составляющих. Поэтому код с ошибками или вредоносное ПО не смогут испортить ОС Android и устройство на её базе. Такая защитная функция, наряду с выполнением программного кода, является одной из ключевых для Android Runtime.

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

Основой всех приложений является набор систем и служб:

Диспетчер действий (ActivityManager) управляет жизненным циклом приложений и предоставляет систему навигации по истории работы с действиями.

Диспетчер окон (Window Manager) управляет окнами, и предоставляет для приложений более высокий уровень абстракции библиотеки Surface Manager.

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

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

Диспетчер уведомлений (NotificationManager) позволяет любому приложению отображать пользовательские уведомления в строке статуса.

Диспетчер пакетов (Package Manager) управляет установленными пакетами на вашем устройстве, отвечает за установку новых и удаление существующих.

Диспетчер телефонии (Telephony Manager) содержит API для взаимодействия с возможностями телефонии (звонки, смс и т.п.)

Диспетчер ресурсов (ResourceManager) предназначен для доступа к строковым, графическим и другим типам ресурсов.

Диспетчер местоположения (Location Manager), позволяющий приложениям получать обновленные данные о текущем географическом положении устройства

Уровень приложений (Applications). На вершине программного стека Android лежит уровень приложений. Сюда относится набор базовых приложений, который предустановлен на ОС Android. Например, в него входят браузер, почтовый клиент, программа для отправки SMS, карты, календарь, менеджер контактов и многие другие. Список интегрированных приложений может меняться в зависимости от модели устройства и версии Android. И помимо этого базового набора к уровню приложений относятся все прикладные приложения под платформу Android, в том числе и установленные пользователем.

Те, кто использует iPhone давно, знают, как работали ранние версии iOS. Фактически это была однозадачная операционка, которая позволяла работать в фоне или прерывать работу текущего приложения только предустановленным приложениям: вы читаете книжку, вам звонят - книгочиталка сворачивается, и на экране появляется окно звонка. А вот обратная операция невозможна: книгочиталка не только не может прервать работу других приложений, но и будет убита сразу после сворачивания.

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

Как устроена операционная система Андроид

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

Имея права root, настройки lowmemorykiller можно регулировать напрямую или с помощью специальных приложений

Важным элементом системы многозадачности были службы (service). Это особые компоненты приложений, которые могли работать в фоне абсолютно в любых условиях: включен экран или выключен, свернуто приложение или развернуто, службам плевать даже на то, запущено ли родительское приложение вообще. Оно просто говорило: «Эй, Android, мне нужны ресурсы процессора, я хочу сделать некоторые расчеты» - и получало эти ресурсы. В терминологии Android такой запрос к системе называется wakelock (а если точнее - процессорный wakelock).

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

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

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

Сама Google также предпринимала определенные действия для борьбы с «ядовитыми» службами. Большой шаг в этом направлении был сделан в Android 4.4, где появился интеллектуальный механизм, который определял, не работает ли служба слишком много времени и не сильно ли она грузит процессор, и, если это оказывалось так, прибивал ее на месте и не давал запуститься. Даже на поверхностный взгляд эта версия системы жила на батарейке заметно дольше предыдущих.

В Android 6.0 Google пошла еще дальше и оснастила ее механизмом Doze , который после определенного времени неактивности смартфона (около одного часа) переводил его в специальный энергосберегающий режим. Одна из особенностей этого режима - запрет на wakelock, то есть ни приложения, ни службы просто не могут разбудить смартфон, чтобы выполнить какую-либо работу. На глаз Android 6.0 не стал жить дольше, так что неизвестно, сработал ли этот механизм вообще.


Шкала работы Doze

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

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

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

На самом деле нет. Google шла к запрету служб еще с версии 5.0, где появился так называемый JobScheduler . Это специальная подсистема, которая позволяет приложениям попросить Android выполнить ту или иную работу в такое-то время или при возникновении такого-то события (подключение к интернету, например). И да, JobScheduler сильно напоминает аналогичную функцию из iOS.

Binder

Вопреки расхожему мнению, Android с самых первых версий использовал песочницы для изоляции приложений. И реализованы они были весьма интересным образом. Каждое приложение запускалось от имени отдельного пользователя Linux и, таким образом, имело доступ только к своему каталогу внутри /data/data .

Друг с другом и с операционной системой приложения могли общаться только через IPC-механизм Binder , который требовал авторизации на выполнение того или иного действия. Этот же механизм использовался и для несколько других целей: с его помощью система оповещала приложения о системных событиях, таких как входящий вызов, пришедшее СМС, втыкание зарядки и так далее. Приложения получали сообщения и могли на них отреагировать.


Работу Binder обеспечивают драйвер в ядре Linux и Service Manager

Эта особенность дала Android очень широкие возможности автоматизации, о которых мы знаем благодаря таким приложениям, как Tasker, Automate или Locale. Все эти приложения доступны и для Android 8, разве что некоторые опасные возможности, такие как включение/выключение режима полета, теперь запрещены для использования обычными приложениями.

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

К сожалению, как и службы, интенты стали проблемой для Google и пользователей Android. Дело в том, что широковещательные интенты, используемые для уведомления приложений о событиях, приходят сразу ко всем приложениям, которые заявили, что способны на них реагировать. А чтобы приложение смогло среагировать на интент, его надо запустить. Картина получается такая: на смартфоне есть двадцать приложений, которые могут реагировать на интент android.net.conn.CONNECTIVITY_CHANGE, и при каждом подключении к сети и отключении от нее система запускает эти приложения, чтобы они смогли среагировать на интент. Как это сказывается на энергопотреблении - представьте сами.

Google исправила это недоразумение опять же в Android 8.0. Теперь приложения могут регистрировать обработчики широковещательных интентов только во время своей работы (за небольшими исключениями).

Сервисы Google

Google любит бравировать тем, что Android - операционная система с открытым исходным кодом. Это, конечно же, не совсем так. С одной стороны, код Android действительно открыт, и именно поэтому мы имеем доступ к такому количеству разнообразных кастомных прошивок. С другой стороны, собрав Android из официальных исходников, вы получите систему без нескольких важных компонентов: 1) отдельных драйверов, исходники которых производитель прячет, как коммерческую тайну, 2) сервисов Google, которые нужны в первую очередь для получения доступа к аккаунту, запуска Google Play и облачного бэкапа.

Сервисы Google (Google Mobile Services) также отвечают за многие другие вещи, включая поддержку push-уведомлений, Instant Apps, Google Maps, доступ к календарю, определение местоположения по сотовым вышкам и Wi-Fi-роутерам, механизм Smart Lock, позволяющий разблокировать устройство в зависимости от некоторых условий.

В современных версиях Android сервисы Google взяли на себя настолько большую часть работы, что жить без них оказывается хоть и возможно, но очень проблематично. А с ними тоже невесело: минимальный вариант пакета GApps (который содержит только сервисы Google и Google Play) весит больше 120 Мбайт, а сами сервисы славятся своей любовью к оперативке и заряду батареи. А еще они закрыты, то есть о том, что они могут делать, знает только сама Google.


Скачать пакет с сервисами и приложениями Google для кастомной прошивки можно с сайта opengapps.org (слово open не означает, что они открыты)

Именно поэтому на свет появился проект microG, задача которого - воссоздать самую важную функциональность сервисов Google в открытом коде. Уже сейчас microG позволяет получить доступ к своему аккаунту, активировать push-уведомления, доступ к картам Google и определению местоположения по сотовым вышкам. И все это при размере в четыре мега и почти полном отсутствии требований к оперативке и ресурсу батареи.

У проекта есть собственная сборка прошивки LineageOS, которая из коробки включает в себя microG и все необходимые для его работы модификации.

Ядро Linux и рантайм

Android основан на ядре Linux. Ядро управляет ресурсами смартфона, в том числе доступом к железу, управлением оперативной и постоянной памятью, запуском, остановкой и переносом процессов между ядрами процессора и многими другими задачами. Как и в любой другой ОС, ядро - это сердце Android, центральная часть, без которой все остальное развалится.


Слоеный пирог Android

Наличие ядра Linux, а также частично совместимой со стандартом POSIX среды исполнения (в первую очередь это библиотека bionic, основанная на реализации стандартной библиотеки языка С из OpenBSD) делает Android совместимым с приложениями для Linux. Например, система аутентификации wpa_supplicant, применяемая для подключения к Wi-Fi-сетям, здесь точно такая же, как в любом дистрибутиве Linux. В ранних версиях Android использовался стандартный bluetooth-стек Linux под названием bluez (позже его заменили реализацией от Qualcomm под названием Bluedroid). Здесь даже есть своя консоль с набором стандартных UNIX/Linux-команд, реализованных в наборе Toybox, изначально созданном для встраиваемых Linux-систем.

Большинство консольных приложений, написанных для Linux, можно портировать в Android простой перекомпиляцией с помощью кросс-компилятора (главное - использовать статическую компиляцию, чтобы не получить конфликт библиотек), а имея права root, на Android-девайсе можно без всяких проблем запустить полноценный . Один нюанс - доступ к нему можно будет получить либо только через консоль, либо используя VNC-соединение. Также существует проект Maru OS, позволяющий использовать смартфон в качестве ПК на базе Debian при подключении к монитору. Ту же функцию обещает при подключении своих смартфонов к монитору с помощью дока DeX.


Старый добрый mc, запущенный в Android

Начиная с версии 4.4 Android умеет использовать систему принудительного контроля доступа SELinux для защиты от взлома и получения прав root. SELinux разработана Агентством национальной безопасности США и, если не вдаваться в детали, позволяет ограничить приложения (в том числе системные низкоуровневые компоненты) в возможностях. И речь вовсе не о полномочиях, которые юзер предоставляет приложениям, а о таких вещах, как системные вызовы и доступ к тем или иным файлам, невзирая на стандартные права доступа UNIX.

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

Все эти баги Google благополучно закрыла, а также поработала над модуляризацией кода фреймворка и его запуском в специальных доменах SELinux. Эти домены запрещают компонентам, ответственным за обработку мультимедиа, использовать большую часть системных вызовов Linux, включая системные вызовы группы execve, которые как раз и были причастны к запуску зловредного кода.

Сегодня SELinux используется для защиты почти всех системных компонентов Android. И это стало причиной резкого снижения количества найденных багов в Android. Но привело к фокусировке взломщиков на ядре, а точнее тех самых закрытых драйверах, аудит кода которых никто не проводил и безопасность которых не гарантирована (а она, как оказалось, находится в плачевном состоянии).

(6 оценок, среднее: 5,00 из 5)

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

Первое, что нужно знать: Android есть вариант ОС Linux, и хоть здесь эта система нашла свою родную нишу. Внутри системы, на самом базовом ее уровне, находится модифицированное ядро Linux, которое, как в больших системах, нужно часто обновлять. Намного чаще, чем в других мобильных ОС, иначе вы лишитесь возможности использовать новые фишки, датчики, приложения и прочие полезные штуки. Ядро работает с приложениями и драйверами через API системы, и только на этом уровне драйверов и системных приложений можно писать программы на языке С++. Среднюю часть машины занимает платформа, которую можно сравнить с.NET от Майкрософт. Она управляется языком Java. Каждое приложение пользователя порождает процесс, когда платформа формирует для него виртуальную среду исполнения. В этот момент Java приложения в виде кода классов и библиотек автоматически переводятся в универсальный байт-код, исполняемый на байт-машине Delvik. Эта машина имеет много общего с Java-машиной JVM.

Приложения работают в изолированной друг от друга среде, захватывая только те ресурсы, которые им необходимы. В этом коренное отличие, например, от программ на ПК, где любая программа может захватить все ресурсы. Хотя приложения оптимизированы по качеству кода и изолированы друг от друга, у них есть не только приоритеты вытесняющей многозадачности, но и способы обмена данными и файлами путем присвоения им одинаковых идентификаторов. Таким образом, можно сделать так, что родственные программы будут тратить минимум лишних ресурсов, обращаясь к API через платформу, а также общим файлам профилей, спискам контактов и т.д. Здесь интересно то, что Delvik можно подсунуть и другой код на Java, который виртуальная машина примет как родной. Это позволяет реализовывать многочисленные виртуальные системы разработки, применять новые языки программирования и даже менять фреймворк приложений на альтернативные варианты – лишь бы он умел генерировать байт-код и соответствовать вызовам API.

Платформа, или фреймворк, работает с данными унифицированным образом, переводя их в формат баз данных SQLite – с таким же форматом работает, к примеру, Mozilla на ПК. Доступом в Сеть заведует оптимизированный для мобильных устройств браузер Chrome. Для создания безупречной графики API поддерживает технологию OpenGL ES v.3.0, а также целый набор жадных графических и видеоформатов и контейнеров для их хранения, включая h.264, mkv, MP4 и т.д.

Связь с другими устройствами – проводная (USB, карты памяти) и беспроводная (BT, IR, Wi-Fi, EDGE/3G/LTE). Множество датчиков завязаны на получение сигналов извне системы: GPS, акселерометр, компас, фотосъемка, датчик темноты, фитнес-датчики.

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

Код проекта должен быть разделён на независимые модули, работающие друг с другом как хорошо смазанный механизм - фото Честера Альвареза .

Экосистема средств разработки под Android развивается очень быстро. Каждую неделю кто-то создаёт новые инструменты, обновляет существующие библиотеки, пишет новые статьи, или выступает с докладами. Если вы уедете в отпуск на месяц, то к моменту вашего возвращения уже будет опубликована свежая версия Support Library и/или Google Play Services.

Я занимаюсь разработкой Android-приложений в компании ribot в течение последних трёх лет, и всё это время и архитектура наших приложений, и используемые нами технологии, постоянно развивались и улучшались. Эта статья проведёт вас путём, пройденным нами, показав вынесенные нами уроки, совершенные нами ошибки, и рассуждения, которые привели ко всем этим архитектурным изменениям.

Старые добрые времена

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


Код был разделён на два уровня: уровень данных (data layer), который отвечал за получение/сохранение данных, получаемых как через REST API, так и через различные локальные хранилища, и уровень представления (view layer), отвечающий за обработку и отображение данных.

APIProvider предоставляет методы, позволяющие активити и фрагментам взаимодействовать с REST API. Эти методы используют URLConnection и AsyncTask , чтобы выполнить запрос в фоновом потоке, а потом доставляют результаты в активити через функции обратного вызова. Аналогично работает и CacheProvider: есть методы, которые достают данные из SharedPreferences или SQLite, и есть функции обратного вызова, которые возвращают результаты.

Проблемы

Главная проблема такого подхода состоит в том, что уровень представления имеет слишком много ответственности. Давайте представим простой сценарий, в котором приложение должно загрузить список постов из блога, закешировать их в SQLite, а потом отобразить в ListView . Activity должна сделать следующее:
  1. Вызвать метод APIProvider#loadPosts(Callback) .
  2. Подождать вызова метода onSuccess() в переданном Callback "е, и потом вызвать CacheProvider#savePosts(Callback) .
  3. Подождать вызова метода onSuccess() в переданном Callback "е, и потом отобразить данные в ListView .
  4. Отдельно обработать две возможные ошибки, которые могут возникнуть как в APIProvider , так и в CacheProvider .
И это ещё простой пример. В реальной жизни может случиться так, что API вернёт данные не в том виде, в котором их ожидает наш уровень представления, а значит Activity должна будет как-то трансформировать и/или отфильтровать данные прежде, чем сможет с ними работать. Или, например, loadPosts() будет принимать аргумент, который нужно откуда-то получить (например, адрес электронной почты, который мы запросим через Play Services SDK). Наверняка SDK будет возвращать адрес асинхронно, через функцию обратного вызова, а значит у нас теперь есть три уровня вложенности функций обратного вызова. Если мы продолжим наворачивать всё больше и больше сложности, то в итоге получим то, что называется callback hell.

Просуммируем:

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

Новая архитектура с применением RxJava

Мы использовали описанный выше подход на протяжении двух лет. В течение этого времени мы внесли несколько изменений, смягчивших боль и страдания от описанных проблем. Например, мы добавили несколько вспомогательных классов, и вынесли в них часть логики, чтобы разгрузить активити и фрагменты, а также мы начали использовать Volley в APIProvider . Несмотря на эти изменения, код всё так же был трудно тестируемым, и callback-hell периодически прорывался то тут, то там.

Ситуация начала меняться в 2014-м году, когда мы прочли несколько статей по RxJava . Мы попробовали её на нескольких пробных проектах, и осознали, что решение проблемы вложенных функций обратного вызова, похоже, найдено. Если вы не знакомы с реактивным программированием, то рекомендуем прочесть вот это введение. Если коротко, RxJava позволяет вам управлять вашими данными через асинхронные потоки (прим. переводчика: в данном случае имеются в виду потоки как streams, не путать с threads - потоками выполнения), и предоставляет множество операторов , которые можно применять к потокам, чтобы трансформировать, фильтровать, или же комбинировать данные так, как вам нужно.

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


Код всё так же разделён на два уровня: уровень данных содержит DataManager и набор классов-помощников, уровень представления состоит из классов Android SDK, таких как Activity , Fragment , ViewGroup , и так далее.

Классы-помощники (третья колонка в диаграмме) имеют очень ограниченные области ответственности, и реализуют их в последовательной манере. Например, большинство проектов имеют классы для доступа к REST API, чтения данных из бд или взаимодействия с SDK от сторонних производителей. У разных приложений будет разный набор классов-помощников, но наиболее часто используемыми будут следующие:

  • PreferencesHelper: работает с данными в SharedPreferences .
  • DatabaseHelper: работает с SQLite.
  • Сервисы Retrofit , выполняющие обращения к REST API. Мы начали использовать Retrofit вместо Volley, потому что он поддерживает работу с RxJava. Да и API у него поприятнее.
Многие публичные методы классов-помощников возвращают RxJava Observables .

DataManager является центральной частью новой архитектуры. Он широко использует операторы RxJava для того, чтобы комбинировать, фильтровать и трансформировать данные, полученные от помощников. Задача DataManager состоит в том, чтобы освободить активити и фрагменты от работы по «причёсыванию» данных - он будет производить все нужные трансформации внутри себя и отдавать наружу данные, готовые к отображению.

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

  1. Загружает список постов через Retrofit.
  2. Кеширует данные в локальной базе данных через DatabaseHelper .
  3. Фильтрует посты, отбирая те, что были опубликованы сегодня, так как уровень представления должен отобразить лишь их.
public ObservableLoadTodayPosts() { return mRetrofitService.loadPosts() .concatMap(new Func1Observable>() { @Override public ObservableCall(ListApiPosts) { return mDatabaseHelper.savePosts(apiPosts); } }) .filter(new Func1() { @Override public Boolean call(Post post) { return isToday(post.date); } }); }
Компоненты уровня представления будут просто вызывать этот метод и подписываться на возвращенный им Observable . Как только подписка завершится, посты, возвращённые полученным Observable могут быть добавлены в Adapter , чтобы отобразить их в RecyclerView или чём-то подобном.

Последний элемент этой архитектуры это event bus . Event bus позволяет нам запускать сообщения о неких событиях, происходящих на уровне данных, а компоненты, находящиеся на уровне представления, могут подписываться на эти сообщения. Например, метод signOut() в DataManager может запустить сообщение, оповещающее о том, что соответствующий Observable завершил свою работу, и тогда активити, подписанные на это событие, могут перерисовать свой интерфейс, чтобы показать, что пользователь вышел из системы.

Чем этот подход лучше?


А какие проблемы остались?

  • В больших и сложных проектах DataManager может стать слишком раздутым, и поддержка его существенно затруднится.
  • Хоть мы и сделали компоненты уровня представления (такие, как активити и фрагменты) более легковесными, они всё ещё содержат заметное количество логики, крутящейся около управления подписками RxJava, анализа ошибок, и прочего.

Пробуем Model View Presenter

В течение прошлого года в Android-сообществе начали набирать популярность отдельные архитектурные шаблоны, так как MVP , или MVVM . После исследования этих шаблонов в тестовом проекте , а также отдельной статье , мы обнаружили, что MVP может привнести значимые изменения в архитектуру наших проектов. Так как мы уже разделили код на два уровня (данных и представления), введение MVP выглядело натурально. Нам просто нужно было добавить новый уровень presenter"ов, и перенести в него часть кода из представлений.


Уровень данных остаётся неизменным, но теперь он называется моделью , чтобы соответствовать имени соответствующего уровня из MVP.

Presenter "ы отвечают за загрузку данных из модели и вызов соответствующих методов на уровне представления, когда данные загружены. Presenter"ы подписываются на Observables , возвращаемые DataManager . Следовательно, они должны работать с такими сущностями как подписки и планировщики . Более того, они могут анализировать возникающие ошибки, или применять дополнительные операторы к потокам данных, если необходимо. Например, если нам нужно отфильтровать некоторые данные, и этот фильтр скорее всего нигде больше использоваться не будет, есть смысл вынести этот фильтр на уровень presenter"а, а не DataManager .

Ниже представлен один из методов, которые могут находиться на уровне presenter"а. Тут происходит подписка на Observable , возвращаемый методом dataManager.loadTodayPosts() , который мы определили в предыдущем разделе.

Public void loadTodayPosts() { mMvpView.showProgressIndicator(true); mSubscription = mDataManager.loadTodayPosts().toList() .observeOn(AndroidSchedulers.mainThread()) .subscribeOn(Schedulers.io()) .subscribe(new Subscriber>() { @Override public void onCompleted() { mMvpView.showProgressIndicator(false); } @Override public void onError(Throwable e) { mMvpView.showProgressIndicator(false); mMvpView.showError(); } @Override public void onNext(ListPostsList) { mMvpView.showPosts(postsList); } }); }
mMvpView - это компонент уровня представления, с которым работает presenter. Обычно это будет Activity , Fragment или ViewGroup .

Как и в предыдущей архитектуре, уровень представления содержит стандартные компоненты из Android SDK. Разница в том, что теперь эти компоненты не подписываются напрямую на Observables . Вместо этого они имплементируют интерфейс MvpView , и предоставляют список внятных и понятных методов, таких как showError() или showProgressIndicator() . Компоненты уровня представления отвечают также за обработку взаимодействия с пользователем (например, события нажатия), и вызов соответствующих методов в presenter"е. Например, если у нас есть кнопка, которая загружает список постов, наша Activity должна будет вызвать в OnClickListener "е метод presenter.loadTodayPosts() .

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

Чем этот подход лучше?

  • Активити и фрагменты становятся ещё более легковесными, так как их работа сводится теперь к отрисовке/обновлению пользовательского интерфейса и обработке событий взаимодействия с пользователем. Тем самым, их становится ещё проще поддерживать.
  • Писать юнит-тесты для presenter"ов очень просто - нужно просто замокировать уровень представления. Раньше этот код был частью уровня представления, и провести его юнит-тестирование не представлялось возможным. Архитектура становится ещё более тестируемой.
  • Если DataManager становится слишком раздутым, мы всегда можем перенести часть кода в presenter"ы.

А какие проблемы остались?

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

Важно упомянуть, что описанный мною подход не является идеалом. Вообще было бы наивно полагать, что есть где-то та самая уникальная и единственная архитектура, которая возьмёт да и решит все ваши проблемы раз и навсегда. Экосистема Android"а будет продолжать развиваться с высокой скоростью, а мы должны будем держаться в курсе событий, исследуя, читая и экспериментируя. Зачем? Чтобы продолжать делать отличные Android-приложения.

Я надеюсь, вам понравилась моя статья, и вы нашли её полезной. Если так, не забудьте нажать на кнопку Recommend (прим. переводчика: перейдите на оригинальную статью, и нажмите на кнопку-сердечко в конце статьи). Также, я хотел бы выслушать ваши мысли по поводу нашего текущего подхода.

В этой статье я попробую рассмотреть безопасность чуть-чуть повыше ядра, а именно: как работает безопасность в Native user space. Мы коснемся темы процесса загрузки операционной системы и рассмотрим структуру файловой системы Android. Как я уже говорил, я не очень силен в Linux, поэтому если заметите неточности, то исправляйте - меня научите и статью улучшите. Так как эта тема довольно обширная, я решил разбить её на две части. В первой части мы рассмотрим процесс загрузки операционной системы и особенности файловой системы. Всем кому интересно, добро пожаловать!

Список статей

Здесь собраны ссылки на мои статьи из этой темы:
  1. Основы безопасности операционной системы Android. Уровень ядра
  2. Основы безопасности операционной системы Android. Native user space, ч.1
  3. Основы безопасности операционной системы Android. Native user space, ч.2
  4. Основы безопасности операционной системы Android. Безопасность на уровне Application Framework. Binder IPC

Что подразумевается под Native user space

Под Native user space подразумеваются все компоненты пространства пользователя, которые выполняются вне Dalvik Virtual Machine, и которые не являются частью Linux kernel.

Файловая система Android

Для начала давайте рассмотрим структуру файловой системы Android. Хотя Android и базируется на Linux kernel, привычную нашему глазу структуру файловой системы мы здесь не увидим. Давайте запустим эмулятор и посмотрим, что у нас есть. Для этого выполним комманду:

Adb shell ls -al
В моем терминале для эмулятора на Android 4.2 я вижу следующий результат:

Drwxr-xr-x root root 2013-04-10 08:13 acct drwxrwx--- system cache 2013-04-10 08:13 cache dr-x------ root root 2013-04-10 08:13 config lrwxrwxrwx root root 2013-04-10 08:13 d -> /sys/kernel/debug drwxrwx--x system system 2013-04-10 08:14 data -rw-r--r-- root root 116 1970-01-01 00:00 default.prop drwxr-xr-x root root 2013-04-10 08:13 dev lrwxrwxrwx root root 2013-04-10 08:13 etc -> /system/etc -rwxr-x--- root root 244536 1970-01-01 00:00 init -rwxr-x--- root root 2487 1970-01-01 00:00 init.goldfish.rc -rwxr-x--- root root 18247 1970-01-01 00:00 init.rc -rwxr-x--- root root 1795 1970-01-01 00:00 init.trace.rc -rwxr-x--- root root 3915 1970-01-01 00:00 init.usb.rc drwxrwxr-x root system 2013-04-10 08:13 mnt dr-xr-xr-x root root 2013-04-10 08:13 proc drwx------ root root 2012-11-15 05:31 root drwxr-x--- root root 1970-01-01 00:00 sbin lrwxrwxrwx root root 2013-04-10 08:13 sdcard -> /mnt/sdcard d---r-x--- root sdcard_r 2013-04-10 08:13 storage drwxr-xr-x root root 2013-04-10 08:13 sys drwxr-xr-x root root 2012-12-31 03:20 system -rw-r--r-- root root 272 1970-01-01 00:00 ueventd.goldfish.rc -rw-r--r-- root root 4024 1970-01-01 00:00 ueventd.rc lrwxrwxrwx root root 2013-04-10 08:13 vendor -> /system/vendor
Я отмечу здесь только главные директории и те, которые нам пригодятся в будущем. В Интернете можно найти описание и предназаначение других директорий. Можно заметить, что некоторые директории такие же, как и в Linux, например, /dev , /proc , /sys , /mnt , /etc И их предназначение в основном такое же, как и в Linux. Кстати, отметьте, что мы не видим /bin и /lib директорий. Где они скрылись, я расскажу чуть позже.

C другой стороны можно заметить директории, которых в Linux вообще нет. Среди них нас интересуют /data , /system , /cache , /init , /init.rc Давайте рассмотрим их назначение поподробнее.
/system Это главная директория, где хранятся неизменяемые компоненты Android системы. Если проводить аналогию, то эта папка похожа на папку C:\windows\ , доступную только для чтения. Т.е. изменять данные в этой директории мы не можем. Как раз здесь можно найти директории /bin и /lib , где хранятся различные исполняемые файлы и shared libraries. Кроме того, здесь же лежат системные приложения, которые встроены в операционку и которые, по умолчанию, нельзя удалить. Содержимое этой директории формируется во время компиляции операционной системы.
/data Т.к. /system у нас доступна только для чтения, то должна быть директория где хранятся изменяемые данные. /data как раз ею и является. Например, в эту директорию в /data/app сохраняются apk файлы устанавливаемых приложений, а в /data/data хранятся их данные (эту директорию мы подробно рассматривали в прошлой статье).
/cache Это просто временное хранилище. Также в эту директорию сохраняются, а потом из неё запускаются системные обновления.

Чтобы понять, что такое /init файл и для чего нужны непонятные файлы с расширением *.rc, рассмотрим процесс загрузки системы.

Процесс загрузки Android

Давайте рассмотрим несколько шагов процесса загрузки операционной системы Android. Эта картинка взята из книги «Embedded Android», там же можно найти и более детальное описание. Хотя в целом я и понимаю процесс, но для меня это больше магия:)

CPU. Когда вы нажимаете на кнопку включения, на процессор вашего устройства начинает подаваться напряжение. Так как до этого момента процессор был выключен, и так как он не способен сохранять свое состояние без подачи напряжения, то сразу после старта он находится в некотором неинициализированном состоянии. В данном случае процессор считывает из своего специального регистра некоторый жестко зашитый адрес и начинает выполнять инструкции начиная с него. Чаще всего, этот адрес указывает на чип, в который зашит bootloader (загрузчик).
Bootloader. Bootloader инициализирует RAM и загружает в неё Linux kernel. Кроме того Bootloader создает RAMdisk.
Linux kernel. Ядро инициализирует различные подсистемы, встроенные драйвера и монтирует root filesystem (корневую файловую систему). После этого ядро может запускать первую программу.
На этом магия заканчивается и дальше всё становится более-менее понятно.

Init

Первой программой в случае Android является init . Исполняемый файл находится в корневой директории (/init ). Именно эту программу стартует ядро после своей загрузки. Её исходники находятся в папке Давайте в них слегка покопаемся. Нас интересует :

Int main(int argc, char **argv) { ... /* clear the umask */ umask(0); /* Get the basic filesystem setup we need put * together in the initramdisk on / and then we will * let the rc file figure out the rest. */ mkdir("/dev", 0755); mkdir("/proc", 0755); mkdir("/sys", 0755); mount("tmpfs", "/dev", "tmpfs", MS_NOSUID, "mode=0755"); mkdir("/dev/pts", 0755); mkdir("/dev/socket", 0755); mount("devpts", "/dev/pts", "devpts", 0, NULL); mount("proc", "/proc", "proc", 0, NULL); mount("sysfs", "/sys", "sysfs", 0, NULL); ... init_parse_config_file("/init.rc"); ... }
Вначале мы создаем и монтируем некоторые необходимые для работы директории, а потом парсим файл /init.rc и выполняем то, что распарсили. Формат /init.rc файла очень хорошо описан в , там же можно найти и пример. Если кратко, то этот файл представляет собой набор actions (секций - именнованная последовательность комманд). Каждая последовательность команд срабатывает по определенному trigger (триггеру). Например, следующая последовательно - это action, в которой trigger - это fs, а последовательность команд - это набор mount команд:


Исходный файл /init.rc находится в Давайте рассмотрим некоторые основные его части, хотя я вам очень советую просмотреть его полность. После этого многие вещи вам должны стать понятны. Итак, начинается наш файл следующими строками:

Import /init.usb.rc import /init.${ro.hardware}.rc import /init.trace.rc
Они означают, что кроме init.rc файла нужно также импортировать настройки из файлов init.usb.rc , init.trace.rc и из файла с непонятным именем init.${ro.hardware}.rc Впрочем, ${ro.hardware} - это просто переменная, значение которая определяет тип железа. В случае эмулятора, её значение, например, - goldfish . Далее определяются переменные окружения:

On init ... # setup the global environment export PATH /sbin:/vendor/bin:/system/sbin:/system/bin:/system/xbin export LD_LIBRARY_PATH /vendor/lib:/system/lib export ANDROID_BOOTLOGO 1 export ANDROID_ROOT /system export ANDROID_ASSETS /system/app export ANDROID_DATA /data export ANDROID_STORAGE /storage export ASEC_MOUNTPOINT /mnt/asec export LOOP_MOUNTPOINT /mnt/obb export BOOTCLASSPATH /system/framework/core.jar:/system/framework/okhttp.jar:/system/framework/core-junit.jar:/system/framework/bouncycastle.jar:/system/framework/ext.jar:/system/framework/framework.jar:/system/framework/telephony-common.jar:/system/framework/mms-common.jar:/system/framework/android.policy.jar:/system/framework/services.jar:/system/framework/apache-xml.jar ...
После этого происходит инициализация переменных, необходимых для работы устройства. Если вас заинтересует эта тема, то вы легко найдете информацию о той или иной комманде. Давайте подробно рассмотрим следующий блок (который я уже приводил в этой статье):

On fs # mount mtd partitions # Mount /system rw first to give the filesystem a chance to save a checkpoint mount yaffs2 mtd@system /system mount yaffs2 mtd@system /system ro remount mount yaffs2 mtd@userdata /data nosuid nodev mount yaffs2 mtd@cache /cache nosuid nodev
MTD - Memory Technology Devices. Если в общих чертах, то MTD - это специальный чип с энергонезависимой (т.е. данные на этом чипе сохраняются после перезагрузки или выключения) flash-памятью (типа NOR или NAND), на который сохраняются образы дисков. В этой статье более подробно рассказывается об этом типе устройств, а также об ограничениях. Специально для этих разновидностей flash-памяти были разработаны специальные файловые системы, например, YAFFS. Одно из самых важных ограничений этих типов памяти заключается в том, что для того чтобы записать данные в сектор, куда уже записаны какие-то данные, вам надо полностью сначала стереть весь сектор. Поэтому производители стали переходить на новый тип блочной flash-памяти (eMMC), на которые можно поставить обычную ext4 файловую систему и избавиться от указанного ограничения. Т.к. я показываю пример init.rc файла для эмулятора, где вся работа эмулируется, то в нем по умолчанию используется файловая система YAFFS2 (думаю, что это пережитки прошлого, т.к. YAFFS2 использовалась для всех устройств до Android 2.2). В реальном устройстве (это как раз один из примеров, когда необходимо использовать init.rc файл для определенного железа) эти комманды будут перезаписаны. Например, в случае устройства herring (Google Nexus S), в файле эта секция выглядит следующим образом:

On fs mkdir /efs 0775 radio radio mount yaffs2 mtd@efs /efs noatime nosuid nodev chmod 770 /efs/bluetooth chmod 770 /efs/imei mount_all /fstab.herring ...
Где fstab.herring - это , содержимое которого выглядит следующим образом:

... /dev/block/platform/s3c-sdhci.0/by-name/system /system ext4 ro wait /dev/block/platform/s3c-sdhci.0/by-name/userdata /data ext4 noatime,nosuid,nodev,nomblk_io_submit,errors=panic wait,encryptable=/efs/userdata_footer
Как вы могли заметить, /system , /data , /cache - это просто mounting points (точки монтирования файловой системы), которые указывают либо на MTD устройства (в случае эмулятора), либо на блочные устройства (в случае настоящего устройства), куда записаны соответствующие дисковые образы (system.img, userdata.img и cache.img). Я не уверен, но думаю, что внутри смартфона находится один единственный чип с flash-памятью, разделенный на partitions (тома), в каждый из которых записан соответствующий образ. Этот чип с flash-памятью - то, что мы знаем под именем Internal storage (внутренняя память), объем которой - один из основных параметров смартфона.

Следует заметить, что /system смонтирован read-only (только для чтения). Это означает, что содержимое данного раздела не изменяется в процессе работы устройства, а только когда вы, например, обновляете систему на вашем устройстве (используя системные обновления).

Продолжим рассматривать наш init.rc . По триггеру post-fs-data формируется базовая структура файловой системы /data раздела. Там, в общем всё понятно - набор mkdir , chown , chmod команд.

Далее init.rc запускает несколько демонов. Если вернуться к рисунку в начале статьи, то они перечислены в блоке Native daemons. На этом мы пока остановимся. Как вы могли заметить из рисунка, я не полностью рассмотрел процесс загрузки операционной системы. Некоторые непокрытые этапы я рассмотрю в следующих статья.

Заключение

В следующей части я расскажу, откуда берутся образы system.img, userdata.img и cache.img и рассмотрю безопасность на уровне Native user space. Как всегда приветствуются исправления, дополнения, а так же предложения, о чем написать. И хотя у меня уже есть некоторый план, о чем писать в следующих статья, я готов его подкорректировать.