iOS Runtime: cos'è, ambiente di esecuzione delle app su iPhone

Autore: IT Sectr Pubblicato: 2026-05-17 Tempo di lettura: 12 min

iOS Runtime è l'ambiente di esecuzione delle applicazioni sul sistema operativo Apple iOS, che include Objective-C Runtime, Swift Runtime, i framework Cocoa Touch e i meccanismi di gestione della memoria tramite Automatic Reference Counting (ARC). iOS Runtime è responsabile del binding dinamico dei metodi (message passing), caricamento delle classi, gestione della memoria e interazione con l'hardware attraverso i framework iOS. Secondo la Documentazione per Sviluppatori Apple, comprendere il runtime è necessario per l'ottimizzazione delle prestazioni, il debug e lo sviluppo di applicazioni iOS stabili.

Punti Chiave

  • iOS Runtime — ambiente di esecuzione delle applicazioni che include Objective-C Runtime, Swift Runtime e Cocoa Touch
  • Objective-C Runtime — binding dinamico dei metodi tramite message passing (objc_msgSend)
  • Swift Runtime — dispatch statico con ottimizzazioni tramite value types e generics
  • ARC (Automatic Reference Counting) — gestione automatica della memoria in fase di compilazione
  • dyld — caricatore dinamico che carica framework e librerie all'avvio dell'applicazione

Cos'è iOS Runtime?

iOS Runtime è un insieme di componenti di sistema che garantiscono l'esecuzione delle applicazioni sui dispositivi Apple con iOS. Include Objective-C Runtime (libreria libobjc.A.dylib), Swift Runtime (libswiftCore.dylib), Core Foundation, i framework Cocoa Touch (UIKit, Foundation), il caricatore dinamico dyld e l'ambiente di esecuzione per la gestione della memoria, dei thread e della comunicazione interprocesso.

Architetturalmente, iOS Runtime opera a tre livelli. Al livello più basso — formato binario Mach-O e dyld, che carica il file eseguibile e le librerie. Il livello intermedio — Objective-C Runtime e Swift Runtime, responsabili del dispatch dei metodi e della gestione degli oggetti. Il livello superiore — i framework Cocoa Touch (UIKit, Foundation, Core Data, Metal), che forniscono API allo sviluppatore.

Comprendere iOS Runtime consente allo sviluppatore di risolvere problemi complessi: lo swizzling dei metodi (Method Swizzling) per test A/B e analisi, il caricamento dinamico delle classi, l'ottimizzazione della memoria tramite la comprensione di ARC, il debugging dei retain cycle e delle perdite di memoria, l'ottimizzazione del tempo di avvio dell'applicazione tramite dyld. Senza la conoscenza del runtime, la profilazione e l'ottimizzazione a livello di sistema sono impossibili.

Componenti di iOS Runtime

ComponenteLibreriaScopo
Objective-C Runtimelibobjc.A.dylibMessage passing, classi dinamiche, swizzling
Swift RuntimelibswiftCore.dylibValue types, generics, protocol witnesses
Core FoundationCoreFoundation.frameworkCFType, toll-free bridging
dylddyld (usr/lib/dyld)Caricamento Mach-O, collegamento librerie
libSystemlibSystem.B.dylibPOSIX threads, libc, libdispatch (GCD)

Formato Mach-O

Le applicazioni per iOS vengono compilate in formato Mach-O (Mach Object). Un file Mach-O contiene un'intestazione, comandi di caricamento e segmenti: __TEXT (codice, costanti), __DATA (variabili globali, metadati Objective-C), __LINKEDIT (simboli, tabelle di rilocazione). dyld analizza il Mach-O e carica le dipendenze prima di eseguire la prima istruzione.

Objective-C Runtime: Message Passing e Dispatch Dinamico

Objective-C Runtime è la parte più potente di iOS Runtime. A differenza del C++ con binding anticipato (early binding), Objective-C utilizza il binding tardivo (late binding) tramite message passing. Una chiamata al metodo [receiver message] non viene compilata in una chiamata di funzione diretta, ma in objc_msgSend(receiver, @selector(message)), che trova dinamicamente l'implementazione del metodo nella classe dell'oggetto.

Ogni oggetto Objective-C memorizza un puntatore isa alla sua classe. La classe contiene un elenco di metodi, una cache di metodi e un puntatore alla superclasse. objc_msgSend percorre la catena di ereditarietà: controlla la cache della classe, poi l'elenco dei metodi, poi passa alla superclasse. Se il metodo non viene trovato, viene attivato il forwarding: resolveInstanceMethod, forwardingTargetForSelector e forwardInvocation.

Method Swizzling è una tecnica per scambiare le implementazioni dei metodi al volo attraverso lo scambio di IMP (puntatore di implementazione) in fase di esecuzione. Viene utilizzato per test A/B, analisi (tracciamento automatico degli schermi) e monitoraggio. Non è raccomandato per la produzione senza necessità critica, poiché potrebbe entrare in conflitto con gli aggiornamenti del sistema operativo.

