一.iOS Objective-C Runtime 原理

Objective-C Runtime

面试回答版

一句话概括

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


核心数据结构

对象 & 类
objc 复制代码
// 对象结构
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
text 复制代码
实例对象  ──isa──→  类对象  ──isa──→  元类  ──isa──→ 根元类 ──isa──→ 自身
                       │                  │
                       ↓                  ↓
                     superclass         superclass
                       │                  │
                       ↓                  ↓
                     父类对象            根元类 ──superclass──→ 根类对象

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

Method / SEL / IMP
objc 复制代码
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 间接访问:

text 复制代码
objc_class
  ├─ isa
  ├─ superclass
  ├─ cache
  └─ bits ───→  class_rw_t(类 realize 后创建,可读写)
                    │
                    ├─ methods      ← 可扩展(含本类 + Category 合并的)
                    ├─ properties   ← 可扩展
                    ├─ protocols    ← 可扩展
                    ├─ firstSubclass / nextSiblingClass
                    │
                    └─ ro ───→  class_ro_t(只读,编译期确定)
                                  ├─ baseMethodList    ← 编译期就有的方法
                                  ├─ baseProtocols
                                  ├─ ivars             ← 成员变量列表
                                  └─ baseProperties
objc 复制代码
// 编译期确定的只读数据(从 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] 时,编译器将其转为:

c 复制代码
objc_msgSend(receiver, @selector(message), arg1, arg2, ...);

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

第一阶段:消息查找
text 复制代码
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 给予三次补救机会

text 复制代码
                    +-----------------------------+
                    | receiver 无法响应当前消息   |
                    +-----------------------------+
                              |
            +-----------------+------------------+
            |                                    |
            ↓                                    ↓
  ① 动态方法解析                         ② 快速转发
  resolveInstanceMethod:               forwardingTargetForSelector:
  resolveClassMethod:                  返回一个能处理此消息的对象
  可在此用 class_addMethod              (最轻量,适合把消息转给备选者)
  动态添加实现                                   |
            |                                    |
            ↓                                    | (返回 nil 则继续)
  ② 快速转发                                    ↓
  (若返回 nil)                        ③ 完整消息转发
            |                       methodSignatureForSelector:
            ↓                       forwardInvocation:
  ③ 完整消息转发                     拿到 NSInvocation 做任意处理
                                     (包裹消息转发、日志、防崩溃)
