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

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

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

https://itsobes.ru
Download Telegram
to view and join the conversation
Что такое SparseArray?

SparseArray<E> – это ассоциативный массив, в котором ключами выступают целые числа. Другими словами это специфичная андроидная замена HashMap<Integer, E>.

SparseArray эффективнее чем HashMap по памяти, т.к. работает с примитивом int в качестве ключа и не выполняет операции боксинга / анбоксинга.

SparseArray упрощенно устроен следующим образом:
• Хранится целочисленный массив ключей: int[] mKeys. Массив ключей отсортирован по возрастанию;
• Хранится массив значений: Object[] mValues;
• Когда происходит вставка пары ключ-значение, сначала алгоритмом бинарного поиска ищется место-индекс в массиве ключей, с сохранением порядка. После этого ключ вставляется по найденному индексу в массив mKeys, а значение вставляется по тому же индексу в массив mValues;
• При получении значения из SparseArray по ключу, бинарным поиском ищется индекс ключа в массиве mKeys, и возвращается значение из mValues по найденному индексу.

SparseArray медленнее, чем HashMap, потому что ищет позицию ключа бинарным поиском, а вставка и удаление элементов требует выполнение вставки и удаления на массивах.

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

Такое решение позволяет переиспользовать места в массиве. Реальное удаление выполняется позже, при вызове других методов SparseArray.

Помимо параметризованного SparseArray<E> Android SDK предоставляет классы SparseLongArray, SparseIntArray, SparseBooleanArray, которые в качестве значений принимают соответствующие примитивы.

#Collections
Чем отличаются hot и cold Observables в RxJava?

В RxJava есть два вида Observable: Hot и Cold.

Cold Observable:
• Не рассылает объекты, пока на него не подписался хотя бы один подписчик;
• Если observable имеет несколько подписчиков, то он будет рассылать всю последовательность объектов каждому подписчику.

Пример cold observable – методы ретрофит-интерфейса. Каждый раз когда вызывается метод subscribe(), выполняется соответствующий запрос на бэкенд и подписчик получает объект-респонс.

Hot Observable:
• Рассылает объекты, когда они появляются, независимо от того есть ли подписчики;
• Каждый новый подписчик получает только новые объекты, а не всю последовательность.

Пример hot observable – observable, рассылающий клик-ивенты на view. Смотрите библиотеку RxBinding.

Подробнее про hot и cold observables.

#RxJava
Что такое HandlerThread?

HandlerThread – это наследник класса Thread, который создан для облегчения работы с лупером.

При старте потока HandlerThread, looper инициализируется методом Looper.prepare() и вызывается HandlerThread.onLooperPrepared(). Этот метод можно переопределить в наследнике класса HandlerThread и использовать для выполнения подготовки перед зацикливанием лупера. По-умолчанию onLooperPrepared() имеет пустую реализацию.

После onLooperPrepared() HandlerThread вызывает looper.loop().

Метод HandlerThread.getThreadHandler() возвращает объект Handler, который создается на связанном с тредом лупере.

Также HandlerThread имеет методы quit() и quitSafe(), которые делегируют вызовы на соответствующие методы класса Looper.

#OS
Что такое ArrayMap и ArraySet?

ArrayMap<K, V> – это хранилище общего назначения пар ключ-значение. ArrayMap реализует интерфейс Map<K, V> и добавлен в Android SDK как более эффективная по памяти замена HashMap.

Реализация ArrayMap похожа на SparseArray. ArrayMap хранит отсортированный массив хэш-кодов ключей и массив объектов ключ-значение.
При добавлении элемента в ArrayMap, бинарным поиском ищется место (index) в массиве, куда будет добавлен хэш-код ключа. После этого ключ добавляется в массив объектов на место index * 2, а значение на место index * 2 + 1.
Для разрешения коллизий используется метод открытой адресации. В случае коллизии, для места вставки хэш-кода используется соседнее место index + 1. В HashMap для разрешения коллизий используется метод цепочек.

Как и SparseArray, ArrayMap медленнее, чем HashMap, потому что ищет позицию ключа бинарным поиском, а вставка и удаление элементов требуют выполнения вставки и удаления на массивах.
В отличие от большинства стандартных Java-коллекций, ArrayMap при удалении элементов сокращает массивы, используемые для хранения данных.

ArraySet<E> реализует интерфейсы Collection<E> и Set<E>. Соотносится с ArrayMap также как HashSet с HashMap, т.е. в качестве ключей выступают добавляемые элементы.

#OS
Как создать Observable в RxJava?

