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
相关推荐
xlp666hub11 分钟前
Linux 设备模型学习笔记(1)
面试·嵌入式
南囝coding1 小时前
CSS终于能做瀑布流了!三行代码搞定,告别JavaScript布局
前端·后端·面试
踏浪无痕2 小时前
Go 的协程是线程吗?别被"轻量级线程"骗了
后端·面试·go
一只叫煤球的猫3 小时前
为什么Java里面,Service 层不直接返回 Result 对象?
java·spring boot·面试
求梦8203 小时前
字节前端面试复盘
面试·职场和发展
C雨后彩虹4 小时前
书籍叠放问题
java·数据结构·算法·华为·面试
码农水水4 小时前
中国电网Java面试被问:流批一体架构的实现和状态管理
java·c语言·开发语言·面试·职场和发展·架构·kafka
程序员清风5 小时前
猿辅导二面:线上出现的OOM是如何排查的?
java·后端·面试
CCPC不拿奖不改名5 小时前
数据处理与分析:pandas基础+面试习题
开发语言·数据结构·python·面试·职场和发展·pandas
小徐不徐说5 小时前
避坑指南:Qt 中 Lambda 表达式崩溃原因与高效使用实践
数据库·c++·qt·面试