Android Interview Review
3.56K members
1 photo
234 links
Популярные вопросы и ответы с собеседований на Android-разработчика.

Канал для Java-разработчиков @JavaSobes

Связаться с нами @SobesAdmin

https://itsobes.ru
Download Telegram
to view and join the conversation
В каком потоке работает сервис? В главном или фоновом?

Service по умолчанию работает в главном потоке, в андроиде это UI thread.
На этот вопрос часто отвечают неправильно, потому что путают понятия фоновой задачи (background task) и фонового потока (background thread).

Андроид приложение может работать в фоновом режиме. Это значит, что пользователь не видит UI компоненты приложения, а активити на верхушке бэкстека находится в stopped состоянии.

Для выполнения асинхронной операции в сервисе можно использовать multithreading api андроида.
Для выполнения всего кода сервиса асинхронно используется специальный вид сервиса, JobIntentService, который по умолчанию работает в фоновом потоке.

#Service
Что такое IntentService / JobIntentService?

IntentService – это Service, который работает (выполняет код метода onHandleIntent()) в фоновом потоке.
IntentService останавливается сам после завершения выполнения метода onHandleIntent(), т.е. не нужно вызывать stopSelf().
IntentService работает на одном фоновом потоке и выполняет задачи в порядке очереди.
Используется когда нужно выполнить фоновую задачу (не привязанную активити) в фоновом потоке (не в UI треде).

Начиная с Android 8.0 (API level 26) ОС ограничивает работу фоновых сервисов. IntentService – не исключение, поэтому если target api приложения – 26 или выше, используйте JobIntentService.

JobIntentService используется для тех же целей, что и IntentService, и имеет похожий API.
Для старта JobIntentService используется статический метод enqueueWork(), который использует Context.startServce() для API level < 26 и JobScheduler.enqueue() для API level ≥ 26. После этого система стартует сервис и вызывает в фоновом потоке метод onHandleWork().

#Service
Что такое Background и Foreground Service?

Любой сервис, не зависимо от того Started или Bound, по умолчанию Background. Это значит, что сервис работает как фоновая задача (не путать с фоновым потоком), не требующая взаимодействия с пользователем.

Foreground Service – это сервис, о котором пользователь осведомлен. Это достигается с помощью отображения нотификации в статус-баре.
Пример foreground сервиса – отображение нотификации при проигрывании музыки в приложении-плеере.

Процесс в котором работает foreground сервис имеет больший приоритет, чем процесс с background сервисом. В примере с плеером foreground сервис выполняет сразу две функции:
1. Говорит системе, что этот процесс убивать не надо, т.к. пользователь взаимодействует с ним;
2. Обрабатывает нажатия на кнопки в нотификации.

Запущенный сервис переводится в состояние foreground методом startForeground(int id, Notification notification), принимающим параметрами id нотификации и саму нотификацию, которая будет показана пользователю. Важно знать, что startForeground() вызывается у сервиса, который уже запущен как background сервис, например методом startService().

Для перевода foreground сервиса в состояние background используется метод stopForeground(boolean removeNotification). Этот метод не останавливает сервис (нужно вызвать stopService() или stopSelf()), но увеличивает шансы того, что система убьет процесс.

#Service
Какие ограничения связанные с фоновыми сервисами были добавлены в Android 8.0?

В Android 8.0 (API level 26) введены ограничения на работу с фоновыми сервисами. Эти ограничения касаются приложений с targetSdkVersion ≥ 26, но пользователь может включить ограничения для всех приложений в настройках.

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

Вместо фоновых сервисов рекомендуется использовать JobScheduler или видимые сервисы.

