iOS Runtimeは、Apple iOSオペレーティングシステム上のアプリケーション実行環境であり、Objective-C Runtime、Swift Runtime、Cocoa Touchフレームワーク、Automatic Reference Counting(ARC)によるメモリ管理メカニズムを含みます。iOS Runtimeは、動的メソッドバインディング(message passing)、クラスロード、メモリ管理、iOSフレームワークを介したハードウェアとの対話を担当します。Apple Developer Documentationによると、パフォーマンス最適化、デバッグ、安定したiOSアプリケーションの開発にはruntimeの理解が必要です。
重要ポイント
iOS Runtimeは、iOSを実行するAppleデバイス上でのアプリケーション実行を保証するシステムコンポーネントの集合です。これにはObjective-C Runtime(libobjc.A.dylibライブラリ)、Swift Runtime(libswiftCore.dylib)、Core Foundation、Cocoa Touchフレームワーク(UIKit、Foundation)、動的ローダーdyld、メモリ管理、スレッド、プロセス間通信のための実行環境が含まれます。
アーキテクチャ的には、iOS Runtimeは3つのレベルで動作します。最下位レベル — Mach-Oバイナリ形式とdyld。実行可能ファイルとライブラリをロードします。中間レベル — Objective-C RuntimeとSwift Runtime。メソッドディスパッチとオブジェクト管理を担当します。上位レベル — Cocoa Touchフレームワーク(UIKit、Foundation、Core Data、Metal)。開発者にAPIを提供します。
iOS Runtimeを理解することで、開発者は複雑な問題を解決できます。A/Bテストと分析のためのメソッドスウィズリング(Method Swizzling)、動的クラスロード、ARCの理解によるメモリ最適化、retain cyclesとメモリリークのデバッグ、dyldによるアプリケーション起動時間の最適化。runtimeの知識なしでは、システムレベルでのプロファイリングと最適化は不可能です。
| コンポーネント | ライブラリ | 目的 |
|---|---|---|
| Objective-C Runtime | libobjc.A.dylib | Message passing、動的クラス、スウィズリング |
| Swift Runtime | libswiftCore.dylib | Value types、generics、protocol witnesses |
| Core Foundation | CoreFoundation.framework | CFType、toll-free bridging |
| dyld | dyld(usr/lib/dyld) | Mach-Oロード、ライブラリリンク |
| libSystem | libSystem.B.dylib | POSIX threads、libc、libdispatch(GCD) |
iOS用アプリケーションはMach-O形式(Mach Object)にコンパイルされます。Mach-Oファイルには、ヘッダー、ロードコマンド、セグメントが含まれます:__TEXT(コード、定数)、__DATA(グローバル変数、Objective-Cメタデータ)、__LINKEDIT(シンボル、再配置テーブル)。dyldは最初の命令を実行する前にMach-Oを解析し、依存関係をロードします。
Objective-C RuntimeはiOS Runtimeの最も強力な部分です。早期バインディング(early binding)のC++とは異なり、Objective-Cはmessage passingによる遅延バインディング(late binding)を使用します。メソッド呼び出し[receiver message]は直接関数呼び出しではなく、objc_msgSend(receiver, @selector(message))にコンパイルされ、オブジェクトのクラスでメソッド実装を動的に見つけます。
各Objective-Cオブジェクトは、そのクラスへのisaポインターを格納します。クラスにはメソッドリスト、メソッドキャッシュ、スーパークラスへのポインターが含まれます。objc_msgSendは継承チェーンをたどります:クラスキャッシュを確認し、次にメソッドリスト、次にスーパークラスに移動します。メソッドが見つからない場合、フォワーディングがトリガーされます:resolveInstanceMethod、forwardingTargetForSelector、forwardInvocation。
Method Swizzlingは、runtimeでIMP(実装ポインター)を交換することでメソッド実装をその場で切り替える手法です。A/Bテスト、分析(自動画面追跡)、モニタリングに使用されます。OSアップデートと競合する可能性があるため、重大な必要性がない限り本番環境では推奨されません。
// viewDidLoad追跡のためのMethod Swizzling
#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)カテゴリは、アプリケーション内のすべてのUIViewControllerでviewDidLoadをswizzled_viewDidLoadに置き換えます。dispatch_onceは一度だけのスウィズリングを保証します。class_addMethodは二重スウィズリングとスーパークラスとの競合を防ぎます。コントローラーのソースコードを変更せずに、分析での自動画面表示追跡に使用されます。
最新のiOS(arm64)では、Appleはisaポインターを最適化しました。これは単なるクラスアドレスではなく、メモリ管理フラグとクラス情報を含むビットフィールド(non-pointer isa)です。タグ付きポインターは別の最適化です。小さなNSNumber、NSDate、NSStringの値はヒープ上のオブジェクトとしてではなく、ポインターに直接格納され、mallocとretain/releaseのオーバーヘッドを排除します。タグ付きポインターはisaの最下位ビットで認識されます。
Swift RuntimeはObjective-C Runtimeとは根本的に異なります。Swiftはデフォルトで、クラスメソッドにはvtableを介した静的ディスパッチ(static dispatch)を、value typesと拡張メソッドにはdirect callを使用します。動的ディスパッチ(dynamic dispatch)は@objcまたはdynamicがマークされたメソッドにのみ使用されます。これにより、Objective-Cと比較して最大40%のパフォーマンス向上が得られます。
Value types(struct、enum)はSwiftにおけるObjective-Cとの重要な違いです。これらはスタック上または別のオブジェクト内に格納され、retain/releaseを使用せず、参照カウントのARCには関与しません。Structにはisaポインターがなく、objc_msgSendを介して送信できません。プロトコルウィットネスは、プロトコル用のvtableに類似したもので、existential containersの動的ディスパッチを可能にします。
Swift Runtimeには、具体化(mangled symbolsによるreified generics)を伴うgenericsと、string、array、dictionary、setを最適化するためのCOW(Copy-on-Write)も含まれています。コレクションをコピーする際、実際のコピーはコピーの1つが変更された場合にのみ発生します。これにより、関数間でコレクションを渡す際のオーバーヘッドが最小限に抑えられます。
import Foundation
// Swift: 静的ディスパッチ(クラスのvtable)
class Animal {
func makeSound() { print("...") } // vtable
}
class Dog: Animal {
override func makeSound() { print("Woof") } // vtable override
}
// @objc dynamic: Objective-C Runtimeディスパッチ
class Cat: Animal {
@objc dynamic override func makeSound() {
print("Meow")
} // objc_msgSend
}
// Struct — runtimeディスパッチなし
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の3種類のディスパッチを示しています。class(Dog)のvtable、@objc dynamic(Cat)のobjc_msgSend、struct(Cow)のdirect callです。existential containers([SoundMaker])内のプロトコルウィットネスはオーバーヘッドを追加します。実際には、Swiftは可能な限り静的ディスパッチを選択し、Cに近いパフォーマンスを提供します。
Swift RuntimeはObjective-C Runtimeとの完全な互換性のために設計されています。NSObjectを継承するSwiftクラスは自動的にObjective-C Runtimeに登録され、objc_msgSendを介して呼び出すことができます。@objc属性はSwiftメソッドをObjective-Cからアクセス可能にします。文字列ブリッジ:Swift StringはObjective-C API(toll-free bridging)に渡されると自動的にNSStringにブリッジされます。
ARC(Automatic Reference Counting)は、コンパイル時に動作するiOSのメモリ管理システムです。コンパイラ(Clang)はオブジェクトのライフタイムを分析し、retain/release/autorelease呼び出しを自動的に挿入します。開発者は手動でこれらを呼び出す必要はありません — iOS 5より前のManual Retain-Release(MRR)とは異なります。ARCはObjective-CオブジェクトとSwiftオブジェクトのレベルで機能しますが、value types(struct、enum)には機能しません。
各Objective-CおよびSwiftクラスオブジェクトには、non-pointer isa内のextra_rcフィールドに格納された参照カウント(retain count)があります。オブジェクト作成時、retain count = 1です。retainでカウンターが増加し、releaseで減少します。カウンターが0になると、オブジェクトはdealloc(Objective-C)またはdeinit(Swift)を介して解放されます。ARCはスレッドセーフです。retain/releaseはアトミック操作(OSAtomicIncrement32/OSAtomicDecrement32)を使用します。
Retain cyclesはARCの主要な問題です。オブジェクトAがBへのstrong参照を持ち、BがAへのstrong参照を持つ場合、両方のオブジェクトの参照カウントがゼロにならないため、決して解放されません。解決策は、weak参照(Objective-Cの__weak、Swiftのweak)またはunowned参照です。weak参照はretain countを増やさず、オブジェクト解放時に自動的にゼロ(nil)になります。
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(保証されたライフタイム用)
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
}
}ParentとChild間のretain cycleの例:両方が互いにstrong参照を保持しているため、ARCはカウンターをゼロにできません。修正はChildのweak parentです。weakはparent解放時に自動的にゼロになります。unownedは、parentのライフタイムがchildより長いことが保証されている場合(viewControllerとviewなど)に使用します。retain cyclesを早期に検出するにはInstruments > Leaksを使用してください。
Autorelease poolは、明示的な所有権なしで作成されたオブジェクトのための遅延解放メカニズムです。SwiftとObjective-Cの@autoreleasepool { }はプールを作成し、ブロックの終わりでプール内の各オブジェクトにreleaseを送信して排出されます。ループ(数千の一時オブジェクトを作成する場合)やRunLoopのないバックグラウンドスレッドで非常に重要です。UIKit RunLoopは各イテレーションでメインのautorelease poolを自動的に排出します。
dyld(dynamic link editor)は、iOSアプリケーション起動時にMach-O実行可能ファイルと関連する動的ライブラリ(dylib)をロードするシステムローダーです。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はiOSでプリビルドdyldキャッシュを使用しています。システムライブラリは単一のキャッシュに事前リンクされており、ロードが高速化されています。
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の数が減少
// 使用されているObjective-Cクラスのみをリンクするには-ObjCフラグを使用
// Xcode:Build Settings > Mach-O Type > Static Librarypre-main timeを測定するには、XcodeスキームでDYLD_PRINT_STATISTICSを使用します。出力は合計時間、dylibロード時間、rebase/bind時間、Objective-Cセットアップ時間、初期化子時間を表示します。目標値:コールドスタートで合計<400ms、ウォームスタートで<200ms。最適化:ライブラリの統合、+loadから+initializeへの置き換え、Objective-Cクラス数の削減(Swiftを使用)、動的フレームワークの最小化。
dyld shared cacheは、iOS上の事前リンクされたシステムライブラリのキャッシュです。すべてのシステムdylib(UIKit、Foundation、CoreGraphics)は1つのファイルに結合されています:/System/Library/Caches/com.apple.dyld/dyld_shared_cache_arm64。これにより、各システムライブラリを個別にロードする必要がなくなり、dyldがキャッシュにアクセスするため、起動が大幅に高速化されます。10以上の動的フレームワークを使用するアプリケーションは最も遅延が大きく、カスタムdylibはdscに含まれていません。
よくある質問
iOS RuntimeはiOS上のアプリケーション実行環境であり、Objective-C Runtime(libobjc.dylib)、Swift Runtime(libswiftCore.dylib)、Cocoa Touchフレームワーク、dyld(動的ローダー)、ARC(メモリ管理)を含みます。Objective-Cのmessage passing、Swiftの静的ディスパッチ、Mach-Oファイルのロード、自動メモリ管理を提供します。
Objective-C Runtimeは遅延バインディングでobjc_msgSend(message passing)による動的バインディングを使用します。Swift Runtimeはパフォーマンスのために静的ディスパッチ(クラスにはvtable、structにはdirect call)を使用します。@objc dynamicはSwiftクラスにObjective-C Runtimeを有効にします。Swift structにはisaポインターがなく、retain/releaseを使用しません。
ARC(Automatic Reference Counting)はコンパイル時のメモリ管理です。Clangコンパイラが自動的にretain/release呼び出しを挿入します。各オブジェクトには参照カウントがあり、ゼロになるとdeallocが呼び出されます。Retain cycles(相互のstrong参照)はweak/unowned参照によって防がれます。リークを検出するにはInstruments > Leaksを使用してください。
dyldはMach-Oファイルの動的ローダーです。実行可能ファイルと依存するすべてのdylibをロードし、再配置(ASLR)を実行し、Objective-C Runtimeを初期化し、main()を呼び出します。Pre-main timeはdylibと+loadメソッドの数に依存します。測定にはDYLD_PRINT_STATISTICSを使用します。最適化:ライブラリの統合、+loadから+initializeへの置き換え。
Method Swizzlingは、Objective-C Runtimeのclass_getInstanceMethodとmethod_exchangeImplementationsを介してメソッドのIMP(実装ポインター)をその場で交換する手法です。A/Bテスト、分析(自動画面追跡)、モニタリングに使用されます。重大な必要性がない限り本番環境では推奨されません。Swiftでは@objc dynamic + Method Swizzlingに置き換えられます。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。