sonyps4.ru

Основные UI паттерны разработки Android приложений. Объявление требований приложения

  • Перевод

В этой статье мы рассмотрим архитектуру Android-приложений.

Откровенно говоря, официальную Google по этой теме я считаю не очень полезной. Детально отвечая на вопрос «как», она совсем не объясняет «что» и «почему». Итак, вот моя версия, и, я надеюсь, она внесёт некоторую ясность. Да, кстати, я полностью одобряю чтение статей Google, поскольку они содержат полезную информацию, повторять которую я не собираюсь.

Архитектура ОС Android - немного истории

Как это часто бывает в IT, многие вещи не могут быть объяснены в отрыве от истории возникновения конкретного программного обеспечения. Вот почему мы должны обратиться к истокам ОС Android.

Разработка ОС Android была начата в 2003 молодой компанией Android Inc. В 2005 году эта компания была куплена Google. Я считаю, что главные особенности архитектуры Android были определены именно в этот период. Это заслуга не только Android Inc; архитектурные концепции и финансовые ресурсы Google оказали решающее влияние на архитектуру Android. Далее я приведу несколько примеров.

Если вы помните, 2003-2005 года были ознаменованы повышенным вниманием к AJAX приложениям. Я думаю, это оказало основополагающее влияние на архитектуру Android: во многих аспектах она ближе к архитектуре типичного AJAX приложения, нежели к десктопному GUI приложению, написанному на Java, C#, C++, VB и тп.

Не знаю, почему так произошло. Моя догадка - это придумал кто-то из Google в тот период, когда насыщенные интернет-приложения (Rich Internet Applications, RIA) в духе Google Docs или Gmail считались решением всех проблем. По-моему, эту идею нельзя назвать ни плохой, ни хорошей. Просто помните, что Android-приложения очень сильно отличаются от десктопных.

Влияние архитектурной философии Eclipse заметно в выборе принципа реализации GUI, который больше похоже на SWT, нежели на Swing.

В стандартах оформления кода Android присутствует «венгерская нотация», рождённая в стенах MS. Можно предположить, что тот, кто писал эти стандарты, ранее занимался разработкой под Windows.

Архитектурные уровни Android
Операционная система Android имеет три весьма различных и сильно отделённых друг от друга уровня:
  1. В основе лежит модифицированная и урезанная версия Linux, как я и упоминал в одной из моих предыдущих статей .
  2. Над уровнем Linux находится уровень инфраструктуры приложения, содержащий виртуальную машину Dalvik , веб-браузер, базу данных SQLite , некие инфраструктурные «костыли» и Java API.
  3. И, наконец, уровень написанных в Google Android-приложений. Вообще говоря, они являются расширением уровня инфраструктуры, поскольку разработчик может использовать эти приложения или их части как строительные блоки для собственных разработок.
Рассмотрим эти слои один за другим и более подробно.

Уровень Linux

Представьте себе, что вы - архитектор в молодой компании. Вы должны разработать ОС для нового типа устройств. Что вы будете делать?

Грубо говоря, у вас два пути: реализовывать собственные идеи, начав с нуля или же использовать существующую ОС и адаптировать её под свои устройства.

Реализация с нуля всегда звучит захватывающе для программистов. В эти моменты мы все верим в то, что в этот раз мы всё сделаем лучше, чем делают другие, и даже лучше, чем мы сами делали ранее.

Тем не менее, это не всегда практично. Например, использование ядра Linux заметно уменьшило стоимость разработки (возможно где-то и без того чрезмерно большую). Согласитесь, если кто-то решит создать нечто, напоминающее ядро Linux в его сегодняшнем состоянии, ему потребуется несколько миллионов долларов.

Если вы руководите Android Inc, то у вас по определению не может быть столько денег. Если вы руководите Google, то у вас такие деньги найдутся, но вы, скорее всего, подумаете дважды, прежде чем потратить их на создание собственной ОС. Так же вы потратите несколько лет, прежде чем достигните сегодняшнего состояния Linux; несколько лет задержки могут стать слишком большим опозданием при выходе на рынок.

В подобной ситуации компания Apple решила построить Mac OS на основе Free BSD. Android Inc приняла решение использовать Linux как основу для Android. Исходники как Free BSD, так и Linux, находятся в свободном доступе и предоставляют собой хорошую основу для любых разработок, будь то Apple или Google.

Но в то время запустить стандартный Linux на мобильном устройстве было невозможно (сейчас это уже не так). Устройства имели слишком мало оперативной и энергонезависимой памяти. Процессоры были значительно медленнее по сравнению с процессорами компьютеров, где обычно используется Linux. Как результат, разработчики Android решили минимизировать системные требования Linux.

Если рассматривать Linux на высоком уровне, то это комбинация ядра (без которого нельзя обойтись) и множества других, необязательных частей. Можно даже запустить одно ядро, без чего бы то ни было ещё. Так, Google вынуждена в любом случае использовать ядро Linux как часть ОС Android. Кроме того, были рассмотрены необязательные части и из них выбрано самое необходимое. Например, были добавлены сетевой фаервол IPTables и оболочка Ash. Любопытно, что добавили именно Ash, а не Bash, не смотря на то, что последний на порядок мощнее; вероятно, это решение было основано на том, что Ash менее требователен к ресурсам.

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

Выбор Linux в качестве основы оказал огромное влияние на все аспекты ОС Android. Сборка Android, по сути, есть вариация процесса сборки Linux. Код Android находится под управлением git (инструмент, разработанный для управления кодом Linux). И так далее.

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

Вы можете спросить, как же быть, если необходимо разработать нативное приложение для Android? Google настоятельно не рекомендует делать этого. Технически, конечно, это возможно, но в дальнейшем у вас не будет возможности распространять это приложение нормальным способом. Так что подумайте дважды, прежде чем начать нативную разработку под Android, если конечно, вы не работает над Android Open Source Project (AOSP), т.е. собственно ОС Android.

Уровень инфраструктуры приложения

Несмотря на некоторое сходство Apple iOS и Android ОС, существуют значительные отличия между архитектурными решениями на инфраструктурном уровне обоих ОС.

Apple решила использовать Objective-C как язык программирования и среду выполнения приложения iOS. Objective-C выглядит более или менее естественным выбором для ОС, в основе которой лежит Free BSD. Можно рассматривать Objective-C как обычный C++ с кастомным препроцессором, который добавляет некоторые специфические лингвистические конструкции. Почему же нельзя использовать стандартный C++, на котором написана Free BSD? Мне кажется причина в том, что Apple старается всё делать в своём, «эппловском» стиле.

Основная идея в том, что приложения iOS написаны более или менее на том же языке, что и стоящая за ними ОС.

Android-приложения сильно отличаются в этом смысле. Они написаны на Java, а это совсем другая технология, нежели C++ (хотя синтаксис и унаследован от C++).

