iOS Runtime은 Apple iOS 운영 체제의 애플리케이션 실행 환경으로, Objective-C Runtime, Swift Runtime, Cocoa Touch 프레임워크 및 Automatic Reference Counting(ARC)을 통한 메모리 관리 메커니즘을 포함합니다. iOS Runtime은 동적 메서드 바인딩(message passing), 클래스 로딩, 메모리 관리 및 iOS 프레임워크를 통한 하드웨어 상호 작용을 담당합니다. Apple Developer Documentation에 따르면, 성능 최적화, 디버깅 및 안정적인 iOS 애플리케이션 개발을 위해 런타임 이해가 필요합니다.
핵심 요점
iOS Runtime은 iOS를 실행하는 Apple 기기에서 애플리케이션 실행을 보장하는 시스템 구성 요소 모음입니다. 여기에는 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을 이해하면 개발자가 복잡한 문제를 해결할 수 있습니다. A/B 테스트 및 분석을 위한 메서드 스위즐링(Method Swizzling), 동적 클래스 로딩, ARC 이해를 통한 메모리 최적화, retain cycles 및 메모리 누수 디버깅, dyld를 통한 앱 시작 시간 최적화. 런타임 지식 없이는 시스템 수준에서의 프로파일링 및 최적화가 불가능합니다.
| 구성 요소 | 라이브러리 | 목적 |
|---|---|---|
| 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은 런타임에서 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에는 구체화(reified generics, mangled symbols 통해)를 포함한 generics와 string, array, dictionary, set을 최적화하기 위한 COW(Copy-on-Write)도 포함됩니다. 컬렉션을 복사할 때 실제 복사는 복사본 중 하나가 수정될 때만 발생합니다. 이는 함수 간 컬렉션 전달 시 오버헤드를 최소화합니다.
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 — 런타임 디스패치 없음
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의 세 가지 디스패치 유형을 보여줍니다: 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 참조를 가지고 있으면 두 객체의 참조 카운트가 절대 0에 도달하지 않아 할당 해제되지 않습니다. 해결책은 weak 참조(Objective-C의 __weak, Swift의 weak) 또는 unowned 참조입니다. weak 참조는 retain count를 증가시키지 않으며 객체 할당 해제 시 자동으로 0(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가 카운터를 0으로 만들 수 없습니다. 수정은 Child의 weak parent입니다. weak는 parent 할당 해제 시 자동으로 0이 됩니다. 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)는 하나의 파일로 결합됩니다: /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 호출을 삽입합니다. 각 객체에는 참조 카운트가 있으며 0에 도달하면 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 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.