iOS Runtime: що це, середовище виконання застосунків на iPhone

Автор: IT Sectr Опубліковано: 2026-05-17 Час читання: 12 хв

iOS Runtime — середовище виконання застосунків на операційній системі Apple iOS, що включає Objective-C Runtime, Swift Runtime, фреймворки Cocoa Touch та механізми керування пам'яттю через Automatic Reference Counting (ARC). iOS Runtime відповідає за динамічне зв'язування методів (message passing), завантаження класів, керування пам'яттю та взаємодію з апаратним забезпеченням через фреймворки iOS. За даними Документації розробника Apple, розуміння runtime необхідне для оптимізації продуктивності, налагодження та розробки стабільних iOS-застосунків.

Головне

  • iOS Runtime — середовище виконання застосунків, що включає Objective-C Runtime, Swift Runtime та Cocoa Touch
  • Objective-C Runtime — динамічне зв'язування методів через message passing (objc_msgSend)
  • Swift Runtime — статична диспетчеризація з оптимізаціями через value types та generics
  • ARC (Automatic Reference Counting) — автоматичне керування пам'яттю на етапі компіляції
  • dyld — динамічний завантажувач, що завантажує фреймворки та бібліотеки під час запуску застосунку

Що таке iOS Runtime?

iOS Runtime — це сукупність системних компонентів, що забезпечують виконання застосунків на пристроях Apple під керуванням iOS. Включає Objective-C Runtime (бібліотека libobjc.A.dylib), Swift Runtime (libswiftCore.dylib), Core Foundation, фреймворки Cocoa Touch (UIKit, Foundation), динамічний завантажувач dyld та середовище виконання для керування пам'яттю, потоками та міжпроцесною взаємодією.

Архітектурно iOS Runtime працює на трьох рівнях. На нижньому рівні — бінарний формат Mach-O та dyld, що завантажує виконуваний файл та бібліотеки. Середній рівень — Objective-C Runtime та Swift Runtime, що відповідають за диспетчеризацію методів та керування об'єктами. Верхній рівень — фреймворки Cocoa Touch (UIKit, Foundation, Core Data, Metal), що надають API для розробника.

Розуміння iOS Runtime дозволяє розробнику вирішувати складні завдання: свізлінг методів (Method Swizzling) для A/B тестування та аналітики, динамічне завантаження класів, оптимізація пам'яті через розуміння ARC, налагодження retain cycles та витоків пам'яті, оптимізація часу запуску застосунку через dyld. Без знання runtime неможливе профілювання та оптимізація на рівні системи.

Компоненти iOS Runtime

КомпонентБібліотекаПризначення
Objective-C Runtimelibobjc.A.dylibMessage passing, динамічні класи, свізлінг
Swift RuntimelibswiftCore.dylibValue types, generics, protocol witnesses
Core FoundationCoreFoundation.frameworkCFType, toll-free bridging
dylddyld (usr/lib/dyld)Завантаження Mach-O, зв'язування бібліотек
libSystemlibSystem.B.dylibPOSIX threads, libc, libdispatch (GCD)

Формат Mach-O

Застосунки для iOS компілюються у формат Mach-O (Mach Object). Файл Mach-O містить заголовок (header), команди завантаження (load commands) та сегменти (segments): __TEXT (код, константи), __DATA (глобальні змінні, метадані Objective-C), __LINKEDIT (символи, таблиці релокацій). dyld аналізує Mach-O та завантажує залежності перед виконанням першої інструкції.

Objective-C Runtime: Message Passing та динамічна диспетчеризація

Objective-C Runtime — найпотужніша частина iOS Runtime. На відміну від C++ з раннім зв'язуванням (early binding), Objective-C використовує пізнє зв'язування (late binding) через message passing. Виклик методу [receiver message] компілюється не в прямий виклик функції, а в objc_msgSend(receiver, @selector(message)), який динамічно знаходить реалізацію методу в класі об'єкта.

