Топ 10 вопросов на iOS собеседовании. Уровень Senior. Часть 1

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

  • Самостоятельность;
  • Обучение junior и middle разработчиков (наставничество);
  • Проработка архитектуры;
  • Наличие Soft skills (управленческие навыки, делегирование, коммуникабельность, инициативность);
  • Помощь в найме сотрудников (техническое собеседование).

Также, нужно ориентироваться на то, что в разных компаниях уровень разработчика (junior, middle или senior) свой. Собеседования в одни компании вам могут показаться простыми, в другие — сложные. Перейдем к теоретическим вопросам, которые чаще всего спрашивают на собеседованиях iOS разработчика уровня Senior.

1. Сколько потоков может выполняться одновременно, если не ограничивать их количество программно?

Чтобы разобраться в этом вопрос, давайте начнем с определений.

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

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

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

Что же такое многопоточность?

Многопоточность — одновременное (параллельное) выполнение потоков, которые выполняют в себе разные задачи.

Принцип многопоточности

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

2. Перечислите состояния операции Operation

Давайте начнем с определения. Хорошее определение операции дано на NSHipster:

Operation представляет собой законченную задачу и является абстрактным классом, который предоставляет вам потокобезопасную структуру для моделирования состояния операции, ее приоритета, зависимостей (dependencies) от других Operations и управления этой операцией.

NSHipster

У каждой операции Operation существует машина состояний (state mashine), которая представляет собой «жизненный цикл» операции

Жизненный цикл операции - собеседование ios-interview

Возможные состояния операции:

  • pending — операция ожидает выполнения;
  • ready — операция готова к выполнению;
  • executing — операция выполняется;
  • finished — операция закончена;
  • cancelled — операция уничтожена.

3. Чему будет равен count после выполнения программы?

import Foundation

var count = 0

let thread1 = Thread {
    for _ in 0...199 {
        count += 1
    }
}

let thread2 = Thread {
    for _ in 0...199 {
        count += 1
    }
}

thread1.start()
thread2.start()

В данном случае сложно сказать какое значение примет count после печати значения, происходит гонка за данными или Data Race — одна из проблем многопоточности.

Мы ожидаем результат count = 400. Но, к сожалению, результат будет всегда меньше этого числа. Это происходит потому, что операция увеличения счетчика не атомарна. Этот пример уже разбирал в статье Проблемы многопоточности в Swift.

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

struct TestStruct {
    let id: String
    let age: Int
    let hasVehicle: Bool
}

let size = MemoryLayout<TestStruct>.size
print(size)

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

struct TestStruct {
    let hasVehicle: Bool
    let id: String
    let age: Int
}

let size = MemoryLayout<TestStruct>.size
print(size)

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

5. Зачем нужен @inline атрибут?

Это способ оптимизации компилятора, используется в Swift для принудительного объявления функции.

Рассмотрим пример:

func func1() -> Int{
    return Int(arc4random_uniform(UInt32.max))
}

func func2(){
    print(func1())
}

Если посмотреть на стек вызова функций, то можно будет увидеть, что func2() включает в себя код из функции func1(), несмотря на то, что этот код находится в отдельной функции. Дело в том, что Swift заметит, что func1() — функция недостаточно длинная, чтобы быть отдельной сущностью сама по себе. Тем более она вызывается только один раз в коде, поэтому Swift встроил в функцию func2(), чтобы сохранить вызов каждый раз, когда ему нужно ее использовать.

Давайте изменим код следующим образом

@inline(never) func func1() -> Int{
    return Int(arc4random_uniform(UInt32.max))
}

func func2(){
    print(func1())
}

если посмотреть стек вызова сейчас, то можно увидеть, что func2() не встраивает код из функции func1().

Когда стоит использовать @inline:

  1. Используйте @inline(never), если функция довольно длинная и вам нужно избежать увеличения размера сегмента кода;
  2. Используйте @inline(__always), если функция маленькая и вы хотите, чтобы ваше приложение работало быстрее;
  3. Не используйте это ключевое слово, если вы не знаете для чего оно используется.

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

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

protocol P {
    func callMe()
}

extension P {
    func callMe() {
        print("from protocol")
    }
}

class A: P { }

class B: A {
    func callMe() {
        print("from class")
    }
}

func printCallMe(_ instance: P) {
    instance.callMe()
}

let instance = B()
printCallMe(instance)

На самом деле с этим вопросом не все так просто, считаю, что это сложный и не самый очевидный вопрос. Чтобы ответить на него, вам необходимо разбираться в диспетчеризации методов. Данную тему уже обсуждали здесь: Топ 20 вопросов на iOS собеседовании. Уровень Middle (8 вопрос).

Итак, ответ на данный вопрос будет:

from protocol

Давайте разбираться. Рассмотрим сначала простой пример:

class A {
    func callMe() {
        print("superclass")
    }
}

class B: A {
    override func callMe() {
        print("subclass")
    }
}