Я думаю, основная причина состоит в необходимости одному и тому же приложению работать на различном аппаратном обеспечении. Эта проблема имеет место лишь для ОС Android; у ребят из Apple такой проблемы нет. iOS работает только на оборудовании собственного производства, и Apple полностью контролирует весь процесс. Для Android же всё наоборот: Google не контролирует производителей аппаратных средств. Например, ОС Android работает на процессорах с архитектурой x86, ARM и Atom (в комментах подсказывают, что x86 включает в себя Atom, и Android работает на x86, ARM, PPC и MIPS - примечание переводчика ). На бинарном уровне эти архитектуры несовместимы.

Если бы архитекторы ОС Android выбрали тот же путь, что и архитекторы из Apple, разработчики приложений под Android были бы вынуждены распространять несколько версий одного и того же приложения одновременно. Это стало бы серьёзной проблемой, которая могла бы привести к краху всего проекта Android.

Для того, чтобы одно и то же приложение могло работать на разном аппаратном обеспечении, компания Google использовала контейнер-ориентированную архитектуру (container-based architecture). В такой архитектуре двоичный код выполняется программным контейнером и изолируется от деталей конкретного аппаратного обеспечения. Примеры всем знакомы - Java и C#. В обоих языках двоичный код не зависит от специфики аппаратного обеспечения и выполняется виртуальной машиной.

Конечно, есть и другой способ достигнуть независимости от аппаратного обеспечения на уровне двоичного кода. Как один из вариантов, можно использовать эмулятор аппаратного обеспечения, так же известный как QEMU . Он позволяет эмулировать, например, устройство с процессором ARM на платформе x86 и так далее. Google могла бы использовать C++ как язык для разработки приложений внутри эмуляторов. Действительно, Google использует такой подход в своих эмуляторах Android, которые построены на основе QEMU.

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

Как бы то ни было, компания Google пришла к решению использовать Java как основной язык разработки приложений и среды их выполнения.

Я думаю, это было критически важное архитектурное решение, которое поставило Android в стороне от остальных мобильных ОС на основе Linux, представленных в настоящее время. Насколько мне известно, ни у одной из них нет совместимости двоичного кода на уровне приложений. Возьмём для примера MeeGo . Она использует C++ и фреймворк Qt ; не смотря на то, что Qt кроссплатформенный, необходимость делать разные сборки для разных платформ не исчезает.

Выбрав Java, нужно было решить, какую виртуальную машину (JVM) использовать. Ввиду ограниченности ресурсов использование стандартной JVM было затруднено. Единственным возможным выбором было использование Java ME JVM, разработанной для мобильных устройств. Однако счастье Google было бы неполным без разработки собственной виртуальной машины, и появилась Dalvik VM .

Dalvik VM отличается от других виртуальных Java-машин следующим:

  • Она использует специальный формат DEX для хранения двоичных кодов, в противовес форматам JAR и Pack200, которые являются стандартом для других виртуальных Java-машинах. Компания Google заявила, что бинарники DEX меньше, чем JAR. Я думаю, с тем же успехом они могли бы использовать Pack200, но они решили пойти своим путём.
  • Dalvik VM оптимизирована для выполнения нескольких процессов одновременно.
  • Dalvik VM использует архитектуру, основанную на регистрах против стековой архитектуры в других JVM, что приводит к увеличению скорости выполнения и уменьшению размеров бинарников.
  • Она использует собственный набор инструкций (а не стандартный байткод JVM)
  • Возможен запуск (если необходимо) нескольких независимых Android-приложений в одном процессе
  • Выполнение приложения может охватывать несколько процессов Dalvik VM «естественным образом» (позже мы обсудим, что это значит). Для поддержи этого добавлено:
    • Специальный механизм сериализации объектов, основанный на классах Parcel и Parcelable. Функционально преследуются те же цели, что и Java Serializable, но в результате данные имеют меньший объём и потенциально более терпимы к версионным изменениям классов.
    • Особый способ для выполнения вызовов между процессами (inter process calls, IPC), основный на Android Interface Definition Language (AIDL).
  • До Android 2.2 Dalvik VM не поддерживала JIT-компиляцию, что было серьёзным ударом по производительности. Начиная с версии 2.2, скорость выполнения часто используемых приложений

Тебя никогда не интересовало, как работают fastboot или ADB? Или почему смартфон под управлением Android практически невозможно превратить в кирпич? Или, может быть, ты давно хотел узнать, где кроется магия фреймворка Xposed и зачем нужны загрузочные скрипты /system/etc/init.d? А как насчет консоли восстановления (recovery)? Это часть Android или вещь в себе и почему для установки сторонней прошивки обычный рекавери не подходит? Ответы на все эти и многие другие вопросы ты найдешь в данной статье.

Как работает Android

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

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

Шаг первый. ABOOT и таблица разделов

Все начинается с первичного загрузчика. После включения питания система исполняет код загрузчика, записанного в постоянную память устройства. Затем он передает управление загрузчику aboot со встроенной поддержкой протокола fastboot, но производитель мобильного чипа или смартфона/планшета имеет право выбрать и любой другой загрузчик на его вкус. Например, компания Rockchip использует собственный, несовместимый с fastboot загрузчик, для перепрограммирования и управления которым приходится использовать проприетарные инструменты.

Протокол fastboot, в свою очередь, представляет собой систему управления загрузчиком с ПК, которая позволяет выполнять такие действия, как разлочка загрузчика, прошивка нового ядра и recovery, установка прошивки и многие другие. Смысл существования fastboot в том, чтобы иметь возможность восстановить смартфон в начальное состояние в ситуации, когда все остальные средства не работают. Fastboot останется на месте, даже если в результате экспериментов ты сотрешь со смартфона все разделы NAND-памяти, содержащие Android и recovery.

Получив управление, aboot проверяет таблицу разделов и передает управление ядру, прошитому в раздел с именем boot, после чего ядро извлекает в память RAM-образ из того же раздела и начинает загрузку либо Android, либо консоли восстановления. NAND-память в Android-устройствах поделена на шесть условно обязательных разделов:

  • boot - содержит ядро и RAM-диск, обычно имеет размер в районе 16 Мб;
  • recovery - консоль восстановления, состоит из ядра, набора консольных приложений и файла настроек, размер 16 Мб;
  • system - содержит Android, в современных девайсах имеет размер не менее 1 Гб;
  • cache - предназначен для хранения кешированных данных, также используется для сохранения прошивки в ходе OTA-обновления и поэтому имеет размер, сходный с размерами раздела system;
  • userdata - содержит настройки, приложения и данные пользователя, ему отводится все оставшееся пространство NAND-памяти;
  • misc - содержит флаг, определяющий, в каком режиме должна грузиться система: Android или recovery.

Кроме них, также могут существовать и другие разделы, однако общая разметка определяется еще на этапе проектирования смартфона и в случае aboot зашивается в код загрузчика. Это значит, что: 1) таблицу разделов нельзя убить, так как ее всегда можно восстановить с помощью команды fastboot oem format; 2) для изменения таблицы разделов придется разлочить и перепрошить загрузчик с новыми параметрами. Из этого правила, однако, бывают исключения. Например, загрузчик того же Rockchip хранит информацию о разделах в первом блоке NAND-памяти, так что для ее изменения перепрошивка загрузчика не нужна.