Кожен об'єкт Objective-C зберігає покажчик isa на свій клас. Клас містить список методів (method list), кеш методів (method cache) та покажчик на суперклас. objc_msgSend обходить ланцюжок наслідування: перевіряє кеш класу, потім список методів, потім переходить до суперкласу. Якщо метод не знайдено, запускається forwarded: resolveInstanceMethod, forwardingTargetForSelector та forwardInvocation.

Method Swizzling — техніка заміни реалізації методу на льоту через обмін IMP (implementation pointer) у runtime. Використовується для A/B тестування, аналітики (автоматичний трекінг екранів) та моніторингу. Не рекомендується для продакшену без крайньої необхідності, оскільки може конфліктувати з оновленнями ОС.

Приклад: Method Swizzling в Objective-C

objective-c
// Method Swizzling для трекінгу viewDidLoad
#import 

@implementation UIViewController (Tracking)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];

        SEL originalSelector = @selector(viewDidLoad);
        SEL swizzledSelector = @selector(swizzled_viewDidLoad);

        Method originalMethod = class_getInstanceMethod(
            class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(
            class, swizzledSelector);

        BOOL didAddMethod = class_addMethod(
            class,
            originalSelector,
            method_getImplementation(swizzledMethod),
            method_getTypeEncoding(swizzledMethod)
        );

        if (didAddMethod) {
            class_replaceMethod(
                class,
                swizzledSelector,
                method_getImplementation(originalMethod),
                method_getTypeEncoding(originalMethod)
            );
        } else {
            method_exchangeImplementations(
                originalMethod, swizzledMethod);
        }
    });
}

- (void)swizzled_viewDidLoad {
    // Трекінг події
    NSLog(@"View Did Load: %@", self.class);
    // Виклик оригінальної реалізації
    [self swizzled_viewDidLoad];
}

@end

Категорія UIViewController (Tracking) замінює viewDidLoad на swizzled_viewDidLoad у всіх UIViewController у застосунку. dispatch_once гарантує одноразове свізлінг. class_addMethod запобігає подвійному свізлінгу та конфліктам із суперкласами. Використовується для автоматичного трекінгу показу екранів в аналітиці без зміни вихідного коду контролерів.

isa pointer та tagged pointers

У сучасних iOS (arm64) компанія Apple оптимізувала isa pointer: це не просто адреса класу, а бітове поле (non-pointer isa), що містить прапорці керування пам'яттю та інформацію про клас. Tagged pointers — ще одна оптимізація: значення NSNumber, NSDate та NSString невеликого розміру зберігаються не як об'єкти в купі, а прямо в покажчику, усуваючи накладні витрати на malloc та retain/release. tagged pointer розпізнається за молодшим бітом isa.

Swift Runtime: статична диспетчеризація та оптимізація

Swift Runtime відрізняється від Objective-C Runtime принципово: Swift за замовчуванням використовує статичну диспетчеризацію (static dispatch) через vtable для методів класу та direct call для value types та extension methods. Динамічна диспетчеризація (dynamic dispatch) використовується лише для методів, позначених @objc або dynamic. Це дає приріст продуктивності до 40% порівняно з Objective-C.

Value types (struct, enum) в Swift — ключова відмінність від Objective-C. Вони зберігаються на стеку (stack) або всередині іншого об'єкта, не використовують retain/release та не беруть участі в ARC для лічильника посилань. Struct не має isa pointer і не може бути відправлений через objc_msgSend. Protocol witnesses — аналог vtable для протоколів, що дозволяє динамічну диспетчеризацію для existential container.

Swift Runtime також включає generics з реіфікацією (reified generics через mangled symbols) та COW (Copy-on-Write) для оптимізації string, array, dictionary, set. При копіюванні колекції реальне копіювання відбувається лише при зміні однієї з копій. Це мінімізує накладні витрати при передачі колекцій між функціями.

Swift vs Objective-C диспетчеризація

swift
import Foundation

// Swift: статична диспетчеризація (vtable для class)
class Animal {
    func makeSound() { print("...") }  // vtable
}

