6 апреля 2016 г.

Основы понимания приложений

    Этой записью я открою короткий цикл длинных и скучных (не просто так же имя блогу дано!) статей о тестировании приложений под Android. Здесь теория, теория, теория, вперемешку с историями «из жизни», которые, конечно, наиболее ценны (для меня).
    Все статьи буду идти от очень общего к частностям. Это поможет сначала разобраться с тем, как всё работает, как устроено, как называется, а затем — жонглировать этими теоретическими знаниями. Например в этой, открывающей статье, я сначала расскажу вскользь о компонентах приложений, затем расскажу о них подробно, а затем — добавлю понимания, где можно найти эти компоненты. Так будет построена каждая последующая статья, но, кроме того, каждая последующая будет сильно завязана на каждую предыдущую. Так что если уж решили подтянуть уровень понимания Android и исследовательского тестирования приложений под Android, стоит читать всё.
    К чёрту, приступим!

    Программный код пишется, в основном, на Java. То есть вы можете писать на чём угодно, хоть на той же Scala, да хоть на Python. Но тут есть свои подводные камни, очевидные и не очевидные. Можно писать и на C и нативный код будет работать также быстро, как и Java (слова «быстро» и «Java» в одном предложении и даже нет слова «память», спешите видеть!), возможно даже быстрее, но это дороже в скорости разработки и затратах на сопровождение.