Особенно интересен раздел misc. Существует предположение, что изначально он был создан для хранения различных настроек независимо от основной системы, но в данный момент используется только для одной цели: указать загрузчику, из какого раздела нужно грузить систему - boot или recovery. Эту возможность, в частности, использует приложение ROM Manager для автоматической перезагрузки системы в recovery с автоматической же установкой прошивки. На ее же основе построен механизм двойной загрузки Ubuntu Touch, которая прошивает загрузчик Ubuntu в recovery и позволяет управлять тем, какую систему грузить в следующий раз. Стер раздел misc - загружается Android, заполнил данными - загружается recovery… то есть Ubuntu Touch.

Шаг второй. Раздел boot

Если в разделе misc не стоит флаг загрузки в recovery, aboot передает управление коду, расположенному в разделе boot. Это не что иное, как ядро Linux; оно находится в начале раздела, а сразу за ним следует упакованный с помощью архиваторов cpio и gzip образ RAM-диска, содержащий необходимые для работы Android каталоги, систему инициализации init и другие инструменты. Никакой файловой системы на разделе boot нет, ядро и RAM-диск просто следуют друг за другом. Содержимое RAM-диска такое:

  • data - каталог для монтирования одноименного раздела;
  • dev - файлы устройств;
  • proc - сюда монтируется procfs;
  • res - набор изображений для charger (см. ниже);
  • sbin - набор подсобных утилит и демонов (adbd, например);
  • sys - сюда монтируется sysfs;
  • system - каталог для монтирования системного раздела;
  • charger - приложение для отображения процесса зарядки;
  • build.prop - системные настройки;
  • init - система инициализации;
  • init.rc - настройки системы инициализации;
  • ueventd.rc - настройки демона uventd, входящего в состав init.

Это, если можно так выразиться, скелет системы: набор каталогов для подключения файловых систем из разделов NAND-памяти и система инициализации, которая займется всей остальной работой по загрузке системы. Центральный элемент здесь - приложение init и его конфиг init.rc, о которых во всех подробностях я расскажу позже. А пока хочу обратить внимание на файлы charger и ueventd.rc, а также каталоги sbin, proc и sys.

Файл charger - это небольшое приложение, единственная задача которого - вывести на экран значок батареи. Он не имеет никакого отношения к Android и используется тогда, когда устройство подключается к заряднику в выключенном состоянии. В этом случае загрузки Android не происходит, а система просто загружает ядро, подключает RAM-диск и запускает charger. Последний выводит на экран иконку батареи, изображение которой во всех возможных состояниях хранится в обычных PNG-файлах внутри каталога res.

Файл ueventd.rc представляет собой конфиг, определяющий, какие файлы устройств в каталоге sys должны быть созданы на этапе загрузки системы. В основанных на ядре Linux системах доступ к железу осуществляется через специальные файлы внутри каталога dev, а за их создание в Android отвечает демон ueventd, являющийся частью init. В нормальной ситуации он работает в автоматическом режиме, принимая команды на создание файлов от ядра, но некоторые файлы необходимо создавать самостоятельно. Они перечислены в ueventd.rc.

Каталог sbin в стоковом Android обычно не содержит ничего, кроме adbd, то есть демона ADB, который отвечает за отладку системы с ПК. Он запускается на раннем этапе загрузки ОС и позволяет выявить возможные проблемы на этапе инициализации ОС. В кастомных прошивках в этом каталоге можно найти кучу других файлов, например mke2fs, которая может потребоваться, если разделы необходимо переформатировать в ext3/4. Также модеры часто помещают туда BusyBox, с помощью которого можно вызвать сотни Linux-команд.

Каталог proc для Linux стандартен, на следующих этапах загрузки init подключит к нему procfs, виртуальную файловую систему, которая предоставляет доступ к информации обо всех процессах системы. К каталогу sys система подключит sysfs, открывающую доступ к информации о железе и его настройкам. С помощью sysfs можно, например, отправить устройство в сон или изменить используемый алгоритм энергосбережения.

Файл build.prop предназначен для хранения низкоуровневых настроек Android. Позже система обнулит эти настройки и перезапишет их значениями из недоступного пока файла system/build.prop.


Выносы из текста

  • Fastboot останется на месте, даже если в результате экспериментов ты сотрешь со смартфона содержимое всех разделов NAND-памяти
  • Раздел recovery полностью самодостаточен и содержит миниатюрную операционную систему, которая никак не связана с Android
  • Слегка изменив файл fstab, мы можем заставить init загрузить систему с карты памяти

Шаг второй, альтернативный. Раздел recovery

В том случае, если флаг загрузки recovery в разделе misc установлен или пользователь включил смартфон с зажатой клавишей уменьшения громкости, aboot передаст управление коду, расположенному в начале раздела recovery. Как и раздел boot, он содержит ядро и RAM-диск, который распаковывается в память и становится корнем файловой системы. Однако содержимое RAM-диска здесь несколько другое.

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

В стандартном (стоковом) recovery таких функций обычно всего три: установка подписанных ключом производителя смартфона прошивок, вайп и перезагрузка. В модифицированных сторонних recovery, таких как ClockworkMod и TWRP, функций гораздо больше. Они умеют форматировать файловые системы, устанавливать прошивки, подписанные любыми ключами (читай: кастомные), монтировать файловые системы на других разделах (в целях отладки ОС) и включают в себя поддержку скриптов, которая позволяет автоматизировать процесс прошивки и многие другие функции.

С помощью скриптов, например, можно сделать так, чтобы после загрузки recovery автоматически нашел на карте памяти нужные прошивки, установил их и перезагрузился в Android. Эта возможность используется инструментами ROM Manager, auto-flasher, а также механизмом автоматического обновления CyanogenMod и других прошивок.

Кастомные рекавери также поддерживают скрипты бэкапа, располагающиеся в каталоге /system/addon.d/. Перед прошивкой recovery проверяет наличие скриптов и выполняет их перед тем, как произвести прошивку. Благодаря таким скриптам gapps не исчезают после установки новой версии прошивки.

Команды fastboot

Чтобы получить доступ к fastboot, необходимо установить Android SDK, подключить смартфон к ПК с помощью кабеля и включить его, зажав обе кнопки громкости. После этого следует перейти в подкаталог platform-tools внутри SDK и запустить команду

Fastboot devices

На экран будет выведено имя устройства. Другие доступные команды:

  • fatsboot oem unlock - разлочка загрузчика на нексусах;
  • update файл.zip - установка прошивки;
  • flash boot boot.img - прошивка образа boot-раздела;
  • flash recovery recovery.img - прошивка образа раздела recovery;
  • flash system system.img - прошивка образа системы;
  • oem format - восстановление разрушенной таблицы разделов;

Шаг третий. Инициализация