class Dog: Animal {
    override func makeSound() { print("Woof") }  // vtable override
}

// @objc dynamic: Objective-C Runtime dispatch
class Cat: Animal {
    @objc dynamic override func makeSound() {
        print("Meow")
    }  // objc_msgSend
}

// Struct — no runtime dispatch
struct Cow {
    func makeSound() { print("Moo") }  // direct call
}

// Protocol with protocol witness
protocol SoundMaker {
    func makeSound()
}

struct Duck: SoundMaker {
    func makeSound() { print("Quack") }
}

// Використання existential container
let soundMakers: [SoundMaker] = [Dog(), Cow(), Duck()]
for maker in soundMakers {
    maker.makeSound()  // protocol witness dispatch
}

// Тестування продуктивності
func testDispatch() {
    let dog = Dog()
    let cat = Cat()
    var cow = Cow()

    let start = CFAbsoluteTimeGetCurrent()
    for _ in 0..<1000000 {
        dog.makeSound()  // vtable: ~3ns
        cat.makeSound()  // objc_msgSend: ~15ns
        cow.makeSound()  // direct: ~1ns
    }
    let elapsed = CFAbsoluteTimeGetCurrent() - start
    print("Elapsed: (elapsed) sec")
}

Приклад демонструє три типи диспетчеризації в Swift: vtable для class (Dog), objc_msgSend для @objc dynamic (Cat) та direct call для struct (Cow). Protocol witnesses в existential container ([SoundMaker]) додають overhead. На практиці Swift вибирає статичну диспетчеризацію скрізь, де можливо, забезпечуючи продуктивність, близьку до C.

Swift Runtime та Objective-C bridge

Swift Runtime спроектований для повної сумісності з Objective-C Runtime. Будь-який Swift клас, що наслідує NSObject, автоматично реєструється в Objective-C Runtime і може бути викликаний через objc_msgSend. @objc атрибут робить Swift method доступним з Objective-C. String bridge: Swift String автоматично bridged до NSString при передачі в Objective-C API (toll-free bridging).

ARC: Automatic Reference Counting та керування пам'яттю

ARC (Automatic Reference Counting) — система керування пам'яттю в iOS, що працює на етапі компіляції. Компілятор (Clang) аналізує час життя об'єктів та автоматично вставляє retain/release/autorelease виклики. Розробнику не потрібно викликати їх вручну — на відміну від Manual Retain-Release (MRR) до iOS 5. ARC працює на рівні Objective-C та Swift об'єктів, але не для value types (struct, enum).

Кожен об'єкт Objective-C та Swift class має лічильник посилань (retain count), що зберігається в extra_rc полі всередині non-pointer isa. При створенні об'єкта retain count = 1. При retain лічильник збільшується, при release — зменшується. Коли лічильник досягає 0, об'єкт деалокується через dealloc (Objective-C) або deinit (Swift). ARC thread-safe: retain/release використовують атомарні операції (OSAtomicIncrement32/OSAtomicDecrement32).

Retain cycles — головна проблема ARC. Якщо об'єкт A зберігає strong посилання на B, а B — strong посилання на A, обидва об'єкти ніколи не будуть деалоковані, оскільки їх лічильники посилань не обнуляться. Рішення — weak посилання (__weak в Objective-C, weak в Swift) або unowned посилання. Weak посилання не збільшують retain count і автоматично обнуляються (nil) при деалокації об'єкта.

Налагодження retain cycles через Instruments

swift
import Foundation

// Приклад retain cycle
class Parent {
    var child: Child?
    deinit { print("Parent deallocated") }
}

class Child {
    var parent: Parent?  // strong — створює retain cycle!
    deinit { print("Child deallocated") }
}

var parent: Parent? = Parent()
var child: Child? = Child()
parent?.child = child
child?.parent = parent  // цикл: Parent -> Child -> Parent
parent = nil
child = nil
// deinit НЕ викликається — витік пам'яті!