A().callMe()          // prints 'superclass'
B().callMe()          // prints 'subclass'
(B() as A).callMe()   // prints 'subclass'
  • При создании экземпляра A мы точно знаем, что будет вызван метод экземпляра A. Соответственно и вывод «superclass».
  • Теперь создадим экземпляр B — подкласс A и переопределим метод класса А. Мы ожидаем, что при вызове этого метода в консоль выведется то, что мы и переопределили. Так оно и есть!
  • То же самое произойдет и в тот момент, когда мы приведем экземпляр B к A, потому что мы все еще работаем с экземпляром В. (* обратите внимание на этот пункт, вернемся к нему в конце вопроса)

Теперь давайте добавим протокол P и дадим ему реализацию по умолчанию через расширение. Далее сделаем класс А наследником этого протокола

protocol P {
    func callMe()
}

extension P {
    func callMe() {
        print("protocol")
    }
}

class A: P {
    func callMe() {
        print("superclass")
    }
}

class B: A {
    override func callMe() {
        print("subclass")
    }
}

A().callMe()           // prints 'superclass'
B().callMe()           // prints 'subclass'
(B() as A).callMe()    // prints 'subclass'

Как мы видим, результат не изменился. В консоль, как и раньше, выводятся ожидаемые результаты.

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

Мы также должны удалить ключевое слово override для функции класса B, так как мы больше не переопределяем никакую реализацию в A. Как только мы это сделаем, мы получим следующее:

protocol P {
    func callMe()
}

extension P {
    func callMe() {
        print("protocol")
    }
}

class A: P {
}

class B: A {
    func callMe() {
        print("subclass")
    }
}

A().callMe()           // prints 'protocol'
B().callMe()           // prints 'subclass'
(B() as A).callMe()    // prints 'protocol'
  • Вызов функции для экземпляра класса A делает ровно то, что мы и ожидали. В экземпляре класса A больше нет реализации функции, поэтому он выводит значение по умолчанию из протокола P.
  • С экземпляром класса B тоже все хорошо — получаем то, что и ожидали. Он просто использует свою реализацию.
  • Странные вещи происходят с последним примером, когда мы пытаемся привести экземпляр B к A. Рассматривая пример выше* мы ожидаем, что в консоль выведется значение из экземпляра B, но по какой-то причине мы игнорируем реализацию в экземпляре B и вместо этого вызываем реализацию протокола.

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

На самом деле это известная проблема в Swift, с которой вы можете ознакомиться по ссылке: [SR-103]. Данный вопрос очень хорошо показывает как размышляет разработчик на собеседовании.

7. Что такое CALayer и в чем отличие от UIView?

UIView может реагировать на события, а CALayer — нет.

UIView — прямоугольная область на экране, которая определяет пространство с системой координат. Наследуется от UIResponder, который определяет интерфейс для обработки различных событий и доставки событий.

CALayer напрямую наследует NSObject и не имеет соответствующего интерфейса для обработки событий.

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

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

8. Что такое RunLoop?

RunLoop — это программный интерфейс для объектов, управляющих источниками ввода (Цикл обработки событий)

RunLoop

Что делает RunLoop?

  1. Ждет пока что-то произойдет и обрабатывает входные данные для источников, таких как события мыши и клавиатуры.
  2. Отправляет сообщение получателю.

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

9. Что такое коллизия хеш-функции?

Хеш-функция (хеширование) — преобразование входного массива данных произвольной длины в выходную битовую строку фиксированной длины.

Хеш-функция

Результат, производимый хеш-функцией, называется «хеш-суммой» или же просто «хешем».

Коллизией хеш-функции называют ситуацию, когда для двух разных входных данных функция возвращает одинаковые выходные данные.

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

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

10. Оцените сложность этого алгоритма поиска?

func searchIndex(from list: [Int], value: Int) -> Int? {
    if !list.isEmpty {
        var indexMin = list.startIndex
        var indexMax = list.endIndex - 1

        while indexMin <= indexMax{
            let indexMid = indexMin + (indexMax - indexMin) / 2

            if list[indexMid] == value{
                return indexMid
            }

            if value < list[indexMid]{
                indexMax = indexMid - 1
            }
            else{
                indexMin = indexMid + 1
            }
        }
    }
    return nil
}

let array = [2, 5, 8]
let index = searchIndex(from: array, value: 8)

Чтобы ответить на этот вопрос, нужно хотя бы на базовом уровне разбираться в сложности алгоритмов. На собеседованиях уровня Senior это скорее необходимый навык. Ранее я уже писал как можно подготовиться к такому вопросу: Как подготовиться к алгоритмической секции

В данном примере реализован бинарный поиск, а сложность бинарного поиска — логарифмическая, то есть O(log n).

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

Что еще почитать?

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

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


3 thoughts on “Топ 10 вопросов на iOS собеседовании. Уровень Senior. Часть 1”

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

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