Для создания произвольного Observable используется статический метод Observable.create(), который принимает параметром функциональный интерфейс ObservableOnSubscribe<T>.

ObservableOnSubscribe имеет один метод subscribe(emitter: ObservableEmitter<T>). Параметром принимается объект типа ObservableEmitter, который имеет методы onNext(value: T), onError(e: Throwable), onComplete(). Эти методы используются для передачи значений вниз по RxJava цепочке.

Помимо create() существует ряд фабричных методов, которые создают Observable из заданных элементов:

#RxJava
Шпаргалка для выбора скоуп функций в Kotlin

#пятница
Чем отличается subscribeOn и observeOn в RxJava?

subscribeOn() – задает Scheduler, на котором выполняется подписка на Observable. Другими словами, код метода Observable.create() выполняется в потоке, заданном subscribeOn(). Scheduler, который задает subscribeOn() действует от создания Observable и вниз по цепочке вызовов RxJava до первого observeOn(). Место вызова subscribeOn() в цепочке не имеет значения.
Если subscribeOn() вызывается несколько раз на одном Rx-стриме, то в большинстве случаев только первый вызов имеет эффект.

observeOn() – задает Scheduler, на котором выполняются операторы, следующие после observeOn().
В Rx-стриме может быть несколько observeOn(), каждый из которых будет менять поток выполнения.

#RxJava
Когда следует использовать ArrayMap и ArraySet?

Выбирая между ArrayMap / ArraySet и стандартными Java коллекциями, нужно учитывать что:

• Алгоритмическая сложность вставки в HashMap: O(1), сложность поиска и удаления: O(1 + α), где α – коэффициент загрузки;
• Алгоритмическая сложность вставки, поиска и удаления в ArrayMap и в TreeMap: O(log N);
ArrayMap эффективнее по памяти, чем HashMap и TreeMap;
TreeMap реализует интерфейс SortedMap, ArrayMap – нет;
• Описанное выше также характерно для ArraySet, HashSet и TreeSet.

ArrayMap и ArraySet следует использовать когда вы работаете с небольшим массивом данных. В данном видео рекомендуется использовать ArrayMap когда количество элементов меньше 1000 или когда вам нужно хранить в Map объекты Map.

#Collections
Какие бывают Subjects в RxJava?

Subject – это абстрактный класс в RxJava, одновременно расширяющий класс Observable и реализующий интерфейс Observer. Subject – это hot observable.

В RxJava есть следующие реализации Subject:

Publish Subject. Подписчики PublishSubject получают только те элементы, которые отправляются после момента подписки. PublishSubject не кэширует и не рассылает прошлые элементы.

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

AsyncSubject. Подписчики получают только последний элемент, который был отправлен перед вызовом onComplete().

ReplaySubject. По-умолчанию каждый новый подписчик получает все элементы, которые были отправлены до подписки, и все последующие элементы.
Если ReplaySubject создается фабричным методом createWithSize(size: Int), то подписчики будут получать только заданное количество элементов, отправленных в прошлом.

UnicastSubject работает также как ReplaySubject, но может иметь только одного подписчика. Все последующие подписчики получают onError() с IllegalStateException.

Более подробный разбор.

#RxJava
Что такое Parcelable?

Parcelable – это интерфейс, который совместно с классом Parcel реализует механизм сериализации в Android.

Если класс реализует интерфейс Parcelable, поля класса сериализуются в методе writeToParcel().
Также parcelable класс обязан иметь статическое ненулевое поле, названное CREATOR типа Creator<T>.

Интерфейс Creator<T> имеет два метода createFromParcel(parcel: Parcel): T и newArray(size: Int): Array<T>. Эти методы обратные writeToParcel() и используются для чтения данных из Parcel и создания объекта.

Объект Parcelable записывается в контейнер Parcel, который имеет метод marshall(): Array<Byte> для представления объекта в виде массива байтов.

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

#Parcelable
Когда Parcelable.describeContents() должен возвращать не 0?

При реализации describeContents() обычно возвращают значение 0. Документация говорит, что этот метод описывает виды «специальных объектов», которые содержатся в Parcelable.

В текущей версии Android API существует только один вид таких объектов – FileDescriptor.
Для него имеется константа CONTENTS_FILE_DESCRIPTOR. Метод describeContents() должен вернуть это значение, если в parcelable-классе сериализуется поле типа FileDescriptor.

