一.iOS Objective-C Runtime 原理

13 阅读17分钟

Objective-C Runtime

面试回答版

一句话概括

Runtime 是 Objective-C 的动态运行时系统,它让 OC 在运行期(而非编译期)完成消息分发、方法解析和动态扩展。OC 的方法调用本质上是消息发送,由 Runtime 库负责查找和执行。


核心数据结构

对象 & 类
// 对象结构
struct objc_object {
    isa_t isa;          // isa 指针,指向类对象
};

// 类结构
struct objc_class : objc_object {
    isa_t isa;          // 指向元类
    Class superclass;   // 父类指针
    cache_t cache;      // 方法缓存
    class_data_bits_t bits; // 类的方法列表、属性列表、协议列表等
};
  • isa 指针:现代 Runtime 中是非指针 isa(non-pointer isa),利用 64 位中的空闲位存储引用计数等额外信息,减少内存访问层级。
  • objc_class 继承自 objc_object,所以类本身也是一个对象。
元类 (Meta-Class)
  • 实例对象isa类对象
  • 类对象isa元类
  • 元类isa根元类(Root Meta Class)→ 指向自身
  • 元类存储"类方法",当调用 [NSObject alloc] 时,Runtime 在元类的方法列表中查找 alloc
实例对象  ──isa──→  类对象  ──isa──→  元类  ──isa──→ 根元类 ──isa──→ 自身
                       │                  │
                       ↓                  ↓
                     superclass         superclass
                       │                  │
                       ↓                  ↓
                     父类对象            根元类 ──superclass──→ 根类对象

关键理解:实例方法存在类对象中,类方法存在元类中。类可以继承,元类同样遵循继承链。

Method / SEL / IMP
typedef struct objc_method *Method;
struct objc_method {
    SEL _Nonnull method_name;      // 方法名(C 字符串,已唯一化)
    char * _Nullable method_types; // 类型编码
    IMP _Nonnull method_imp;       // 函数指针
};

typedef struct objc_selector *SEL;  // 方法名在 Runtime 中的唯一标识,相同名字对应同一个 SEL
typedef id (*IMP)(id, SEL, ...);    // 方法实现的函数指针
  • SEL:方法名在 Runtime 中的唯一标识,用 @selector()sel_registerName() 获取,相同的名字对应同一个 SEL。
  • IMP:真正的函数实现指针,拿到它就可以直接调用。
  • Method:SEL 和 IMP 的配对关系,外加类型编码描述参数/返回值类型。
class_rw_t / class_ro_t

这两个结构体不直接属于 objc_class 的成员,而是通过 objc_classclass_data_bits_t bits 间接访问:

objc_class
  ├─ isa
  ├─ superclass
  ├─ cache
  └─ bits ───→  class_rw_t(类 realize 后创建,可读写)
                    │
                    ├─ methods      ← 可扩展(含本类 + Category 合并的)
                    ├─ properties   ← 可扩展
                    ├─ protocols    ← 可扩展
                    ├─ firstSubclass / nextSiblingClass
                    │
                    └─ ro ───→  class_ro_t(只读,编译期确定)
                                  ├─ baseMethodList    ← 编译期就有的方法
                                  ├─ baseProtocols
                                  ├─ ivars             ← 成员变量列表
                                  └─ baseProperties
// 编译期确定的只读数据(从 Mach-O __DATA,__objc_data 段直接加载)
struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;
    const uint8_t * ivarLayout;
    const char * name;
    method_list_t * baseMethodList;    // 编译期确定的方法(不含 Category)
    protocol_list_t * baseProtocols;
    const ivar_list_t * ivars;
    const uint8_t * weakIvarLayout;
    property_list_t * baseProperties;
};

// 运行期创建的可读写数据(类首次使用前 realize 时分配)
struct class_rw_t {
    uint32_t flags;
    uint32_t version;
    const class_ro_t *ro;            // 指向只读数据
    method_array_t methods;          // 可扩展的方法列表(含 Category 添加的)
    property_array_t properties;
    protocol_array_t protocols;
    Class firstSubclass;
    Class nextSiblingClass;
};
  • class_ro_t:App 启动时从 Mach-O 的 __DATA,__objc_data 段加载,编译期就确定下来——包括本类直接声明的方法、ivar、属性、协议。只读,不可修改
  • class_rw_t:类在首次被使用前("realize" 阶段),Runtime 为其分配 class_rw_t,将 class_ro_t 的内容复制/引用过来,然后空出 methods/properties/protocols 供后续 Category 和 Runtime 动态修改合并。
  • 从 iOS 14 / macOS Big Sur 开始,Apple 引入了 class_rw_ext_t 延迟分配优化——对于没有 Category、没有 Runtime 动态修改的类,不需要分配完整的可写方法/属性/协议数组,节省约 30% 的 class_rw_t 内存。
  • class_data_bits_t 内部利用指针的对齐位做标志位,来区分当前指向的是 class_ro_t(realize 之前)还是 class_rw_t(realize 之后)。