Как вы уже знаете из предыдущего поста, чтобы запустить видимый сервис, нужно сначала запустить фоновый сервис методом startService(), а потом вызвать метод startForeground() на этом сервисе. Но, начиная с Android 8.0, нельзя запустить фоновый сервис, если пользователь не взаимодействует с приложением. Поэтому в API level 26 был добавлен метод startForegroundService(), который делает то же, что и startService(), но обещает системе, что метод startForeground() будет вызван сразу после старта сервиса. Если не вызывать startForeground(), то система убьет сервис через несколько секунд.

Документация.

#Service
Опишите методы жизненного цикла сервиса.

Жизненный цикл сервиса различается для started и bound сервисов.

Методы жизненного цикла started сервиса:

onCreate() – вызывается, когда сервис создается системой. Для создания started сервиса используется метод startService().

onStartCommand() – вызывается, когда сервис переходит в активное состояние. Код, который выполняет сервис, должен быть написан в этом методе.

onDestroy() – вызывается, когда сервис уничтожается системой. Это происходит после вызова stopSelf() или stopService(). Также система может убить процесс с фоновым сервисом когда не хватает ресурсов или, начиная с Android 8.0, для ограничения фоновых работы.

Методы жизненного цикла bound сервиса:

onCreate() – вызывается когда первый клиент присоединяется к сервису вызовом bindService() с флагом BIND_AUTO_CREATE.

onBind() – вызывается системой, когда первый клиент присоединяется к сервису вызовом метода bindService(). После вызова этого метода bound сервис переходит в активное состояние.

onUnbind() – вызывается системой, когда все клиенты отсоединились от сервиса вызовом метода unbindService().

onDestroy() – вызывается после onUnbind(), перед тем как система уничтожит сервис.

Сервис может быть одновременно started и bound. В этом случае вызываются все методы жизненного цикла обоих типов сервисов.
onDestroy() у такого сервиса вызывается когда все клиенты отсоединены и сервис остановлен вызовом метода stopSelf() или stopService().
Если же все клиенты отсоединяются, но сервис не остановлен, то вызывается метод onUnbind() и сервис продолжает работать.
onUnbind() возвращает boolean. Если вернуть true, то при присоединении первого клиента после onUnbind() вызывается метод onRebind(), иначе вызывается onBind().

Документация жизненного цикла сервиса.
Документация жизненного цикла bound сервиса.

#Service
Что такое и для чего используется BroadcastReceiver?

BroadcastReceiver – один из четырех основных компонентов. Реализует шаблон publisher-subscriber и используется для получения сообщений системы, других компонентов приложения и сторонних приложений.

События обрабатываются в методе BroadcastReceiver.onReceive(), который вызывает система.

BroadcastReceiver может быть точкой входа в приложение. Ресиверы, зарегистрированные в манифесте, принимают сообщения даже если приложение не запущено. В этом случае система стартует процесс с приложением и вызывает Application.onCreate() до вызова BroadcastReceiver.onReceive().

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

#BroadcastReceiver
#Components
Какие способы регистрации BroadcastReceiver вы знаете? Чем они отличаются?

BroadcastReceiver можно зарегистрировать статически и динамически.

Статическая регистрация – это добавление элемента <receiver> в AndroidManifest.

<receiver android:name=".MyBroadcastReceiver">
<intent-filter>
<action android:name="android.intent.action.LOCALE_CHANGED"/>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
</intent-filter>
</receiver>


Элемент <intent-filter> объявляет действия (<action>) на которые реагирует BroadcastReceiver. Система регистрирует ресиверы, прописанные в манифесте и вызывает их независимо от того запущено приложение или нет.

Динамическая регистрация – это регистрация на контексте в коде приложения. Выполняется методом context.registerReceiver(receiver: BroadcastReceiver, intentFilter: IntentFilter). Этот метод принимает параметром объект класса IntentFilter, который определяет на какие действия будет реагировать зарегистрированный ресивер.

val receiver: BroadcastReceiver = MyBroadcastReceiver()
val filter = IntentFilter().apply {
addAction(ConnectivityManager.CONNECTIVITY_ACTION)
addAction(Intent.LOCALE_CHANGED)
addAction("my_custom_action")
}
context.registerReceiver(receiver, filter)