// Виправлення: weak
class WeakChild {
    weak var parent: Parent?  // weak — не збільшує retain count
    deinit { print("WeakChild deallocated") }
}

// Виправлення: unowned (для guaranteed lifetime)
class UnownedChild {
    unowned let parent: Parent
    init(parent: Parent) { self.parent = parent }
    deinit { print("UnownedChild deallocated") }
}

// Перевірка через Instruments
func profileMemory() {
    // 1. Запустити Instruments > Leaks
    // 2. Виконати дію, що створює об'єкти
    // 3. Перевірити Leaks на наявність витоків
    // 4. В Allocations знайти об'єкти без dealloc
    for _ in 0..<1000 {
        let p = Parent()
        let c = WeakChild()
        p.child = c as? Child
        // c.parent = p — НЕ додаємо, weak
    }
}

Приклад retain cycle між Parent та Child: обидва тримають strong посилання один на одного, ARC не може обнулити лічильники. Виправлення — weak parent в Child. weak автоматично обнуляється при деалокації parent. unowned — для випадків, коли час життя parent гарантовано більший за child (наприклад, viewController та view). Використовуйте Instruments > Leaks для виявлення retain cycles на ранніх етапах.

Autorelease pool

Autorelease pool — механізм відкладеного release для об'єктів, створених без явного володіння. @autoreleasepool { } в Swift та Objective-C створює пул, який дренується в кінці блоку, надсилаючи release кожному об'єкту в пулі. Критично важливий в циклах (створення тисяч тимчасових об'єктів) та на фонових потоках без RunLoop. UIKit RunLoop автоматично дренує головний autorelease pool на кожній ітерації.

dyld: динамічний завантажувач та запуск застосунку

dyld (dynamic link editor) — системний завантажувач, що відповідає за завантаження Mach-O виконуваних файлів та пов'язаних динамічних бібліотек (dylib) при запуску iOS-застосунку. dyld розташований за шляхом /usr/lib/dyld та є частиною libSystem. Процес завантаження включає кілька етапів: парсинг Mach-O, завантаження залежностей (Library Loader, LC_LOAD_DYLIB), релокація адрес (ASLR), ініціалізація Objective-C Runtime та виклик main().

Час запуску застосунку критично залежить від dyld: чим більше динамічних бібліотек та Objective-C класів, тим довший pre-main time. Apple рекомендує мінімізувати кількість +load методів (вони виконуються до main), замінювати їх на +initialize (лінива ініціалізація). З 2020 року Apple використовує prebuilt dyld cache на iOS: системні бібліотеки попередньо зв'язані в єдиний кеш, що прискорює завантаження.

Вимірювання pre-main time

swift
import Foundation

// Вимірювання часу запуску через DYLD_PRINT_STATISTICS
// В Xcode: Edit Scheme > Run > Arguments > Environment Variables
// DYLD_PRINT_STATISTICS = 1
// DYLD_PRINT_STATISTICS_DETAILS = 1

// Програмне вимірювання pre-main time
@main
struct AppMain {
    static func main() {
        let launchStart = CFAbsoluteTimeGetCurrent()

        // UIApplicationMain відбувається тут
        AppDelegate.main()

        let launchEnd = CFAbsoluteTimeGetCurrent()
        let preMainTime = launchEnd - launchStart
        print("Pre-main time: (preMainTime) sec")
    }
}

// Оптимізація: заміна +load на +initialize
class OptimizedClass {
    // ❌ +load виконується до main
    // override class func load() { }

    // ✅ +initialize виконується при першому зверненні
    static let shared = OptimizedClass()
    private init() {
        // Ініціалізація тут
    }
}

// Оптимізація кількості dylib
// Об'єднання статичних бібліотек зменшує число LC_LOAD_DYLIB
// Використовуйте -ObjC флаг для лінковки лише використовуваних Objective-C класів
// Xcode: Build Settings > Mach-O Type > Static Library

Для вимірювання pre-main time використовуйте DYLD_PRINT_STATISTICS в схемі Xcode. Вивід покаже загальний час, час завантаження dylib, час rebase/bind, час налаштування Objective-C та час ініціалізації. Цільові значення: загальний < 400ms для холодного старту, < 200ms для теплого. Оптимізації: об'єднання бібліотек, заміна +load на +initialize, зменшення кількості Objective-C класів (використовуйте Swift), мінімальна кількість динамічних фреймворків.

dsc (dyld shared cache)

dyld shared cache — кеш попередньо зв'язаних системних бібліотек на iOS. Всі системні dylib (UIKit, Foundation, CoreGraphics) об'єднані в один файл: /System/Library/Caches/com.apple.dyld/dyld_shared_cache_arm64. Це усуває необхідність завантажувати кожну системну бібліотеку окремо — dyld звертається до кешу, що значно прискорює запуск. Застосунки з 10+ динамічними фреймворками відчувають найбільшу затримку, оскільки кастомні dylib не входять до dsc.

Часті запитання

Що таке iOS Runtime та з яких компонентів він складається?

iOS Runtime — середовище виконання застосунків на iOS, що включає Objective-C Runtime (libobjc.dylib), Swift Runtime (libswiftCore.dylib), фреймворки Cocoa Touch, dyld (динамічний завантажувач) та ARC (керування пам'яттю). Забезпечує message passing для Objective-C, статичну диспетчеризацію для Swift, завантаження Mach-O файлів та автоматичне керування пам'яттю.

Чим відрізняється Objective-C Runtime від Swift Runtime?

Objective-C Runtime використовує динамічне зв'язування через objc_msgSend (message passing) з пізнім зв'язуванням. Swift Runtime використовує статичну диспетчеризацію (vtable для класів, direct call для struct) для продуктивності. @objc dynamic включає Objective-C Runtime для Swift-класів. Swift struct не має isa pointer і не використовує retain/release.

Як працює ARC в iOS?

ARC (Automatic Reference Counting) — керування пам'яттю на етапі компіляції. Компілятор Clang вставляє retain/release виклики автоматично. Кожен об'єкт має лічильник посилань, при обнуленні якого викликається dealloc. Retain cycles (взаємні strong посилання) запобігаються weak/unowned посиланнями. Використовуйте Instruments > Leaks для виявлення витоків.

Що таке dyld та як він впливає на запуск застосунку?

dyld — динамічний завантажувач Mach-O файлів. Завантажує виконуваний файл та всі залежні dylib, виконує релокацію (ASLR), ініціалізує Objective-C Runtime та викликає main(). Pre-main time залежить від кількості dylib та +load методів. Використовуйте DYLD_PRINT_STATISTICS для вимірювання. Оптимізація: об'єднання бібліотек, заміна +load на +initialize.

Що таке Method Swizzling та коли його використовувати?

Method Swizzling — техніка заміни IMP (implementation pointer) методу на льоту через Objective-C Runtime class_getInstanceMethod та method_exchangeImplementations. Використовується для A/B тестування, аналітики (автоматичний трекінг екранів) та моніторингу. Не рекомендується в продакшені без крайньої необхідності. В Swift замінюється на @objc dynamic + Method Swizzling.

Підсумки

  • iOS Runtime — середовище виконання застосунків iOS, що включає Objective-C Runtime, Swift Runtime, dyld та ARC
  • Objective-C Runtime — message passing (objc_msgSend), isa pointer, swizzling, динамічні класи
  • Swift Runtime — статична диспетчеризація (vtable, direct call), value types, protocol witnesses
  • ARC (Automatic Reference Counting) — автоматичне керування пам'яттю з retain/release на етапі компіляції
  • dyld — динамічний завантажувач Mach-O, що визначає швидкість запуску застосунку (pre-main time)
  • Retain cycles — запобігаються weak/unowned посиланнями; налагодження через Instruments Leaks
  • Оптимізація — мінімум +load, об'єднання dylib, використання Swift struct для value types

Ми розробимо мобільний застосунок під ключ

IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.

Обговорити проект

Читайте також