消息发送完整流程

调用 [receiver message] 时,编译器将其转为:

objc_msgSend(receiver, @selector(message), arg1, arg2, ...);

Runtime 在运行时执行以下查找链:

第一阶段:消息查找
objc_msgSend(receiver, sel)
    │
    ├─ 1. 检测 receiver 是否为 nil
    │     → nil 直接返回(OC 向 nil 发消息不会崩溃)
    │
    ├─ 2. 从 receiver->isa 找到类对象
    │
    ├─ 3. 在类对象的 cache_t 中查找缓存
    │     → 找到则直接调用 IMP(哈希查找,O(1))
    │
    ├─ 4. 缓存未命中 → 在 class_rw_t 的方法列表中查找
    │     → 二分查找(已排序)或遍历
    │
    ├─ 5. 本类未找到 → 沿 superclass 链逐级查找
    │     → 每级先查 cache,再查方法列表
    │
    └─ 6. 到达 nil(根类的 superclass 为 nil)仍未找到
          → 进入第二阶段:消息转发

缓存通过 cache_t 实现,采用哈希表结构,以 SEL 为 key,IMP 为 value。每次调用后结果会被缓存,后续调用 O(1)。

第二阶段:消息转发 (Message Forwarding)

未找到实现时,Runtime 给予三次补救机会

                    +-----------------------------+
                    | receiver 无法响应当前消息   |
                    +-----------------------------+
                              |
            +-----------------+------------------+
            |                                    |
            ↓                                    ↓
  ① 动态方法解析                         ② 快速转发
  resolveInstanceMethod:               forwardingTargetForSelector:
  resolveClassMethod:                  返回一个能处理此消息的对象
  可在此用 class_addMethod              (最轻量,适合把消息转给备选者)
  动态添加实现                                   |
            |                                    |
            ↓                                    | (返回 nil 则继续)
  ② 快速转发                                    ↓
  (若返回 nil)                        ③ 完整消息转发
            |                       methodSignatureForSelector:
            ↓                       forwardInvocation:
  ③ 完整消息转发                     拿到 NSInvocation 做任意处理
                                     (包裹消息转发、日志、防崩溃)
① 动态方法解析 (Dynamic Method Resolution)
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(dynamicMethod)) {
        class_addMethod(self, sel, (IMP)dynamicIMP, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}
② 备用接收者 (Fast Forwarding)
- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(someMethod)) {
        return self.backupObject; // 交由备用对象处理
    }
    return [super forwardingTargetForSelector:aSelector];
}
③ 完整消息转发 (Normal Forwarding)
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if (aSelector == @selector(someMethod)) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    if ([self.backupObject respondsToSelector:anInvocation.selector]) {
        [anInvocation invokeWithTarget:self.backupObject];
    } else {
        // 可以做防崩溃兜底,不要默认调用 doesNotRecognizeSelector:
        NSLog(@"[runtime] 未处理的方法: %@", NSStringFromSelector(anInvocation.selector));
    }
}

高频追问:消息转发 vs 继承的区别? 继承是静态编译关系,转发是运行时动态决策。转发可以让对象把消息交给"运行时才知道"的目标处理,更灵活但也更难调试。


Category (分类)

底层原理
  • 分类的方法在编译时存储在独立的 category_t 结构体中。
  • Runtime 的 load 阶段_objc_initmap_imagesload_images),Runtime 将分类的方法、属性、协议合并到主类的 class_rw_t 中。
  • 分类的方法被添加到类的方法列表前面,因此分类方法会"覆盖"主类方法(实际上是同名前移,主类方法仍在列表中)。
方法覆盖规则
类的方法列表:  [mainMethod1, mainMethod2]
分类1的方法:   [categoryMethod1]
分类2的方法:   [categoryMethod2]