Класс Parcel не использует метод describeContents() при сериализации и десериализации, а документация умалчивает, зачем для FileDescriptor создана специальная константа и как она используется системой.
В комментариях к ответу на StackOverflow высказывается предположение, что это значение используется для запрета передачи FileDescriptor в интенте при старте компонентов андроида.

Также следует возвращать не 0, если в сериализуемом классе есть поля типа Parcelable. В этом случае нужно делегировать вызов describeContents() в parcelable-поля и использовать побитовое ИЛИ:

#Parcelable
Что будет, если применить subscribeOn() к Subject?

Зависит от Subject.

Чтобы ответить на вопрос, надо понимать, как работает subscribeOn().
subscribeOn(), как и другие операторы RxJava, создает враппер вокруг исходного Observable. Вызов метода subscribe() идет вверх по rx-цепочке в текущем треде, пока не доходит до враппер-класса ObservableSubscribeOn. После этого вызов subscribe() делегируется на исходный Observable, но тред меняется на заданный в Scheduler оператора subscribeOn().

Далее ситуация различается для Observable и Subject.

В случае обычного cold observable вызов subscribe() инициализирует рассылку элементов вниз по цепочке. Другими словами, начинаются вызовы onNext() на подписчике, переданном в методе subscribe(). При этом поток остается тот же, в котором вызван subscribe(), а именно заданный оператором subscribeOn().

В случае Subject (hot observable) вызов subscribe() добавляет подписчика в массив и, в зависимости от вида Subject, рассылает или нет предыдущие элементы. Рассылка кэшированных элементов инициализируется в том же потоке, в котором вызван subscribe(), в потоке шедулера subscribeOn().

Когда же на Subject вызывается onNext() после подписки, вызов идет вниз по rx-цепочке без изменения треда, до первого observeOn(). Т.е. subscribeOn() больше не имеет эффекта.

Итог:
Для PublishSubject subscribeOn() не меняет тред, на котором вызывается onNext();
В случае BehaviorSubject, ReplaySubject и UnicastSubject, subscribeOn() применяется только к элементам, отправленным до вызова subscribe().
С AsyncSubject все совсем запутанно. Если subscribe() вызван после onComplete(), то onNext() получает кэшированный элемент и вызов происходит в потоке шедулера subscribeOn(). Если же subscribe() вызывается до onComplete(), то onNext() подписчика получает элемент в том же потоке, в котором вызван asyncSubject.onNext(), т.е. subsrcibeOn() не имеет эффекта.

Подробнее о том, как работает subscribeOn().
Еще подробнее.

#RxJava
Что быстрее parcelable или serializable?

Спойлер: Serializable.

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

Но на самом деле такое сравнение Serializable и Parcelable не совсем честное. Дело в том, что в Serializable тоже есть режим «ручного управления».
Чтобы не использовать рефлекшн и задать сериализуемые поля вручную, нужно использовать методы writeObject() и readObject() в serializable-классе.
В этом случае Serializable работает быстрее, чем Parcelable.

Исходный код приложения, в котором измеряется время (де)сериализации Parcelable и Serializable на больших объектах.

#Parcelable
Что происходит, когда пользователь кликает на иконку приложения?

Иконки установленных на телефоне приложений отображаются приложением-лаунчером.
Когда пользователь кликает на иконку приложения, лаунчер вызывает startActivity(), в который передается интент с main активити этого приложения. Вызов startActivity() делегируется в ActivityManagerService через Binder IPC.

После этого ActivityManagerService:
1. Получает информацию об активити методом PackageManager.resolveIntent();
2. Сохраняет информацию об активити в интент, чтобы не выполнять резолюшн в будущем;
3. Проверяет, имеет ли приложение-лаунчер достаточно прав, чтобы стартовать активити. Проверка выполняется в методе checkGrantUriPermissionLocked();
4. Проверяет, существует ли ProcessRecord для процесса приложения, которое нужно запустить. Если ProcessRecord null, то создается новый процесс для запуска main активити.

#OS
Синхронно или асинхронно выполняется Activity.runOnUiThread(), если вызывается из UI потока?

– Синхронно.

Этот вопрос аналогичен вопросу о Handler.post(). Handler.post() выполняется асинхронно и для синхронного выполнения предлагается проверять текущий поток. В runOnUiThread() есть эта проверка.
Если текущий поток – это UI тред, то параметр-runnable запускается сразу. Иначе выполнение делегируется в mHandler.post(), где mHandler – это хэндлер, созданный на UI потоке.

#Activity
Как система создает новый процесс и стартует приложение? (1/2)