Для разрегистрации динамических ресиверов используется метод context.unregisterReceiver(receiver: BroadcastReceiver).

Ресиверы, зарегистрированные динамически, живут не дольше чем объект context, на котором они зарегистрированы. Если метод registerReceiver() вызывается на активити, то ресивер будет получать события, пока система не уничтожит активити. Если вызвать registerReceiver() на application context, то ресивер останется зарегистрированным, пока запущено приложение. Пост о различии activity и application контекстов.

#BroadcastReceiver
Можно ли выполнять длительные операции в методе onReceive()?

Скорее нет, чем да. Зависит от того, что считать длительной операцией. Отвечая на этот вопрос, расскажите, что происходит с процессом приложения, в котором запущен только BroadcastReceiver.

Допустим приложение не запущено и ресивер, зарегистрированный в манифесте, получает событие. В этом случае система стартует процесс приложения и вызывает BroadcastReceiver.onReceive() в главном потоке. Фоновые процессы имеют низкий приоритет, поэтому после завершения метода onReceive() система убивает процесс приложения. Если в onRecieve() запустить фоновый поток, о котором не знает система, например, чтобы выполнить запрос на бэкенд, процесс все равно будет убит.
Для выполнения фоновых потоков в onReceive() используется метод goAsync(), который сохраняет BroadcastReceiver после завершения onReceive(). Но даже с использованием метода goAsync() жизнь ресивера ограничивается 10 секундами.

Для выполнения действительно длительных операций рекомендуется использовать Foregorund service или JobIntentService, которые можно запустить в onReceive().

Подробнее в документации.

#BroadcastReceiver
Какие существуют способы отправки броадкастов?

1. Обычный броадкаст. Для отправки броадкаст сообщения используется метод sendBroadcast(intent: Intent). Intent может явно определять ресивер, который получит сообщение:

val intent = Intent(MyBroadcastReceiver::class.java) 
context.sendBroadcast(intent)


В примере выше сообщение получит только MyBroadcastReceiver.

Если intent содержит action, то сообщение получат все ресиверы, зарегистрированные с соответствующим интент фильтром:

val intent = Intent("my_custom_action")
context.sendBroadcast(intent)


Этот интент получит ресивер, содержащий my_custom_action в IntentFilter. Пример такого ресивера.

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

2. Упорядоченный броадкаст. Отправляется методом sendOrderedBroadcast(intent: Intent, permission: String). Сообщение принимается в порядке, заданном атрибутом priority объекта IntentFilter. Ресиверы с одинаковым приоритетом получают сообщение в произвольном порядке.
Когда отправляется упорядоченный броадкаст, ресиверы могут передавать данные (result сode / result data / result extras) следующим ресиверам или останавливать отправку.
Упорядоченные броадкасты разберем подробнее в следующих постах.

3. Локальный броадкаст. Отправляется методом LocalBroadcastManager.sendBroadcast(intent: Intent). Сообщения доставляются только тем ресиверам, которые зарегистрированны в этом же приложении. Отправка локальных броадкастов эффективнее, чем обычный броадкаст, т.к. не требуется коммуникация между процессами.

#BroadcastReceiver
Как задать приоритет упорядоченного броадкаста?

Как было сказано в предыдущем посте, упорядоченные броадкасты доставляются в порядке приоритета.
Приоритет задается не у самого ресивера, а у объекта IntentFilter.
Есть два способа задать приоритет.

1. Статически в манифесте атрибутом priority:

<receiver android:name=".MyBroadcastReceiver">
<intent-filter android:priority="10">
<action android:name="my_custom_action"/>
</intent-filter>
</receiver>


2. Динамически методом IntentFilter.setPriority():