合并后:       [categoryMethod2, categoryMethod1, mainMethod1, mainMethod2]
                                        ↑
                             查找时先找到分类方法,"覆盖"了主类方法
  • 多个分类都有同名方法 → 最后编译的分类获胜(Build Phases → Compile Sources 中的文件顺序决定)。
  • 如果希望调用主类的"原始"方法,可以拿到主类的 IMP 直接调用绕过查找。 具体实现:Category 的方法被合并到 class_rw_t.methods 数组的前面,objc_msgSend 的查找顺序是从头到尾,找到第一个就返回,所以分类的版本"覆盖"了主类。但主类的原始 IMP 仍然在数组后面,没有被删除。先获取所有方法列表Method *methods = class_copyMethodList(cls, &count); 然后通过sel == method_getName(m)去倒序匹配sel。匹配上了method_getImplementation(m) 取出末尾的那个原始 IMP 然后通过函数指针调用,就绕过了消息查找的"先到先得"规则。具体实现如下:
// 调用时 — 绕过查找,直接拿到主类的 IMP
IMP getOriginalIMP(Class cls, SEL sel) {
    unsigned int count = 0;
    Method *methods = class_copyMethodList(cls, &count);
    IMP result = NULL;

    // 方法列表顺序:[categoryMethod, mainClassMethod]
    // index=0 是分类的,index=1 才是主类的
    for (unsigned int i = 0; i < count; i++) {
        Method m = methods[i];
        if (sel == method_getName(m)) {
            result = method_getImplementation(m);
            // 不 break,继续遍历取最后一个(主类的在最后)
        }
    }
    free(methods);
    return result;
}
+load 和 +initialize 的区别
+load+initialize
调用时机App 启动时,类被加载进内存立即调用类第一次收到消息之前
调用顺序父类 → 子类 → 分类(分类晚于本类)父类先初始化,子类未实现则调用父类的
线程安全主线程串行可能多线程,需要加锁(Runtime 保证只会调用一次,但不保证线程安全)
分类行为每个分类的 +load 都执行分类实现了则覆盖本类的 +initialize
调用方式直接通过函数指针调用(不是消息发送)通过 objc_msgSend 调用

关联对象 (Associated Objects)

允许在分类中为类添加存储属性(分类不能直接添加成员变量)。

#import <objc/runtime.h>

static const void *kAssociatedKey = &kAssociatedKey;

// setter
- (void)setCustomProperty:(id)value {
    objc_setAssociatedObject(self, kAssociatedKey, value, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

// getter
- (id)customProperty {
    return objc_getAssociatedObject(self, kAssociatedKey);
}

关联策略

策略对应 property 语义
OBJC_ASSOCIATION_ASSIGNassign
OBJC_ASSOCIATION_RETAIN_NONATOMICstrong, nonatomic
OBJC_ASSOCIATION_COPY_NONATOMICcopy, nonatomic
OBJC_ASSOCIATION_RETAINstrong, atomic
OBJC_ASSOCIATION_COPYcopy, atomic

注意:关联对象在对象释放时由 Runtime 负责清理,但不会像 dealloc 那样及时。另外,关联对象的值会被系统 objc_removeAssociatedObjects 清除(不建议手动调用,影响其他关联)。


方法交换 (Method Swizzling)

安全实现模板
+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];

        SEL originalSelector = @selector(originalMethod);
        SEL swizzledSelector = @selector(swizzledMethod);

        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) {
            // 主类没有 originalMethod(从父类继承来的)
            // 把 swizzledMethod 的 IMP 换成父类原始的 IMP
            class_replaceMethod(
                class,
                swizzledSelector,
                method_getImplementation(originalMethod),
                method_getTypeEncoding(originalMethod)
            );
        } else {
            // 主类自己有 originalMethod,直接互换
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}
常见陷阱
  1. +load vs +initialize:Swizzle 必须在 +load 中执行,如果在 +initialize 中做,可能因为子类未实现 +initialize 而被父类的 Swizzle 意外影响。
  2. dispatch_once:必须保证只执行一次,否则反复交换会回到原始状态。
  3. 线程安全:Swizzle 本身只应在单线程中做一次,但 Swizzle 之后的方法调用应该是线程安全的(取决于具体实现)。
  4. 命名冲突:分类方法名加前缀(如 track_sendAction:to:forEvent:),降低冲突概率。

高频追问清单