① 动态方法解析 (Dynamic Method Resolution)
objc 复制代码
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(dynamicMethod)) {
        class_addMethod(self, sel, (IMP)dynamicIMP, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}
② 备用接收者 (Fast Forwarding)
objc 复制代码
- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(someMethod)) {
        return self.backupObject; // 交由备用对象处理
    }
    return [super forwardingTargetForSelector:aSelector];
}
③ 完整消息转发 (Normal Forwarding)
objc 复制代码
- (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 中。
  • 分类的方法被添加到类的方法列表前面,因此分类方法会"覆盖"主类方法(实际上是同名前移,主类方法仍在列表中)。
方法覆盖规则
text 复制代码
类的方法列表:  [mainMethod1, mainMethod2]
分类1的方法:   [categoryMethod1]
分类2的方法:   [categoryMethod2]

合并后:       [categoryMethod2, categoryMethod1, mainMethod1, mainMethod2]
                                        ↑
                             查找时先找到分类方法,"覆盖"了主类方法
  • 多个分类都有同名方法 → 最后编译的分类获胜(Build Phases → Compile Sources 中的文件顺序决定)。
  • 如果希望调用主类的"原始"方法,可以拿到主类的 IMP 直接调用绕过查找。 -- 具体实现:Category 的方法被合并到 class_rw_t.methods 数组的前面,objc_msgSend 的查找顺序是从头到尾,找到第一个就返回,所以分类的版本"覆盖"了主类。但主类的原始 IMP 仍然在数组后面,没有被删除。直接用 method_getImplementation 取出末尾的那个原始 IMP 然后通过函数指针调用,就绕过了消息查找的"先到先得"规则
+load 和 +initialize 的区别
+load +initialize
调用时机 App 启动时,类被加载进内存立即调用 类第一次收到消息之前
调用顺序 父类 → 子类 → 分类(分类晚于本类) 父类先初始化,子类未实现则调用父类的
线程安全 主线程串行 可能多线程,需要加锁(Runtime 保证只会调用一次,但不保证线程安全)
分类行为 每个分类的 +load 都执行 分类实现了则覆盖本类的 +initialize
调用方式 直接通过函数指针调用(不是消息发送) 通过 objc_msgSend 调用

关联对象 (Associated Objects)

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

objc 复制代码
#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_ASSIGN assign
OBJC_ASSOCIATION_RETAIN_NONATOMIC strong, nonatomic
OBJC_ASSOCIATION_COPY_NONATOMIC copy, nonatomic
OBJC_ASSOCIATION_RETAIN strong, atomic
OBJC_ASSOCIATION_COPY copy, atomic

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


方法交换 (Method Swizzling)

安全实现模板
objc 复制代码
+ (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:无侵入点击埋点(方法交换)

objc 复制代码
@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 工具 + 防崩溃封装

objc 复制代码
// NSObject+SafeSwizzle.h
@interface NSObject (SafeSwizzle)
+ (void)swizzleInstanceMethod:(SEL)original withMethod:(SEL)swizzled;
+ (void)swizzleClassMethod:(SEL)original withMethod:(SEL)swizzled;
@end

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

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

    if (class_addMethod(cls, original, method_getImplementation(newMethod),
                        method_getTypeEncoding(newMethod))) {
        class_replaceMethod(cls, swizzled, method_getImplementation(origMethod),
                            method_getTypeEncoding(origMethod));
    } else {
        method_exchangeImplementations(origMethod, newMethod);
    }
}

+ (void)swizzleClassMethod:(SEL)original withMethod:(SEL)swizzled {
    Class cls = object_getClass((id)[self class]);
    Method origMethod = class_getInstanceMethod(cls, original);
    Method newMethod = class_getInstanceMethod(cls, swizzled);
    method_exchangeImplementations(origMethod, newMethod);
}

@end

场景 3:Unrecognized Selector 防崩溃

objc 复制代码
@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 获取类的属性列表,遍历并自动赋值:

objc 复制代码
- (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 声明)

objc 复制代码
@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 角色
KVO isa-swizzling:运行时创建 NSKVONotifying_XXX 中间子类,重写 setter
KVC class_copyIvarList + objc_msgSend:按 key 路径查找 getter/setter/ivar
CoreData 动态生成 NSManagedObject 子类的存取方法,基于 @dynamic + resolveInstanceMethod:
NSCoding / NSSecureCoding class_copyIvarList 实现自动归档/解档
UIStoryboard / XIB initWithCoder: + awakeFromNib 由 Runtime 驱动对象重建
NSDictionary / NSArray 桥接 Toll-free bridging 依赖 isa 指针的运行时转换
Swift 的 @objc 动态派发 Swift 类标记 @objcMembers 后,会生成 OC 兼容的 vtable 和消息派发表
相关推荐
hhemin6 分钟前
web前端给项目加入skills目录,Ai自动查找技能(后端也能参考)
前端
代码煮茶12 分钟前
Vue3 组件库二次封装实战 | 基于 Element Plus 封装企业级 UI 组件库
前端·javascript·vue.js
KaMeidebaby12 分钟前
卡梅德生物技术快报|单克隆抗体人源化 PEG 修饰质控方法体系构建与验证
服务器·前端·数据库·人工智能·算法·百度·新浪微博
元宵大师18 分钟前
[升级V2.1.5]回测模块重构:参数确认+异步进度+日志持久化!本地Web版多因子轮动系统
前端·重构
咋吃都不胖lyh27 分钟前
限流重试、指数退避、随机抖动
前端
之歆1 小时前
DAY_11JavaScript BOM与DOM深度解析:底层原理与工程实践(上)
开发语言·前端·javascript·ecmascript
冴羽yayujs1 小时前
GitHub 前端热榜项目 - 日榜(2026-05-17)
前端·github
老马95271 小时前
opencode8-桌面应用实战 3
前端·人工智能·后端
largecode1 小时前
企业名称能在来电显示吗?号码显示公司名服务打通多终端展示
android·xml·ios·iphone·xcode·webview·phonegap