Всем привет! Меня зовут Никита, я работаю в компании Технократия и занимаюсь iOS-разработкой. С сегодняшнего дня мы начинаем регулярный выпуск статей, в которых я буду рассказывать о современном подходе к написанию асинхронного кода в Swift.
Данный мини-курс будет логически разбит на серию небольших статей, в которых мы постепенно будем усложнять темы и смотреть на все более интересные примеры с использованием новой технологии.
Введение
Со времен iOS 2 разработчикам было предложено работать с NSOperation
и NSOperationQueue
для реализации асинхронных задач. Данная библиотека давала возможность выстраивать цепочку задач и исполнять их на абстрактных очередях, инкапсулирующих работу с потоками.
В 2009 году Apple представила новую библиотеку — Grand Central Dispatch
(GCD), которая также работала с очередями, но была легче в использовании, в связи с уходом от ООП концепции по работе с задачами. Их стало легче создавать, запускать, но стало сложнее выстраивать логику и взаимосвязь между ними.
Шли годы, никаких новых технологий по работе с многопоточность Apple не представляла, если не считать Combine, представленный в 2019 году. Однако в прошлом году Apple представила новую концепцию по работе с асинхронным кодом, которая обещает увеличить производительность, упростить написание кода и снизить порог входа новых разработчиков.
Недостатки Swift ниже 5.5
Прежде чем знакомиться с новой технологией давайте разберёмся с недостатками текущих решений. Начнем по порядку:
1. Pyramid of doom ?
Разберем небольшой пример, который иллюстрирует данную проблему.
struct User { } func saveUserToCoreData(user: User, completion: @escaping (User) -> Void) { } func loadUserFromNetworkFromNetwork(for id: String, completion: @escaping (User) -> Void) { }
У нас есть структура User
, которая хранит данные о пользователе, а также реализованы 2 метода:
-
saveUserToCoreData
— сохраняет пользователя в CoreData. -
loadUserFromNetwork
— загружает данные о пользователе из сети
Последние две метода принимают замыкание, которое вызовется по завершению их работы.
Нам необходимо реализовать метод, который сначала скачает данные, потом сохранит их в CoreData, а после передаст сохраненного пользователя в замыкание.
Решение будет выглядеть так:
func fetchUserData(for id: String, completion: @escaping ((User) -> Void)) { loadUserDataFromNetwork(for: id) { user in saveToCoreData(user: user) { savedUser in completion(savedUser) } } } let userID = "testID" fetchUserData(for: userID) { print("User is \($0))") }
Его недостаток виден сразу — это вложенность замыканий. Такой код становится трудным для чтения, и в нем сложно разобраться новичку, что повышает порог входа на проект новых разработчиков. Стоит учитывать, что это очень легкий пример, на реальный проектах это превращается в большую кучу вложенного кода, иногда превышающая 100 строк.
2. Error handling
В примере выше игнорировалась работа с ошибками. Что это означает? Если у пользователя не будет подключения к сети или произойдет ошибка с записью в локальное хранилище, то он никак не будет уведомлен, данные не покажутся и мы потеряем дорогого для нас пользователя.
Давайте попробуем решить данный вопрос и добавим в наш код возможность обрабатывать ошибки. Для этого в замыкания будем передавать тип Result
— который хранит либо данные, либо ошибку. Обновленные методы выглядят так:
func saveUserToCoreDataWithError(data: User, completion: @escaping (Result<User, Error>) -> Void) { } func loadUserFromNetworkWithError(for id: String, completion: @escaping (Result<User, Error>) -> Void) { }
Теперь наши методы работают с Result
, необходимо актуализировать метод
func fetchUserDataWithError(for id: String, completion: @escaping ((Result<User, Error>) -> Void)) { loadUserFromNetworkWithError(for: id) { result in do { saveUserToCoreDataWithError(data: try result.get()) { savedResult in guard let savedUser = try? savedResult.get() else { return } completion(.success(savedUser)) } } catch { // completion(.failure(_)) - Обработка ошибки } } }
В таком случае вложенность еще увеличилась и код стал трудным для восприятия. Также к недостатку можно отнести сам принцип обработки ошибки — он работает благодаря тому, что мы внедрили дополнительный тип данных Result
, хотя в Swift уже есть try-catch
блоки для обработки ошибок в синхронных функциях.
3. Many mistakes are easy to make ?
Следующий пункт гласит, что при написании кода с замыканиями легко допустить ошибку. Надеюсь, что вы уже заметили ее в примере выше, вот же она, на 5-ой строчке:
guard let savedUser = try? savedResult.get() else { return }
В случае, если данные пользователя не получилось сохранить, то мы не выполним completion блок и логика работы нашей программы будет нарушена.
Поэтому здесь перед return
необходимо вернуть .failure
в completion блоке.
Суть проблемы заключается в том, что ни компилятор на момент сборки программы, ни Runtime не могут подсказать нам, где была допущена ошибка. Собственноручно найти ее трудно даже разработчику, который писал код, не говоря о другом разработчике, который будет фиксить баг.
4. Недостатки библиотек от Apple ?
Однако помимо проблем у языка Swift, они также присутствуют в представленных Apple библиотеках, а именно:
GCD
-
Задачу, поставленную в очередь, трудно отменить, только если не костылять с
DispatchWorkItem
. -
При множественном вызове .
sync
метода у concurrent очереди может произойти создание большого количества потоков, что приведет к крэшу приложения. Такая проблема называется Thread Explosion. -
Сложно выстраивать цепочки из задач, делить ресурсы.
NSOperation
Их необходимо наследовать от NSOperation
, переопределять методы и выносить всю логику в отдельную сущность, что ведет к написанию большого количества кода.
-
Возможна циклическая зависимость задач, что приведет к deadlock.
-
Трудно написать асинхронную задачу.
5. Работа с потоками
Так или иначе наш код сейчас взаимодействует с потоками. Мы можем работать с ними вручную, либо использовать библиотеки, которые просто инкапсулируют работу с ними. Однако у самой модели потоков есть ряд недостатков:
-
У каждого
Thread
есть свой стек, что дает минус по памяти. -
Переключение между ними занимает время.
-
Возможна проблема “Thread explosion” — когда система не справляется с большим количеством потоков и не может грамотно и эффективно управлять ими -> мы получаем крэш.
Выводы
Таким образом, мы вспомнили основные недостатки написания асинхронного кода в Swift ниже 5.5, а также основные недостатки текущих библиотек от Apple.
В следующих частях мы ознакомимся с новой для языка Swift концепцией асинхронного программирования, ее синтаксисом и возможностями.
Также подписывайтесь на наш телеграм-канал «Голос Технократии». Каждое утро мы публикуем новостной дайджест из мира ИТ, а по вечерам делимся интересными и полезными мастридами.
ссылка на оригинал статьи https://habr.com/ru/post/668018/
Добавить комментарий