Топ 20 вопросов на iOS собеседовании. Уровень Middle

Содержание

В продолжении статьи Топ 20 вопросов на iOS собеседовании. Уровень Junior рассмотрим вопросы на собеседовании iOS разработчика уровня Middle. Как и в прошлый раз, рассмотрим возможный вариант ответа на каждый вопрос. Перед тем, как приступите к изучению вопросов, обратим внимание на следующий факт: нельзя однозначно определить уровень разработчика как Junior/Middle/Senior.

На одних собеседованиях вопросы на высокую позицию будут сопровождаться сложными и каверзными вопросами, на других — тривиальными и «простыми». Это зависит как от политики компании, в которой проходите собеседование, так и от знаний интервьюера.

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

На текущий момент компании еще часто включают вопросы на собеседовании как на Swift, так и на Objective-C для подтверждения уровня знаний позиции Middle и выше. Подобная практика встречается на больших и «старых» проектах. Причина заключается в том, что проекты еще не успели переписать на Swift, но на практике интервьюеры не отказывают в просьбе о прохождении собеседования на привычном для кандидата языке.

1. Посмотрите на код. Что выведется в консоль после выполнения этого код?

import Foundation

func printMultithreading() {
    print("2")
    
    DispatchQueue.global().async {
        print("3")
        
        DispatchQueue.main.sync {
            print("4")
        }
        
        print("5")
    }
    
    print("6")
}

print("1")
printMultithreading()
print("7")

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

Если ответ не сходится с правильным, кандидату дают время подумать и просят объяснить почему указанный ответ не сходится с верным. Разберем корректный ответ:

1
2
6
7
3
4
5

1 — запуск нашего кода. Код выполняется в главном потоке, указатель доходит до print («1») и печатает результат.

2 — вызываем функцию printMultithreading() в главном потоке, указатель заходит в функцию и печатает первый принт print («2»).

6 — так как функция printMultithreading() находится в главном потоке, то указатель попадает на 6 строчку кода, и последующий блок с кодом (7-13 строчки) отправляется в глобальную очередь. Глобальная очередь приступит к выполнению после завершения работ, которые содержатся на главном потоке. Поэтому, следующая команда, на которую упадет наш указатель — print («6»).

7 — после печати 16 строчки указатель выходит из функции printMultithreading() и продолжает выполнять код на главном потоке — print («7»).

3 — после выполнения кода в главном потоке, очередь наступает для выполнения блока с кодом в глобальной очереди — print («3»).

4 — далее указатель попадает на 9 строчку, где выполняется код в главном потоке — выводим print («4»).

5 —  указатель попадает на 13 строчку, после выполнения блока с кодом в главном потоке — печатаем print («5»).

*Обратите внимание на следующий момент: если запустить код несколько раз, то результат может отличаться друг от друга — вывод 7 и 3 может меняться местами. На 6 строчке мы отправляем часть кода выполняться в асинхронный поток и продолжаем работу с главным потоком. Но если у системы есть свободные ресурсы, то асинхронный поток может начать выполняться быстрее, чем указатель дойдет до 21 строчки кода.

2. Расскажите о приоритетах Quality of Service

 QualityofService или QoS — качество обслуживания, которое появилось с приходом iOS 8. QoS помогает выставить приоритет, с которым будет выполняться наша задача DispatchQueue.

 QoS используется с функцией .async(). Приоритеты делятся на четыре группы, каждая из которых помогает той или иной работе приложения.

  • User Interactive — используется для взаимодействия с пользователем. Это может быть любая работа, которая проходит в главном потоке, например, анимация или обновление интерфейса.
  • User Initiated — используется при инициации работы пользователем. Это может быть такая задача как загрузка данных по API. Работа должна быть завершена, чтобы пользователь мог продолжить пользоваться приложением.
  • Utility — используется для задач, которые не отслеживаются пользователем приложения и не требует немедленного их завершения. Это может быть работа прогресс бара.
  • Background — используется для фоновых работ, которые не отслеживаются пользователем. Это может быть сохранение данных в БД или любая другая работа, которая может быть выполнена с низким приоритетом.

Как вы уже понимаете, User Interactive имеет самый высокий приоритет и будет выполняться в первую очередь, Background — самый низкий приоритет.

При ответе на вопрос стоит также добавить, что есть еще два типа приоритета: Default и Unspecified.

  • Default — приоритет размещен между User Initiated и Utility. Такой приоритет чаще используется в коде.
  • Unspecified — означает отсутствие приоритета. Приоритет выбирается самостоятельно в зависимости от окружающей среды (текущей загруженности системы).