Итак, код пишется на Java. Далее Android SDK (Software Development Kit — набор средств для разработки) собирает код и все требуемые файлы ресурсов в файл APK (Android Application Package) — программный пакет Android, который суть — zip файл с расширением .apk. То, что это zip, легко убедиться, посмотрев на заголовок, где мы увидим буквы PK:
    Ну или «РК» — РосКом и многозначительное многоточие… Кстати, а кто помнит, какие буквы находятся в заголовках exe и dll файлов и что они означают? Это буквы MZ — инициалы Марка Зибовски, бывшего архитектора компании Microsoft, который был одним из создателей MS DOS. Он и разработал этот формат исполняемых файлов, который заменил собой устаревший .com. Пятиминутка бесполезных знаний!

    Итак, в apk находится всё, что нужно приложению для работы. Его можно установить на любом устройстве под Android, если это не противоречит объявленным программным и аппаратным требованиям (да и то эти ограничения «интересны» в основном самому Play Store и не играют особой роли при локальной установке apk). Здесь нужно ввести некоторые базовые понимания, которые очень нужны будут в будущем, особенно когда речь будет идти об ограничениях возможности приложений:
  • Android представляет собой, кроме прочего, многопользовательскую систему на основе Linux, где каждое приложение — это отдельный пользователь
  • Каждому такому пользователю назначается уникальный идентификатор пользователя Linux, который используется только системой и не известен приложению (здесь и по всей статье я подразумеваю, что система не рутована). Всем файлам, которые были в пакете, устанавливаются права доступа таким образом, чтобы доступ к ним имело только это самое приложение. То есть одни пользователи-приложения не имеют доступа к файлам других пользователей-приложений
  • Каждому приложению выделяется собственная виртуальная машина, внутри которой это приложение и живёт. За пределы виртуальной машины выход строго контролируется и общение между приложениями в основном реализуется через пинки системы, а не прямыми доступами к данным друг друга
  • По умолчанию каждое приложение запускается в собственном процессе Linux. Когда одному приложению требуется нечто, что предоставляет другое приложение (скажем, клиент вконтакта запускает фотокамеру, чтобы сделать фото для аватара), то сам Android запускает приложение, компонент которого запрашивается другим приложением. Когда Android решит, что пришло время убивать бездельников, он убьёт процесс приложения, который предоставлял фотокамеру, при условии, что его сейчас никто не использует, а пользователь устройства всё ещё взаимодействует клиентом вконтакта.
    Всё это нужно для реализации принципа предоставления минимальных прав. То есть каждое приложение живёт только в своём мирке, имеет доступ только к своим файлам, не может получить доступа туда, куда лезть не следует, а для выполнения задач, которые не может выполнить само, просит помощи у других, обычно через систему. И это мы ещё не затрагиваем SELinux.

    Кроме такого общения через систему («сделайте вот эту задачу вместо меня»), есть и пара других возможностей иметь доступ не только к своим собственным данным.
  • Двум приложениям можно назначить один индикатор пользователя Linux. И, соответственно, они будут иметь доступ к файлам друг друга, так как, с точки зрения разграничения прав, это один пользователь. Также, в целях экономии ресурсов, эти приложения могут находиться в одной песочнице и использовали одну виртуальную машину. Эти приложения должны быть подписаны одним сертификатом. Часто эту возможность используют, например, для добавления новых возможностей к существующим приложениям (плагины). Хотя это вовсе и не обязательно реализуется именно так
  • Другим способом общения с внешним миром является обращение к реальным данным там, где это, в целом, дозволено. Это доступ к данным на карте памяти (и распаянной на устройстве, и сменные), прямое обращение к камере, к bluetooth и прочее. Как правило, такие желания нужно объявлять в файле манифеста, в противном случае система просто не даст разрешения на доступ к устройствам, к картам памяти, к устройствам

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

    Выше я уже упомянул про компоненты приложений. Пришло время понять, что это за компоненты, какие они бывают.

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

    Операции. Если вы произнесёте это слово, говоря про Android приложения, то, скорее всего, вас неправильно поймут другие участники беседы. Чаще всего используется оригинальное название — активити. Так вот активити (activity), они же операции — это, грубо говоря, то, на что смотрит пользователь, даже если он этого не видит того, на что смотрит, потому что эта активити полностью прозрачна. Активити — это экран с пользовательским интерфейсом, точнее даже экран для интерфейса.
    То есть у нас есть Hello World с двумя экранами. На одном написано «Hello!» и есть кнопка, а второй появляется по нажатию на кнопку и на нём написано «Ваше устройство зашифровано, давайте ваши денежки». Скорее всего, это будут два разных активити со своим наобором элементов интерфейса. Можно даже попытаться из другого приложения вызывать любое из этих активити в любом порядке, используя их как точки входа, если разработчиком приложения это было не запрещено явным образом.
    Более реальный пример. Без труда можно вызвать тот активити в «Kaspersky Internet Security для Android», на котором написано, что лицензия добавлена в чёрный список и это никак не повлияет ни на продукт, ни на лицензию. Просто активити — это, как было сказано ранее, точка входа. Вот мы и вошли. Это как избушка на курьих ножках и Иван. У избы была куча разных активити, — четыре стены, крыша, днище. Иван вызывал нужную ему, используя документированную возможность: «к лесу задом, а ко мне — передом». Делал он это, кстати, явным намеринием, о котором позже.
    Понятное дело, что на одно активити можно забросать сотню кнопок, текстов, полей и скрывать и показывать всё, что нужно в каждый момент времени и тогда это будет одно активити на кучу разных вариантов интерфейсов. Если вы всерьёз рассматриваете такой подход к построению приложения, то я даже не знаю...
    Раз уж упомянул про невидимые активити, приведу один из примеров. Если кто не знает, есть такой тип матрицы, который пилят в основном в Samsung — AMOLED (Active Matrix Organic Light-Emitting Diode). Ну и куча его разновидностей, которые выходят по три раза в год. Одна и ключевых, одна из важнейших фишек AMOLED в том, что чёрный цвет — это, если всё правильно сделано, это отсутствие свечения. А одна из негативных сторон — эти экраны очень яркие. Ночью они настолько яркие, что даже минимальная яркость в Android всё равно выжигает глаза. Вот для таких случаев (минимальная яркость всё равно слишком яркая) сделали приложения, которые показывают свою прозрачную активити поверх всех прочих. Пользователь настраивает затенение прозрачной активити, что воспринимается как ещё большее приглушение яркости, хотя по факту это уменьшение прозрачности. А благодаря другой ошибке, простите, фиче, приложение может пробрасывать тапы, которые пользователь делает по его активити, ниже, туда, куда пользователь реально целился. Называется эта «фича», кстати, Tapjacking. Её обратная сторона — можно снимать, к примеру, пин-коды, регистрируя координаты и последовательности тапов: //blog.trendmicro.com/trendlabs-security-intelligence/tapjacking-an-untapped-threat-in-android/. И да, от этой штатной фичи есть не менее штатная защита.

    Службы. Хотя обычно говорят сервисы (services). Это компоненты приложений, которые работают в фоне, выполняя длительные операции и не имеют интерфейса. Играющая в фоне музыка — типичный пример работы сервиса. Google Play Store скачивает в фоне обновление приложений именно сервисом. Сервисы более живучи, чем активити. Если пользователь включил музыку в плеере и свернул приложение плеера, то активити этого плеера, как и его сервис, будут висеть и жить в фоне. Активити можно мгновенно поднять и это будет комфортное взаимодействие с приложением. Но если при сворачивании плеера системе понадобиться свободная оперативная память (потому что пользователь запустил некое «тяжёлое» приложение), система в первую очередь убьёт активити плеера, а сервис продолжит играть музыку. Но если памяти нужно будет ещё больше, и ещё, и ещё, система всё-таки прибьёт и сервис.

    Поставщики контента, они же контент провайдеры (content provider). Здесь мы переходим к сущностям, о которых многие из тех, кому следует их знать, всё же имеют о них слабое представление. А некоторые даже не знают, что лично меня печалит. Поставщики контента управляют некими данными и дают, или не дают, возможность управлять этими данными другим приложениями, но через себя. Например в Android есть поставщики контента для календарей, для контактов, для работы с базами данных и прочее. Если вашему приложению нужно получать данные о контактах, добавлять контакты в пользовательский список контактов, удалять их, изменять данные о них, то вам не нужно делать свою реализацию приложения «Контакты». Более того, подавляющее большинство альтернативных приложений, которые дают иной, более красивый, удобный/глючный, быстрый/тормозной доступ к пользовательским контактам, не реализуют эту работу сами. Правильный подход  — обращаться ко всем контактам через поставщика контента. Формируете запрос — получаете ответ. «Правильный» имеется в виду для типичных приложений. Скажем, если вы делаете специальное приложение, где будут храниться контакты, доступ к которым должен быть только у этого приложения и только при подтверждении через пароль/отпечаток пальцев, то логично, что системный контент провайдер не нужен.
    «Добавить Васю с телефоном «03» и фотографией кота в системный список контактов», — говорит WhatsApp контент провайдеру. «Есть», — говорит контент провайдер. В итоге вы заходите в приложение контакты и видите тысячу совершенно ненужных вам людей. А если вы есть в Google Plus и активно его используете через штатный клиент, то у этой тысячи людей ещё и номеров телефонов не будет. Приходится фильтровать вывод. Фильтрация показа тоже может быть реализована через контент провайдера. Скажем, вы желаете видеть только тех, у кого номер начинается на «+1». Запрашиваете данные (конкретно этот поставщик контента поддерживает SQL-подобные запросы, что сильно облегчает работу с ним), отсеиваете лишнее, отображаете нужное.

    Приёмники широковещательных сообщений. Они же бродкаст ресиверы (brodcast reciver) в обиходной речи. Бродкаст ресиверы — это компоненты приложений, которые получают эти самые бродкасты. А дальше в приложении можно навесить нужные действия в качестве реакции. Многие широковещательные сообщения рассылает сама система, если на них подписаться. Например система громко кричит в толпу, что выключился экран, что устройство поставили на зарядку, что пришло SMS сообщение, что произошла смена статуса сетевого подключения и даже что внутри текущего сетевого подключения появился Интернет.
    Подобных событий достаточно много «из коробки» (если у вас установлен Android SDK, то смотрите файл %sdk_dir%\platforms\android-%API_level%\data\broadcast_actions.txt). Сверх того приложения также могут рассылать свои широковещательные сообщения, чтобы сообщить другим о чём-то. Например, загрузчик (приложение Downloads) может сказать, что завершил загрузку файла. Вы можете написать несколько своих приложений, которые будут действовать сообща, рассылая бродкасты и получая их. Бродкаст ресиверы сами по себе не имеют интерфейса, но они могут создавать свои уведомления в строке состояния. Что ещё крайней важно, даже принципиально важно, регистрация бродкаст ресивера означает, что при получении нужного бродкаста, приложение получит возможность отработать некую задачу, даже если приложение было мертво. Конечно, если на это в данный момент есть ресурсы.
    Например, вашему приложению нужно отслеживать установку приложений для каких-то целей. Установили в систему новое приложение, неважно откуда — ваше приложение выполняет некое действие. Скажем, проверяет это приложение по списку известных чистых и, если его нет в списке, показывает активити, где чёрным по красному написано «Опасно! Приложение не прошло проверку доверенности». Сделать монитор можно, например, чисто сервисом. Этот сервис будет работать в фоне и получать каждые 5 секунд список приложений и смотреть, нет ли нового. Но это будет такое «правильно», как и тысяча элементов интерфейса на одном активити. Правильно — это подписаться на широковещательное сообщение об установке приложений. И тогда вашему приложению даже не нужно будет добавляться в автозапуск, совсем. Оно не будет работать почти всё время работы устройства и, соответственно, не будет влиять на батарею никак. Но как только будет установлено приложение, система пошлёт бродкаст об этом. Этот бродкаст запустит ваше приложение и оно выполнит свою задачу. И продолжит висеть в фоне, если вы не сделали ему самоубийство (что, кстати, делать не рекомендуется, это не Android путь!), до тех пор, пока сама система его не прибьёт по необходимости.
    Кстати, есть предположение, почему не стоит делать самострел? Почему вообще не стоит убивать приложения, а все чистилки памяти только вредят, когда «освобождают память» путём отстрела приложения? Потому что может возникнуть ситуация, когда нужно обработать много событий за короткий промежуток времени. Запуск приложения может быть ресурсозатратной операцией (CPU, i/o), тогда как поднятие его из фона — это мгновенная и очень дешёвая операция. Очень неприятно, когда открытие браузера вызывает перезагрузку страницы, а запуск Twitter клиента — перезагрузку ленты, которую ты читал всего несколько часов назад. И очень удобно, когда вы жмёте на иконку приложения, а оно просто поднялось из фона с полностью сохранённым состоянием. Вот это — Android путь. Кроме того, приложение может защищаться от киллеров (к примеру, защитное ПО, типа антивирусов, это делает) и перезапускаться снова и снова.
    Пожалуй самым наглядным примером регистрации бродкаст ресивера будет посадка в маршрутку. Вы впервые в неком районе и точно не знаете, где находится остановка «Водный Стадион». Вы заходите в маршрутку и регистрирует приёмник широковещательных сообщений: «Остановите на остановке «Водный Стадион», пожалуйста». Когда маршрутка подъезжает к нужному месту, водитель посылает широковещательное сообщение: «Остановка «Водный Стадион», кто просил?». Вы получили сообщение и выполняете необходимые действия. Если бы вас не было и никто другой не просил бы этой остановки, то система бы просто не посылала бродкаст. А если вы и сами знаете, где эта остановка, сами её отслеживаете и выполняете нужные действия, то это уже служба, она же сервис.

    Как я говорил ранее, одна из ключевых особенностей Android в том, что каждое приложение может обращаться к компонентам любого другого приложения, используя конкретную, необходимую ему сейчас точку входа. Попытка сделать фотографию из чат-приложения запустит фотокамеру. Для этого не то что не нужно писать свою камеру, для этого даже не нужно знать, как называется любимое приложение камеры у пользователя. Вы формируете определённый запрос (намерение/Intent), система сама запустит нужную активити фотокамеры и, по завершению процедуры, вернёт пользователя обратно в приложение, откуда фотокамера и вызывалась. Это будет бесшовный переход, который пользователем будет восприниматься так, будто фотокамера встроена в ваше собственное приложение. При этом сама операция фотографирования реально будет происходить не в вашем процессе. А раз одно приложение может использовать компоненты другого приложения, бесшовно перебрасывая пользователя между приложениями, то нет и единой точки входа. Это вам не prog.exe с ключами /q и /yes запускать. В приложениях под Android, в общем случае, нет привычной функции main().

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

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

    Три из четырёх видов компонентов приложений — операции, они же активити, службы, они же сервисы и приёмники широковещательных сообщений, они же бродкаст ресиверы — активируются асинхронным сообщением, которое называется Intent, «намерение». Намерения создаются с помощью объекта Intent и посылают либо запрос на активацию какого-то конкретного компонента, либо запрос на активацию компонента конкретного типа.
    Соответственно намерение может быть явным или неявным. Если вы просите показать кусок карты с заданными параметрами, выдернув его из Яндекс.Карт — это явное намерение. Если вы вы делаете примерно тоже самое, но не для Яндекс.Карт, а просто «Эй, Android, покажи пользователю какую-нибудь карту вот с такими условиями!», — это неявное намерение и на него откликнутся все те, кто понимает, о чём идёт речь.
    Или ещё проще. Можно попросить конкретный браузер загрузить некий сайт и тогда браузер это сделает, а можно попросить загрузить сайт без уточнения браузера и тогда это сделает либо тот, кого раньше назначили любимой женой, т. е. приложением по умолчанию для конкретно этой ситуации (на http:// можно назначить один браузер, на https:// — другой, на ftp:// — третий, на http*://site.ru/demo/ — четвёртый), либо браузер молча это сделает, если он такой один умелец во всей системе, либо пользователь увидит список всех приложений, которые могут обработать этот запрос и решит, кто же это сделает.

    Компоненты четвёртого вида — поставщики контента — интентами не активируются. Они активируются по запросам от ContentResolver. Именно от ContentResolver, а не напрямую от вашего приложения. Ваше приложение должно использовать экземпляр ContentResolver и делать запросы ему, а сам ContentResolver уже будет общаться с Content Provider. В итоге получается слой, который отделяет поставщики контента от приложений, заставляя эти приложения становиться клиентами контент провайдеров через контент резолверы, что обеспечивает определённую безопасность.

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

  • Можно запустить операцию, она же активити, передав объект Intent методу startActivity() или startActivityForResult(), если нужно, чтобы операция вернула результат
  • Можно запустить службу, она же сервис, передав объект Intent методу startService(). Подобным же образом можно передать работающей функции новые параметры. Можно подключиться к работающей службе, передав объект Intent методу bindService()
  • Можно инициировать рассылку широковещательных сообщений, они же бродкасты, передав объект Intent методу, скажем, sentBrodcast()
  • Можно выполнить запрос к поставщику контента, он же контент провайдер, вызвав метод query() объекта ContentResolver

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

    Чтобы система могла запустить некий компонент приложения, она должна знать о его существовании. Все компоненты она ищет в файле манифеста, который называется AndroidManifest.xml и лежит в корне apk файла. То есть если наше приложение — это файл application.apk, то манифест можно найти по пути application.apk//AndroidManifest.xml. В этом файле должны быть объявлены все компоненты приложения, даже если их десятки или сотни. Само-собой, переживать за это особо не стоит, потому что их туда добавляет сама среда разработки, если всё идёт нормально.

    Кроме компонентов приложения, в фале манифеста есть ещё критичные данные, требуемые системе:

  • Все требуемые полномочия. Они же пермишены. Если приложение лезет в Интернет или пытается отправить SMS, здесь будет сказано об этом в явном виде
  • Объявление уровней API. Минимального, целевого, максимального.
    • Минимальное не даст установить приложение на версию Android ниже указанного. Например Android 5.0 имеет API версии (или API level) 21.
    • Целевая, она же Target, не обязательна. Она указывает, какую версию API приложение заведомо поддерживает. Благодаря этому могут включаться или не включаться некоторые фишки указанной версии Android. Скажем, если Target level задать 22, это Android 6, и ничего не делать дополнительно, то Android будет делать резервную копию приложения в облако и восстанавливать это приложение, со всеми его настройками, после сброса устройства. Если же Target не задать, то система будет смотреть на минимальную версию и, видя API lvl 21, будет считать приложение легаси говном (простите, устаревшим).
    • Максимальный уровень также не обязателен. Если его задать, то нельзя будет установить приложение на версию Android выше, чем указанный уровень API. Это может быть полезно для очень строгих ситуаций, когда какая-то ошибка, вызванная несовместимостью с новейшей версией Android, может стоить компании кучу денег.
  • Объявление необходимых аппаратных функций, которые нужны приложению. Указывать можно как те, которые нужны и без них нельзя работать — в этом случае приложение нельзя будет установить из Google Play, потому что сам магазин будет скрывать приложение, если устройство не имеет нужной хардварной фичи,  — так и те, которые нужны, но жить без них можно. В этом случае Google Play позволит установить приложение, а вам нужно будет позаботиться о проверке, есть же в итоге нужная железка или нет. Критичность определяется флагом, который будет true или false. Стоит понимать, что сам Android вообще игнорирует такие мелочи, как отсутствие критично важного аппаратного обеспечения, и всей фильтрацией занимается сам магазин приложений. Потому всегда возможна ситуация, когда приложения установили в обход магазина или даже из магазина, благодаря root правам и модификации системного файла build.prop, после чего устройство начинает сообщать о себе ложные сведения
  • Указание сторонних необходимых библиотек, если они нужны приложению
  • И другое

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

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

    Все эти стили, цвета, макеты, взаимное расположение элементов — всё это описано не в коде приложения, а в россыпи xml файлов в специальных папках. Благодаря этому можно быстро перетасовывать все эти ресурсы, не залезая в код. При этом apk построен так, что для разных разрешений экрана и даже для разных ориентаций (вертикальная, горизонтальная) одного устройства можно использовать разные макеты. Потому что когда вы держите устройство горизонтально, вам может быть вполне комфортно дотянуться до кнопки «Написать сообщение» в правом верхнем углу. Но когда вы держите его вертикально одной рукой, то тянуться в правый верхний угол — тот ещё акробатический трюк.

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

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

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