Итак, получив управление, ядро подключает RAM-диск и по окончании инициализации всех своих подсистем и драйверов запускает процесс init, с которого начинается инициализация Android. Как я уже говорил, у init есть конфигурационный файл init.rc, из которого процесс узнает о том, что конкретно он должен сделать, чтобы поднять систему. В современных смартфонах этот конфиг имеет внушительную длину в несколько сот строк и к тому же снабжен прицепом из нескольких дочерних конфигов, которые подключаются к основному с помощью директивы import. Тем не менее его формат достаточно простой и по сути представляет собой набор команд, разделенных на блоки.

Каждый блок определяет стадию загрузки или, выражаясь языком разработчиков Android, действие. Блоки отделены друг от друга директивой on, за которой следует имя действия, например on early-init или on post-fs. Блок команд будет выполнен только в том случае, если сработает одноименный триггер. По мере загрузки init будет по очереди активировать триггеры early-init, init, early-fs, fs, post-fs, early-boot и boot, запуская таким образом соответствующие блоки команд.


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

Наиболее примечательный из дополнительных конфигов носит имя initrc.имя_устройства.rc, где имя устройства определяется автоматически на основе содержимого системной переменной ro.hardware. Это платформенно-зависимый конфигурационный файл, который содержит блоки команд, специфичные для конкретного устройства. Кроме команд, отвечающих за тюнинг ядра, он также содержит примерно такую команду:

Mount_all ./fstab.имя_устройства

Она означает, что теперь init должен подключить все файловые системы, перечисленные в файле./fstab.имя_устройства, который имеет следующую структуру:

Имя_устройства_(раздела) точка_монтирования файловая_система опции_фс прочие опции

Обычно в нем содержатся инструкции по подключению файловых систем из внутренних NAND-разделов к каталогам /system (ОС), /data (настройки приложений) и /cache (кешированные данные). Однако слегка изменив этот файл, мы можем заставить init загрузить систему с карты памяти. Для этого достаточно разбить карту памяти на три 4 раздела: 1 Гб / ext4, 2 Гб / ext4, 1 Гб / ext4 и оставшееся пространство fat32. Далее необходимо определить имена разделов карты памяти в каталоге /dev (для разных устройств они отличаются) и заменить ими оригинальные имена устройств в файле fstab.


В конце блока boot init, скорее всего, встретит команду class_start default, которая сообщит, что далее следует запустить все перечисленные в конфиге службы, имеющие отношение к классу default. Описание служб начинается с директивы service, за которой следует имя службы и команда, которая должна быть выполнена для ее запуска. В отличие от команд, перечисленных в блоках, службы должны работать все время, поэтому на протяжении всей жизни смартфона init будет висеть в фоне и следить за этим.

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

Команды init.rc

Процесс init имеет встроенный набор команд, многие из которых повторяют стандартный набор команд Linux. Наиболее примечательные из них:

  • exec /путь/до/команды - запустить внешнюю команду;
  • ifup интерфейс - поднять сетевой интерфейс;
  • class_start имя_класса - запустить службы, относящиеся к указанному классу;
  • class_stop имя_класса - остановить службы;
  • insmod /путь/до/модуля - загрузить модуль ядра;
  • mount ФС устройство каталог - подключить файловую систему;
  • setprop имя значение - установить системную переменную;
  • start имя_службы - запустить указанную службу;
  • trigger имя - включить триггер (выполнить указанный блок команд);
  • write /путь/до/файла строка - записать строку в файл.

Шаг четвертый. Zygote и app_process

На определенном этапе загрузки init встретит в конце конфига примерно такой блок:

Service zygote /system/bin/app_process -Xzygote /system/bin --zygote --start-system-server class default socket zygote stream 660 root system onrestart write /sys/android_power/request_state wake onrestart write /sys/power/state on onrestart restart media onrestart restart netd

Это описание службы Zygote, ключевого компонента любой Android-системы, который ответственен за инициализацию, старт системных служб, запуск и остановку пользовательских приложений и многие другие задачи. Zygote запускается с помощью небольшого приложения /system/bin/app_process, что очень хорошо видно на приведенном выше куске конфига. Задача app_proccess - запустить виртуальную машину Dalvik, код которой располагается в разделяемой библиотеке /system/lib/libandroid_runtime.so, а затем поверх нее запустить Zygote.

Когда все это будет сделано и Zygote получит управление, он начинает формирование среды исполнения Java-приложений с помощью загрузки всех Java-классов фреймворка (сейчас их более 2000). Затем он запускает system_server, включающий в себя большинство высокоуровневых (написанных на Java) системных сервисов, в том числе Window Manager, Status Bar, Package Manager и, что самое важное, Activity Manager, который в будущем будет ответственен за получение сигналов о старте и завершении приложений.

После этого Zygote открывает сокет /dev/socket/zygote и уходит в сон, ожидая данные. В это время запущенный ранее Activity Manager посылает широковещательный интент Intent.CATEGORY_HOME, чтобы найти приложение, отвечающее за формирование рабочего стола, и отдает его имя Zygote через сокет. Последний, в свою очередь, форкается и запускает приложение поверх виртуальной машины. Вуаля, у нас на экране появляется рабочий стол, найденный Activity Manager и запущенный Zygote, и статусная строка, запущенная system_server в рамках службы Status Bar. После тапа по иконке рабочий стол пошлет интент с именем этого приложения, его примет Activity Manager и передаст команду на старт приложения демону Zygote

INFO

В терминологии Linux RAM-диск - это своего рода виртуальный жесткий диск, существующий только в оперативной памяти. На раннем этапе загрузки ядро извлекает содержимое диска из образа и подключает его как корневую файловую систему (rootfs).

В процессе загрузки Android отображает три разных загрузочных экрана: первый появляется сразу после нажатия кнопки питания и прошит в ядро Linux, второй отображается на ранних этапах инициализации и записан в файл /initlogo.rle (сегодня почти не используется), последний запускается с помощью приложения bootanimation и содержится в файле /system/media/bootanimation.zip.

Кроме стандартных триггеров, init позволяет определять собственные триггеры, которые могут срабатывать от самых разных событий: подключения устройства к USB, изменения состояния смартфона или изменения состояния системных переменных.

Кроме всего прочего, Activity Manager также занимается убийством фоновых приложений при нехватке памяти. Значения порогов свободной памяти содержатся в файле /sys/module/lowmemorykiller/parameters/minfree.

Все это может выглядеть несколько непонятно, но самое главное - запомнить три простые вещи:

Во многом Android сильно отличается от других ОС, и с наскоку в нем не разобраться. Однако, если понять, как все работает, открываются просто безграничные возможности. В отличие от iOS и Windows Phone, операционка от гугла имеет очень гибкую архитектуру, которая позволяет серьезно менять ее поведение без необходимости писать код. В большинстве случаев достаточно подправить нужные конфиги и скрипты.

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

APK (Android Package) -- формат архивных исполняемых файлов-приложений для Android.

Каждое приложение Android скомпилировано и упаковано в один файл, который включает в себя весь код приложения (.DEX файлы), ресурсы, активы и файл manifest. Файл приложения может иметь любое имя, но расширение должно быть.APK. Например: myAppFile.apk.

