LXF160:Android-устройство
|
|
|
» Изучаем внутреннее устройство системы
Содержание |
Android: А что у нас внутри?
Роман Ярыженко решил попристальнее приглядеться к отличительным особенностям платформы Android.
О платформе Android не слышал нынче, кажется, только ленивый. Это неудивительно – по многим причинам; некоторые из них будут рассматриваться дальше. Пока же можно отметить беспрецедентную открытость по сравнению с другими популярными платформами (конечно, на момент написания статьи вслед за Google исходные коды начали открывать и другие фирмы, но Android был первой ласточкой). На этом вступление можно закончить и перейти к предмету написания статьи.
История успеха
Google купил стартап Android Inc в 2005... Нет, пожалуй, надо сначала сделать небольшой экскурс в прошлое. Прежде всего отметим, что Linux стал использоваться в сотовых телефонах за 4 года до выпуска первой версии Apple iOS, то есть в 2003 году. И первым смартфоном (не прототипом), в котором официально использовался Linux, был Motorola A760. В этом же году свой смартфон на базе Linux, SCH-i519, представил Samsung. Предлагаю рассмотреть первый – хотя бы ради интереса.
Каким же он был, первый смартфон на основе Linux? MontaVista CEE 3.0 на основе ядра 2.4.20, Intel Xscale PXA262, 16 МБ ОЗУ, 32 ПЗУ... слабоват по современным меркам. И несмотря на то, что там стоял Linux с QT, на нем без особых телодвижений... нельзя было запускать ничего, кроме J2ME-приложений! Ладно. Смотрим, скажем, в 2005 год. В конце года был анонсирован китайский смартфон Haier N60 (первый официальный Linux-смартфон на российском рынке). Характеристики – процессор Intel Xscale PXA271, 312 MHz, 59 МБ ПЗУ... и опять же невозможность запускать что-либо, помимо J2ME.
Картина ясна: несмотря на наличие Linux в смартфонах того времени, они были почти такими же закрытыми, как и те, что были основаны на проприетарных платформах (OSE, Nucleus...).
В июле 2007 года фирмой OpenMoko был выпущен смартфон Neo1973. Смартфон был полностью (за исключением, быть может, стека GSM) открытым. Но из-за того, что целевой аудиторией были технари (а также, возможно, из-за не слишком удачной маркетинговой политики), он не получил широкого распространения.
В том же 2007 году Google был основан OHA – Open Handset Alliance. День его основания и считается началом Android-эры.
Первым устройством на основе Android стал HTC Dream, он же T-Mobile G1. Сей гаджет имел на борту 192 МБ ОЗУ, 256 МБ ПЗУ, процессор Qualcomm 7201 528 МГц и QWERTY-клавиатуру.
Возникает резонный вопрос: почему именно Android, а не какой-нибудь Mizi Linux? Помимо закулисных интриг Google (без которые, как мне кажется, действительно не обошлось) и прочих конспиративных теорий заговора, имеется и вполне резонный ответ на этот вопрос. Производителей привлекает низкая стоимость разработки. Ну еще бы – за ядро платить не надо, за интерфейс – тоже. А также, что немаловажно, большая часть фреймворка Android кроссплатформенна, что, впрочем, больше касается разработчиков ПО. Конечным же пользователям нравится централизованный Android Market – своего рода репозиторий, идея которого была подсмотрена у Apple, ну а Apple же, по всей видимости, взяла эту идею у дистрибьюторов Linux.
...устройства на основе Android штампуются десятками тысяч в день, и не только с именитыми брендами (Motorola, Alcatel), но и безвестными фирмами. Давайте же перейдем к описанию архитектуры и возможностей этой платформы.
Внутреннее устройство
Начнем с очевидного. В основе Android лежит ядро Linux. Нет, исключения, конечно же, имеются – поскольку, строго говоря, то, что большинство пользователей называет словом «Android», теоретически может запускаться даже на Windows CE. Но здесь и далее мы не будем рассматривать подобные случаи (точнее говоря, мы будем рассматривать Android 2.2 с ядром 2.6.39, но, думается, все что будет описано, применимо и к старшим версиям, а частично – и к младшим).
Архитектура ядра, в общем-то, такая же, как и везде (за исключением, может быть, IPC – но IPC частично относится к пользовательскому режиму), поэтому мы не будем на ней останавливаться, а поднимемся выше Однако, если это будет необходимо, мы опишем механизмы ядра – тот же IPC.
Выше у нас libc. В качестве такового используется не glibc, а Bionic, построенный на базе libc BSD – Google посчитала, что Glibc не подходит из-за тяжеловесности и лицензионных ограничений. Но, поскольку Bionic создавался с учетом встраиваемых систем, разработчикам пришлось поступиться некоторыми функциями «старшего брата». Так, он не поддерживает исключений C++, у него собственная реализация pthread, вместо использования, к примеру, ./etc/resolv.conf используется область разделяемой памяти, многие функции, типичные для «настольных» дистрибутивов, такие как getpwent(), просто-напросто отсутствуют из-за специфики архитектуры.
Что же дальше? А дальше идет слой HAL – набор библиотек, написанный на C/C++, служит для обеспечения прозрачности работы с оборудованием. И на этом же примерно уровне остальные Native-библиотеки (мы не будем их подробно рассматривать, но перечислим некоторые) – WebKit, SSL, OpenGL ES...
Поднимаемся выше – на уровень Native-приложений. К таковым относятся, например, installd – демон, через который проходит установка пакетов, и rild – демон-прослойка между аппаратно-специфичным и платформо-независимым ПО GSM.
Едем дальше... что мы видим? Видим фреймворк и виртуальную машину, очень похожие на JRE, но формально им не являющиеся. Тем не менее, набор базовых классов и синтаксис основного языка программирования ясно дают понять, что в основе фреймворка лежит творение Sun.
Но как же выполняется деление на процессы и выполняется ли оно вообще? Для ответа на первый вопрос необходимо ответить на второй. Да, выполняется. Но зачем? Неужели нельзя положиться на фреймворк? Дело в том, что, если на обычном компьютере процесс виртуальной машины Java «упадет», с системой ничего не случится. А если он упадет на Android, где на Java написан практически весь средний и верхний уровень, то последствия будут фатальными. Таким образом, разделение на процессы оказывается необходимым. Теперь ответим на вопрос «как?». В JRE 5 была спецификация Isolates, и кое-где они применяются до сих пор (JNode), но в JRE 6 их убрали, а в Android используются обычные Linux-процессы. Каждый новый процесс Android порождается ответвлением процесса Zygote (исключение составляет system_server, в котором выполняется, например, графическая подсистема) и имеет название вида com.android.Launcher. Фактически, каждый процесс запускается в собственной ВМ, которая носит название Dalvik VM. Особенности Dalvik VM таковы: собственный формат хранения классов DEX, собственный байт-код, регистровая архитектура, а начиная с версии Android 2.2 – еще и JIT-компиляция.
Рассмотрим некоторые части фреймворка, а также некоторые специфичные понятия Android.
Приложения Android строятся на основе уже имеющихся классов. То есть, к примеру, если потребуется разработать собственный редактор, для этого берется уже готовый класс и расширяется/дополняется до необходимого. В этом (да и не только в этом) смысле платформа Android более похожа на Web-приложения и фреймворки.
У приложения всегда есть манифест (нет-нет, это ни в коем разе не манифест Коммунистической партии! Это всего-навсего конфигурационный файл формата XML, который при сборке приложения преобразуется в двоичный вид). Он предназначен для описания структуры приложения, его привилегий, требований к железу... в общем, много для чего.
Приложение может быть многоуровневым. Рассмотрим список основных классов фреймворка Android, на основе которых строятся большинство приложений (этот список далеко не полный; сюда включены лишь самые основные классы):
» Activity – грубо говоря, логика GUI; сам графический интерфейс описывается в XML-файлах слоев. Activity доступен только на переднем плане, во всех остальных случаях он уничтожается – исключение составляет разве что случай, когда теряется фокус, в этом случае Activity приостанавливается.
В Android не реализован многооконный интерфейс – по той простой причине, что экран не позволяет вместить большое количество окон. Вместо этого используется модель стека: на вершине находится тот Activity, который сейчас на переднем плане. При нажатии аппаратной кнопки «Back» текущий Activity уничтожается, а тот, который находился ниже в стеке, создается вновь.
» ContentProvider – если Activity можно считать клиентом, то ContentProvider в терминах web-фреймворков выполняется на стороне «сервера» (на самом деле, все несколько сложнее, но рассмотрение шаблона проектирования MVVM выходит за рамки днной статьи). ContentProvider предоставляет Activity данные и в большинстве случаев является оберткой вокруг SQLite. Выполняется он в контексте требующего данных Activity, поэтому не может работать сам по себе.
» BroadcastReceiver является некой «прослойкой», которая перехватывает широковещательные сообщения (Intents; о них чуть позже) и решает, что делать в ответ. Иначе говоря, это обработчик событий, которые создаются путем посылки сообщения методом sendBroadcast (либо sendOrderedBroadcast) соответствующего класса. Необходимо отметить, что экземпляры класса BroadcastReceiver «одноразовы», т. е. их необходимо либо регистрировать в манифесте, либо после срабатывания регистрировать заново (в случае, если BroadcastReceiver использовался в контексте Activity).
» Service... думается, что с этим все более-менее понятно. Это компонент приложения, работающий в фоновом режиме и не имеющий графического интерфейса. Примером может служить MP3-плейер или, допустим, служба предупреждения о низком заряде аккумулятора. В отличие от Activities, которые уничтожаются, если пользователь запустил другое приложение, службы уничтожаются либо сами, либо в случае, если имеет место исчерпание памяти.
Теперь, кажется, можно перейти к следующему разделу данной статьи.
IPC и безопасность
IPC в Android довольно своеобразна. Помимо стандартных средств Unix IPC (каналы, сигналы, сокеты) в платформе от Google появилось новое средство – BINDER. Но, как водится, «Новое – это хорошо забытое старое», поэтому совершим краткий исторический экскурс.
Предтеча BINDER, OpenBinder, был разработан в ныне уже не существующей Be Inc. в пику попыткам разработки объектно-ориентированной ОС. Большинство из этих попыток предполагали поддержку ООП ядром. OpenBinder же, напротив, мог базироваться на существующих платформах. Практически, аналогом может служить технология COM от Microsoft или (возможно, в меньшей степени) CORBA.
Почему именно BINDER, а не SysV IPC? Дело в том, что SysV IPC подвержен утечкам ресурсов. А одним из требований к архитектуре BINDER было как раз требование не допускать утечек.
Посмотрим, как устроен BINDER в Android. В самом низу лежит модуль ядра, написанный на C. Что делает этот модуль? Он предоставляет базовые операции-примитивы над передаваемыми ресурсами. Файл устройства /dev/binder поддерживает такие операции, как open, mmap, release, poll, а сам драйвер поддерживает ioctl. Файловые операции мы рассматривать не будем, а рассмотрим самую «вкусную» – ioctl. Аргументы системного вызова – код команды и буфер данных. Коды команд могут быть такими:
» BINDER_WRITE_READ – наиболее часто используемая команда, передает пакет(ы) данных драйверу.
» BINDER_SET_MAX_THREADS – используется для задания максимального количества потоков для каждого процесса, которые должны отвечать на запросы BINDER. Иначе говоря, это ограничивает число соединений с каждым процессом.
» BINDER_SET_CONTEXT_MGR – задает Context Manager (см. далее), может быть выполнена только один раз при загрузке Android.
» BINDER_THREAD_EXIT – используется, если связанный поток завершился.
» BINDER_VERSION – ну... тут без коментариев.
У читателя может возникнуть резонный вопрос: зачем реализовывать BINDER в режиме ядра? Если не вдаваться в подробности (а мы не будем этого делать, ибо размер статьи ограничен), здесь есть два ответа: во-первых, это дает выигрыш в производительности, а во-вторых – по историческим причинам.
Выше модуля ядра лежит часть фреймворка BINDER, реализованная в режиме пользователя и написанная на C++. Здесь не имеется ничего такого, на что можно было бы обратить внимание, разве что тот момент, что программы на «чистом» C++ могут использовать фреймворк напрямую, но при этом придется заново описывать абстракции более высокого уровня, которые на Java уже реализованы.
Переходим на уровень Java. Тут появляются новые термины:
» Binder – в англоязычных источниках так называется как архитектура BINDER, так и конкретная реализация интерфейса. Самое время вспомнить, что «Binder» в переводе с английского означает... в общем, вещество, которое связывает. Далее мы будем именовать саму идею, архитектуру «связывания» прописными буквами – «BINDER», конкретную реализацию – строчными, а в случае, когда более подходящим будет русский термин, будем использовать его.
» Интерфейс IBinder [IBinder interface] – строго определенный набор методов, свойств и событий, который может поддерживать конкретная реализация Binder.
» Объект Binder [Binder Object] – экземпляр класса, который применяет Binder. Может реализовывать несколько связываний.
» Маркер Binder [Binder Token] – уникальный идентификатор Binder.
» Объекты Intent – объекты, инкапсулирующие сообщения. Эти объекты могут использоваться как внутри одного процесса, так и для межпроцессного взаимодействия. Необходимо отметить, что сообщения асинхронны. Очевидно, они могут использоваться и для оповещения о каком-то событии.
» AIDL (Android Interface Definition Language) – используется для описания преобразования объектов Java в объекты, которые могут передаваться между процессами. Передавать можно только примитивы, строки, списки и объекты, реализующие интерфейс Parcelable. AIDL на практике на этапе компиляции преобразуется в код Java, который «запаковывает» и «распаковывает» соответственно передаваемые методу параметры и возвращаемые значения в объекты Parcel.
» Parcel – образно, упаковка для передачи более сложных объектов, чем примитивы Java. Класс должен реализовывать «запаковку» и «распаковку» этих объектов.
Читателю, скорее всего, уже надоели все эти перечисления. Тем не менее, код мы рассматривать не будем, но и излишне теоретизировать тоже не будем. Вместо этого ответим на вопрос: как же это все работает? Итак...
При запуске Android диспетчер служб [Service Manager] регистрируется в модуле ядра Binder в качестве Context Manager путем посылки соответствующего кода команды, и получает идентификатор Binder, равный нулю. Это делается из соображений безопасности, так как «сферический клиент в вакууме» не должен заранее знать маркер связывания сервера. Все операции по связыванию проходят через диспетчер служб.
Когда сервер (в роли которого обычно выступает служба) запускается, он вызывает метод onBind и таким образом регистрируется в системе. Если клиент хочет связаться с сервером, он вызывает метод bindService, среди параметров которого имеется, в том числе, и Intent, в коем находится имя службы. После этого, если не вдаваться в технические подробности, клиенту диспетчером служб выдается маркер Binder, с помощью которого и выполняются дальнейшие операции. Если сервер находится в одном процессе с клиентом, модуль ядра не используется.
В данном описании все очень упрощено (к примеру, не рассматривается, что должно получиться в результате компиляции AIDL-файла).
Упомянем еще один механизм IPC Android – ashmem, механизм разделяемой памяти. Основное отличие его от SysV shmem – то, что имеется счетчик ссылок сегментов; таким образом уменьшается вероятность утечки памяти.
Теперь о принципах безопасности. Безопасность в Android обеспечивается в основном при посредстве трех следующих механизмов:
1 Поскольку устройства Android по идее однопользовательские (в самом деле, не будет же среднестатистический пользователь сотового телефона разрешать звонить с него другим лицам? Сотовые телефоны нынче – что-то наподобие предмета личной гигиены), Google сделал «финт ушами» – он не стал изобретать велосипед, а использовал для изоляции каждого приложения систему UID и GID. Таким образом, если какое-либо приложение попробует удалить или изменить данные другого, у него этого не получится.
2 Механизм привилегированных API. Доступ к некоторым ресурсам Android, таким, к примеру, как телефония и SMS, ограничен. Это необходимо, поскольку Android – система многозадачная, а некорректное использование данных ресурсов может навредить системе, пользователю или его кошельку. Чтобы использовать какие-либо привилегии, при разработке приложения необходимо их прописать в манифесте.Пользователь при установке приложения может либо согласиться, либо не согласиться со всем набором привилегий полностью; выбирать их по отдельности он не может. Но в некоторых модифицированных прошивках (таких, например, как CyanogenMod) подобная возможность предусмотрена.
3 И, наконец, каждое Android-приложение должно быть подписано цифровым сертификатом (проверяется только при установке). Это позволяет уникально идентифицировать автора и, в теории, должно препятствовать распространению вредоносных программ. На практике же абсолютное большинство приложений является самоподписанными, в отличие от аналогичной системы, например, в Symbian, где подписанными приложения могут быть только с использованием сертификата от центра сертификации, привязанного к IMEI и к тому же предоставляющего ограниченный набор привилегий (за расширенный набор уже надо платить и, возможно, отправлять подписанное приложение на проверку). Автор не нашел в исходных кодах Android возможности отключить самоподписанные сертификаты; судя по всему, таковой там и не имеется.
Но пришла пора закруглять статью. Скажем лишь, что в одной статье трудно описать все возможности того или иного программного проекта. Это в полной мере относится и к Android. Можно лишь с тем или иным успехом выделить отличительные особенности платформы, что мы и попытались сделать.
Итоги
В заключение следует отметить, что Google сейчас задает направления в области развития высоких технологий, и мобильных платформ это тоже касается. Но что ждет Android в будущем? Не прекратят ли его разработку в результате патентных козней «злых дядей» из Редмонда?.. На этот вопрос можно будет дать ответ лет через 5; пока же шествие Android по планете выглядит триумфальным. |