问题关键回答要点
isa 指针在 ARM64 下如何优化?non-pointer isa,利用 64 位中的 32+ 位存储引用计数、是否被 weak 引用等信息
方法缓存如何实现?cache_t 哈希表,以 SEL 为 key,bucket_t 存储 SEL+IMP,散列后查找
objc_msgSend 为什么用汇编写?需要支持未知参数数量的跳转、寄存器保护,C 无法做到;同时汇编有最优性能
消息转发三阶段能举个例子吗?第一阶段:动态添加方法(@dynamic);第二阶段:转给代理对象;第三阶段:防崩溃统一处理
Category 能添加成员变量吗?不能直接添加(编译期 ivar 偏移已固定),但可以用关联对象实现存储
Swizzle 多个 Category 同时 Swizzle 怎么办?顺序取决于 Compile Sources 顺序,后加载的方法优先。建议统一在一个地方管理 Swizzle
Runtime 的 methodsel 区别?SEL 是方法名注册后的唯一 ID;Method 包含 SEL + IMP + 类型编码的完整结构体
loadinitialize 哪个适合 Swizzle?load,因为确定且安全,initialize 交换可能导致死循环

项目落地版

场景 1:无侵入点击埋点(方法交换)

@interface UIButton (Track)
@end

@implementation UIButton (Track)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [self swizzleInstanceMethod:@selector(sendAction:to:forEvent:)
                        withMethod:@selector(track_sendAction:to:forEvent:)];
    });
}

- (void)track_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {
    // 埋点上报(可在异步线程、控制采样率)
    NSLog(@"[跟踪] 按钮事件: %@ - %@", NSStringFromClass([target class]), NSStringFromSelector(action));
    // 调用原始实现
    [self track_sendAction:action to:target forEvent:event];
}

@end

场景 2:通用 Swizzle 工具 + 防崩溃封装

// NSObject+SafeSwizzle.h
@interface NSObject (SafeSwizzle)

/// 安全交换实例方法
/// @return 交换是否成功;失败时 error 包含原因
+ (BOOL)swizzleInstanceMethod:(SEL)original withMethod:(SEL)swizzled error:(NSError **)error;

/// 安全交换类方法
+ (BOOL)swizzleClassMethod:(SEL)original withMethod:(SEL)swizzled error:(NSError **)error;

@end

// NSObject+SafeSwizzle.m
@implementation NSObject (SafeSwizzle)

+ (BOOL)swizzleInstanceMethod:(SEL)original withMethod:(SEL)swizzled error:(NSError **)error {
    Class cls = [self class];
    Method origMethod = class_getInstanceMethod(cls, original);
    Method newMethod = class_getInstanceMethod(cls, swizzled);

    // 防护1:任一方法不存在,不 crash 而是返回错误
    if (!origMethod || !newMethod) {
        if (error) {
            *error = [NSError errorWithDomain:@"SwizzleError"
                                         code:-1
                                     userInfo:@{NSLocalizedDescriptionKey:
                                                   [NSString stringWithFormat:
                                                    @"Swizzle 失败: %@/%@ 不存在",
                                                    NSStringFromSelector(original),
                                                    NSStringFromSelector(swizzled)]}];
        }
        return NO;
    }

    // 防护2:class_addMethod 兜底——防止 original 是父类实现导致交换到父类
    BOOL didAdd = class_addMethod(cls, original,
                                  method_getImplementation(newMethod),
                                  method_getTypeEncoding(newMethod));
    if (didAdd) {
        class_replaceMethod(cls, swizzled,
                            method_getImplementation(origMethod),
                            method_getTypeEncoding(origMethod));
    } else {
        method_exchangeImplementations(origMethod, newMethod);
    }

    // 防护3:验证交换后 original 的 IMP 确实是新方法的 IMP
    Method checkOrig = class_getInstanceMethod(cls, original);
    if (method_getImplementation(checkOrig) == method_getImplementation(origMethod)) {
        // 交换未生效——说明 class_addMethod 没走,method_exchangeImplementations 也未修改
        // 此时记录日志告警
        NSLog(@"[Swizzle Warning] 方法交换疑似未生效: %@", NSStringFromSelector(original));
    }

    return YES;
}

+ (BOOL)swizzleClassMethod:(SEL)original withMethod:(SEL)swizzled error:(NSError **)error {
    Class metaCls = object_getClass((id)[self class]);
    Method origMethod = class_getInstanceMethod(metaCls, original);
    Method newMethod = class_getInstanceMethod(metaCls, swizzled);

    if (!origMethod || !newMethod) {
        if (error) {
            *error = [NSError errorWithDomain:@"SwizzleError" code:-1
                                     userInfo:@{NSLocalizedDescriptionKey: @"类方法不存在"}];
        }
        return NO;
    }

    method_exchangeImplementations(origMethod, newMethod);
    return YES;
}

@end