Мы разобрали, что происходит, когда пользователь кликает на иконку приложения, от вызова startActivity() до создания нового процесса. Следующий вопрос: как создается новый процесс и стартует приложение.

1. ActivityManagerService стартует новый процесс методом startProcessLocked().

2. Вызов startProcessLocked() делегируется в Process и далее делегируется в класс ZygoteProcess, который недоступен в публичном API.

3. Класс ZygoteProcess работает с процессом zygote через сокет соединение. Вызов ZygoteProcess.start() делегируется в startViaZygote(), в котором аргументы метода start() преобразуются в ArrayList<String> в виде флагов, подобных флагам командной строки, и их значений.

4. Далее список аргументов передается в метод zygoteSendArgsAndGetResult(). В этом методе осуществляется передача аргументов в процесс zygote и получение результата. Работа с сокетом происходит через класс ZygoteState, который хранит локальный сокет и input/output стримы.

5. Zygote форкает себя и вызывает ZygoteInit.main(), который находит статический метод main() приложения в классе ActivityThread и стартует его.

#OS
Как система создает новый процесс и стартует приложение? (2/2)

Статический метод main() – это входная точка любого Java-приложения. В приложениях под Android этот метод написан за нас и находится в классе ActivityThread.

Метод ActivityThread.main() инициализирует ресурсы, необходимые для работы Android-приложения, и создает очередь сообщений главного потока. Для этого:

1. Вызывается статический метод Looper.prepareMainLooper(). Этот метод делегирует вызов в метод prepare() и сохраняет созданный Looper в статической переменной sMainLooper. Если при вызове prepareMainLooper() переменная sMainLooper не null, то бросается исключение.

2. Создается объект ActivityThread. При этом на лупере главного потока создается объект Handler, который хранится как приватная переменная класса ActivityThread и реализует метод handleMessage(). Именно в этом методе обрабатываются сообщения системы, регулирующие жизненные циклы компонентов приложения.

3. Вызывается метод Looper.loop(). На данном этапе создана очередь сообщений и связанный с ней handler. Можно сказать, что Java-приложение уже инициализировано, но еще не стартовало как Android-приложение, т.к. Application.onCreate() еще не вызван.

#OS
Какие виды стримов существуют в RxJava?

Observable – представляет собой стрим объектов. Подписчики на Observable имеют коллбэки onNext(value), onComplete(), onError(throwable).
onNext() может не вызываться, или вызываться произвольное количество раз.
При завершении стрима вызывается onComplete() или onError().

Single – отправляет объект, который принимается в коллбэке onSuccess(value), или бросает исключение в коллбэк onError(throwable) в случае ошибки.

Completable – не возвращает никакого значения. На подписчиках вызывается onComplete() при удачном завершении или onError(throwable) в случае ошибки.

Maybe – может отработать как Single или как Completable. На подписчиках вызывается один из трех коллбэков: onSuccess(value), onComplete() без какого-либо значения, или onError(throwable). Каждый из коллбэков может быть вызван один раз или не вызван вообще.

Flowable – работает как Observable, но поддерживает backpressure по умолчанию.

#RxJava
Как и когда вызывается метод Application.onCreate()?

Следующий шаг после старта процесса с java-приложением – это вызов коллбэка Application.onCreate(). Инициализация приложения происходит в методе ActivityThread.main() до зацикливания очереди сообщений вызовом Looper.loop():

1. При создании объекта ActivityThread инициализируется приватное поле типа ApplicationThread;

2. На ActivityThread вызывается метод attach(), в котором на ActivityManagerService вызывается метод attachApplication(). Этот метод получает объект ApplicationThread как параметр.

3. В ActivityManagerService.attachApplication() происходит инициализация параметров, необходимых для старта приложения, и вызывается метод ApplicationThread.bindApplication().

4. В методе bindApplication() составляется объект AppBindData, содержащий информацию о стартующем приложении, и отправляется сообщение через Handler c кодом BIND_APPLICATION в поле what и объектом AppBindData в поле data. Хэндлер добавляет сообщение в MessageQueue.

5. После окончания отработки ActivityThread.main() и вызова метода Looper.loop() сообщение из очереди обрабатывается в методе handleMessage() хэндлера, созданного в ActivityThread.

6. По what-коду сообщение передается в метод handleBindApplication(), который вызывает LoadedApk.makeApplication(), где создается объект класса Application методом Instrumentation.newApplication();

7. Далее с созданным объектом Application в качестве параметра вызывается метод Instrumentation.callApplicationOnCreate(app), который и вызывает метод Application.onCreate().

#OS