Файлы с данным расширением хранятся в магазине Google Play, и загружаются с его помощью в смартфон для их использования, либо устанавливаются пользователем вручную на устройстве.

Файлы этого формата не шифруются, являются подмножеством формата архива ZIP.

Каждый.APK файл -- это сжатый архив для исполнения в DalvikVM (виртуальная машина), который может быть установлен не только на операционной системе Android.

APK файл как архив обычно содержит следующие директории:

· META-INF:

§ MANIFEST.MF: манифест файл

§ CERT.RSA: сертификат приложения

§ CERT.SF: список ресурсов и их SHA1 хеш-сумма например на Рисунке 6:

· Signature-Version: 1.0

· Created-By: 1.0 (Android)

· SHA1-Digest-Manifest: wxqnEAI0UA5nO5QJ8CGMwjkGGWE=

· Name: res/layout/exchange_component_back_bottom.xml

· SHA1-Digest: eACjMjESj7Zkf0cBFTZ0nqWrt7w=

· Name: res/drawable-hdpi/icon.png

· SHA1-Digest: DGEqylP8W0n0iV/ZzBx3MW0WGCA=

Рисунок 6. Структура файла со списком ресурсов и их хеш-сумм.

· Директория lib: содержит скомпилированный исполняемый код адаптированный под различные типы процессоров, обычно разделена на следующие директории:

Ш armeabi: код только для ARM процессоров

Ш armeabi-v7a: код для для процессоров ARMv7 и ниже только.

Ш x86: скомпилированный код только для архитектуры x86

Ш mips: скомпилированный код только для архитектуры MIPS

· Директория res: директория содержит файлы ресурсы не вошедшие в файл resources.arsc(см. ниже)

· Директория assets: содержит активы которые могут получены с помощью AssetManager

· Файл AndroidManifest.xml: дополнительный манифест файл, описывающий версию приложения, разрешения, используемые библиотеки. Как правило это файл идёт в формате binary XML, это формат файлов можно привести к читаемому виду с помощью сторонних утилит таких как AXMLPrinter2, apktool, или Androguard.

· Файл classes.dex: исполняемый файл виртуальной машины Dalvik, полученный путём преобразования скомпилированных JAVA классов с помощью утилиты DX. Утилита входит в состав Android SDK.

· Файл resources.arsc: файл содержит пре-компилированные ресурсы, например в виде бинарных XML файлов.

Из всего выше обозначенного в данной работе при анализе уровня опасности будут использоваться только два файла это AndroidManifest.xml и classes.dex. Ни их структуре остановимся более подробно.

AndroidManifest.xml

Данный файл, как уже говорилось ранее, содержит информацию о приложении, в том числе список требуемых разрешений приложения. В том числе на основе этих данных можно ранжировать уровень опасности приложения. Обратимся как его структуре: в первую очередь необходимо отметить что в установочном пакете androidmanifest.xml имеет бинарный вид, то есть преобразованный xml, хотя в оригинальном состоянии этот файл имеет структуру, обозначенную на Рисунке 7:

Рисунок 7. Подтверждение установки приложения из стороннего источника.

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

В Таблице 1 приведены некоторые разрешения которые представляют наибольшую опасность:

Таблица 1. Описание некоторых опасных разрешений в ОС Android.

ACCESS_COARSE_LOCATION

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

ACCESS_FINE_LOCATION

Приложение сможет получить доступ к точному местоположению от места расположения источников, таких как GPS, вышек сотовой связи и Wi-Fi.

CALL_PHONE

Приложение сможет инициировать телефонный звонок, минуя пользовательский интерфейс Dialer для пользователя.

Приложение сможет сделать снимок встроенной камерой

DELETE_PACKAGES

Позволяет приложению удалять пакеты.

DEVICE_POWER

Позволяет приложению отключить питание устройства

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

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

Данный файл носит в себе основной функционал приложения, содержит байт-код, понятный виртуальной машине Dalvik. Имеет следующую внутреннюю структуру, представленную в Таблице 2:

Таблица 2. Структура Dex файла.

На самом деле это файл, содержащий в себе программный код для виртуальной машины Dalvik. Приложения для Android пишутся на языке Java, но после компиляции кода в.class-файлы, вызывается утилита dx, которая транслирует их в один файл classes.dex, являющийся основной составляющей APK файла. Общий алгоритм формирования dex представлен на Рисунке 8.


Рисунок 8. Механизм формирования файла classes.dex.

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

Так нужно понимать, что файл имеет четкую устоявшуюся структуру, которая позволяет получить из файла путём статического анализа необходимую информацию для понимания функционала. Для этого необходимо разобрать формат файла и научится извлекать строки, имена методов.

Данный туториал поможет вам разобраться в очень полезном подходе по разработке приложений Clean Architecture.

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

Целью статьи является предоставление пошаговой инструкции разработки Android-приложений с применением подхода Clean Architecture. Суть моего подхода заключается в том, что я на довольно успешных примерах покажу вам все достоинства Clean Architecture.

Что такое Clean Architecture?

Я не собираюсь вдаваться в подробности, потому что есть статьи, в которых это объясняется лучше, чем смогу сделать я. Однако в следующем абзаце рассматривается ключевой вопрос, который вам необходимо знать, чтобы понять, как устроен подход Clean Architecture.

Как правило, в Clean Architecture код разделен на несколько уровней, по структуре схожей со структурой обычного лука, с одним правилом зависимости : внутренний уровень не должен зависеть от каких-либо внешних уровней. Это означает, что зависимости должны указываться внутри каждого уровня, чтобы не было зависимостей между уровнями (слоями) .

Clean Architecture, делает ваш код:
  • Независящим от фреймворков;
  • Тестируемым;
  • Независящим от UI;
  • Независящим от Базы данных;
  • Независимым от какого-либо внешнего воздействия.

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

Что это значит для Android?

Как правило, ваше приложение имеет произвольное количество уровней (слоев), однако если вам не нужна бизнес-логика Enterprise, то скорее всего у вас будет только 3 уровня:

  • Внешний: Уровень реализации;
  • Средний: Уровень интерфейса;
  • Внутренний: Уровень бизнес-логики.

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

Целью уровня интерфейса является обеспечение взаимодействия/коммуникации между уровнем реализации и уровнем бизнес-логики.

Самым важным уровнем считается уровень бизнес логики . Данный уровень - это то, где вы фактически решаете поставленную задачу, собственно ради которой и создавалось приложение. Уровень бизнес-логики не содержит какого-либо структурного кода, и вы должны уметь запускать его без эмулятора. Таким образом, если вы будете придерживаться подобного подхода при построении бизнес-логики, то получится уровень легко тестируемый, разрабатываемый и его будет легко поддерживать. Пожалуй, это самая большая выгода при использовании Clean Architecture.

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

Почему преобразование является обязательным?

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

Еще одним примером может быть следующее: давайте скажем, что объект Cursor , принадлежит ContentProvider во внешнем уровне базы данных. Значит что внешний уровень, в первую очередь преобразует его в бизнес-модель внутреннего уровня, а затем уже отдаст его на обработку соответствующему уровню бизнес-логики.