这个方法是工具方法,本身不在任何 +load 里——它由其他分类的 +load 来调用:

// 调用方:某个具体的 Swizzle 场景
@implementation UIViewController (Tracking)

+ (void)load {
    NSError *error = nil;
    BOOL ok = [UIViewController swizzleInstanceMethod:@selector(viewDidAppear:)
                                           withMethod:@selector(track_viewDidAppear:)
                                               error:&error];
    if (!ok) {
        // ⚠️ 交换失败,不会 crash,可以告警/降级
        NSLog(@"Swizzle 失败: %@", error);
        // 生产环境可以上报到 APM,而不是静默崩溃
    }
}

它防的是这几种真实 crash 场景:

场景 A:方法名写错了(拼写错误)

// 假设手滑写成 viewDidAppear: → viewDidAppear:(冒号变位置)
[UIViewController swizzleInstanceMethod:@selector(viewDidAppear:)
                             withMethod:@selector(track_viewDidAppear)
                                 error:&error];
//                                    ↑ 少了冒号,这个方法不存在
  • 没防护前:class_getInstanceMethod 返回 NULL,传给 method_exchangeImplementations → EXC_BAD_ACCESS
  • 有防护:返回 NO,error 里有"track_viewDidAppear 不存在"

场景 B:类没有实现这个方法(从父类继承的)

@interface MyView : UIView  // UIView 没有 viewDidAppear:
@end

// 如果有人对 MyView 做 viewDidAppear: 的 Swizzle
// class_addMethod 会成功(因为 MyView 没有),但等价于给 MyView 新增了一个
// 实际场景:Swift 类继承,OC 方法只在父类有
  • 没防护前:虽然不 crash,但 IMP 替换行为不符合预期(可能改到了父类)
  • 有防护:class_addMethod 正确兜底

场景 C:同一个 Swizzle 被执行了两次(+load 在子类/分类中被重复触发)

// 场景2 的代码还特意返回 BOOL,让调用方可以判断是否重复执行
// 配合 dispatch_once,确保不会发生"交换一次→恢复→再交换→回到原点"的经典问题

所以这个封装的实际价值不是 Swizzle 执行过程中防 crash(Swizzle 本身很少 crash),而是当人为失误(拼写错误、选错类、重复触发)发生时,把 ObjC 的隐式崩溃变成显式的错误返回值,让调用方能感知到问题。

场景 3:Unrecognized Selector 防崩溃

@interface NSObject (CrashGuard)
@end

@implementation NSObject (CrashGuard)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [self swizzleInstanceMethod:@selector(forwardingTargetForSelector:)
                         withMethod:@selector(safe_forwardingTargetForSelector:)];
    });
}

- (id)safe_forwardingTargetForSelector:(SEL)aSelector {
    // 如果是当前类没有的方法,返回一个兜底处理对象
    if (![self respondsToSelector:aSelector]) {
        // 返回一个临时对象来接收消息,避免崩溃
        return [SafeReceiver sharedReceiver];
    }
    return [self safe_forwardingTargetForSelector:aSelector];
}

@end

场景 4:字典转 Model(KVC + Runtime)

利用 Runtime 获取类的属性列表,遍历并自动赋值:

- (instancetype)initWithDictionary:(NSDictionary *)dict {
    if (self = [self init]) {
        unsigned int count = 0;
        objc_property_t *properties = class_copyPropertyList([self class], &count);

        for (unsigned int i = 0; i < count; i++) {
            objc_property_t property = properties[i];
            const char *name = property_getName(property);
            NSString *key = [NSString stringWithUTF8String:name];
            id value = dict[key];

            if (value && value != [NSNull null]) {
                // 根据属性类型自动转换(略)
                [self setValue:value forKey:key];
            }
        }
        free(properties);
    }
    return self;
}

场景 5:动态添加方法(用于 @dynamic 声明)

@interface DynamicObject : NSObject
@property (nonatomic, copy) NSString *name;
@end

