Irbisa · Шпаргалка

Objective-C runtime и интероп

Отправка сообщений, swizzling, бриджинг, KVO изнутри, когда ObjC всё ещё нужен

Senior-разработчик · 18 вопросов

  1. 01

    Что такое Objective-C runtime и что значит, что Objective-C отправляет сообщения, а не вызывает методы?

    Objective-C runtime (libobjc) представляет собой библиотеку, которая хранит классы, методы и селекторы как данные во время работы приложения; [obj doThing] компилируется в objc_msgSend(obj, @selector(doThing)), и реализация ищется только в момент вызова.

  2. 02

    Что произойдёт, если в Objective-C отправить сообщение nil, и как Swift ведёт себя в той же ситуации?

    Ничего не произойдёт и ничего не упадёт: objc_msgSend первым делом проверяет nil и возвращает ноль, то есть nil, 0, NO или 0.0.

  3. 03

    Чем отличаются @objc, @objcMembers и dynamic, и почему одного @objc не хватает для KVO или swizzling?

    @objc публикует член в Objective-C runtime (селектор и запись в списке методов), @objcMembers делает это для всего класса, а dynamic заставляет и сам Swift вызывать член через objc_msgSend.

    final class Player: NSObject {  @objc var a = 0          // Objective-C sees it; Swift callers bypass the runtime  @objc dynamic var b = 0  // every call is a message send: KVO and swizzling work  var c = 0                // Swift only}…
  4. 04

    Что такое KVC и KVO, как наблюдать за свойством из Swift через observe(_:options:changeHandler:) и что заменяет KVO в новом Swift-коде?

    KVC (key-value coding) читает и записывает свойства по строковому ключу во время выполнения; KVO (key-value observing) уведомляет наблюдателей, когда такое свойство меняется.

    final class Order: NSObject {  @objc dynamic var status = "new"     // @objc for KVC, dynamic for KVO} // KVC: read and write by string key at run timelet order = Order()…
  5. 05

    Чем категория в Objective-C отличается от class extension, и что будет, если две категории добавят метод с одинаковым именем?

    Категория представляет собой именованное дополнение (@interface UIView (Badge)), которое добавляет методы любому классу при загрузке бинарника, даже без доступа к его исходникам; class extension (@interface Foo ()) является анонимной частью самого класса, компилируется с его @implementation и может добавлять ivar.

  6. 06

    Чем +load отличается от +initialize, и почему Swift-класс не может переопределить ни один из них?

    +load выполняется сразу, один раз для каждого класса и каждой категории, где он реализован, при загрузке бинарника и до main(); +initialize выполняется лениво, прямо перед первым сообщением классу, и наследуется.

    @implementation Analytics// Called by the runtime for every class and category that implements it,// when the image is loaded, before main(). Not inherited. Costs launch time.+ (void)load {  // keep it tiny: no other classes may be ready yet}…
  7. 07

    Что означают атрибуты свойств Objective-C atomic, nonatomic, strong, weak, copy и assign, и делает ли atomic свойство потокобезопасным?

    Они говорят компилятору, как синтезировать аксессоры: strong удерживает объект, weak не удерживает и становится nil, copy хранит копию, assign хранит значение как есть, а atomic (по умолчанию) делает атомарным каждый отдельный get или set.

    @protocol ProfileDelegate; @interface Profile : NSObject@property (nonatomic, copy) NSString *name;               // stores a copy: a mutable string passed in cannot change later@property (nonatomic, strong) NSArray<NSString *> *tags;  // +1, the default for objects@property (nonatomic, weak) id<ProfileDelegate> delegate; // no +1, becomes nil when the delegate is freed…
  8. 08

    Как добавить хранимое свойство классу, который вам не принадлежит, и чего стоят associated objects?

    Extension и категории не умеют добавлять хранение, поэтому используют associated objects: objc_setAssociatedObject сохраняет значение для пары «объект + уникальный ключ» в отдельной таблице, а рантайм освобождает его при деаллокации объекта.

    // The key only needs a unique, stable address; its value is never read.private nonisolated(unsafe) var badgeTextKey: UInt8 = 0 extension UIView {  var badgeText: String? {    get { objc_getAssociatedObject(self, &badgeTextKey) as? String }…
  9. 09

    Зачем в блоке Objective-C писать связку weakSelf/strongSelf, и что меняет __block под ARC?

    Блок удерживает каждый захваченный объект, поэтому блок, который хранится в self и упоминает self, создаёт retain cycle; захват __weak weakSelf разрывает цикл, а превращение его в strongSelf внутри блока держит объект живым до конца этого выполнения блока.

    - (void)reload {  __weak typeof(self) weakSelf = self;          // the block must not own self  [self.client loadWithCompletion:^(NSArray<NSString *> *items) {    __strong typeof(weakSelf) strongSelf = weakSelf;  // pin it for the whole block    if (!strongSelf) { return; }                      // screen already closed    strongSelf.items = items;…
  10. 10

    Как сделать заголовок Objective-C удобным для Swift: nullability, NS_SWIFT_NAME, NS_REFINED_FOR_SWIFT и другие макросы?

    Аннотируйте заголовок, чтобы импортёр выдал нативные Swift-типы: nullability превращает T! в T или T?, lightweight generics превращают NSArray * в [String], а NS_SWIFT_NAME и NS_REFINED_FOR_SWIFT исправляют имена и формы, которые плохо переводятся.

    // PaymentService.hNS_ASSUME_NONNULL_BEGIN typedef NS_CLOSED_ENUM(NSInteger, PMTStatus) {   // a frozen Swift enum  PMTStatusPending,  PMTStatusPaid,…
  11. 11

    Что происходит, когда объект получает сообщение, для которого у него нет метода, и как можно вмешаться в этот процесс?

    Прежде чем упасть с «unrecognized selector sent to instance», рантайм даёт объекту три шанса: добавить метод (resolveInstanceMethod:), назвать другой объект, который его обработает (forwardingTargetForSelector:), или принять весь вызов как NSInvocation (forwardInvocation:).

    // Intercepts one delegate callback and forwards everything else untouchedfinal class ScrollDelegateProxy: NSObject, UIScrollViewDelegate {  // read by the nonisolated NSObject overrides below; UIKit uses it on the main thread  nonisolated(unsafe) weak var original: UIScrollViewDelegate?  var onScroll: ((CGPoint) -> Void)?…
  12. 12

    Что такое method swizzling, как делать его безопасно и чем он опасен в продакшене?

    Swizzling меняет местами реализации двух селекторов во время работы приложения, и каждый вызов, например, viewWillAppear: сначала выполняет ваш код.

    /// Swizzles `original` on `cls` without touching its superclass. Call it once.func swizzle(_ cls: AnyClass, _ original: Selector, with replacement: Selector) {  guard    let originalMethod = class_getInstanceMethod(cls, original),    let replacementMethod = class_getInstanceMethod(cls, replacement)  else { return }…
  13. 13

    Что такое isa, и как objc_msgSend с его помощью, через кеш методов и цепочку суперклассов находит реализацию?

    Каждый объект начинается с поля isa, которое указывает на его класс; objc_msgSend читает его, ищет селектор в кеше методов этого класса, а при промахе идёт по спискам методов вверх по цепочке суперклассов, кладёт найденный IMP в кеш и прыгает на него.

  14. 14

    Что выведет этот код, и что KVO делает с объектом, из-за чего вывод именно такой?

    class Upload: NSObject {  @objc dynamic var progress = 0.0  @objc var status = "idle"} let upload = Upload()let a = upload.observe(\.progress) { u, _ in print("progress", u.progress) }let b = upload.observe(\.status) { u, _ in print("status", u.status) } upload.progress = 0.5upload.status = "running"upload.setValue("done", forKey: "status")

    Выведет progress 0.5, а затем status done.

  15. 15

    Найдите баг: аналитика экранов работает, пока пользователь iPad не откроет второе окно, а потом пропадает для всех экранов. Что не так с этим swizzling?

    extension UIViewController {  static func enableScreenTracking() {    let original = class_getInstanceMethod(UIViewController.self, #selector(viewDidAppear(_:)))!    let tracked = class_getInstanceMethod(UIViewController.self, #selector(tracked_viewDidAppear(_:)))!    method_exchangeImplementations(original, tracked)  }  @objc dynamic func tracked_viewDidAppear(_ animated: Bool) {    tracked_viewDidAppear(animated)  // runs the original viewDidAppear    Analytics.screen(String(describing: type(of: self)))  }}final class SceneDelegate: UIResponder, UIWindowSceneDelegate {  func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {    UIViewController.enableScreenTracking()  }}

    Swizzling выполняется при каждом подключении сцены, а не один раз за процесс: второе окно меняет реализации обратно, viewDidAppear снова оригинальный, и трекинг молча перестаёт работать, пока третье окно не включит его снова.

  16. 16

    Что такое toll-free bridging, и как bridge-приведения ARC и Unmanaged определяют, кто освобождает объект Core Foundation?

    Toll-free пары вроде CFStringRef и NSString имеют одинаковое устройство в памяти, поэтому приведение использует тот же самый объект.

    // Toll-free: NSString and CFString are one object, the cast is freelet ns: NSString = "Café"let length = CFStringGetLength(ns as CFString)   // 4 // Annotated CF API: Swift's ARC releases the result for youlet uuid = CFUUIDCreateString(nil, CFUUIDCreate(nil))! as String…
  17. 17

    Чего стоит бриджинг между String и NSString, и почему NSRange, построенный из text.count, пропускает совпадения?

    Swift-String хранит UTF-8 и считает Character (графемные кластеры), а NSString работает в кодовых единицах UTF-16, поэтому бридж или перевод индексов может означать перекодирование и проход O(n), а text.count возвращает не ту длину, которую ждёт NSRange.

    let text = "Cafe\u{301} \u{1F44D}\u{1F3FD} rocks"print(text.count, text.utf16.count)             // 12 16 let regex = try! NSRegularExpression(pattern: "rocks") // wrong: Character count is not a UTF-16 length, the range stops short…
  18. 18

    Когда в 2026 году в iOS-проекте ещё нужен Objective-C, и как бы вы перевели большой Objective-C-модуль на Swift?

    Новый код пишется на Swift, но Objective-C всё ещё оправдан для перехвата NSException, работы с рантаймом вроде forwardInvocation: и +load, обвязки на Objective-C++ для C++-кода, который C++-интероп Swift не умеет импортировать, и больших легаси-модулей и SDK, которые уже работают.