val receiver: BroadcastReceiver = MyBroadcastReceiver()
val filter = IntentFilter("my_custom_action")
filter.setPriority(10)
context.registerReceiver(receiver, filter)


Приоритет – целое число между SYSTEM_LOW_PRIORITY и SYSTEM_HIGH_PRIORITY включительно, где SYSTEM_LOW_PRIORITY = -1000, SYSTEM_HIGH_PRIORITY = 1000. Приоритет по умолчанию равен 0.

#BroadcastReceiver
Как передать результат следующему ресиверу при упорядоченном броадкасте? Как остановить упорядоченный броадкаст?

Чтобы остановить упорядоченный броадкаст используется метод abortBroadcast().

Для передачи и изменения результата между ресиверами используются следующие методы:

setResultCode(code: Int) / getResultCode() – позволяют передать целочисленное значение. getResultCode() по умолчанию возвращает -1.

setResultData(data: String) / getResultData() – передают строковое значение следующим ресиверам. Значение по умолчанию – null.

setResultExtras(extras: Bundle) / getResultExtras(makeMap: Boolean) – самые полезные методы, потому что позволяют передать практически все что угодно в объекте Bundle. getResultExtras(makeMap: Boolean) по умолчанию возвращает пустой объект Bundle, если параметр makeMap = true, иначе возвращает null.

Метод setResult(code: Int, data: String, extras: Bundle) задает три типа результата одновременно.

Все перечисленные методы работают только с упорядоченными броадкастами. Узнать упорядоченный ли броадкаст внутри ресивера можно с помощью метода isOrderedBroadcast().

Результат, заданный в одном из упорядоченных ресиверов, передается всем последующим пока не будет изменен.
Допустим зарегистрированы ресиверы R1, R2, R3, R4. Сначала R1 получает resultCode = -1. R1 вызывает метод setResultCode(1). R2 получает resultCode = 1 и не изменяет его. R3 тоже получает resultCode = 1 и вызывает метод setResultCode(3). R4 получает resultCode = 3.

#BroadcastReceiver
Что такое sticky broadcast?

Механизм Sticky Broadcast – deprecated начиная с API level 21, так что лучше его не использовать.

Для отправки используется метод sendStickyBroadcast(intent: Intent).
Суть заключается в том, что система сохраняет Intent, отправленный как sticky. Если после отправки вызвать метод registerReceiver() с соответствующим интент фильтром, то registerReceiver() вернет отправленный intent и система вызовет onReceive() на зарегистрированном ресивере.

#BroadcastReceiver
Какие ограничения на BroadcastReceiver были введены в Android 8.0?

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

Ограничения на броадкасты начали вводить в Android 7, но в 8й версии они стали более строгими и общими. Ограничения касаются приложений с targetSdkVersion = 26.

* Неявные (implicit) броадкасты, зарегистрированные в андроид манифесте, больше не работают (кроме некоторых исключений). Неявный броадкаст – это тот, который предназначается не для конкретного приложения. Например ACTION_LOCALE_CHANGED – неявный броадкаст, т.к. отправляется при изменении языка в системных настройках и принимается всеми приложениями с интент фильтром, содержащим этот action.

* Ограничения не касаются Явных (explicit) броадкастов, даже если они зарегистрированы в андроид манифесте. Пример явного броадкаста: ACTION_MY_PACKAGE_REPLACED отправляется только тому приложению, которое было обновлено.

* Разрешается динамическая регистрация неявных броадкастов через context.registerReceiver(). Такая регистрация действует только пока запущен процесс приложения.

* Ограничения не действуют на неявные броадкасты, которые требуют signature permission.

Начиная с Android 8, вместо прослушивания неявных броадкастов предлагается использовать JobScheduler.

#BroadcastReceiver
Что такое утечка активити / контекста?