@implementation DynamicObject
@dynamic name; // 告诉编译器不做 synthesize

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(setName:)) {
        class_addMethod(self, sel, imp_implementationWithBlock(^(id self, NSString *name) {
            objc_setAssociatedObject(self, @selector(name), name, OBJC_ASSOCIATION_COPY_NONATOMIC);
        }), "v@:@");
        return YES;
    } else if (sel == @selector(name)) {
        class_addMethod(self, sel, imp_implementationWithBlock(^(id self) {
            return objc_getAssociatedObject(self, _cmd);
        }), "@@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}
@end

深入学习路径与优先级

初级 (P1) — 理清概念

目标:理解对象、类、元类的关系,能用 Runtime 做基础操作。

  • 理解 isa 指针的作用和指向关系
  • 画一张实例对象、类对象、元类的指向图
  • 区分 instanceMethod vs classMethod 的存储位置
  • 知道 @selector() / sel_registerName() / NSSelectorFromString() 的区别
  • 理解 Category 的原理:它能把方法放到哪里?不能放什么?
  • 掌握关联对象(Associated Object)的基本使用
  • 读 Runtime 源码的关键数据结构定义(objc_object, objc_class, method_t

自我检查

  • 能说清楚 [NSObject alloc] 的查找过程(通过元类链)
  • 能用 Runtime 打印一个类的所有属性/方法/协议

中级 (P0) — 掌握消息传递与转发

目标:理解消息发送的全链路,能安全使用方法交换。

  • 掌握 objc_msgSend 的完整查找流程(缓存 → 本类 → 父类 → 转发)
  • 手动实现一次消息转发(三个阶段都走一遍)
  • 理解 cache_t 哈希表结构:查找和插入策略
  • 方法交换(Swizzle)的安全模板(含 class_addMethod 兜底)
  • 理解 +load+initialize 的调用时机差异,以及为什么 Swizzle 在 +load 中做
  • 理清 class_ro_t vs class_rw_t 的职责划分
  • 了解 swift_allocObjectobjc_alloc 的关系
  • 理解 KVO 的 Runtime 实现原理(NSKVONotifying_* 中间类 + isa-swizzling)

动手实践

  1. 写一个小工具,打印一个 NSObject 子类的完整继承链 + 方法列表
  2. 实现一个安全 Swizzle 的 Category(上面场景 2 的代码)
  3. 写一段代码,验证消息转发三阶段(用异常断点配合观察)

自我检查

  • 不用查资料,手写安全 Swizzle 模板
  • 能解释:为什么两个 Category Swizzle 同一个方法,行为不可预测?
  • 能画图:一个对象收到未知消息后,Runtime 内部走了哪些路径

高级 (P1) — 设计 AOP 与动态架构

目标:能用 Runtime 做架构层面的动态化设计,并控制其风险。

  • 设计一个 AOP 框架:基于 Swizzle 的面向切面编程(Hook 任意方法的前后)
  • 理解 class_copyIvarList / class_copyPropertyList / class_copyMethodList 的底层差异
  • 掌握 super 关键字的本质:objc_msgSendSuper 发送给父类 vs self
  • 理解 _block invoke 与 Block 的 Runtime 结构
  • 理解现代 Runtime 的优化:class_rw_ext_t 延迟分配、non-pointer isa
  • Runtime 与 Swift 的交互:@objc / @objcMembers / dynamic 在 Swift 中的行为
  • 了解 NSProxy 的实现原理:它是一个不需要继承 NSObject 的纯消息转发类
  • 应对面试追问:"Runtime 还能用来做什么?"

实战项目

  1. 实现一个方法级别的 AOP 切面库(类似 Aspects),支持 before/after/around 三种模式
  2. NSProxy 实现一个延迟初始化代理或网络请求重试代理
  3. 为已有的网络库(如 NSURLSession)加上无侵入的请求日志/计时 AOP

自我检查

  • 能设计一套方案,在不动业务代码的前提下给所有 VC 的 viewWillAppear: 注入统一的打点
  • 能分析 Swizzle 引入的风险,并给出工程层面的治理方案(统一注册 + 白名单 + 测试覆盖)
  • 能解释 Apple 为什么在 iOS 14+ 收紧了对 objc_msgSend 的调用方式限制

Runtime 相关的系统框架应用

框架/机制Runtime 角色
KVOisa-swizzling:运行时创建 NSKVONotifying_XXX 中间子类,重写 setter
KVCclass_copyIvarList + objc_msgSend:按 key 路径查找 getter/setter/ivar
CoreData动态生成 NSManagedObject 子类的存取方法,基于 @dynamic + resolveInstanceMethod:
NSCoding / NSSecureCodingclass_copyIvarList 实现自动归档/解档
UIStoryboard / XIBinitWithCoder: + awakeFromNib 由 Runtime 驱动对象重建
NSDictionary / NSArray 桥接Toll-free bridging 依赖 isa 指针的运行时转换
Swift 的 @objc 动态派发Swift 类标记 @objcMembers 后,会生成 OC 兼容的 vtable 和消息派发表