Напоследок давайте рассмотрим пример использования приоритетов:

import Foundation

let queue = DispatchQueue(label: "Queue")

queue.async(qos: .background) {
    print("Background Code")
}

queue.async(qos: .userInitiated) {
    print("User Initiated Code")
}

Дополнительное чтение: DispatchQoS

3. Какие проблемы многопоточности вы знаете?

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

Подробно о проблемах многопоточности читать здесь: Проблемы многопоточности в Swift.

В этой статье только перечислим проблемы:

  • Race condition (Состояние гонки)
  • Priority inversion (Инверсия приоритетов)
  • Deadlock (Взаимная блокировка)
  • Livelock (Активная блокировка)
  • Starvation (Голодание)
  • Data Race (Гонка за данными)

4. Есть ли разница между GCD и NSOperation?

NSOperation — оболочка GCD. В случае использования NSOperation, неявно используется Grand Central Dispatch.

Преимущество GCD

  • Реализация GCD проста.

Преимущества NSOperation

  • NSOperation обеспечивает поддержку зависимостей. Это преимущество разрешает разработчикам выполнять задачи в конкретном порядке.
  • Операции можно приостанавливать, возобновлять и отменять. Как только вы отправляете задачу с помощью Grand Central Dispatch, вы теряете контроль над жизненным циклом задачи. NSOperation предоставляет разработчику контроль над операцией.
  • Вы можете указать максимальное количество операций в очереди, которые могут выполняться одновременно.

Дополнительное чтение: NSOperation или Grand Central Dispatch

5. Может ли протокол быть унаследован от другого протокола?

import Foundation

protocol SomeProtocol {
    // определение протокола SomeProtocol
}

protocol AnotherProtocol {
    // определение протокола AnotherProtocol
}

protocol InheritingProtocol: SomeProtocol, AnotherProtocol {
    // определение протокола InheritingProtocol
}

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

В нашем примере протокол InheritingProtocol должен удовлетворять всем требованиям протоколов SomeProtocol и AnotherProtocol.

Дополнительное чтение: Протоколы

6. Можно ли переопределить методы класса А в классе В?

import Foundation

class classA {
    class func classFunc() {}
    static func staticFunc() {}
}

class classB: classA {
    override class func classFunc() {}
    override class func staticFunc() {}
}

Нет, в нашем случае код не скомпилируется. Все дело в ключевых словах class и static. Помечая функцию как class — вы позволяете переопределять методы в классах наследниках, static — не позволяет переопределять классы. Это главное отличие при использовании методов.

Так или иначе подобные вопросы на собеседовании сводятся к обсуждению разницы static и class. Также, вы можете сделать свойства как static, так и class. Static свойство может быть stored property (хранимое свойство) или computed property (вычисляемое свойство). Class свойство — только computed property. Что это значит? Пример ниже:

import Foundation

class classA {
    class var classStoredPropertyVariable = "classVariable" // Ошибка: не может быть stored property
    class var classComputedPropertyVariable: Int {
        get {
            return 1
        }
    }
    
    static var staticStoredPropertyVariable = "staticVariable"
    static var staticComputedPropertyVariable: Int {
        get {
            return 1
        }
    }
}

7. Что выведется в консоль после выполнения кода?

func getIndex() -> Int {
    var index = 1

    defer {
        index = 0
    }
    return index
}

let index = getIndex()
print(index) // 1

Это не магия, а особенность оператора defer. Оператор выполняет код непосредственно перед тем, как функция, в которой расположен оператор, выйдет за пределы области видимости программы. Поэтому в нашем примере index обновит свое значение уже после того, как сработает оператор return.

Дополнительное чтение: Defer Statement

8. Расскажите о диспетчеризации методов

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

