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 è 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.
| Componente | Libreria | Scopo |
|---|---|---|
| Objective-C Runtime | libobjc.A.dylib | Message passing, classi dinamiche, swizzling |
| Swift Runtime | libswiftCore.dylib | Value types, generics, protocol witnesses |
| Core Foundation | CoreFoundation.framework | CFType, toll-free bridging |
| dyld | dyld (usr/lib/dyld) | Caricamento Mach-O, collegamento librerie |
| libSystem | libSystem.B.dylib | POSIX threads, libc, libdispatch (GCD) |
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 è 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.
// 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.
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 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.
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 è 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 (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.
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 è 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 (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.
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 LibraryPer 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.
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
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.
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.
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.
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.
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
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.
Leggi anche