Внизу статьи я добавлю больше ресурсов для изучения данного вопроса. Сейчас же мы уже знаем об основных принципах подхода Clean Architecture, а значит давайте «замараем» руки кодом. Далее я покажу вам как создать рабочий функционал с использованием Clean Architecture.

Как начать создание Чистых приложений?

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

Первые шаги по написанию новых прецедентов

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

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

Структура

Общая структура Android-приложения выглядит, как показано ниже:

  • Пакеты внешнего уровня: Интерфейс пользователя, хранилище, сеть и т.д.;
  • Пакеты среднего уровня: Представители , конвертеры;
  • Пакеты внутреннего уровня: Interactors, модели, репозитории, исполнители.

Внешний уровень

Как уже было сказано, это то, где описываются детали структуры.

Интерфейс пользователя (UI ) – Это то, куда вы помещаете все ваши Операции, Фрагменты, Адаптеры и любой другой Android-код, связанный с интерфейсом пользователя.

Хранилище Отдельный код для базы данных, который реализует интерфейс наших Интеракторов, используемых для доступа к базе данных и для хранения данных. Например, сюда включается Поставщик контента или ORM-ы такие, как DBFlow .

Исполнитель (executor ) данный пакет содержит код для запуска Interactor-классов в фоновом режиме с помощью рабочего потока-исполнителя. Чаще всего вам не придется изменять этот пакет.

Простой пример

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

  • Пакет представления ;
  • Пакет хранилища ;
  • Пакет домена .

Первые два пункта относятся к внешнему уровню, в то время как последний относится к внутреннему/основному уровню.

Пакет представления ответственен за все, что связано с отображением вещей на экране, он содержит весь стек шаблона проектирования MVP . Это означает, что он содержит в себе как UI, так и Presenter-пакеты, даже если они относятся к разным уровням.

Отлично – меньше слов, больше кода!

Создание нового Interactor-а (внутренний/основной уровень)

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

Итак, давайте начнем создание Interactor-а. Interactor – это то место, где располагается основная логика работы нашего прецедента. Все Interactor -ы запускаются в фоновом потоке, поэтому не должно быть никакого воздействия на производительность интерфейса пользователя . Давайте создадим новый Interactor с приятным названием «WelcomingInteractor ».

public interface WelcomingInteractor extends Interactor { interface Callback { void onMessageRetrieved(String message); void onRetrievalFailed(String error); } }