Итак, диспетчеризация метода — это то, как программа будет определять, какие инструменты использовать при вызове функции. Диспетчеризация широко используется, знание этого механизма поможет выйти из запутанных ситуаций. Swift поддерживает три типа диспетчеризации:

  • Direct Dispatch (Статическая диспетчеризация) — самый быстрый тип диспетчеризации. Адрес вызываемой функции определяется во время компиляции, поэтому затраты на такие вызовы минимальны. Для использования статической диспетчеризации вы можете пометить методы ключевым словом private или классы ключевым словом final.
  • Table Dispatch (Динамическая диспетчеризация) — распространенный тип. Адрес вызываемой функции определяется во время выполнения. У каждого подкласса есть собственная таблица с указателем на функцию для каждого метода. По мере того как подклассы добавляют к классу новые методы, эти методы добавляются в конец этой таблицы. Затем к таблице обращаются во время выполнения, чтобы определить метод для выполнения. Это и есть динамическая диспетчеризация. В Swift данный подтип делится на два подтипа:
    • Virtual Table — используется при наследовании классов, что приносит дополнительные затраты.
    • Witness Table — используется для реализации протоколов, наследование отсутствует.
  • Message Dispatch (Отправка сообщений) — самый долгий (по времени выполнения) тип диспетчеризации. Обеспечивает работу таких механизмов, как KVC/KVO или Core Data. Главная особенность этого типа —  у разработчиков появляется возможность изменять поведение отправки во время выполнения с помощью механизма swizzling.

9. Расскажите о методе hitTest

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

hitTest — это рекурсивный поиск среди иерархии views, к которой прикоснулся пользователь. iOS пытается определить какая UIView является самой низко расположенной вьюшкой под пальцем пользователя. Она и будет получать события касания.

Дополнительное чтение: Держим удар с hitTest, Обработка жестов в iOS

10. Что такое назначенный инициализатор?

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

init(параметры) {
    // ...
}

11. Какова временная сложность добавления элемента в начало массива?

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

Похожих вопросов на собеседовании может быть много, например:

  • Оцените сложность поиска в хэш-таблице?
  • Какова сложность сортировки пузырьком?

Ответим на текущий вопрос — сложность O(n): при операциях удаления или вставки в начало массива потребуется сдвинуть каждый элемент.

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

12. Что делает NSPersistentStoreCoordinator?

Вам необходимо знать основной стек Core Data: NSManagedObjectModel (managed object model), NSPersistentStoreCoordinator (persistent store coordinator) и NSManagedObjectContext (managed object contexts).

NSManagedObjectModel — объектная модель данных. Содержит информацию обо всех моделях: какие атрибуты содержат эти модели и как они связаны друг с другом.

NSPersistentStoreCoordinator — координатор постоянного хранилища. Общается с постоянными хранилищами и гарантирует сохранение, загрузку и кэширование данных.

NSManagedObjectContext — управляет коллекцией объектов модели. Приложение может иметь несколько контекстов управляемого объекта. Каждый контекст опирается на NSPersistentStoreCoordinator (координатор постоянного хранилища).

13. Что такое миграция базы данных?

Миграция базы данных — это изменение структуры базы данных от одной версии к другой (более новой).

Миграции разделяют на lightweight и heavyweight (простые и сложные). Простые — автоматизированные миграции, сложные — ручные.

Дополнительное чтение: Core Data: Часть 2. Lightweight Миграции

14. Будет ли ошибка при выполнении кода? Если да, то какая?

import Foundation

struct AA {
    var structB: BB?
}

struct BB {
    var structA: AA?
}

Этот код не соберется. Компилятор укажет на ошибку:

// Value type 'AA' cannot have a stored property that recursively contains it
// Value type 'BB' cannot have a stored property that recursively contains it

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

15. Расскажите о механизме Copy-on-write

На собеседованиях любят тему Copy-on-write, иногда меняется только формулировка вопроса: в одних случаях сотрудники компании спросят определение, в других — попросят привести пример.

Copy-on-write — механизм оптимизации в Swift, когда при присвоении переменной значений или при передаче коллекции в функцию не происходит копирование этой коллекции. Подробнее о механизме Copy-on-wirite написаны в статье: Механизм Copy-on-Write.

16. Что выведется в консоль после выполнения кода?

import Foundation

func viewDidLoad() {
    print("2")

    DispatchQueue.main.async {
        print("3")

        DispatchQueue.main.sync {
            print("4")    
        }

        print("5")
    }

    print("6")
}

print("1")
viewDidLoad()
print("7")

Похожий пример мы рассматривали в первом вопросе, но обратите внимание на строчки 6 и 9.

При запуске данного кода порядок вывода начинается так, как и в первом примере: 1, 2, 6, 7, 3, но дальше произойдет Deadlock. Это происходит потому, что несколько потоков блокируют друг друга. Разбираемся.