Утечка активити и контекста в сущности одно и то же, т.к. класс Activity – наследник класса Context. Когда говорят об утечке активити или контекста, имеют в виду memory leak. Memory leak в Java – это ситуация, когда объект больше не используется, но сборщик мусора не может удалить объект из памяти, потому что на него ссылаются другие объекты.

Об утечке активити говорят, когда у активити вызван метод onDestroy(), но на активити все еще есть внешние ссылки. Например сохранение Activity Context в статическую переменную приводит к утечке.

Activity – тяжелый объект, занимающий относительно много места в памяти, поэтому утечка объектов Activity – большая проблема для Android приложения. Частые утечки активити могут привести к OutOfMemoryError и крэшу приложения.
Leakcanary – библиотека, которая помогает отслеживать утечки памяти в Android приложении.

#Activity
Что такое и для чего используется ContentProvider?

ContentProvider – один из четырех компонентов андроид приложения. ContentProvider используется для предоставления доступа к хранилищу данных приложения другим приложениям. ContentProvider прописывается в андроид манифесте, как элемент <provider>.

Для доступа к данным контент провайдера используется класс ContentResolver.
ContentResolver предоставляет CRUD API для доступа к хранилищу данных. ContentResolver определяет к какому провайдеру направить запрос. Выбранный ContentProvider обрабатывает запрос и возвращает данные обратно резолверу. ContentResolver передает результат пользователю, вызвавшему метод.

#ContentProvider
#ContentResolver
#Components
Приведите примеры системных контент провайдеров.

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

Контент провайдер календарей предоставляет доступ к связанным с календарями таблицам, таким как Calendars, Events, Reminders. Полный список таблиц можно найти здесь. Для работы с контент провайдером календаря нужны пользовательские разрешения: READ_CALENDAR для чтения данных и WRITE_CALENDAR для изменения.

Контент провайдер контактов предоставляет доступ к таблицам Contacts, Profile и другим. Для чтения и записи в провайдер контактов необходимы разрешения READ_CONTACTS и WRITE_CONTACTS соответственно. Для работы с Profile таблицей нужно дополнительно запросить разрешения android.permission.READ_PROFILE или android.permission.WRITE_PROFILE.

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

#ContentProvider
Привет!
Мы постоянно стараемся улучшать каналы Interview Review. Хочется делать их лучше лично для вас. Поэтому, чтобы познакомиться с вами поближе, просим вас пройти маленький анонимный опрос. Это займет всего 3 минуты (мы честно замеряли).

Спасибо!
Админы канала
Как работать с данными через ContentResolver? (1/2)

ContentResolver имеет методы аналогичные CRUD API, а именно insert(), query(), delete(), update().

Все четыре метода принимают первым параметром объект класса Uri, который позволяет определить какой ContentProvider будет использован.

Метод query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) – аналогичен запросу SELECT в SQL и используется для получения данных.
Разберем параметры этого метода:

projection – массив имен колонок таблицы, которые будут возвращены в результате запроса.

selection – аналогичен WHERE в SQL. Задает условие, по которому выбираются данные из таблицы, например: "_id = ?".

selectionArgs – массив аргументов, которые используются в selection. Каждый знак вопроса в строке selection будет заменен на аргумент из массива selectionArgs.

sortOrder – аналогичен ORDER BY в SQL. Задает порядок, в котором будут возвращены результаты запроса. Например: "_id ASC" – по возрастанию значения id.

Пример использования метода query():

val projection = arrayOf("_id", "calendar_access_level", "visible")
val selection = "calendar_access_level = ? AND visible = ?"
val selectionArgs = arrayOf(Calendars.CAL_ACCESS_OWNER.toString(), "1")
val sortOrder = "_id ASC"

val result = query(Calendars.CONTENT_URI, projection, selection, selectionArgs, sortOrder)


CAL_ACCESS_OWNER – константа, определяющая уровень доступа owner.
"1" для параметра "visible" соответствует булевому значению true.

Такой код соответствует следующему SQL запросу:

SELECT id, calendar_access_level, visible, is_primary
FROM content://com.android.calendar/calendars
WHERE calendar_access_level=700 AND visible=1 AND is_primary=1
ORDER BY id ASC


Этот запрос возвращает все календари пользователя, к которым он имеет owner доступ. Далее id календарей могут быть использованы для получения или добавления событий в пользовательский календарь.

#ContentProvider
#ContentResolver
Как работать с данными через ContentResolver? (2/2)

В предыдущем посте мы разобрали метод query(), который используется для получения данных через ContentResolver. Для изменения данных используются методы insert(), update(), delete().

insert(url: Uri, values: ContentValues) вставляет строку (row) в хранилище контент провайдера. Принимает вторым параметром объект ContentValues, который содержит пары ключ-значение. Ключ соответствует имени столбца (column) таблицы базы данных, в который записывается значение.
insert() возвращает Uri созданной строки или null, если строка не была создана.

update(uri: Uri, values: ContentValues, selection: String, selectionArgs: String[]) обновляет строки хранилища контент провайдера, которые соответствуют условиям selection и selectionArgs (как работают эти параметры смотрите в предыдущем посте). update() возвращает значение int – количество обновленных строк.

delete(url: Uri, selection: String, selectionArgs: String[]) удаляет строки, которые соответствуют условиям selection и selectionArgs, и возвращает количество удаленных строк.

Примеры использования этих методов.

#ContentProvider
#ContentResolver
Из чего состоит Content URI?

URIUniform Resource Identifier.
URI контента в андроиде – это объект класса Uri, который имеет структуру content://{authority}/{path}/{id}.

content: – это схема. Константа content определена в документации как SCHEME_CONTENT. Именно схема говорит, что данный URI – это URI контента, а не, например, файла.

authority – идентифицирует ContentProvider. Обрабатывая запросы, ContentResolver использует authority, чтобы найти ContentProvider в системной таблице зарегистрированных провайдеров. После этого запросы делегируются в провайдер.

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

id – опциональная часть content URI и поддерживается не всеми провайдерами. Определяет строку в таблице провайдера. Метод insert() возвращает URI, содержащий id созданной строки.

Примеры content URI таблиц провайдера календарей:
Таблица календарей: content://com.android.calendar/calendars
Таблица событий: content://com.android.calendar/events

#ContentProvider
Как защититься от SQL инъекции при запросе в ContentResolver?

SQL-инъекция – вид атаки на сайты и приложения, основанный на внедрении вредоносного SQL-кода.

Например приложение предоставляет пользователю возможность найти контакт по имени. Для этого читается пользовательский ввод и выполняется запрос через ContentResolver. Такую логику можно реализовать следующим образом:

val projection = arrayOf("first_name", "last_name")
val selection = "first_name = ${searchEditText.getText()}"
val result = contentResolver.query("content://my_contacts/contacts", projection, selection, null, null)


Например пользователь вводит "Ivan", тогда выполняется SQL-запрос
SELECT first_name, last_name FROM content://my_contacts/contacts WHERE first_name = Ivan.


При такой реализации злоумышленник может ввести вредоносный SQL-код в поле поиска: nothing; DROP TABLE *;, что приведет к запросу
SELECT first_name, last_name FROM content://my_contacts/contacts WHERE first_name = nothing; DROP TABLE *;

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

Чтобы защититься от SQL-инъекции, используется параметризованный selection. Для этого пишут "?" на месте параметров в строке selection и передают значения параметров в массиве selectionArgs:

val projection = arrayOf("first_name", "last_name")
val selection = "first_name = ?"
val selectionArgs = arrayOf(searchEditText.getText().toString())
val result = contentResolver.query("content://my_contacts/contacts", projection, selection, selectionArgs, null)


Аргументы selectionArgs экранируются перед выполнением SQL-запроса, поэтому описанная SQL-инъекция не сработает.

#ContentProvider