Esempio: Method Swizzling in Objective-C

objective-c
// Method Swizzling per il tracciamento di 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 {
    // Tracciamento evento
    NSLog(@"View Did Load: %@", self.class);
    // Chiamata implementazione originale
    [self swizzled_viewDidLoad];
}

@end

La categoria UIViewController (Tracking) sostituisce viewDidLoad con swizzled_viewDidLoad in tutti gli UIViewController dell'applicazione. dispatch_once garantisce uno swizzling unico. class_addMethod previene il doppio swizzling e i conflitti con le superclassi. Viene utilizzato per il tracciamento automatico delle viste dello schermo nell'analisi senza modificare il codice sorgente del controller.

Puntatore isa e Tagged Pointers

Nell'iOS moderno (arm64), Apple ha ottimizzato il puntatore isa: non è solo un indirizzo di classe, ma un campo di bit (non-pointer isa) contenente flag di gestione della memoria e informazioni sulla classe. I tagged pointers sono un'altra ottimizzazione: i valori piccoli di NSNumber, NSDate e NSString non vengono memorizzati come oggetti nell'heap, ma direttamente nel puntatore, eliminando il sovraccarico di malloc e retain/release. Un tagged pointer viene riconosciuto dal bit meno significativo di isa.

Swift Runtime: Dispatch Statico e Ottimizzazione

Swift Runtime differisce fondamentalmente da Objective-C Runtime: Swift per impostazione predefinita utilizza il dispatch statico (static dispatch) tramite vtable per i metodi di classe e direct call per i value types e i metodi di estensione. Il dispatch dinamico (dynamic dispatch) viene utilizzato solo per i metodi contrassegnati con @objc o dynamic. Ciò offre un miglioramento delle prestazioni fino al 40% rispetto a Objective-C.

Value types (struct, enum) in Swift sono una differenza chiave rispetto a Objective-C. Vengono memorizzati nello stack o all'interno di un altro oggetto, non utilizzano retain/release e non partecipano all'ARC per il conteggio dei riferimenti. Struct non ha un puntatore isa e non può essere inviato tramite objc_msgSend. I protocol witnesses sono un analogo della vtable per i protocolli, che consentono il dispatch dinamico per gli existential containers.

Swift Runtime include anche generics con reificazione (reified generics tramite mangled symbols) e COW (Copy-on-Write) per ottimizzare string, array, dictionary, set. Quando si copia una collezione, la copia effettiva avviene solo quando una delle copie viene modificata. Questo riduce al minimo il sovraccarico quando si passano collezioni tra funzioni.

Dispatch Swift vs Objective-C

swift
import Foundation

// Swift: dispatch statico (vtable per class)
class Animal {
    func makeSound() { print("...") }  // vtable
}

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

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

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

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

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

// Utilizzo di existential container
let soundMakers: [SoundMaker] = [Dog(), Cow(), Duck()]
for maker in soundMakers {
    maker.makeSound()  // protocol witness dispatch
}

// Test delle prestazioni
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")
}

L'esempio mostra tre tipi di dispatch in Swift: vtable per class (Dog), objc_msgSend per @objc dynamic (Cat) e direct call per struct (Cow). I protocol witnesses negli existential containers ([SoundMaker]) aggiungono sovraccarico. In pratica, Swift sceglie il dispatch statico ovunque possibile, offrendo prestazioni vicine a C.

Swift Runtime e Ponte Objective-C

Swift Runtime è progettato per la piena compatibilità con Objective-C Runtime. Qualsiasi classe Swift che eredita da NSObject viene automaticamente registrata in Objective-C Runtime e può essere chiamata tramite objc_msgSend. L'attributo @objc rende un metodo Swift accessibile da Objective-C. Ponte String: Swift String viene automaticamente convertito in NSString quando passato a un'API Objective-C (toll-free bridging).

ARC: Conteggio Automatico dei Riferimenti e Gestione della Memoria

ARC (Automatic Reference Counting) è un sistema di gestione della memoria in iOS che opera in fase di compilazione. Il compilatore (Clang) analizza i cicli di vita degli oggetti e inserisce automaticamente chiamate retain/release/autorelease. Lo sviluppatore non deve chiamarle manualmente — a differenza di Manual Retain-Release (MRR) precedente a iOS 5. ARC funziona a livello di oggetti Objective-C e Swift, ma non per i value types (struct, enum).

Ogni oggetto Objective-C e classe Swift ha un contatore di riferimenti (retain count), memorizzato nel campo extra_rc all'interno del non-pointer isa. Quando viene creato un oggetto, retain count = 1. Con retain, il contatore aumenta; con release, diminuisce. Quando il contatore raggiunge 0, l'oggetto viene deallocato tramite dealloc (Objective-C) o deinit (Swift). ARC è thread-safe: retain/release utilizzano operazioni atomiche (OSAtomicIncrement32/OSAtomicDecrement32).

