tgindex
i

iOS & ISS | Dev Blog

Статистика
@ios_iss_blogрусский

Блог iOS команды компании ISS. Наша работа: - приложение «Московский транспорт» - приложение «ДОБРО.РФ» - приложение «Онлайнинспекция.рф» - приложение «Паркоматика» - приложенеие «‎Dhamer | ضامر» Для связи: @Savva_Shuliatev

Последний пост
1 дек.
Последнее чтение
ещё не заходили
Постов за неделю
0
Всего постов
20
Тип
открытый
Язык
русский
В каталоге с
16 авг.
Подписчики
253
мало замеров
Замеров пока мало
Сутки
0
0,00%
Неделя
 
Месяц
 
Просмотров на пост
475
20 постов
Вовлечённость
187,7%
к подписчикам
Постов в день
0,0
всего 20
Упоминаний
0
каналов
Охват размещения
оценка
1/24сутки в ленте
1/48двое суток
1/72трое суток

Оценка по просмотрам недавних постов: пост набирает почти всё за первые сутки.

Посты

  • без подписи

  • без подписи

  • без подписи

  • #concurrency Игра по правилам компилятора Компилятор — наш лучший друг, который защищает от data races. Но иногда его строгость заставляет искать обходные пути, особенно при работе с "наследством" из мира UIKit и ObjC. Разберем реальный кейс из моей практики: проксирование делегатов, например, UIScrollViewDelegate или UINavigationControllerDelegate. Это частая задача, когда нужно перехватить часть событий, а остальные — перенаправить оригинальному делегату. Целиком проки код можете посмотреть в моем публичном репозитории для DynamicBottomSheet, остановимся на важном в рамках обсуждения: @MainActor internal final class UIScrollViewHolder: NSObject, UIScrollViewDelegate { private weak var originalDelegate: UIScrollViewDelegate? // MARK: - Forwarding Unhandled Messages override func responds(to aSelector: Selector!) -> Bool { return super.responds(to: aSelector) || (originalDelegate?.responds(to: aSelector) ?? false) } override func forwardingTarget(for aSelector: Selector!) -> Any? { if originalDelegate?.responds(to: aSelector) ?? false { return originalDelegate } return super.forwardingTarget(for: aSelector) } } В Swift 5 многие методы NSObject, такие как responds(to:) и forwardingTarget(for:), аннотировались как @MainActor, если реализующий тип так же аннотирован @MainActor. И все просто работало. В Swift 6 эта магия исчезла... 🤔 Первая мысль: "Просто вызову их вместе с MainActor.assumeIsolated или с моим кастомным MainActor.syncSafe из предыдущего поста!" И тут появляются нюансы, о которых вам не рассказывают в блогах, а именно, что MainActor.assumeIsolated<T> требует T where T : Sendable: // MARK: - Forwarding Unhandled Messages override func responds(to aSelector: Selector!) -> Bool { MainActor.syncSafe { // Its ok, Selector is @unchecked Sendable return super.responds(to: aSelector) || (originalDelegate?.responds(to: aSelector) ?? false) } } override func forwardingTarget(for aSelector: Selector!) -> Any? { MainActor.syncSafe { ⛔️ // Type 'Any' does not conform to the 'Sendable' protocol if originalDelegate?.responds(to: aSelector) ?? false { return originalDelegate } return super.forwardingTarget(for: aSelector) } } Мы не можем передавать Any между тасками (хотя тасок как таковых тут нет), и как тут не крути типы, получаем ошибку... 🪄 Включаем чит-код Для этого создадим простую обертку, которая маркирует значение как @unchecked Sendable локально: public struct UncheckedSendableContainer<T>: @unchecked Sendable { public let value: T public init(_ value: T) { self.value = value } } И получаем: // MARK: - Forwarding Unhandled Messages override func responds(to aSelector: Selector!) -> Bool { MainActor.syncSafe { // ✅ Selector is @unchecked Sendable return super.responds(to: aSelector) || (originalDelegate?.responds(to: aSelector) ?? false) } } override func forwardingTarget(for aSelector: Selector!) -> Any? { let result: UncheckedSendableContainer<Any?> = MainActor.syncSafe { /// ✅ UncheckedSendableContainer is @unchecked Sendable if originalDelegate?.responds(to: aSelector) ?? false { return UncheckedSendableContainer(originalDelegate) } let result = super.forwardingTarget(for: aSelector) return UncheckedSendableContainer(result) } return result.value } Что произошло: - Мы обернули не-Sendable Any в наш UncheckedSendableContainer. - Теперь компилятор пропускает наше замыкание в MainActor.syncSafe, так как контейнер помечен как @unchecked Sendable. - Внутри замыкания мы безопасно извлекаем .value, используя изолированные main актором данные. 🧠 Итог: - Swift активно меняется, компилятор становится строже, ломая старые подходы с UIKit/NSObject/ObjC. - Когда Sendable недоступен, @unchecked Sendable — наш безопасный "пропуск" через границы акторов. - Относитесь к @unchecked Sendable не как к костылю, а как к осознанному хаку: мы берем на себя ответственность за Data Race и deadlock-и.

  • #concurrency Взломать актор: Синхронный доступ извне В прошлом посте я писал, что изоляция акторов заставляет нас писать асинхронный код для доступа к защищенным данным. Это цена за безопасность, которую платит разработчик, что напрямую влияет на дизайн кода и фичей. И все же, если мы работаем со старым кодом, legacy API или в контексте, который не может быть async? При этом внутри происходит обращение к подакторным данным, то вы, скорее, получите такую ошибку или предупреждение: nonisolated func getDevice() -> String { // 'UIDevice' изолирован на MainActor let model = UIDevice.current.model // ⚠️/⛔️ Main actor-isolated class property 'current' can not be referenced from a nonisolated context let systemVersion = UIDevice.current.systemVersion // Same return [model, systemVersion].joined(separator: " ") } Вы все еще можете оставаться на версии компилятора Swift 5, либо минимизировать строгие проверки параллелизма: В настройках проекта (Build Settings) для параметра Strict Concurrency Checking устанавливается значение Minimal (или false для флага -warn-concurrency). Многие источники и без меня рассказывают, как обойти это ограничение, используя MainActor.assumeIsolated, забывая сказать, что: If the current context is not running on the actor’s serial executor, or if the actor is a reference to a remote actor, this method will crash with a fatal error (similar to preconditionIsolated()). Так что, если ваш код исполняется не на main потоке, то синхронности не добиться? Что ж, мы можем вспомнить пару хаков из GCD, и получить вполне рабочий метод: extension MainActor { @discardableResult public static func syncSafe<T: Sendable>(_ action: @MainActor () -> T) -> T { Thread.isMainThread ? MainActor.assumeIsolated(action) : DispatchQueue.main.sync(execute: action) } } И теперь нам доступен хак по синхронному доступу изолированных данных из неизолированной среды, и да, это работает: nonisolated func getDevice() -> String { MainActor.syncSafe { // Безопасно, но с блокировкой let model = UIDevice.current.model let systemVersion = UIDevice.current.systemVersion return [model, systemVersion].joined(separator: " ") } } Окей, мы ушли от Data Race, взломали Swift Concurrency (в рамках main актора), и таким образом вы вполне можете фиксить свой код, либо работать с legacy API, которого в iOS с головой. 🛑 Но! Теперь наш код подвержен deadlock-ам. Создать его в связке MainActor.syncSafe и async/await оказалось сложно (можете попробовать, лучшие ответы выложу), поэтому вот пример с GCD. И явно отследить deadlock на этапе написания кода — задача непростая для большинства: @MainActor func causeADeadlock() { let semaphore = DispatchSemaphore(value: 0) DispatchQueue.global().async { ... // Any work /// Наш метод с `syncSafe` и `DispatchQueue.main.sync(execute: action)` /// Из-за ожидания и случится deadlock _ = getDevice() semaphore.signal() } semaphore.wait() print("Ничего не распечается, deadlock") // <-- Эта строка не выполнится } Пример хоть и в вакууме, но показывает хрупкость таких решений, как MainActor.syncSafe, так что используйте только при крайней необходимости! При проектировании дизайна своего кода, всегда хочется сделать его простым, безопасным и оптимизированным. А синхронный код в разы проще асинхронного во всех аспектах. Поэтому частенько я стараюсь играться с типами, функциями, асинхронщиной (примеры в следующем посте), и все же с опытом приходишь к основной мысли: Золотое правило: если можете использовать async/await — используйте. Синхронные "хаки" — это крайняя мера, а не норма.

  • #concurrency Изоляция vs. Синхронизация Современный Swift (> 5.5) опирается на две ключевые концепции: изоляция и синхронизация. Часто их путают, хотя они решают одну и ту же проблему — data race, при этом совершенно по разному. 👮🏻‍♂️ Синхронизация (часто подразумевается, как ручное управление, работает в runtime) Суть: Координировать потоки так, чтобы только один из них мог получить доступ к общему ресурсу в конкретный момент времени. Доступ возможен с любого потока, но он регулируется вручную. Инструменты: 🔸 Локи и мьютексы (NSLock, DispatchSemaphore) 🔸 Серийные очереди (DispatchQueue) 🔸 Atomic operations Пример: final class Counter: @unchecked Sendable { private var value = 0 private let lock = NSLock() func increment() { lock.lock() defer { lock.unlock() } value += 1 } } Ответственность: Полностью на разработчике. Забыли поставить лок или создали deadlock — узнаете только в рантайме 🔞 🛡Изоляция (акторная защита на уровне компилятора) Суть: Спроектировать код так, чтобы общие изменяемые данные были защищены от одновременного доступа по определению в рамках актора. Не путайте изоляцию с Sendable. Если тип Sendable, значит, что его можно безопасно передавать между потоками и акторами. Но это не означает, что его состояние изолировано. Изоляция акторов — это статический механизм. Правила доступа проверяются компилятором еще до запуска программы. Это не просто "синтаксический сахар" поверх локов, а фундаментальное свойство языка, встроенное на на уровне ABI. Пример: @MainActor var value = 0 // Изолировали на мейн акторе actor MyActor { var value = 0 // Изолировали на кастомном акторе } 💸 Цена за безопасность Но за эту автоматическую защиту есть своя цена. Изоляция делает (почти)невозможным синхронный доступ к своим данным из неизолированной среды. Компилятор заставляет нас явно обозначать точки в коде, где происходит переключение между конкурентными доменами. Код становится асинхронным, что требует порой кардинального изменения в написании кода и пересмотра архитектуры. Однако о хаках в следующем посте... 🔗 Эти механизмы разные, это почти противоположные философии для решения Data Race. - Изоляция запрещает совместный доступ к изменяемому состоянию. - Синхронизация упорядочивает совместный доступ к изменяемому состоянию. Я бы хотел сказать, что изоляция актора под капотом использует синхронизацию, но для этого нужно раскрывать Serial Executor, Mailbox актора и прочие его хитрости, ибо ключевой фишкой является не сам механизм синхронизации, а его использование для реализации неблокирующей, асинхронной модели с приостановкой задач. Так что это было бы большим упрощением. Ссылки: - proposals/0306-actors - WWDC21: Protect mutable state with Swift actors | Apple

  • без подписи

  • без подписи

  • без подписи

  • без подписи

  • без подписи

  • без подписи

  • без подписи

  • без подписи

  • без подписи

  • без подписи

  • без подписи

  • без подписи

  • без подписи

  • без подписи