На 6 строчке мы запускаем первый блок кода в главном потоке, то есть, выполняем код синхронно. Далее спускаемся до 7 строчки и выводим print(«3»). На 9 строчке мы запускаем второй блок кода, тоже в главной очереди (и тоже синхронно), но мы не можем это сделать до тех пор, пока не завершится первый. Первый не может быть завершен до тех пор, пока не будет запущен второй, так как он вызывается тоже синхронно. Отсюда и возникает блокировка потоков или Deadlock.

Подробнее про проблемы многопоточности читать здесь: Проблемы многопоточности в Swift.

17. Что выведется в консоль после выполнения кода?

import Foundation

func getIndex() -> Int {
    var index = 0
    defer {
        index = 1
    }
    
    index = 2
    return index
}

let index = getIndex()
print(index) // 2

Оператор defer рассмотрен в 7 вопросе. В указанном примере приведено больше переменных, чтобы запутать разработчика. Задача на собеседовании — четко понимать, в какой момент сработает оператор defer.

18. Как работает UIGestureRecognizer?

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

Далее, с помощью функции hitTest находится самая глубокая в иерархии UIView, координаты которой содержат в себе касание. Найденная UIView становится firstResponder и начинает получать уведомления о UITouch:

  • touchesBegan — начало касания;
  • touchesMoved — изменение параметров касания;
  • touchesEnded — конец касания;
  • touchesCancelled — отмена касания.

19. Посмотрите на код ниже. Что произойдет, если открыть ViewController и закрыть его через 3 секунды?

final class ViewController: UIViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(5)) {
            self.log("text")
        }
    }
    
    private func log(_ message: String) {
        print(message)
    }
}

Начнем с того, что в данном примере будет удержание сильной ссылки при вызове self.log(«text»). Для решения этой проблемы необходимо вызывать функцию log с weak ссылкой.

Если открыть экран ViewController, то произойдет захват сильной ссылки, и если через 3 секунды закрыть экран, то через 5 секунд выведется сообщение в консоль.

20. Что такое typealias в Swift?

typealias является псевдонимом для существующего типа данных. Рассмотрим пример:

typealias Dollar = Double

Теперь вы можете использовать новый псевдоним Dollar, который по факту является Double:

let totalCosts: Dollar

Важно заметить, что Dollar не является новым типов, это всего лишь псевдоним.

Дополнительное чтение:

Выразить благодарность или найти уникальный материал вы можете в boosty.

Подписывайтесь на мой Telegram-канал iOS Interview Channel, чтобы не пропустить новый материал.


8 thoughts on “Топ 20 вопросов на iOS собеседовании. Уровень Middle”

  1. В 8 вопросе написано, что у Virtual Table диспетчеризации отсутствует наследование, но классы как раз (в большинстве своем) и обладают такой диспетчеризацией.

  2. В ответе на 1 вопрос:
    — разве можно вызывать метод .sync на main.queue? вроде должен быть deadlock
    — глобальная очередь не ждет выполнения задач на главном потоке. Поэтому цифры будут выводиться 1,2,6 дальше либо 3 либо 7, а потом уже 4 и 5 (если перевести в мэйн асинхронно, то 5,4)

    1. Привет,

      разве можно вызывать метод .sync на main.queue? вроде должен быть deadlock

      Можно, но нужно смотреть на какой очереди вы это делаете. В данном примере deadlock не произойдет, так как мы работаем с разными очередями, ситуация deadlock — блокировка очереди.

      deadlock можно получить, если, например, вместо global() написать main. Или как-то так:

      func printMultithreading() {
      print("2")
      let queue = DispatchQueue(label: "Queue")
      queue.async {
      print("3")
      queue.sync {
      print("4")
      }
      print("5")
      }
      print("6")
      }

      глобальная очередь не ждет выполнения задач на главном потоке. Поэтому цифры будут выводиться 1,2,6 дальше либо 3 либо 7, а потом уже 4 и 5 (если перевести в мэйн асинхронно, то 5,4)

      Да, все верно, можно запустить код несколько раз на выполнение и получить разный результат. 7 действительно будет либо после 6, либо после 3 (зависит от загруженности системы).

      Спасибо за замечание! Добавлю в ответ, будет полезно

  3. ответ на 19 вопрос:
    Нет, если вы закроете ViewController, функция self.log(«text») не будет вызвана. Код, находящийся внутри замыкания, переданного в DispatchQueue.main.asyncAfter, будет отменен, если view controller будет закрыт до истечения задержки. Это происходит потому, что замыкание захватывает ссылку на self, и если self перестает существовать (например, если view controller закрыт), замыкание больше не будет выполняться.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *