iOS 社招 - Runtime 相关知识点

核心概念

本质runtime是 oc 的一个运行时库(libobjc.A,dylib),它为 oc 添加了 面向对象的能力 以及 运行时的动态特性

  • 面向对象的能力rutime用 C 语言实现了类、对象、封装、继承、多态等面向对象的核心概念。
  • 运行时的动态特性runtime可以让很多操作可以在 运行时 确定,如 动态方法调用、动态类型检查、动态方法添加、动态属性访问、动态类创建消息转发机制等。
    • 动态语言 的优劣势
      • 优势
        1. 灵活性:可以在运行时修改程序的行为
        2. 可扩展性:可以动态添加功能
        3. 反射能力:可以在运行时检查和操作对象
      • 代价
        1. 性能开销:运行时查找比编译时确定慢
        2. 类型安全:可编译时无法检查所有错误
        3. 调试困难:动态行为难以追踪

Runtime数据结构

首先我们要清晰,所有的 OC 对象(包括 NSObject 自身)在内存中,本质上都是一个 objc_object 结构体实例。然后了解对象体系之间的关系(见下文)。

swift 复制代码
/// 核心数据结构
/// 
/// 1. 任何实例(`id` 就是 objc_object * 类型)
struct objc_object {
  Class _Nonnull isa;    // isa 指针,指向对象的类
};

/// 2. 类对象 / 元类对象
struct objc_class {
    Class _Nonnull isa;  // 继承自 objc_object
    Class _Nullable super_class;
    cache_t cache;           // 方法缓存
    class_data_bits_t bits;  // 类的具体信息 -> class_rw_t
};

struct class_rw_t {
    method_array_t methods;     // -> objc_method[](元类对象存储类方法、类对象存储实例方法)
    property_array_t properties;  // -> objc_property[]
    ivar_array_t ivars;                // -> objc_ivar[]
    const class_ro_t *ro; // 编译期只读信息
};

/// 3. 方法描述
struct objc_method {
    SEL name;      // 方法名(方法选择子)
    const char *types;  // 方法类型编码
    IMP imp;        // 方法实现(函数指针)
};

/// 4. 属性描述
struct   objc_property {
    const char * _Nonnull name;           // 属性名
    const char * _Nullable attributes;    // 属性特性
};

/// 5. 实例变量
struct objc_ivar {
    char * _Nullable name;     // 变量名
    char * _Nullable type;      // 变量类型
    int offset;                      // 变量偏移量
    uint32_t alignment_raw;         // 对齐信息
    uint32_t size;                  // 变量大小
}; 

1. isa 指针

含义isa指针是 OC 对象中,指向其类对象和元类对象的指针。当对象接收到消息时,系统通过isa定位类对象,从而查找方法实现。 作用 :决定对象的类型、以及方法查找起点。比如实例对象的isa决定它属于哪个类,类方法的查找从元类对象开始。

优化 (64-bit) :在 64 位系统中,isa被设计为了叫做「位域 (bitfield)」的结构。除了存放类指针外,还内嵌了一些状态位(引用计数、弱引用、析构标志等),这样做可以将小对象的数据直接编码进指针本身,节省了小对象的内存与访问开销。

arduino 复制代码
union isa_t {
    Class cls;
    uintptr_t bits;
    struct {
        uintptr_t nonpointer        : 1; // 是否启用优化
        uintptr_t has_assoc         : 1;
        uintptr_t has_cxx_dtor      : 1;
        uintptr_t shiftcls          : 33; // 存真正的类指针
        uintptr_t magic             : 6;
        uintptr_t weakly_referenced : 1;
        uintptr_t deallocating      : 1;
        uintptr_t has_sidetable_rc  : 1;
        uintptr_t extra_rc          : 19;
    };
};

对象模型

对象、类、元类关系:

Person类举个例子:

objectivec 复制代码
@interface Person : NSObject
@property (nonatomic, strong) NSString *name;
- (void)changeNameWith:(NSString *)newName;
+ (instancetype)mockPerson;
@end

Person *p1 = [Person new];

实际的对象体系链路:

scss 复制代码
实例对象:object_object (p1) 
            _____
           | isa | ---> 类对象:object_class (Person.class)
            _____             ____________
                             | isa         | -------------
c_object (NSObject.class) <- | super_class |              |         methods[] (-changeNameWith:)
                             | bits        |  ------------------> { properties[] (name)
                              ____________                |         ivars[] (_name)
                                                          ▼
                                           元类对象:object_class (Person.meta)
                                    ____________
                                   | isa         | ---------
     objc_class (NSObject.meta) <- | super_class |          |            methods[] (+alloc、+mockPerson)
                                   | bits        |  ------------------>{ properties[] (无)
                                    ____________            |            ivars[] (无) 
                                                            ▼
                                              根元类对象:objc_class (NSObject.meta)
                                                      ____________
                                                     | isa         | ---> 指向自己
                      objc_class (NSObject.class) <- | super_class |       
                                                     | bits        |  
                                                      ____________  

消息传递机制

1. 消息发送机制

当代码写:

ini 复制代码
[p sayHello];

编译器会转化成:

less 复制代码
objc_msgSend(p, @selector(sayHello));

objc_msgSend 查找流程:

  1. 检查 实例对象 是否为 nil
  2. 实例对象isa指针找到 类对象
  3. 类对象cache_t中查找方法
  4. 没命中缓存,则在 类对象 的方法列表中查找
  5. 没找到,沿着继承链向上查找
  6. 如果都未找到,进入消息转发流程

缓存机制(cache_t):哈希查找 SEL

arduino 复制代码
struct cache_t {
    explicit_atomic<uintptr_t> _bucketsAndMaybeMask; // 哈希表
    union {
        struct {
            explicit_atomic<mask_t> _maybeMask;      // 掩码
            uint16_t _flags;                         // 标志位
            uint16_t _occupied;                      // 已占用数量
        };
        explicit_atomic<preopt_cache_t *> _originalPreoptCache;
    };
    
    // 哈希查找算法
    bucket_t *find(SEL s, id receiver);
    void insert(SEL sel, IMP imp, id receiver);
};

2. 消息转发机制

  1. 阶段一:动态方法解析 a. 运行时动态添加 实例方法 实现:+ (BOOL)resolveInstanceMethod:(SEL)sel b. 在运行时动态添加 类方法 实现:+ (BOOL)resolveClassMethod:(SEL)sel 「成功返回 YES;失败返回 NO,继续快速转发」

  2. 阶段二:快速转发 a. 返回一个能处理该消息的对象,Runtime 会将消息转发给该对象:- (id)forwardingTargetForSelector:(SEL)aSelector 「成功返回对象;失败返回nil,继续完整转发」

  3. 阶段三:完整转发 a. 返回方法签名:- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector b. 执行实际的消息转发逻辑:- (void)forwardInvocation:(NSInvocation *)anInvocation 「失败抛出异常」

  4. 抛出异常 a. - (void)doesNotRecognizeSelector:(SEL)aSelector

快速转发 vs 完整转发

快速转发 完整转发
调用时机 动态方法解析失败 快速转发失败
返回值 目标对象 void
调用机制 重新调用 objc_msgSend(newTarget, sel) 手动通过 NSInvocation 调用
性能 🚀 快 🐢慢
使用场景 简单转发给另一个对象 复杂消息路由(动态修改调用)
常见用途 代理、代理转发 消息分发中心、AOP

NSInvocation 介绍

完整转发最后会用到的类,NSInvocation是什么?

  • 封装方法调用的对象
  • 存储目标对象、选择器、参数、返回值
  • 可以延迟执行或多次执行

Runtime API 与动态能力

1. 方法交换(Method Swizzing)

本质 :修改方法列表中 SELIMP 的对应关系 注意事项

  1. 在 +load 方法中进行,确保类加载时执行。
    • 原因:+load 在类加载到内存时调用,早于任何消息发送,避免竞态。
  2. 使用 dispatch_once 确保只执行一次。
    • 原因:避免重复交换,导致方法实现被换回原样,保证线程安全。
  3. 考虑继承关系,避免重复交换。
    • 原因:子类继承父类方法,如果父子交换同一个方法,会导致重复交换,交换失败。
  4. 方法签名要匹配。
    • 原因:方法签名不匹配,会导致参数传递错误、返回值错误、内存问题等。
  5. 在交换的方法中调用原方法
    • 原因:保持原有功能,避免功能丢失;保证调用链完整;避免破坏其他依赖。
scss 复制代码
@implementation UIViewController (Swizzling)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [self swizzleMethod:@selector(viewDidLoad) 
                 withMethod:@selector(swizzled_viewDidLoad)];
    });
}

+ (void)swizzleMethod:(SEL)originalSelector withMethod:(SEL)swizzledSelector {
    Class class = [self class];
    
    Method originalMethod = class_getInstanceMethod(class, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
    
    // 尝试添加原方法,如果已存在则返回 NO
    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 {
    // 调用原始实现(此时已经交换,所以调用自己就是调用原方法)
    [self swizzled_viewDidLoad];
    
    // 添加自定义逻辑
    NSLog(@"viewDidLoad called for %@", NSStringFromClass([self class]));
}

@end

2. 动态特性应用

2.1. 动态添加方法

less 复制代码
void dynamicRun(id self, SEL _cmd) {
    NSLog(@"动态添加了 run 方法 ✅");
}

@implementation Dog
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(run)) {
        class_addMethod(self, sel, (IMP)dynamicRun, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}
@end

// 调用
Dog *d = [Dog new];
[d run]; // 输出:动态添加了 run 方法 ✅

📍应用: • 动态桥接(比如 JS ↔ OC 交互) • 模块化架构下的延迟加载方法 • 动态生成代理类 / Mock 类 • 序列化或模型自动解析(根据字段动态添加访问方法)

2.2. 关联对象

less 复制代码
objc_setAssociatedObject(self, @selector(name), @"齐生", OBJC_ASSOCIATION_COPY_NONATOMIC);
NSString *name = objc_getAssociatedObject(self, @selector(name));

📍应用: • 在 Category 中「模拟添加属性」 • 附加元数据(缓存、tag、状态) • 替代继承实现扩展

2.3. 动态创建类

less 复制代码
Class MyClass = objc_allocateClassPair([NSObject class], "MyDynamicClass", 0);
class_addMethod(MyClass, @selector(sayHi), (IMP)sayHiIMP, "v@:");
objc_registerClassPair(MyClass);
id obj = [[MyClass alloc] init];
[obj performSelector:@selector(sayHi)];

📍应用:

  1. Runtime 自动生成 model
  2. Mock 测试
  3. 动态代理
    • 含义:在运行时把消息转发给另一个对象来处理。

3. Category 解析

arduino 复制代码
// Category 的数据结构
struct category_t {
    const char *name;                    // 类名
    classref_t cls;                      // 类对象
    struct method_list_t *instanceMethods; // 实例方法列表
    struct method_list_t *classMethods;    // 类方法列表
    struct protocol_list_t *protocols;     // 协议列表
    struct property_list_t *instanceProperties; // 属性列表
    struct property_list_t *_classProperties;   // 类属性列表
};

// Category 加载过程
1. 编译时:Category 被编译成 category_t 结构体
2. 运行时:通过 Runtime 加载到类对象中
3. 合并:Category 的方法、属性、协议合并到类的 rw 数据中

多个 Category 的加载顺序?

  1. 编译顺序决定加载顺序
  2. 后编译的 Category 方法会"覆盖"先编译的
  3. 实际上是插入到方法列表前面,优先被查找到

+load 方法的调用顺序?

  1. 先调用类的 +load 方法(按继承关系,父类优先)
  2. 再调用 Category+load 方法(按编译顺序)
  3. 每个 +load 方法只会被调用一次

+initialize 方法的调用顺序?

  1. 类第一次接收消息时调用
  2. 按继承关系调用,父类优先
  3. Category 的 +initialize 会"覆盖"类的 +initialize

Category vs Extension

Category Extension
定义位置 任意文件 必须和类的声明在同一个编译单元(.m)
时机 运行时加载 编译器期处理
性能 不如 Extension 好,因为在编译期合并,不需要runtime注册
是否能添加成员变量 ❌ 不行 ✅ 可以
是否能重写原方法 ✅ 可以,但容易引发冲突 ✅ 可以
应用场景 添加方法、拆分类实现、mock系统类 声明私有属性

Category 可以添加实例变量吗?

  • 不行🙅,因为类结构(class_ro_t)已经在编译时确定,runtime 动态加载Category时无法修改对象的内存布局。

Category中可以添加属性吗?

  • 可以✅,可以声明,并通过关联对象来实现。

4. KVO 实现原理

4.1. KVO 的 isa-swizzling 机制

实现原理

  1. 动态创建子类
  2. 修改对象的 isa 指针,指向新子类
  3. 重写子类的 setter,添加通知逻辑
  4. 重写 class 方法,返回原始类
csharp 复制代码
/// KVO 子类的方法实现伪代码
- (void)setProperty:(id)value {
    [self willChangeValueForKey:@"property"];
    [super setProperty:value];  // 调用原始实现
    [self didChangeValueForKey:@"property"];
}

- (Class)class {
    return [OriginalClass class];  // 隐藏子类的存在
}

- (void)dealloc {
    // 清理 KVO 相关资源
    [super dealloc];
}

相似名词 KVC

定义 :通过 字符串键名(key) 访问对象属性的机制。

功能

  • 直接访问属性,不经过 setter/getter。
  • 可以通过 keyPath 访问嵌套对象。
  • 支持字典与模型的动态映射(JSON -> Model)。

流程 :当调用 [person setValue:@"齐生" forKey:@"name"];

  1. 查找 setter 方法
  2. 如果没有 setter,查看有无成员变量_name_isNamenameisName
  3. 如果没有相似成员变量,调用 - (void)setValue:forUndefinedKey: 抛异常或自定义处理。

注意⚠️

  • KVC 会绕过封装,直接访问 ivar,违反封装性
  • 可用于"动态赋值"、"私有属性访问"等

应用场景

  1. JSON 转 Model(属性遍历、KVC)
  2. 方法耗时统计(方法交换)
  3. 简单的 AOP (面向切面编程) 框架(NSInvocation、关联对象、Around Advice)

关联知识点 AOP (面向切面编程)

含义:一种编程思想,能够在「不改动原有代码逻辑」的前提下,在指定的"切面点"上插入额外逻辑(如埋点、日志、监控、权限校验等)。

核心概念 含义
切点(Pointcut) 想"切入"的位置,比如方法调用前/后
通知(Advice) 在切点执行的额外逻辑(before、after、around)
切面(Aspect) 切点 + 通知 的组合
织入(Weaving) 把这些逻辑动态插入代码执行流程的过程

iOS 实现 AOP 的方法

  1. 方法交换:在交换方法中实现新的逻辑
  2. 消息转发:利用 forwardInvocation:resolveInstanceMethod: 在运行时拦截消息,再"转发"到自己的处理逻辑。
  3. 三方库:Aspects
相关推荐
Croa-vo2 小时前
Citadel SDE 面试复盘:直面硬核算法与思维挑战的双重压力
算法·面试·职场和发展
不说别的就是很菜3 小时前
【前端面试】Vue篇
前端·vue.js·面试
在等晚安么4 小时前
力扣面试150题打卡
算法·leetcode·面试
月弦笙音4 小时前
【vue3】这些不常用的API,却很实用
前端·vue.js·面试
创码小奇客5 小时前
Spring Boot依赖排坑指南:冲突、循环依赖全解析+实操方案
后端·面试·架构
小时前端5 小时前
当递归引爆调用栈:你的前端应用还能优雅降落吗?
前端·javascript·面试
T___T5 小时前
从定时器到 Promise:一次 JS 异步编程的进阶之旅
javascript·面试
R.lin6 小时前
MyBatis 专题深度细化解析
oracle·面试·mybatis
绝无仅有6 小时前
Redis 面试题解析:某度互联网大厂
后端·面试·架构