Retain cycles sono il problema principale di ARC. Se l'oggetto A ha un riferimento strong a B, e B ha un riferimento strong ad A, entrambi gli oggetti non verranno mai deallocati perché i loro contatori di riferimenti non raggiungeranno mai zero. La soluzione sono i riferimenti weak (__weak in Objective-C, weak in Swift) o i riferimenti unowned. I riferimenti weak non aumentano il retain count e vengono automaticamente azzerati (nil) quando l'oggetto viene deallocato.

Debugging dei Retain Cycles con Instruments

swift
import Foundation

// Esempio di retain cycle
class Parent {
    var child: Child?
    deinit { print("Parent deallocated") }
}

class Child {
    var parent: Parent?  // strong — crea un retain cycle!
    deinit { print("Child deallocated") }
}

var parent: Parent? = Parent()
var child: Child? = Child()
parent?.child = child
child?.parent = parent  // ciclo: Parent -> Child -> Parent
parent = nil
child = nil
// deinit NON chiamato — perdita di memoria!

// Correzione: weak
class WeakChild {
    weak var parent: Parent?  // weak — non aumenta il retain count
    deinit { print("WeakChild deallocated") }
}

// Correzione: unowned (per durata garantita)
class UnownedChild {
    unowned let parent: Parent
    init(parent: Parent) { self.parent = parent }
    deinit { print("UnownedChild deallocated") }
}

// Verifica tramite Instruments
func profileMemory() {
    // 1. Eseguire Instruments > Leaks
    // 2. Eseguire un'azione che crea oggetti
    // 3. Controllare Leaks per perdite
    // 4. In Allocations trovare oggetti senza dealloc
    for _ in 0..<1000 {
        let p = Parent()
        let c = WeakChild()
        p.child = c as? Child
        // c.parent = p — NON aggiungiamo, weak
    }
}

L'esempio di retain cycle tra Parent e Child: entrambi mantengono riferimenti strong l'uno verso l'altro, ARC non può azzerare i contatori. La correzione è weak parent in Child. weak viene automaticamente azzerato quando parent viene deallocato. unowned è per i casi in cui la vita di parent è garantita più lunga di quella di child (ad esempio, viewController e view). Utilizzare Instruments > Leaks per rilevare i retain cycle precocemente.

Autorelease Pool

Autorelease pool è un meccanismo di rilascio differito per gli oggetti creati senza proprietà esplicita. @autoreleasepool { } in Swift e Objective-C crea un pool che viene drenato alla fine del blocco, inviando release a ogni oggetto nel pool. È criticamente importante nei cicli (creazione di migliaia di oggetti temporanei) e nei thread in background senza RunLoop. L'UIKit RunLoop drena automaticamente l'autorelease pool principale a ogni iterazione.

dyld: Caricatore Dinamico e Avvio dell'Applicazione

dyld (dynamic link editor) è il caricatore di sistema responsabile del caricamento dei file eseguibili Mach-O e delle librerie dinamiche correlate (dylib) all'avvio di un'applicazione iOS. dyld si trova in /usr/lib/dyld e fa parte di libSystem. Il processo di caricamento include diverse fasi: analisi Mach-O, caricamento delle dipendenze (Library Loader, LC_LOAD_DYLIB), rilocazione degli indirizzi (ASLR), inizializzazione di Objective-C Runtime e chiamata a main().

Il tempo di avvio dell'applicazione dipende criticamente da dyld: più librerie dinamiche e classi Objective-C ci sono, più lungo è il pre-main time. Apple raccomanda di ridurre al minimo il numero di metodi +load (vengono eseguiti prima di main), sostituendoli con +initialize (inizializzazione pigra). Dal 2020, Apple utilizza una cache dyld precompilata su iOS: le librerie di sistema sono precollegate in una singola cache, accelerando il caricamento.

Misurazione del Pre-main Time

swift
import Foundation

// Misurazione del tempo di avvio tramite DYLD_PRINT_STATISTICS
// In Xcode: Edit Scheme > Run > Arguments > Environment Variables
// DYLD_PRINT_STATISTICS = 1
// DYLD_PRINT_STATISTICS_DETAILS = 1