public interface WelcomingInteractor extends Interactor {

interface Callback {

void onMessageRetrieved (String message ) ;

void onRetrievalFailed (String error ) ;

Callback отвечает за общение с интерфейсом пользователя (UI) в основном потоке, мы помещаем его в интерфейс Interactor-а, поэтому нет необходимости в подобном названии «WelcomingInteractorCallback», чтобы отличать его от других callback-ов. Теперь реализуем логику получения сообщения. Давайте скажем, что у нас есть интерфейс MessageRepository , в котором будет наше сообщение приветствия.

public interface MessageRepository { String getWelcomeMessage(); }

public interface MessageRepository {

String getWelcomeMessage () ;

public class WelcomingInteractorImpl extends AbstractInteractor implements WelcomingInteractor { ... private void notifyError() { mMainThread.post(new Runnable() { @Override public void run() { mCallback.onRetrievalFailed("Nothing to welcome you with:("); } }); } private void postMessage(final String msg) { mMainThread.post(new Runnable() { @Override public void run() { mCallback.onMessageRetrieved(msg); } }); } @Override public void run() { // получение сообщения final String message = mMessageRepository.getWelcomeMessage(); // проверяем, получили ли мы сообщение if (message == null || message.length() == 0) { // уведомляем об ошибке основной поток notifyError(); return; } // мы получили наше сообщение, уведомляем об этом UI в основном потоке postMessage(message); }

public class WelcomingInteractorImpl extends AbstractInteractor implements WelcomingInteractor {

. . .

private void notifyError () {

@ Override

public void run () {

mCallback . onRetrievalFailed ("Nothing to welcome you with:(" ) ;

} ) ;

private void postMessage (final String msg ) {

mMainThread . post (new Runnable () {

@ Override

public void run () {

mCallback . onMessageRetrieved (msg ) ;

} ) ;

@ Override

public void run () {

// получение сообщения

Final String message = mMessageRepository . getWelcomeMessage () ;

// проверяем, получили ли мы сообщение

if (message == null || message . length () == 0 ) {

// уведомляем об ошибке основной поток

notifyError () ;

return ;

// мы получили наше сообщение, уведомляем об этом UI в основном потоке

postMessage (message ) ;

Что же, взглянем на зависимости, создаваемые нашим Interactor:Этот фрагмент кода, пытается получить сообщение, затем переслать его или же отправить сообщение об ошибке интерфейсу пользователя, чтобы он отобразил сообщение или ошибку. Для этого мы уведомляем интерфейс пользователя с помощью нашего callback-а, который по факту и будет Presenter-ом. Собственно, в этом и заключается суть всей нашей бизнес-логики . Все что нам остается – это построить структурные зависимости.

import com.kodelabs.boilerplate.domain.executor.Executor; import com.kodelabs.boilerplate.domain.executor.MainThread; import com.kodelabs.boilerplate.domain.interactors.WelcomingInteractor; import com.kodelabs.boilerplate.domain.interactors.base.AbstractInteractor; import com.kodelabs.boilerplate.domain.repository.MessageRepository;

import com . kodelabs . boilerplate . domain . executor . Executor ;

import com . kodelabs . boilerplate . domain . executor . MainThread ;

import com . kodelabs . boilerplate . domain . interactors . WelcomingInteractor ;

import com . kodelabs . boilerplate . domain . interactors . base . AbstractInteractor ;

import com . kodelabs . boilerplate . domain . repository . MessageRepository ;

Как вы можете заметить, здесь нет ни одного упоминания о каком-либо Android -коде . Это и есть главное преимущество данного подхода. Также вы можете увидеть, что пункт: «Независимость от фреймворков» все также соблюдается. Кроме того, нам не нужно отдельно определять интерфейс пользователя или базу данных, мы просто вызываем методы интерфейса, которые кто-то, где-то на внешнем уровне реализует. Следовательно, мы независим от UI и независим от Базы данных .

Тестирование нашего Interactor-а

На данный момент мы можем запустить и начать тестирование нашего Interactor -а без запуска эмулятора . Поэтому давайте напишем простой Junit-тест, чтобы убедиться, что все работает:

... @Test public void testWelcomeMessageFound() throws Exception { String msg = "Welcome, friend!"; when(mMessageRepository.getWelcomeMessage()) .thenReturn(msg); WelcomingInteractorImpl interactor = new WelcomingInteractorImpl(mExecutor, mMainThread, mMockedCallback, mMessageRepository); interactor.run(); Mockito.verify(mMessageRepository).getWelcomeMessage(); Mockito.verifyNoMoreInteractions(mMessageRepository); Mockito.verify(mMockedCallback).onMessageRetrieved(msg); }

. . .

@ Test

public void testWelcomeMessageFound () throws Exception {

String msg = "Welcome, friend!" ;

when (mMessageRepository . getWelcomeMessage () )

ThenReturn (msg ) ;

WelcomingInteractorImpl interactor = new WelcomingInteractorImpl (

mExecutor ,

mMainThread ,

mMockedCallback ,

mMessageRepository

interactor . run () ;

Mockito . verify (mMessageRepository ) . getWelcomeMessage () ;

Mockito . verifyNoMoreInteractions (mMessageRepository ) ;

Mockito . verify (mMockedCallback ) . onMessageRetrieved (msg ) ;

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

Создание уровня представления

Код представления относится ко внешнему уровню подхода Clean Architecture. Уровень представления состоит из структурно зависимого кода, который отвечает за отображение интерфейса пользователя, собственно, пользователю. Мы будем использовать класс MainActivity для отображения нашего приветствующего сообщения пользователю, когда приложение возобновляет свою работу.

Давайте начнем с создания интерфейса нашего Presenter и Отображения (View ). Единственное, что должно делать наше отображение – это отображать приветствующее сообщение:

public interface MainPresenter extends BasePresenter { interface View extends BaseView { void displayWelcomeMessage(String msg); } }

public interface MainPresenter extends BasePresenter {

interface View extends BaseView {

void displayWelcomeMessage (String msg ) ;

Итак, как и где мы запускаем наш Interactor, когда приложение возобновляет работу? Все, что не имеет строгой привязки к отображению, должно помещаться в класс Presenter. Это помогает достичь принципа Разделения ответственности и предотвратить классы Операций от чрезмерного увеличения размера кода. Сюда включается весь код, который работает с Interactor-ми.

В нашем классе MainActivity мы переопределяем метод onResume() :

@Override protected void onResume() { super.onResume(); // начнем возврат приветствующего сообщения, при возобновлении работы приложения mPresenter.resume(); }

Все Presenter-объекты реализуют метод resume() , при наследовании BasePresenter .

Примечание : Самые внимательные читатели могли заметить, что я добавил Android-методы жизненного цикла в интерфейс BasePresenter в качестве вспомогательных методов, хотя сам Presenter находится на более низком уровне. Наш Presenter должен знать все на уровне UI, к примеру, что что-то на этом уровне имеет жизненный цикл. Тем не менее, здесь я не указываю конкретное событие , так как каждый UI для конкретного пользователя может отрабатывать разные события, в зависимости от действий пользователя. Представьте, я назвал его onUIShow() вместо onResume() . Теперь все хорошо, верно? 🙂

Мы запускаем Interactor внутри класса MainPresenter в методе resume() :

@Override public void resume() { mView.showProgress(); // инициализируем interactor WelcomingInteractor interactor = new WelcomingInteractorImpl(mExecutor, mMainThread, this, mMessageRepository); // запускаем interactor interactor.execute(); }

@ Override

public void resume () { mView . showProgress () ; // инициализируем interactor

WelcomingInteractor interactor = new WelcomingInteractorImpl (

mExecutor ,

mMainThread ,

this ,

mMessageRepository

) ; // запускаем interactor

interactor . execute () ;

Метод execute () просто выполняет метод run () объекта WelcomingInteractorImpl в фоновом потоке. Метод run () вы можете увидеть в разделе Создание нового Interactor .

Вы также могли заметить, что поведение Interactor-а схоже с поведением класса AsyncTask . Так как вы предоставляете все необходимое для его запуска и выполнения. Тут вы можете спросить, а почему мы не используем AsyncTask ? Да потому что это Android-код, и вам нужен будет эмулятор для его запуска и тестирования.

Мы предоставляем несколько вещей нашему Interactor-у:

  • Экземпляр ThreadExecutor , который отвечает за выполенение Interactor-а в фоновом потоке. Я чаще всего создаю его как singleton. Этот класс также располагается внутри domain-пакета и нет необходимости реализовывать его во внешнем уровне;
  • Экземпляр MainThreadImpl , который отвечает за отправку запущенных потоков Interactor-а в главный поток приложения. Основные потоки имеют доступ к использованию определённого структурного кода и поэтому мы должны реализовывать их во внешнем уровне;
  • Также вы могли обратить внимание на то, что мы предоставляем this нашему Interactor-у. MainPresenter – это callback-объект, который используется Interactor-ом для уведомления UI о каких-либо событиях;
  • Кроме того, мы предоставляем экземпляр WelcomeMessageRepository , который отвечает за реализацию интерфейса MessageRepository , который в свою очередь использует Interactor. WelcomeMessageRepository будет рассмотрен позже, в разделе Создание уровня хранения .

Примечание : Поскольку существует множество вещей, которые необходимо связывать каждый раз с Interactor-ом, то будет полезен следующий фреймворк для внедрения зависимостей: Dagger 2 (и подобные ему). Но я его использую здесь не для того чтобы что-то упростить. Свою структуру вы вольны сами выбирать, и то какие фреймворки использовать также ваше право.

Что же касается this , то MainPresenter класса MainActivity действительно реализует callback-интерфейс:

public class MainPresenterImpl extends AbstractPresenter implements MainPresenter, WelcomingInteractor.Callback {

public class MainPresenterImpl extends AbstractPresenter implements MainPresenter , WelcomingInteractor . Callback {

@Override public void onMessageRetrieved(String message) { mView.hideProgress(); mView.displayWelcomeMessage(message); } @Override public void onRetrievalFailed(String error) { mView.hideProgress(); onError(error); }

Приложения для Android пишутся на языке программирования Java. Инструменты Android SDK (Software Development Kit – комплект разработки программного обеспечения) компилируют написанный вами код — и все требуемые файлы данных и ресурсов — в файл APK – программный пакет Android , который представляет собой файл архива с расширением.apk . В файле APK находится все, что требуется для работы Android-приложения, и он позволяет установить приложение на любом устройстве под управлением системы Android.

Каждое приложение Android, установленное на устройстве, работает в собственной "песочнице" (изолированной программной среде):

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

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

Однако у приложения есть варианты предоставления своих данных другим приложениям и доступа к системным службам:

  • двум приложениям можно назначить один идентификатор пользователя Linux. В этом случае каждый из них сможет обращаться к файлам другого приложения. Для экономии ресурсов системы также можно сделать так, чтобы приложения с одинаковым идентификатором пользователя выполнялись в одном процессе Linux и использовали одну ВМ (приложения также должны быть подписаны одним сертификатом);
  • приложение может запросить разрешение на доступ к данным устройства, например к контактам пользователя, SMS-сообщениям, подключаемой карте памяти (SD-карте), камере, Bluetooth и др. Все разрешения должны предоставляться приложению при его установке.

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

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

Компоненты приложения

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

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

Четыре типа компонентов:

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

Операция относится к подклассу класса . Подробные сведения об этом можно найти в руководстве для разработчиков в статье .

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

Служба относится к подклассу класса . Подробные сведения об этом можно найти в руководстве для разработчиков в статье .

Поставщики контента Поставщик контента (Content provider) управляет общим набором данных приложения. Данные можно хранить в файловой системе, базе данных SQLite, в Интернете или любом другом постоянном месте хранения, к которому у вашего приложения имеется доступ. Посредством поставщика контента другие приложения могут запрашивать или даже изменять данные (если поставщик контента позволяет делать это). Например, в системе Android есть поставщик контента, который управляет информацией контактов пользователя. Любое приложение, получившее соответствующие разрешения, может запросить часть этого поставщика контента (например ), для чтения и записи сведений об определенном человеке.

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

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

Приемники широковещательных сообщений Приемник широковещательных сообщений (Broadcast receiver) представляет собой компонент, который реагирует на объявления распространяемые по всей системе. Многие из этих объявлений рассылает система — например объявление о том, что экран выключился, аккумулятор разряжен или был сделан фотоснимок. Объявления также могут рассылаться приложениями, — например, чтобы сообщить другим приложениям о том, что какие-то данные были загружены на устройство и теперь готовы для использования. Несмотря на то что приемники широковещательных сообщений не имеют пользовательского интерфейса, они могут чтобы предупредить пользователя о событии "рассылка объявления". Однако чаще всего они являются просто "шлюзом" для других компонентов и предназначены для выполнения минимального объема работы. Например, они могут инициировать выполнение службой определенных действий при возникновении события.

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

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

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

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

Активация компонентов

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

Объект Intent создается с помощью объекта , который описывает запрос на активацию либо конкретного компонента, либо компонента конкретного типа — соответственно, намерение Intent может быть явным или неявным.

Для операций и служб Объект Intent определяет действие, которое требуется выполнить (например, просмотреть (view) или отправить (send) что-то), а также может указывать URI (Uniform Resource Identifier – унифицированный идентификатор ресурса) данных, с которыми это действие нужно выполнить (помимо прочих сведений, которые нужно знать запускаемому компоненту). Например, объект Intent может передавать запрос на выполнение операции "показать изображение" или "открыть веб-страницу". В некоторых ситуациях операцию можно запустить, чтобы получить результат. В этом случае операция возвращает результат также в виде объекта (например, можно отправить сообщение Intent, чтобы дать пользователю возможность выбрать контакт и вернуть его вам — в ответном сообщении Intent будет содержаться URI, указывающий на выбранный контакт).

Для приемников широковещательных сообщений Intent просто определяет передаваемое объявление (например, широковещательное сообщение о низком уровне заряда аккумулятора содержит только строку "аккумулятор разряжен").

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

Для активации компонентов каждого типа имеются отдельные методы:

  • Можно запустить операцию (или определить для нее какое-то новое действие), передав объект методу или (если требуется, чтобы операция вернула результат).
  • Можно запустить службу (либо выдать работающей службе новые инструкции), передав объект методу . Либо можно установить привязку к службе, передав объект методу .
  • Можно инициировать рассылку сообщений, передав объект таким методам, как , и .
  • Можно выполнить запрос к поставщику контента, вызвав метод для объекта .

Подробные сведения об использовании объектов Intent приведены в документе . Более подробная информация об активации определенных компонентов также приведена в следующих документах: , и .

Файл манифеста

Для запуска компонента приложения системе Android необходимо знать, что компонент существует. Для этого она читает файл AndroidManifest.xml приложения (файл манифеста). В этом файле, который должен находиться в корневой папке приложения, должны быть объявлены все компоненты приложения.

Помимо объявления компонентов приложения, манифест служит и для других целей, среди которых:

  • указание всех полномочий пользователя, которые требуются приложению, например разрешения на доступ в Интернет или на чтение контактов пользователя;
  • объявление минимального , требуемого приложению, с учетом того, какие API-интерфейсы оно использует;
  • объявление аппаратных и программных функций, которые нужны приложению или используются им, например камеры, службы Bluetooth или сенсорного экрана;
  • указание библиотек API, с которыми необходимо связать приложение (отличные от API-интерфейсов платформы Android), например библиотеки Google Maps ;
  • и многое другое.

Объявление компонентов

Основная задача манифеста – это информировать систему о компонентах приложения. Например, файл манифеста может объявлять операцию следующим образом:

...

Подробные сведения о структуризации файла манифеста для приложения см. в документе .

Объявление возможностей компонентов

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

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

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

Например, если вы создали приложение для работы с электронной почтой с операцией составления нового сообщения, вы можете объявить фильтр для ответа на сообщения Intent типа "send" (для отправки нового сообщения электронной почты) следующим образом:

...

Затем, если другое приложение создаст объект Intent с действием и передаст его в , система сможет запустить вашу операцию, дав пользователю возможность написать и отправить сообщение электронной почты.

Подробные сведения о создании фильтров объектов Intent приведены в документе .

Объявление требований приложения

Существует огромное количество устройств, работающих под управлением Android, и не все они имеют одинаковые функциональные возможности. Чтобы ваше приложение не могло быть установлено на устройствах, в которых отсутствуют функции, необходимые приложению, важно четко определить профиль для типов устройств, поддерживаемых вашим приложением, указав требования к аппаратному и программному обеспечению в файле манифеста. Эти объявления по большей части носят информационный характер, система их не читает. Однако их читают внешние службы, например Google Play, с целью обеспечения фильтрации для пользователей, которые ищут приложения для своих устройств.

Например, если вашему приложению требуется камера и оно использует API-интерфейсы из Android 2.1 ( 7), эти параметры следует объявить в файле манифеста в качестве требований следующим образом:

...

Теперь ваше приложение нельзя будет установить из Google Play на устройствах, в которых нет камеры, а также на устройствах, работающих под управлением Android версии ниже 2.1.

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

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

Ресурсы приложения

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

Для каждого ресурса, включаемого в проект Android, инструменты SDK задают уникальный целочисленный идентификатор, который может использоваться, чтобы сослаться на ресурс из кода приложения или из других ресурсов, определенных в XML. Например, если в вашем приложении имеется файл изображения с именем logo.png (сохраненный в папке res/drawable/), инструменты SDK сформируют идентификатор ресурса под именем R.drawable.logo , с помощью которого на изображение можно будет ссылаться и вставлять его в пользовательский интерфейс.

Один из наиболее важных аспектов предоставления ресурсов отдельно от исходного кода заключается в возможности использовать альтернативные ресурсы для различных конфигураций устройств. Например, определив строки пользовательского интерфейса в XML, вы сможете перевести их на другие языки и сохранить эти переводы в отдельных файлах. Затем по квалификатору языка, добавленному к имени каталога ресурса (скажем res/values-fr/ для строк на французском языке), и выбранному пользователем языку система Android применит к вашему пользовательскому интерфейсу строки на соответствующем языке.

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

Предоставление ресурсов Описание структуры приложений Android, в которой ресурсы приложения существуют отдельно от его кода, а также сведения о том, как предоставлять альтернативные ресурсы для определенных конфигураций устройств.

Возможно, вас также заинтересует:

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

Загрузка...