// Misurazione programmatica del pre-main time
@main
struct AppMain {
    static func main() {
        let launchStart = CFAbsoluteTimeGetCurrent()

        // UIApplicationMain avviene qui
        AppDelegate.main()

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

// Ottimizzazione: sostituire +load con +initialize
class OptimizedClass {
    // ❌ +load viene eseguito prima di main
    // override class func load() { }

    // ✅ +initialize viene eseguito al primo accesso
    static let shared = OptimizedClass()
    private init() {
        // Inizializzazione qui
    }
}

// Ottimizzazione del numero di dylib
// L'unione di librerie statiche riduce il numero di LC_LOAD_DYLIB
// Utilizzare il flag -ObjC per collegare solo le classi Objective-C utilizzate
// Xcode: Build Settings > Mach-O Type > Static Library

Per misurare il pre-main time, utilizzare DYLD_PRINT_STATISTICS nello schema Xcode. L'output mostra il tempo totale, il tempo di caricamento dylib, il tempo di rebase/bind, il tempo di configurazione di Objective-C e il tempo di inizializzazione. Valori target: totale < 400ms per avvio a freddo, < 200ms per avvio a caldo. Ottimizzazioni: unire le librerie, sostituire +load con +initialize, ridurre il numero di classi Objective-C (usare Swift), numero minimo di framework dinamici.

dsc (dyld Shared Cache)

dyld shared cache è una cache di librerie di sistema precollegate su iOS. Tutte le dylib di sistema (UIKit, Foundation, CoreGraphics) sono combinate in un unico file: /System/Library/Caches/com.apple.dyld/dyld_shared_cache_arm64. Ciò elimina la necessità di caricare ogni libreria di sistema separatamente — dyld accede alla cache, accelerando notevolmente l'avvio. Le applicazioni con 10+ framework dinamici subiscono il ritardo maggiore, poiché le dylib personalizzate non sono incluse nel dsc.

Domande Frequenti

Cos'è iOS Runtime e di quali componenti è composto?

iOS Runtime è l'ambiente di esecuzione delle applicazioni su iOS, che include Objective-C Runtime (libobjc.dylib), Swift Runtime (libswiftCore.dylib), i framework Cocoa Touch, dyld (caricatore dinamico) e ARC (gestione della memoria). Fornisce message passing per Objective-C, dispatch statico per Swift, caricamento di file Mach-O e gestione automatica della memoria.

Qual è la differenza tra Objective-C Runtime e Swift Runtime?

Objective-C Runtime utilizza il binding dinamico tramite objc_msgSend (message passing) con binding tardivo. Swift Runtime utilizza il dispatch statico (vtable per le classi, direct call per struct) per le prestazioni. @objc dynamic attiva Objective-C Runtime per le classi Swift. Swift struct non ha puntatore isa e non utilizza retain/release.

Come funziona ARC in iOS?

ARC (Automatic Reference Counting) è la gestione della memoria in fase di compilazione. Il compilatore Clang inserisce automaticamente chiamate retain/release. Ogni oggetto ha un contatore di riferimenti; quando raggiunge zero, viene chiamato dealloc. I retain cycle (riferimenti strong reciproci) vengono prevenuti da riferimenti weak/unowned. Utilizzare Instruments > Leaks per rilevare le perdite.

Cos'è dyld e come influisce sull'avvio dell'applicazione?

dyld è il caricatore dinamico dei file Mach-O. Carica il file eseguibile e tutti i dylib dipendenti, esegue la rilocazione (ASLR), inizializza Objective-C Runtime e chiama main(). Il pre-main time dipende dal numero di dylib e metodi +load. Utilizzare DYLD_PRINT_STATISTICS per la misurazione. Ottimizzazione: unire le librerie, sostituire +load con +initialize.

Cos'è Method Swizzling e quando usarlo?

Method Swizzling è una tecnica per scambiare l'IMP (puntatore di implementazione) di un metodo al volo tramite class_getInstanceMethod e method_exchangeImplementations di Objective-C Runtime. Utilizzato per test A/B, analisi (tracciamento automatico degli schermi) e monitoraggio. Non raccomandato in produzione senza necessità critica. In Swift viene sostituito da @objc dynamic + Method Swizzling.

Riepilogo

  • iOS Runtime — ambiente di esecuzione delle applicazioni iOS che include Objective-C Runtime, Swift Runtime, dyld e ARC
  • Objective-C Runtime — message passing (objc_msgSend), puntatore isa, swizzling, classi dinamiche
  • Swift Runtime — dispatch statico (vtable, direct call), value types, protocol witnesses
  • ARC (Automatic Reference Counting) — gestione automatica della memoria con retain/release in fase di compilazione
  • dyld — caricatore dinamico Mach-O che determina la velocità di avvio dell'applicazione (pre-main time)
  • Retain cycles — prevenuti da riferimenti weak/unowned; debug tramite Instruments Leaks
  • Ottimizzazione — minimizzare +load, unire dylib, utilizzare Swift struct per i value types

Svilupperemo un'applicazione mobile chiavi in mano

IT Sectr crea applicazioni iOS e Android per startup e aziende dal 2017. Ti consulteremo e ti proporremo la soluzione migliore.

Discuti il progetto

Leggi anche