普通视图

发现新文章,点击刷新页面。
昨天以前掘金 iOS

iOS Runtime 深度解析

作者 MonkeyKing
2026年4月8日 21:06

iOS Runtime 深度解析:原理、实战与前沿趋势

在 iOS 开发中,Runtime(运行时)是 Objective-C(以下简称 OC)语言的灵魂,也是区分 iOS 初级开发者与中高级开发者的核心门槛。它赋予 OC 动态特性,让代码在编译期无法确定的逻辑,能在运行时灵活调整、动态扩展。随着 Swift 生态的完善和 Apple 技术的迭代,Runtime 并未过时,反而在组件化、性能优化、逆向开发等场景中发挥着不可替代的作用。本文将从原理、实战、前沿三个维度,带你全面吃透 iOS Runtime,结合代码示例拆解核心用法,助力你在实际开发中灵活运用这门“黑魔法”。

一、Runtime 核心基础:是什么与为什么

1.1 什么是 Runtime

Runtime 本质上是一套用 C 和汇编语言编写的 API 集合,是 OC 语言与底层系统之间的桥梁,负责将 OC 代码转换为底层可执行的机器指令,实现动态类型、动态绑定、动态加载等核心特性。简单来说,OC 是“动态语言”,核心就在于 Runtime——编译期我们写的 OC 方法调用、属性访问,最终都会被转换为 Runtime 的 C 函数调用,直到运行时才真正确定具体执行逻辑。

举个直观的例子:我们调用 [object method] 时,编译器并不会直接确定 method 方法的具体实现,而是在运行时通过 Runtime 查找该方法的实现并执行,这也是 Runtime 与静态语言(如 C++)的核心区别。

1.2 Runtime 的核心价值

  • 动态扩展:无需修改类的源码,即可为类添加方法、属性,突破 OC 语法限制;
  • 解耦优化:在组件化、插件化开发中,通过 Runtime 实现组件间通信,降低耦合度;
  • 底层适配:解决系统 API 兼容、私有方法调用、逆向开发等场景的核心问题;
  • 性能优化:通过方法缓存、动态解析等机制,提升 App 运行效率。

1.3 核心数据结构

Runtime 的所有功能,都围绕以下几个核心结构体展开,理解它们是掌握 Runtime 的基础:

(1)objc_object:对象的本质

OC 中所有对象的底层都是 objc_object 结构体,核心字段是 isa 指针,用于指向对象所属的类。

// objc 对象的底层结构体
struct objc_object {
    Class isa; // 指向类对象的指针,核心字段
};

// OC 对象的本质就是 objc_object 的指针
typedef struct objc_object *id;

(2)objc_class:类的本质

类对象(Class)的底层是 objc_class 结构体,存储着类的元信息(方法列表、属性列表、协议列表等)。

struct objc_class {
    Class isa; // 指向元类(Meta Class),用于存储类方法
    Class super_class; // 指向父类
    const char *name; // 类名
    long instance_size; // 实例对象的内存大小
    struct objc_ivar_list *ivars; // 实例变量列表
    struct objc_method_list **methodLists; // 方法列表(可动态修改)
    struct objc_cache *cache; // 方法缓存(提升查找效率)
    struct objc_protocol_list *protocols; // 协议列表
};

(3)Method、SEL、IMP:方法的三要素

  • SEL:方法选择器,本质是字符串,用于唯一标识一个方法(如 @selector(method:));
  • IMP:函数指针,指向方法的具体实现,是方法执行的核心;
  • Method:方法结构体,封装了 SELIMP 的对应关系。
// 方法结构体
struct objc_method {
    SEL method_name; // 方法选择器
    char *method_types; // 方法类型编码(返回值、参数类型)
    IMP method_imp; // 方法实现的函数指针
};

二、Runtime 核心机制:从原理到实战

Runtime 的核心机制包括消息传递、方法缓存、动态解析、消息转发、方法交换等,其中消息传递是基础,其他机制都是基于消息传递的扩展。以下结合实战代码,拆解每个机制的原理与用法。

2.1 消息传递:OC 方法调用的本质

OC 中所有方法调用,本质上都是 Runtime 的 objc_msgSend 函数调用。当我们写下 [object method:arg] 时,编译器会自动转换为:

objc_msgSend(object, @selector(method:), arg);

消息传递的完整流程

  1. 通过对象的 isa 指针,找到对象所属的类;
  2. 优先在类的 cache(方法缓存)中查找对应 SELIMP
  3. 若缓存未命中,遍历类的 methodLists 查找方法;
  4. 若当前类未找到,沿着 super_class 父类链向上查找,直到找到 NSObject;
  5. 若找到方法,执行 IMP 并将方法加入缓存(提升下次查找效率);
  6. 若未找到方法,进入消息转发流程(后续详解)。

实战:手动调用 objc_msgSend

需导入 Runtime 头文件 #import <objc/runtime.h>,手动调用消息传递函数:

#import <objc/runtime.h>

@interface Person : NSObject
- (void)sayHello:(NSString *)name;
@end

@implementation Person
- (void)sayHello:(NSString *)name {
    NSLog(@"Hello, %@", name);
}
@end

// 调用方式
Person *person = [[Person alloc] init];
// 1. 常规调用
[person sayHello:@"Runtime"];
// 2. 手动调用 objc_msgSend
SEL sel = @selector(sayHello:);
objc_msgSend(person, sel, @"Runtime"); // 输出:Hello, Runtime

2.2 方法缓存:提升消息传递效率

Runtime 为每个类维护了一个 objc_cache(方法缓存),用于存储最近调用过的方法(SEL + IMP)。缓存采用哈希表实现,查找速度远快于遍历方法列表,这是 Runtime 优化性能的核心手段之一。

核心特点:

  • 缓存只存储“最近调用”的方法,避免缓存过大;
  • 每次调用方法后,若缓存未命中,找到 IMP 后会自动加入缓存;
  • 类的缓存会随着方法调用动态更新,优先保留高频调用的方法。

2.3 动态解析与消息转发:方法未找到的“补救机制”

当消息传递流程中未找到方法时,Runtime 不会直接崩溃,而是提供了三层“补救机制”,让我们有机会动态补充方法实现,避免 App 闪退。

(1)动态方法解析(第一层补救)

通过重写 +resolveInstanceMethod:(实例方法)或 +resolveClassMethod:(类方法),动态为未实现的方法添加实现。

@implementation Person
// 动态解析实例方法
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(sayHello:)) {
        // 为 sel 动态添加实现:参数1=类,参数2=SEL,参数3=IMP,参数4=方法类型编码
        class_addMethod(self, sel, (IMP)dynamicSayHello, "v@:@");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

// 动态添加的方法实现(C语言函数)
void dynamicSayHello(id self, SEL _cmd, NSString *name) {
    NSLog(@"动态解析:Hello, %@", name);
}
@end

// 调用未声明的方法(不会崩溃)
Person *person = [[Person alloc] init];
[person sayHello:@"Dynamic Resolve"]; // 输出:动态解析:Hello, Dynamic Resolve

(2)消息转发(第二层+第三层补救)

若动态解析未处理(返回 NO),则进入消息转发流程,分为两步:

  1. 快速转发:通过 -forwardingTargetForSelector:,将消息转发给另一个对象处理;
  2. 完整转发:若快速转发未处理,通过 -methodSignatureForSelector: 获取方法签名,再通过 -forwardInvocation: 手动处理消息。
实战:快速转发
@interface Student : NSObject
- (void)sayHello:(NSString *)name;
@end

@implementation Student
- (void)sayHello:(NSString *)name {
    NSLog(@"Student 打招呼:Hello, %@", name);
}
@end

@implementation Person
// 快速转发:将消息转发给 Student 对象
- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(sayHello:)) {
        return [[Student alloc] init];
    }
    return [super forwardingTargetForSelector:aSelector];
}
@end

// 调用方法,消息会转发给 Student
Person *person = [[Person alloc] init];
[person sayHello:@"Forward"]; // 输出:Student 打招呼:Hello, Forward
实战:完整转发
@implementation Person
// 1. 获取方法签名(必须实现,否则崩溃)
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if (aSelector == @selector(sayHello:)) {
        // 方法签名:返回值void(v),参数id(@)、SEL(:)、NSString(@)
        return [NSMethodSignature signatureWithObjCTypes:"v@:@"];
    }
    return [super methodSignatureForSelector:aSelector];
}

// 2. 手动处理消息
- (void)forwardInvocation:(NSInvocation *)anInvocation {
    SEL sel = anInvocation.selector;
    Student *student = [[Student alloc] init];
    if ([student respondsToSelector:sel]) {
        [anInvocation invokeWithTarget:student]; // 转发给 Student
    } else {
        [super forwardInvocation:anInvocation];
    }
}
@end

2.4 方法交换(Method Swizzling):Runtime 黑魔法

Method Swizzling(方法交换)是 Runtime 最常用的实战技巧,通过交换两个方法的 IMP,实现“hook”效果,无需修改原方法源码,即可拦截、扩展原方法的功能(如埋点、日志、性能监控)。

核心原理:交换两个 Method 结构体中的 IMP 指针,让原 SEL 指向新的实现,新 SEL 指向原实现。

实战:拦截 UIViewController 的 viewDidLoad 方法

#import <objc/runtime.h>
#import <UIKit/UIKit.h>

@implementation UIViewController (Swizzling)
// 在 +load 方法中执行方法交换(+load 方法会在类加载时自动调用,且只调用一次)
+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        // 1. 获取两个方法
        Class cls = [self class];
        SEL originalSel = @selector(viewDidLoad);
        SEL swizzledSel = @selector(swizzled_viewDidLoad);
        
        Method originalMethod = class_getInstanceMethod(cls, originalSel);
        Method swizzledMethod = class_getInstanceMethod(cls, swizzledSel);
        
        // 2. 交换方法实现
        method_exchangeImplementations(originalMethod, swizzledMethod);
    });
}

// 新的方法实现(拦截 viewDidLoad)
- (void)swizzled_viewDidLoad {
    // 1. 执行原 viewDidLoad 方法(此时 swizzled_viewDidLoad 指向原实现)
    [self swizzled_viewDidLoad];
    
    // 2. 扩展功能(如埋点、日志)
    NSLog(@"拦截到 %@ 的 viewDidLoad", self.class);
}
@end

方法交换的注意事项

  • dispatch_once_t 保证方法交换只执行一次,避免多次交换导致逻辑错乱;
  • 优先在 +load 方法中执行交换(类加载时执行,时机最早),避免在 +initialize 中执行(可能被多次调用);
  • 交换类方法时,需使用 class_getClassMethod 获取方法,而非 class_getInstanceMethod
  • 避免交换系统私有方法,可能导致 App 审核失败或系统崩溃。

2.5 动态添加属性与关联对象

OC 中,分类(Category)默认不能添加实例变量(ivar),但通过 Runtime 的关联对象(Associated Object),可以间接为分类添加“属性”,本质是将属性值存储在外部哈希表中,与对象关联起来。

实战:为 UIButton 分类添加属性

#import <objc/runtime.h>
#import <UIKit/UIKit.h>

@interface UIButton (Extension)
// 声明属性
@property (nonatomic, copy) NSString *customName;
@end

@implementation UIButton (Extension)
// 定义关联对象的 key(唯一标识)
static const void *CustomNameKey = &CustomNameKey;

// 重写 setter 方法
- (void)setCustomName:(NSString *)customName {
    // 关联对象:参数1=对象,参数2=key,参数3=值,参数4=内存管理策略
    objc_setAssociatedObject(self, CustomNameKey, customName, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

// 重写 getter 方法
- (NSString *)customName {
    // 获取关联对象
    return objc_getAssociatedObject(self, CustomNameKey);
}
@end

// 使用
UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem];
button.customName = @"我的按钮";
NSLog(@"按钮名称:%@", button.customName); // 输出:按钮名称:我的按钮

关联对象的内存管理策略

// 对应 OC 属性的内存修饰符
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 前沿趋势:适配 Swift 与 Apple 新生态

随着 Swift 成为 iOS 开发的主流语言,以及 Apple 推出的新工具、新框架(如 Xcode 26、基础模型框架),Runtime 的应用场景也在不断扩展,不再局限于 OC 开发,而是与 Swift 生态深度融合,呈现出全新的发展趋势。

3.1 Runtime 与 Swift 的协同发展

Swift 是静态语言,编译期会进行类型检查,但其底层仍然依赖 Runtime(尤其是与 OC 交互时),同时 Swift 也提供了自己的动态特性(如 @dynamicMemberLookup@objc 关键字),与 OC Runtime 形成互补。

  • Swift 中使用 @objc 修饰的方法、属性,会被暴露给 Runtime,可通过 OC Runtime API 调用;
  • Swift 5.0+ 引入的 @dynamicMemberLookup,允许动态访问属性,本质是 Runtime 动态特性的 Swift 封装;
  • 在 Swift 组件化开发中,通过 Runtime 实现跨模块调用(如通过类名字符串创建对象),解决 Swift 静态编译的限制。

实战:Swift 中调用 Runtime API

import ObjectiveC

class Person: NSObject {
    @objc func sayHello(_ name: String) {
        print("Hello, (name)")
    }
}

// 1. 动态创建对象
let className = "RuntimeDemo.Person"
guard let cls = NSClassFromString(className) as? Person.Type else { return }
let person = cls.init()

// 2. 动态调用方法
let sel = NSSelectorFromString("sayHello:")
person.perform(sel, with: "Swift Runtime") // 输出:Hello, Swift Runtime

// 3. 动态添加关联对象
extension UIButton {
    private static let customKey = UnsafeRawPointer(bitPattern: 0x123456)!
    var customName: String? {
        get {
            objc_getAssociatedObject(self, UIButton.customKey) as? String
        }
        set {
            objc_setAssociatedObject(self, UIButton.customKey, newValue, .OBJC_ASSOCIATION_COPY_NONATOMIC)
        }
    }
}

3.2 Runtime 在 Apple 新生态中的应用

随着 Apple 发布 Xcode 26、基础模型框架等新工具,Runtime 的应用场景进一步扩展,尤其在智能开发、性能优化、跨平台适配等方面发挥着重要作用:

  1. 智能开发辅助:Xcode 26 集成了大语言模型,可通过 Runtime 分析类的结构、方法列表,自动生成代码、修复错误,提升开发效率;
  2. 隐私保护与性能优化:基础模型框架支持设备端 AI 推理,Runtime 可动态管理模型调用的生命周期,避免敏感数据泄露,同时通过方法缓存优化 AI 推理的响应速度;
  3. 跨平台适配:Swift 6.2 支持 WebAssembly,Runtime 可帮助开发者实现 OC/Swift 代码与 Web 端的交互,动态适配不同平台的 API 差异;
  4. 逆向开发与安全防护:在 App 安全领域,通过 Runtime Hook 系统方法,可拦截敏感操作(如密码输入、网络请求),防止数据泄露;同时,也可通过 Runtime 混淆方法名、类名,提升 App 反逆向能力。

3.3 Runtime 的未来展望

尽管 Swift 生态日益完善,但 Runtime 作为 iOS 底层核心技术,短期内不会被替代,反而会随着 Apple 技术的迭代不断升级:

  • 更高效的方法缓存机制:Apple 可能进一步优化 objc_cache 的哈希算法,提升消息传递效率;
  • 更安全的动态扩展:加强 Runtime API 的权限管理,避免恶意代码通过 Runtime 篡改 App 逻辑;
  • 与 AI 深度融合:通过 Runtime 动态适配 AI 模型的调用,实现更智能的代码生成、性能优化。

四、结语

iOS Runtime 是 OC 语言的灵魂,也是 iOS 开发的“内功”。它不仅能帮助我们理解 iOS 底层原理,更能在实际开发中解决很多常规语法无法解决的问题——从组件化解耦、性能优化,到逆向开发、安全防护,Runtime 都发挥着不可替代的作用。

随着 Swift 与 Apple 新生态的发展,Runtime 的应用场景不断扩展,它不再是“小众黑魔法”,而是中高级 iOS 开发者必须掌握的核心技能。学习 Runtime,不仅是学习一套 API,更是培养一种“底层思维”——跳出上层语法的限制,从底层理解代码的执行逻辑,才能写出更高效、更健壮、更具扩展性的 iOS 应用。

最后,希望本文能帮助你快速吃透 Runtime 的核心原理与实战用法,在实际开发中灵活运用这门技术,突破自身开发瓶颈,成为更优秀的 iOS 开发者。未来,Runtime 还会不断进化,期待我们一起探索它的更多可能性。

SwiftUI 如何实现 Infinite Scroll?

作者 RickeyBoy
2026年3月28日 00:56

欢迎点个 star:github.com/RickeyBoy/R…

面试题:用 SwiftUI 实现一个无限滚动列表,支持分页加载。

这道题我在面试中遇到过好几次,说实话第一次答的时候以为随便写个 LazyVStack + onAppear 就完事了。后来才发现,面试官真正想考的不是你会不会用 API,而是你对状态管理、性能优化、Task 生命周期这些东西到底理解多深。

我的思路是从最简方案出发,一步步暴露问题、一步步优化。在开始写代码之前,先聊一下架构选型。

为什么选 MVVM?

先说一下 SwiftUI 里常见的架构选择。MVC 就不聊了,那是 UIKit 时代的标配,Controller 跟 UIKit 强耦合,到了 SwiftUI 里根本没有 UIViewController 这个角色,MVC 自然也就退出舞台了。

SwiftUI 里最常见的架构,从简单到复杂大概是这么几个:

架构 特点 适合场景
MV(Model-View) 没有 ViewModel,状态直接放 View 里,Apple 官方示例的典型写法 逻辑简单的页面
MVVM 抽出 ViewModel 管理状态和逻辑,SwiftUI 里最主流的选择 中等复杂度,需要可测试性
TCA 单向数据流,State + Action + Reducer + Effect,强约束 大型项目,需要严格的状态管理

其中 MV 是最基础的,逻辑简单的页面,@State 往 View 里一放就完事了,Apple 自己的 WWDC 示例大量都是这么写的。但 infinite scroll 涉及分页状态、加载状态、错误处理、Task 生命周期管理这些东西,全塞 View 里会很乱。抽一个 ViewModel 出来专门管理这些状态,View 只负责渲染和转发用户操作,职责就清晰多了。

所以这道题用 MVVM 是最合适的,不是因为 MVVM 最好,而是这个场景的复杂度刚好适合。并且采用 MVVM 结构规整,可拓展性也强,从面试回答的角度来讲也是正好的。

而 SwiftUI 天然就鼓励这种模式,@Observable 本身就是 binding 机制,ViewModel 状态一变,View 自动更新,不需要手动同步。我们后面的代码就是按这个思路来的。

一、最小可用版本

先写一个能跑的最简版本。

核心思路很简单:LazyVStack 只在 item 即将可见时才实例化 View,我们利用 onAppear 检测"最后一个 item 出现了",然后触发下一页请求。

Model

struct Item: Identifiable, Equatable {
    let id: String
    let title: String
}

struct PageInfo {
    let endCursor: String?
    let hasNextPage: Bool
}

ViewModel

@MainActor @Observable
final class ItemListViewModel {
    private(set) var items: [Item] = []
    private var pageInfo: PageInfo?

    func loadNextPage() async {
        let response = try? await APIService.fetchItems(after: pageInfo?.endCursor)
        guard let response else { return }
        items.append(contentsOf: response.items)
        pageInfo = response.pageInfo
    }
}

View

struct ItemListView: View {
    @State private var viewModel = ItemListViewModel()

    var body: some View {
        ScrollView {
            LazyVStack {
                ForEach(viewModel.items) { item in
                    ItemRow(item: item)
                        .onAppear {
                            if item == viewModel.items.last {
                                Task { await viewModel.loadNextPage() }
                            }
                        }
                }
            }
        }
        .task { await viewModel.loadNextPage() }
    }
}

代码很短,逻辑也直白:

  1. 每当最后一个 item 出现在屏幕上,就触发 loadNextPage()
  2. loadNextPage() 请求后台去 fetch,拿到数据然后塞进 items 中
  3. View 检测到有更新,自动刷新页面

一句话总结:最后一个 item onAppear 的时候,就进行请求。

1.1 分页方式:cursor vs offset

可能有同学会问:为什么 fetchItems(after: cursor) 用的是 cursor,而不是传统的 pageoffset

分页一般有两种方式:

  • Offset-basedfetchItems(page: 3, size: 20),按页码或偏移量取数据
  • Cursor-basedfetchItems(after: "abc123"),传上一页最后一条的标识,从那里往后取

对于 infinite scroll 这种场景,cursor-based 更合适。详细对比一下:

Cursor-based Offset-based
数据一致性 不受中间插入/删除影响 插入新数据会导致重复或遗漏
性能 数据库只需定位到 cursor 后续 大 offset 需要 skip N 行
适用场景 实时 feed、社交流 固定数据集、后台管理列表

简单来说,cursor-based 更适合"数据随时在变"的场景(比如社交 feed),offset-based 更适合"数据基本不变"的场景(比如后台管理列表)。infinite scroll 的数据通常是动态的,所以用 cursor-based。

1.2 LazyVStack vs List

可能有同学会问:为什么用 LazyVStack 而不是 List

先说浅显的回答:LazyVStack 布局更自由,没有 List 自带的分割线、背景色、cell 样式这些限制,适合高度自定义的 UI。而 List 开箱即用,自带滑动删除、拖拽排序这些交互,适合标准列表场景。

当然,如果想要深入回答,还有可以继续。二者还有一个关键区别其实是内存模型

LazyVStack List
View 回收 ❌ 不回收,创建后常驻内存 ✅ 内部回收机制
内存增长 随滚动距离线性增长 基本恒定
自定义布局 完全自由 受限于 List 样式
万级数据 可能有内存压力 表现更好

为什么会有这个区别?因为它们底层的实现不一样。List 底层是基于 UICollectionView(iOS 16 之前是 UITableView),天然有 cell 回收复用机制,滚出屏幕的 cell 会被回收,滚入时再复用,所以内存占用基本恒定。而 LazyVStack 底层只是一个普通的布局容器,"Lazy" 的意思是延迟创建,item 滚入可见区域时才创建 View,但创建之后就一直留在内存里,不会回收。

所以如果列表数据量很大(比如社交 feed 那种上万条的),List 在内存上更有优势。如果需要高度自定义的 UI,那就用 LazyVStack,但要心里有数:用户滚得越远,内存占用越大。

1.3 为什么加 @MainActor

上面的 ViewModel 代码加了 @MainActor,这个很容易被忽略但其实很关键。

@Observable 本身不会自动保证在主线程更新状态。而我们的 loadNextPage() 是在 Task 里通过 await 拿数据,await 之后的代码在哪个线程执行是不确定的。如果恰好在后台线程执行了 items.append(...),SwiftUI 收到状态变更通知后会在后台线程刷新 UI,这就会导致紫色警告("Publishing changes from background threads is not allowed")甚至崩溃。

加上 @MainActor 之后,这个类的所有属性访问和方法调用都会被隔离到主线程,从根源上避免线程安全问题。

另外补充一下:Swift 6.2(Xcode 26)引入了模块级别的 Default Actor Isolation 设置,可以把整个模块的默认隔离改为 MainActor,开启之后所有类型都默认跑在主线程,不用再手动加 @MainActor。但这是一个 opt-in 的设置,默认值还是 nonisolated,而且不是所有项目都会立刻升级。所以目前来说,显式写 @MainActor 仍然是更稳妥的做法。

1.4 几个小细节

有几个代码细节,不影响功能,但代码质量会好不少,属于面试加分项。

private(set) 控制可见性

itemsprivate(set) 修饰,外部只能读不能写。这样 View 就没法直接改 items,所有数据变更都必须经过 ViewModel 的方法,数据流向是单向的。这个习惯在 MVVM 里很重要,不然 View 和 ViewModel 的职责边界很容易模糊。

让 Item 遵循 Equatable

上面的代码里 Item 已经加了 Equatable,所以判断"是不是最后一个"可以直接写 item == viewModel.items.last,不用绕一圈去比 id。后面加叠加更多功能的时候也可以用,代码更简洁。

.task = .onAppear + Task

View 里首次加载用的是 .task { await viewModel.loadNextPage() },这其实等价于在 .onAppear 里手动创建一个 Task。但 .task 有个好处:当 View 消失时会自动 cancel 这个 Task。手动写 Task {} 的话你得自己管 cancel,容易漏掉,所以首次加载优先用 .task

二、防重复请求

上一个最基础的版本,有个明显的问题:快速滚动时 onAppear 有可能会被多次触发,导致同一页被重复请求了。

怎么解决?思路也很直接:加一个 isLoading 标记 + hasNextPage 判断,双重 guard,然后通过这些属性来判断是否需要发送请求。

@MainActor @Observable
final class ItemListViewModel {
    private(set) var items: [Item] = []
    private(set) var isLoading = false
    private var pageInfo: PageInfo?

    var canLoadMore: Bool {
        guard let pageInfo else { return items.isEmpty } // 首次加载
        return pageInfo.hasNextPage && !isLoading
    }

    func loadNextPage() async {
        guard canLoadMore else { return }
        isLoading = true
        defer { isLoading = false }

        let response = try? await APIService.fetchItems(after: pageInfo?.endCursor)
        guard let response else { return }
        items.append(contentsOf: response.items)
        pageInfo = response.pageInfo
    }
}

canLoadMore 这个 computed property 干了两件事:

  • 没有下一页时不请求(通过后端返回的 pageInfo.hasNextPage 来判断)
  • 正在加载时不重复请求(通过 isLoading 来判断)

2.1 小细节

defer 管理状态翻转

注意 isLoading 的写法:开头设为 true,然后紧接着 defer { isLoading = false }。这样不管后面是正常返回还是提前 returnisLoading 都会被重置回 false

如果不用 defer,你就得在每个 return 之前手动加一句 isLoading = false,路径一多很容易漏掉,漏掉的后果就是列表永远卡在 loading 状态,再也加载不了下一页。

canLoadMore 作为 computed property

把"能不能加载"的判断收到一个 computed property 里,而不是在 loadNextPage() 里写一堆 if。好处是逻辑集中,后面要加新条件(比如错误状态下不加载)直接改这一个地方就行,调用方不用动。

三、提前预加载:Threshold Prefetch

目前的逻辑是"最后一个 item 出现了才开始加载",那么用户的感受就是:滚到底 → 停顿 → 等数据 → 新数据出现。那个停顿虽然可能只有几百毫秒,但体感上还是挺明显的。

怎么办?提前触发。 不等最后一个 item,而是在还剩 N 个 item 时就开始加载下一页。

// View
ForEach(viewModel.items) { item in
    ItemRow(item: item)
        .onAppear { viewModel.onItemAppear(item) } // View 层仅透传,将逻辑交给 ViewModel
}

// ViewModel,新增 prefetch threshold
private let prefetchThreshold = 5

func onItemAppear(_ item: Item) {
    guard let index = items.firstIndex(of: item),
          index >= items.count - prefetchThreshold else { return } // 判断是否该加载下一页了
    Task { await loadNextPage() }
}

这样一来,用户还剩 5 个 item 可以滚的时候,网络请求就已经在跑了,等滚到底部时,数据大概率已经回来了,体验上就是"无缝衔接"。

那 threshold 到底设多少合适?这个纯属经验值,根据具体的数据量、UI 复杂度都相关,5 只是一个经验值。总的来讲就是一个 trade-off:

  • threshold 太小:快速滚动还是会看到停顿
  • threshold 太大:用户可能只看前几条就走了,白白浪费请求

四、Task 取消 + 错误处理

到这里基本功能已经没问题了。接下来聊聊 Task 生命周期管理和错误恢复,这部分在面试里属于加分项。

4.1 Task 取消

为什么需要管理 Task 取消?我们目前的例子中,单一列表的情况可能不需要考虑。但是如果是搜索页面的列表,或者叠加筛选功能,问题就复杂了。

举个具体的例子:

  1. 用户在搜索页搜"咖啡",然后在列表页向下滑动,触发了一个 loadNextPage 的请求 A
  2. 还没等数据回来,用户改成搜"奶茶",请求 B 又发出去了
  3. 这时候网络上同时有两个请求在跑。如果请求 B 先于 A 回来,那么等请求 A 回来的时候,用户就会发现明明搜索的是“奶茶”,但是却又展示了不少“咖啡”内容。

这种 bug 不是每次都能复现(取决于网络时序),但一旦出现用户会很困惑,所以解决方式就是:发新请求前先 cancel 旧的,被 cancel 的任务即便返回了 response 也不处理

@MainActor @Observable
final class ItemListViewModel {
    private(set) var items: [Item] = []
    private(set) var isLoading = false
    private(set) var error: Error?
    private var pageInfo: PageInfo?
    private var loadTask: Task<Void, Never>? // 💾 持有当前请求的引用

    func loadNextPage() {
        guard canLoadMore else { return }
        loadTask?.cancel() // ❌ 发新请求前,先 cancel 旧的
        isLoading = true

        loadTask = Task { [weak self] in // 🔒 weak self 防止循环引用
            guard let self else { return }
            defer { self.isLoading = false }

            do {
                let response = try await APIService.fetchItems(after: pageInfo?.endCursor)
                guard !Task.isCancelled else { return } // 🛡️ 被 cancel 了就不写入
                self.items.append(contentsOf: response.items)
                self.pageInfo = response.pageInfo
                self.error = nil
            } catch {
                guard !Task.isCancelled else { return } // 🛡️ 同上
                self.error = error
            }
        }
    }

    func reset() {
        loadTask?.cancel() // ❌ 先 cancel,再清空
        items = []
        pageInfo = nil
        isLoading = false
        error = nil
    }

    // ...
}

4.2 错误重试

错误处理其实是一个很容易被忽略,同时也非常复杂的事情。这里我们的方案是当出现错误的时候,展现一个重试按钮。从 UI 的角度来讲不好看,但实际上面试阶段时间有限,能够展示出有错误处理的思维就可以了。

@MainActor @Observable
final class ItemListViewModel {
    // ...
    private(set) var error: Error?

    func retry() {
        error = nil
        loadNextPage()
    }
}
// View — 列表底部
if viewModel.error != nil {
    RetryButton { viewModel.retry() }
} else if viewModel.isLoading {
    ProgressView()
}

4.3 空状态处理

还有一个容易忽略的边界情况:首次加载完成后,后端返回了 0 条数据。

当前的代码里,items 为空有两种可能:一种是"还在加载第一页",另一种是"加载完了但确实没数据"。如果不区分这两种状态,用户看到的就是一片空白,不知道是在等数据还是真的没有内容。

处理方式也很简单,加一个 computed property 判断一下:

var isEmpty: Bool {
    !isLoading && items.isEmpty && error == nil && pageInfo != nil
}

这里的关键是 pageInfo != nil,说明至少请求过一次了(首次加载前 pageInfonil)。四个条件同时满足,才说明"确实没数据"。

View 里对应的处理:

if viewModel.isEmpty {
    ContentUnavailableView("暂无数据", systemImage: "tray")
} else if viewModel.isLoading && viewModel.items.isEmpty {
    ProgressView() // 首次加载中
} else {
    // 正常的列表内容
}

这样用户就能清楚地区分"加载中"和"没有数据"这两种状态了。

4.4 用 enum 收敛 View 状态

到这里你会发现,View 层需要处理的状态越来越多:首次加载中、有数据、空数据、出错。如果全用 if/else if 判断,条件一多很容易写乱,漏掉某个分支也不会有编译器提醒。

可以定义一个 enum 来收敛这些状态:

enum ViewState {
    case initialLoading    // 首次加载中
    case loaded            // 有数据,正常展示列表
    case empty             // 加载完了但没数据
    case error(String)     // 出错了
}

然后在 ViewModel 里加一个 computed property,从现有属性推导出当前的 View 状态:

var viewState: ViewState {
    if let error, items.isEmpty {
        return .error(error.localizedDescription)
    }
    if isLoading && items.isEmpty {
        return .initialLoading
    }
    if isEmpty {
        return .empty
    }
    return .loaded
}

注意这里的关键:ViewState 是 computed property,不是存储属性。底层的数据源还是 isLoadingitemserrorpageInfo 这些独立属性,viewState 只是把它们组合成 View 更容易消费的形式。这样既不会出现之前 LoadingState enum 耦合状态的问题,又让 View 的代码变得很干净:

var body: some View {
    Group {
        switch viewModel.viewState {
        case .initialLoading:
            ProgressView()
        case .empty:
            ContentUnavailableView("暂无数据", systemImage: "tray")
        case .error(let message):
            ErrorView(message: message) { viewModel.retry() }
        case .loaded:
            ScrollView {
                LazyVStack(spacing: 0) {
                    ForEach(viewModel.items) { item in
                        ItemRow(item: item)
                            .onAppear { viewModel.onItemAppear(item) }
                    }
                    loadingFooter
                }
            }
        }
    }
    .task { viewModel.loadNextPage() }
}

switch 替代 if/else if,每个分支对应一种状态,漏掉任何一个编译器都会报错。用 Group 包裹 switch 是为了能在外层挂 .task 触发首次加载。

五、完整代码

前面一步步拆解完了,最后把所有东西整合到一起。先看一下整体架构:

graph LR
    View -->|用户操作| ViewModel
    ViewModel -->|状态更新| View
    ViewModel -->|网络请求| APIService
    APIService -->|响应数据| ViewModel

    style View fill:#E8F5E9,stroke:#4CAF50
    style ViewModel fill:#E3F2FD,stroke:#2196F3
    style APIService fill:#FFF3E0,stroke:#FF9800

View 只管渲染和转发用户操作,ViewModel 管状态和请求编排,APIService 做实际的网络调用。数据流向是单向的:用户操作 → ViewModel 处理 → APIService 请求 → 数据回来更新状态 → View 自动刷新。

Model

struct Item: Identifiable, Equatable {
    let id: String
    let title: String
}

struct PageInfo: Equatable {
    let endCursor: String?
    let hasNextPage: Bool
}

struct PagedResponse {
    let items: [Item]
    let pageInfo: PageInfo
}

ViewState

enum ViewState {
    case initialLoading
    case loaded
    case empty
    case error(String)
}

ViewModel

@MainActor @Observable
final class ItemListViewModel {
    // MARK: - State

    private(set) var items: [Item] = []
    private(set) var isLoading = false
    private(set) var error: Error?

    // MARK: - Private

    private let prefetchThreshold = 5
    private var pageInfo: PageInfo?
    private var loadTask: Task<Void, Never>?

    // MARK: - Computed

    var canLoadMore: Bool {
        guard !isLoading else { return false }
        guard let pageInfo else { return items.isEmpty }
        return pageInfo.hasNextPage
    }

    var isEmpty: Bool {
        !isLoading && items.isEmpty && error == nil && pageInfo != nil
    }

    var viewState: ViewState {
        if let error, items.isEmpty {
            return .error(error.localizedDescription)
        }
        if isLoading && items.isEmpty {
            return .initialLoading
        }
        if isEmpty {
            return .empty
        }
        return .loaded
    }

    // MARK: - Trigger

    func onItemAppear(_ item: Item) {
        guard let index = items.firstIndex(of: item),
              index >= items.count - prefetchThreshold else { return }
        loadNextPage()
    }

    // MARK: - Actions

    func loadNextPage() {
        guard canLoadMore else { return }
        loadTask?.cancel()
        isLoading = true

        loadTask = Task { [weak self] in
            guard let self else { return }
            defer { self.isLoading = false }

            do {
                let response = try await APIService.fetchItems(after: pageInfo?.endCursor)
                guard !Task.isCancelled else { return }
                self.items.append(contentsOf: response.items)
                self.pageInfo = response.pageInfo
                self.error = nil
            } catch is CancellationError {
                // Task was cancelled, do nothing
            } catch {
                guard !Task.isCancelled else { return }
                self.error = error
            }
        }
    }

    func retry() {
        error = nil
        loadNextPage()
    }

    func reset() {
        loadTask?.cancel()
        items = []
        pageInfo = nil
        isLoading = false
        error = nil
    }
}

View

struct ItemListView: View {
    @State private var viewModel = ItemListViewModel()

    var body: some View {
        Group {
            switch viewModel.viewState {
            case .initialLoading:
                ProgressView()
            case .empty:
                ContentUnavailableView("暂无数据", systemImage: "tray")
            case .error(let message):
                ErrorView(message: message) { viewModel.retry() }
            case .loaded:
                ScrollView {
                    LazyVStack(spacing: 0) {
                        ForEach(viewModel.items) { item in
                            ItemRow(item: item)
                                .onAppear { viewModel.onItemAppear(item) }
                        }
                        loadingFooter
                    }
                }
            }
        }
        .task { viewModel.loadNextPage() }
    }

    @ViewBuilder
    private var loadingFooter: some View {
        if viewModel.error != nil {
            VStack(spacing: 8) {
                Text("加载失败")
                    .font(.caption)
                    .foregroundStyle(.secondary)
                Button("Retry") { viewModel.retry() }
                    .buttonStyle(.bordered)
            }
            .frame(maxWidth: .infinity)
            .padding()
        } else if viewModel.isLoading {
            ProgressView()
                .frame(maxWidth: .infinity)
                .padding()
        }
    }
}

总结

回顾一下整个思路:

  1. 从简单方案说起LazyVStack + onAppear last item,先把原理讲清楚
  2. 暴露问题并优化 — 重复请求 → guard;体验停顿 → threshold prefetch
  3. 展示工程素养 — Task 取消、error handling、retry
  4. 完整架构 — View 只渲染 + 转发,ViewModel 管状态 + 编排

iOS必看!Deepseek给的Runtime实现原理,通俗易懂~

作者 WaywardOne
2026年3月12日 19:03

iOS Runtime 消息转发机制完全解析

写在前面

在Objective-C的世界里,方法调用并不是像C++那样在编译时就确定要执行的函数地址,而是一个运行时动态绑定的过程。当我们写下 [receiver message] 这样的代码时,编译器实际上会将其转换为 objc_msgSend(receiver, @selector(message)) 的调用。这个 objc_msgSend 函数会负责在接收者所属的类及其父类的方法列表中查找对应的实现并执行。

那么问题来了:如果一直找到根类NSObject都没有找到这个方法的实现,会发生什么?

很多开发者都见过这样的崩溃信息:unrecognized selector sent to instance 0xXXXXXXXX。这正是因为消息发送失败,而Runtime也没有找到合适的方式处理这条消息,最终通过 doesNotRecognizeSelector: 抛出的异常。

但在这个崩溃发生之前,Objective-C的Runtime给了我们三次"拯救"的机会,这就是本文要详细讲解的消息转发机制


第一章:消息发送机制回顾

在深入探讨消息转发之前,有必要先回顾一下完整的消息发送流程,因为消息转发正是这个流程中处理失败情况的最后保障。

1.1 objc_msgSend的工作流程

当我们向一个对象发送消息时,Runtime系统会按照以下步骤查找方法的实现:

  1. 检查目标对象是否为nil:如果接收者为nil,Objective-C的特性是忽略该消息,程序不会崩溃(这在很多情况下简化了代码逻辑)。如果为nil且消息有返回值,基本数据类型的返回值为0,对象类型的返回值为nil。

  2. 查找缓存:每个类都有一个缓存(cache),用于存储最近使用过的方法。Runtime会首先在该类的缓存中查找方法的实现(IMP)。如果找到,直接调用该实现。

  3. 查找当前类的方法列表:如果在缓存中没有找到,Runtime会从当前类的方法列表中查找。方法列表以数组形式组织,查找过程会遍历整个列表(已排序的列表使用二分查找,否则线性查找)。

  4. 沿着继承链向上查找:如果在当前类中没有找到,Runtime会沿着继承链逐级向上查找父类的方法列表和缓存,直到根类NSObject为止。

  5. 动态方法解析:如果一直找到根类都没有找到方法的实现,Runtime会进入"动态方法解析"阶段,给类一个机会动态添加方法的实现。

  6. 消息转发:如果动态方法解析没有添加实现(或者添加后仍然无法处理),Runtime会进入"消息转发"流程。

  7. 抛出异常:如果所有转发尝试都失败,最终会调用 doesNotRecognizeSelector: 抛出异常,程序崩溃。

这个流程可以用下面的流程图清晰地展示:

flowchart TD
    A[向对象发送消息] --> B{接收者为nil?}
    B -->|是| C[忽略消息/返回0/nil]
    B -->|否| D[查找缓存]
    
    D --> E{缓存中找到IMP?}
    E -->|是| F[调用IMP]
    E -->|否| G[在当前类方法列表中查找]
    
    G --> H{当前类中找到?}
    H -->|是| F
    H -->|否| I[在父类方法列表中查找]
    
    I --> J{父类中找到?}
    J -->|是| F
    J -->|否| I
    
    J -->|一直查到NSObject仍未找到| K[动态方法解析]
    
    K --> L{动态添加了实现?}
    L -->|是| F
    L -->|否| M[消息转发流程]
    
    M --> N{转发成功?}
    N -->|是| F
    N -->|否| O[doesNotRecognizeSelector:\n抛出异常]

1.2 方法的本质:SEL、IMP与Method

要深入理解消息转发,我们需要先了解Objective-C中方法的三个核心概念:

SEL(选择器):是方法的名字,在Runtime中用 objc_selector 结构体表示。在运行时,不同类的同名方法的选择器是相同的。SEL在Runtime中会被唯一化,因此可以使用 == 来比较两个SEL是否相等。

IMP(函数指针):是方法的实现,本质上是一个函数指针,指向方法实现的首地址。它的定义如下:

typedef id (*IMP)(id self, SEL _cmd, ...);

每个IMP都至少包含两个参数:self(消息接收者)和_cmd(这个方法的SEL)。

Method(方法):是用于表示方法定义的结构体,包含三个成员:

struct method_t {
    SEL name;      // 方法名
    const char *types;  // 方法类型编码
    IMP imp;       // 方法实现
}

当我们调用一个方法时,就是从SEL到IMP的映射过程。Runtime维护了每个类的方法列表(method list),这个列表存储了该类定义的所有方法。消息转发机制本质上是在这个映射过程失败后的补救措施。


第二章:消息转发的三个阶段

当消息发送流程无法找到对应的IMP时,Runtime会启动消息转发机制。这个机制分为三个阶段,每个阶段都给开发者一次处理这条"无法识别"的消息的机会。

2.1 第一阶段:动态方法解析

这是消息转发的第一道防线。当Runtime在当前类和父类中都找不到方法的实现时,会首先调用 +resolveInstanceMethod:(对于实例方法)或 +resolveClassMethod:(对于类方法)。

2.1.1 resolveInstanceMethod的工作原理

这个方法的定义如下:

+ (BOOL)resolveInstanceMethod:(SEL)sel

当这个方法被调用时,Runtime给了我们一个机会:可以动态地为这个SEL添加一个实现。如果添加成功并返回YES,Runtime会重新启动消息发送流程,这次就能找到方法的实现了。

这个方法最典型的应用场景是处理 @dynamic 属性。@dynamic 告诉编译器不要自动生成属性的getter和setter方法,我们会在运行时动态提供它们。

2.1.2 实战:动态添加方法实现

让我们通过一个具体的例子来理解这个过程:

#import <objc/runtime.h>

@interface Person : NSObject
@property (nonatomic, copy) NSString *name;  // 注意:我们使用@dynamic
@end

@implementation Person
@dynamic name;  // 告诉编译器不要自动生成getter/setter

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(name)) {
        // 动态添加getter方法
        class_addMethod(self, sel, (IMP)dynamicNameGetter, "@@:");
        return YES;
    }
    else if (sel == @selector(setName:)) {
        // 动态添加setter方法
        class_addMethod(self, sel, (IMP)dynamicNameSetter, "v@:@");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

// getter方法的实现
id dynamicNameGetter(id self, SEL _cmd) {
    // 通过关联对象获取存储的值
    return objc_getAssociatedObject(self, @selector(name));
}

// setter方法的实现
void dynamicNameSetter(id self, SEL _cmd, NSString *newName) {
    // 通过关联对象存储值
    objc_setAssociatedObject(self, @selector(name), newName, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
@end

现在,当我们调用:

Person *p = [[Person alloc] init];
[p setName:@"张三"];
NSLog(@"%@", [p name]);  // 输出:张三

尽管Person类并没有真正实现name的getter和setter方法,但在消息发送过程中,Runtime调用了 resolveInstanceMethod:,我们动态添加了这两个方法的实现,因此程序能够正常运行。

2.1.3 方法签名的类型编码

在调用 class_addMethod 时,我们需要指定方法的类型编码(types)。这个编码字符串描述了方法的返回类型和参数类型。例如:

  • "v@:" 表示返回void,有两个参数:id和SEL(即标准的实例方法)
  • "@@" 表示返回id,有两个参数:id和SEL(标准的getter方法)
  • "v@:@" 表示返回void,有三个参数:id、SEL和id(标准的setter方法)

完整的类型编码表:

编码 含义
c char
i int
s short
l long
q long long
C unsigned char
I unsigned int
S unsigned short
L unsigned long
Q unsigned long long
f float
d double
B BOOL/C++ bool
v void
* char* (字符串)
@ id (对象)
# Class (类对象)
: SEL (选择器)
^type 指向type的指针

2.1.4 类方法的动态解析

对于类方法,我们需要重写 +resolveClassMethod:

+ (BOOL)resolveClassMethod:(SEL)sel {
    if (sel == @selector(classMethod)) {
        // 注意:这里添加方法的目标是元类(metaclass)
        Class metaClass = objc_getMetaClass(class_getName(self));
        class_addMethod(metaClass, sel, (IMP)dynamicClassMethodImp, "v@:");
        return YES;
    }
    return [super resolveClassMethod:sel];
}

需要注意的是,类方法是存储在元类(metaclass)中的,因此我们需要获取元类来添加类方法的实现。

2.1.5 动态方法解析的时机

动态方法解析发生在消息发送流程失败之后,但在消息转发之前。如果你希望每次调用这个方法时都能走动态解析,注意这个方法只会被调用一次(因为一旦添加了实现,后续调用就能直接找到IMP了)。

2.2 第二阶段:快速消息转发

如果动态方法解析没有添加实现(或者返回NO),Runtime会进入消息转发的第二阶段:快速消息转发。

这个阶段的核心是 forwardingTargetForSelector: 方法。Runtime会调用这个方法,期望它能返回一个能够处理这条消息的对象。

2.2.1 forwardingTargetForSelector的定义

- (id)forwardingTargetForSelector:(SEL)aSelector

这个方法的职责是:当对象无法处理某个消息时,返回一个能够处理该消息的对象。Runtime会将原始消息转发给这个返回的对象,就好像它才是原始的消息接收者一样。

这个机制非常高效,因为它只是简单地改变消息的接收者,不需要创建 NSInvocation 对象,也没有复杂的参数处理。

2.2.2 实战:将消息转发给备用对象

假设我们有一个 Person 类,它不包含 run 方法,但我们有一个 Car 类实现了 run 方法:

@interface Car : NSObject
- (void)run;
@end

@implementation Car
- (void)run {
    NSLog(@"Car is running");
}
@end

@interface Person : NSObject
@end

@implementation Person
- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(run)) {
        // 返回一个可以处理run消息的Car对象
        return [[Car alloc] init];
    }
    return [super forwardingTargetForSelector:aSelector];
}
@end

现在执行以下代码:

Person *person = [[Person alloc] init];
[person run];  // 输出:Car is running

尽管 Person 对象并没有 run 方法,但通过 forwardingTargetForSelector:,我们将 run 消息转发给了 Car 对象,程序能够正常运行。

2.2.3 模拟多重继承

Objective-C不支持多重继承,但通过快速消息转发,我们可以实现类似多重继承的效果。一个对象可以将自己没有实现的方法转发给其他对象,从外部看就像这个对象继承了多个类的功能。

例如,我们可以创建一个类,它能够处理来自多个不同类的方法:

@interface MultiClass : NSObject
@property (nonatomic, strong) Car *car;
@property (nonatomic, strong) House *house;
@end

@implementation MultiClass
- (instancetype)init {
    if (self = [super init]) {
        _car = [[Car alloc] init];
        _house = [[House alloc] init];
    }
    return self;
}

- (id)forwardingTargetForSelector:(SEL)aSelector {
    if ([_car respondsToSelector:aSelector]) {
        return _car;
    } else if ([_house respondsToSelector:aSelector]) {
        return _house;
    }
    return [super forwardingTargetForSelector:aSelector];
}
@end

这样,MultiClass 的实例就能同时响应 CarHouse 的方法,达到了类似多重继承的效果。

2.2.4 注意事项

使用 forwardingTargetForSelector: 时有几点需要注意:

  1. 不要返回self:如果在这个方法中返回self,会造成无限循环,因为Runtime会再次尝试向self发送消息。
  2. 这个方法主要用于转发给其他对象,不适合修改消息本身。
  3. 返回的对象不必与原始接收者有继承关系,任何对象都可以。
  4. 如果返回nil或self,则进入下一阶段:完整消息转发。

2.3 第三阶段:完整消息转发

如果前两个阶段都无法处理消息,Runtime会进入最后一个阶段:完整消息转发。这是消息转发机制中最强大、最灵活但也最复杂的阶段。

这个阶段涉及两个方法:

  • methodSignatureForSelector::获取方法的签名(参数类型和返回类型)
  • forwardInvocation::转发封装了消息的 NSInvocation 对象
flowchart TD
    A[消息转发第二阶段返回nil] --> B[调用methodSignatureForSelector:]
    
    B --> C{返回有效的方法签名?}
    C -->|否| D[调用doesNotRecognizeSelector:\n抛出异常]
    C -->|是| E[创建NSInvocation对象]
    
    E --> F[调用forwardInvocation:\n并将NSInvocation传入]
    
    F --> G{在forwardInvocation:中\n处理消息?}
    G -->|否| D
    G -->|是| H[消息处理成功]
    
    H --> I[将返回值传递给\n原始消息发送者]

2.3.1 methodSignatureForSelector: 的作用

methodSignatureForSelector: 的定义如下:

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector

Runtime调用这个方法的目的是获取方法的签名信息,包括方法的返回类型和参数类型。有了这些信息,Runtime才能创建 NSInvocation 对象。

如果这个方法返回nil,Runtime会直接调用 doesNotRecognizeSelector: 并抛出异常,程序崩溃。因此,在实现完整消息转发时,我们必须为无法处理的消息提供一个有效的方法签名。

2.3.2 创建方法签名

方法签名可以通过多种方式创建:

// 方式1:使用字符串创建(类型编码)
NSMethodSignature *signature1 = [NSMethodSignature signatureWithObjCTypes:"v@:"];

// 方式2:从已有方法获取
NSMethodSignature *signature2 = [self methodSignatureForSelector:@selector(existingMethod)];

// 方式3:从协议获取
struct objc_method_description desc = protocol_getMethodDescription(protocol, selector, YES, YES);
NSMethodSignature *signature3 = [NSMethodSignature signatureWithObjCTypes:desc.types];

类型编码字符串的格式和之前 class_addMethod 中使用的格式一致。

2.3.3 forwardInvocation: 的核心作用

forwardInvocation: 的定义如下:

- (void)forwardInvocation:(NSInvocation *)anInvocation

methodSignatureForSelector: 返回了有效的方法签名后,Runtime会创建一个 NSInvocation 对象,该对象封装了这条消息的所有信息:

  • 消息的目标(target)
  • 消息的选择器(selector)
  • 所有的参数
  • 等待填充的返回值

然后将这个 NSInvocation 对象作为参数传递给 forwardInvocation: 方法。在这个方法中,我们可以:

  1. 将消息转发给其他对象
  2. 修改消息的选择器、参数或目标
  3. 直接处理消息并设置返回值
  4. 甚至"吃掉"消息,什么都不做(这样就不会崩溃)

2.3.4 实战:完整消息转发的实现

下面是一个完整的示例,演示如何实现完整消息转发:

@interface Person : NSObject
@end

@interface Car : NSObject
- (void)run;
@end

@implementation Car
- (void)run {
    NSLog(@"Car is running");
}
@end

@implementation Person
// 第一步:提供方法签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if (aSelector == @selector(run)) {
        // 返回run方法的签名:"v@:" 表示返回void,两个参数:id, SEL
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}

// 第二步:转发调用
- (void)forwardInvocation:(NSInvocation *)anInvocation {
    SEL selector = [anInvocation selector];
    
    // 创建备用对象
    Car *car = [[Car alloc] init];
    
    // 检查备用对象是否能响应这个选择器
    if ([car respondsToSelector:selector]) {
        // 将消息转发给备用对象
        [anInvocation invokeWithTarget:car];
    } else {
        // 如果备用对象也不能处理,调用父类实现(最终会抛出异常)
        [super forwardInvocation:anInvocation];
    }
}
@end

执行测试代码:

Person *person = [[Person alloc] init];
[person run];  // 输出:Car is running

2.3.5 修改消息内容后转发

完整消息转发的一个强大之处在于,我们可以在转发前修改消息的内容。例如,我们可以修改方法的选择器:

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    SEL originalSelector = [anInvocation selector];
    
    if (originalSelector == @selector(run)) {
        // 修改选择器为drive
        [anInvocation setSelector:@selector(drive)];
        
        Car *car = [[Car alloc] init];
        if ([car respondsToSelector:@selector(drive)]) {
            [anInvocation invokeWithTarget:car];
            return;
        }
    }
    
    [super forwardInvocation:anInvocation];
}

我们也可以修改参数:

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    SEL selector = [anInvocation selector];
    
    if (selector == @selector(setAge:)) {
        // 获取原始参数
        int age;
        [anInvocation getArgument:&age atIndex:2]; // 前两个参数是self和_cmd
        
        // 修改参数值(例如:限制年龄范围)
        if (age < 0) age = 0;
        if (age > 150) age = 150;
        
        // 设置修改后的参数
        [anInvocation setArgument:&age atIndex:2];
    }
    
    // 转发给实际处理的对象
    if ([_realObject respondsToSelector:selector]) {
        [anInvocation invokeWithTarget:_realObject];
    } else {
        [super forwardInvocation:anInvocation];
    }
}

2.3.6 处理返回值

NSInvocation 也能处理返回值。我们可以从 anInvocation 中获取返回值,修改它,或者设置自己的返回值:

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    // 先尝试转发给备用对象
    if ([_backup respondsToSelector:[anInvocation selector]]) {
        [anInvocation invokeWithTarget:_backup];
        
        // 获取返回值
        char returnType[10];
        strcpy(returnType, [[anInvocation methodSignature] methodReturnType]);
        
        if (returnType[0] == '@') { // 返回对象类型
            id result = nil;
            [anInvocation getReturnValue:&result];
            
            // 可以修改返回值
            if (result == nil) {
                result = @"Default Value";
                [anInvocation setReturnValue:&result];
            }
        }
        return;
    }
    
    [super forwardInvocation:anInvocation];
}

2.3.7 转发给多个对象

完整消息转发甚至可以将一个消息转发给多个对象。这在某些设计模式中很有用,例如观察者模式或责任链模式:

@interface MessageChain : NSObject
@property (nonatomic, strong) NSArray *handlers;
@end

@implementation MessageChain
- (void)forwardInvocation:(NSInvocation *)anInvocation {
    SEL selector = [anInvocation selector];
    BOOL handled = NO;
    
    for (id handler in self.handlers) {
        if ([handler respondsToSelector:selector]) {
            [anInvocation invokeWithTarget:handler];
            handled = YES;
            // 可以选择是否继续转发给下一个处理器
            // break;
        }
    }
    
    if (!handled) {
        [super forwardInvocation:anInvocation];
    }
}
@end

2.4 三个阶段的关系与选择

这三个阶段是递进的关系:如果第一阶段处理了,第二阶段就不会触发;如果第二阶段处理了,第三阶段就不会触发。

选择使用哪个阶段取决于你的需求:

  • 动态方法解析:适合在运行时动态添加方法实现,例如处理 @dynamic 属性、实现轻量级的代理模式。
  • 快速消息转发:适合简单地将消息转发给另一个对象,性能最好,但不能修改消息内容。
  • 完整消息转发:最强大、最灵活,可以修改消息内容、参数、返回值,甚至可以将消息转发给多个对象,但性能开销也最大。

第三章:深入源码分析

了解理论之后,让我们深入Runtime的源码,看看消息转发机制究竟是如何实现的。这里我们基于苹果开源的objc4源码进行分析。

3.1 从消息发送到消息转发的转折点

objc_msgSend 的核心实现中,如果方法查找失败,会调用 lookUpImpOrForward 函数。这个函数的简化逻辑如下:

IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                       bool initialize, bool cache, bool resolver) {
    IMP imp = nil;
    bool triedResolver = NO;
    
    // 尝试从缓存和方法列表中查找
    // ...
    
    // 如果没有找到实现
    if (resolver && !triedResolver) {
        // 调用动态方法解析
        _class_resolveMethod(cls, sel, inst);
        triedResolver = YES;
        // 重新尝试查找
        goto retry;
    }
    
    // 动态解析失败,返回转发IMP
    imp = (IMP)_objc_msgForward_impcache;
    
    return imp;
}

关键点在于:当动态方法解析失败后,lookUpImpOrForward 会返回一个特殊的IMP:_objc_msgForward_impcache。这个IMP指向的是消息转发的入口函数。

3.2 消息转发的入口:__objc_msgForward

_objc_msgForward_impcache 最终会调用到 __objc_msgForward 函数。在x86_64架构的汇编实现中,这个函数的逻辑大致是:

ENTRY __objc_msgForward
    // 跳转到消息转发的核心实现
    jmp __objc_forward_handler
END_ENTRY __objc_msgForward

__objc_forward_handler 是一个C函数,它会调用到CoreFoundation框架中的 __forwarding__ 函数。这就是消息转发的真正核心实现。

3.3 CoreFoundation中的__forwarding__函数

__forwarding__ 函数是消息转发机制的心脏。虽然苹果没有开源CoreFoundation的全部代码,但通过反汇编和分析,我们可以还原其大致逻辑:

int __forwarding__(void *frameStackPointer, int isStret) {
    // 获取消息的接收者和选择器
    id receiver = *(id *)frameStackPointer;
    SEL sel = *(SEL *)(frameStackPointer + sizeof(id));
    
    // 尝试快速转发
    id forwardingTarget = nil;
    if ([receiver respondsToSelector:@selector(forwardingTargetForSelector:)]) {
        forwardingTarget = [receiver forwardingTargetForSelector:sel];
        if (forwardingTarget != nil && forwardingTarget != receiver) {
            // 转发给目标对象
            return objc_msgSend(forwardingTarget, sel, ...);
        }
    }
    
    // 快速转发失败,尝试完整转发
    NSMethodSignature *signature = nil;
    if ([receiver respondsToSelector:@selector(methodSignatureForSelector:)]) {
        signature = [receiver methodSignatureForSelector:sel];
    }
    
    if (signature == nil) {
        // 没有方法签名,无法继续
        [receiver doesNotRecognizeSelector:sel];
        return 0;
    }
    
    // 创建NSInvocation对象
    NSInvocation *invocation = [NSInvocation _invocationWithMethodSignature:signature frame:frameStackPointer];
    
    // 调用forwardInvocation:
    if ([receiver respondsToSelector:@selector(forwardInvocation:)]) {
        [receiver forwardInvocation:invocation];
    } else {
        [receiver doesNotRecognizeSelector:sel];
    }
    
    // 获取返回值
    // ...
    return 0;
}

从这个伪代码可以看出,__forwarding__ 函数完整地实现了我们之前讨论的消息转发流程:

  1. 尝试快速转发
  2. 如果快速转发没有返回合适的对象,尝试获取方法签名
  3. 如果方法签名有效,创建 NSInvocation 并调用 forwardInvocation:
  4. 如果所有步骤都失败,调用 doesNotRecognizeSelector: 抛出异常

3.4 日志调试技巧

Runtime提供了一个调试函数 instrumentObjcMessageSends,可以让我们查看消息发送和转发的详细过程:

extern void instrumentObjcMessageSends(BOOL flag);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 开启消息发送日志
        instrumentObjcMessageSends(YES);
        
        Person *person = [[Person alloc] init];
        [person run];
        
        // 关闭日志
        instrumentObjcMessageSends(NO);
    }
    return 0;
}

运行程序后,在 /tmp/msgSends- 目录下会生成日志文件,内容类似于:

+ Person NSObject initialize
+ Person NSObject new
- Person NSObject init
- Person forwardingTargetForSelector: run
- Person methodSignatureForSelector: run
- Person forwardInvocation:
- Person doesNotRecognizeSelector: run

通过这个日志,我们可以清楚地看到消息转发的每一步调用过程,对于理解和调试消息转发非常有帮助。


第四章:消息转发的应用场景

消息转发机制不仅仅是理论上的知识点,它在实际开发中有很多实用的应用场景。

4.1 防止崩溃:安全的消息调用

一个常见的应用场景是防止因为调用未实现方法而导致的崩溃。例如,我们可以创建一个安全的代理对象,当目标对象不能响应某个消息时,不是崩溃而是返回一个默认值:

@interface SafeProxy : NSObject
@property (nonatomic, weak) id target;
@end

@implementation SafeProxy
- (id)forwardingTargetForSelector:(SEL)aSelector {
    // 如果target可以响应,直接转发
    if ([_target respondsToSelector:aSelector]) {
        return _target;
    }
    return self; // 让完整转发来处理
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    // 为任何方法提供默认签名(返回对象类型)
    return [NSMethodSignature signatureWithObjCTypes:"@@:"];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    // 不处理消息,只设置返回值为nil
    id nilValue = nil;
    [anInvocation setReturnValue:&nilValue];
}
@end

使用这个SafeProxy,我们可以安全地调用任何方法:

Person *person = [[Person alloc] init];
SafeProxy *proxy = [[SafeProxy alloc] init];
proxy.target = person;

// 如果person实现了run方法,正常执行
[proxy run]; 

// 如果person没有实现fly方法,不会崩溃,而是返回nil
id result = [proxy fly]; // result = nil,没有崩溃

4.2 模拟多继承

如前所述,通过消息转发可以实现类似多继承的效果。这在某些设计模式中非常有用,例如"装饰器"模式或"代理"模式。

4.3 API兼容性处理

在开发中,我们经常会遇到iOS系统版本升级导致API变化的情况。通过消息转发,我们可以优雅地处理这种变化:

@interface CompatibilityHandler : NSObject
@end

@implementation CompatibilityHandler
+ (void)load {
    // 交换forwardInvocation:方法
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class cls = [UIDevice class];
        SEL originalSelector = @selector(forwardInvocation:);
        SEL swizzledSelector = @selector(compatibility_forwardInvocation:);
        
        // 方法交换的实现...
    });
}

- (void)compatibility_forwardInvocation:(NSInvocation *)invocation {
    SEL selector = [invocation selector];
    
    if (selector == @selector(isLowPowerModeEnabled)) {
        // 低电量模式是iOS 9.0引入的
        if (@available(iOS 9.0, *)) {
            // 如果系统支持,转发给原始实现
            [invocation invoke];
        } else {
            // 如果不支持,返回默认值NO
            BOOL defaultValue = NO;
            [invocation setReturnValue:&defaultValue];
        }
    } else {
        // 其他消息正常转发
        [self compatibility_forwardInvocation:invocation];
    }
}
@end

4.4 实现AOP(面向切面编程)

通过消息转发,我们可以实现简单的AOP编程,在不修改原有类的情况下添加额外的逻辑:

@interface AspectProxy : NSObject
@property (nonatomic, strong) id target;
@property (nonatomic, copy) void (^beforeBlock)(SEL);
@property (nonatomic, copy) void (^afterBlock)(SEL);
@end

@implementation AspectProxy
- (id)forwardingTargetForSelector:(SEL)aSelector {
    // 必须返回nil才能进入完整转发
    return nil;
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    return [_target methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    SEL selector = [anInvocation selector];
    
    // 执行前置逻辑
    if (_beforeBlock) {
        _beforeBlock(selector);
    }
    
    // 转发给目标对象
    if ([_target respondsToSelector:selector]) {
        [anInvocation invokeWithTarget:_target];
    }
    
    // 执行后置逻辑
    if (_afterBlock) {
        _afterBlock(selector);
    }
}
@end

4.5 实现动态代理

在RxSwift等响应式编程框架中,消息转发被广泛用于实现动态代理,拦截方法调用并将它们转换为信号流:

// RxSwift中拦截方法的简化实现
@interface RXMessageSentObserver : NSObject
// ... 
@end

@implementation _RXObjCRuntime
- (void)interceptMethod:(SEL)selector ofClass:(Class)cls {
    // 1. 创建子类
    // 2. 重写forwardInvocation:
    // 3. 在forwardInvocation:中创建信号
}
@end

4.6 JSPatch等热修复框架的实现原理

热修复框架如JSPatch利用消息转发机制来实现动态替换OC方法的实现。基本原理是:

  1. 将要修复的类的 forwardInvocation: 方法替换为自己的实现
  2. 将原方法的IMP指向 _objc_msgForward,强制进入消息转发流程
  3. forwardInvocation: 中,执行JavaScript代码

第五章:性能考量与最佳实践

消息转发机制虽然强大,但使用不当可能会带来性能问题。

5.1 性能开销分析

不同阶段的消息转发性能开销不同:

阶段 性能开销 主要原因
正常消息发送 极小 直接查找IMP并调用
动态方法解析 较小 只执行一次,后续调用正常
快速消息转发 中等 需要调用Cocoa方法,但流程简单
完整消息转发 较大 需要创建NSInvocation对象,处理参数和返回值

为什么完整消息转发开销大

  1. 需要调用 methodSignatureForSelector: 获取方法签名
  2. Runtime需要根据方法签名创建 NSInvocation 对象
  3. NSInvocation 需要拷贝参数和设置返回值
  4. 整个流程涉及多次Objective-C方法调用

5.2 性能优化建议

根据性能开销,我们应遵循以下最佳实践:

  1. 优先使用快速消息转发:如果只是简单地将消息转发给另一个对象,尽量使用 forwardingTargetForSelector:,避免使用完整转发。

  2. 缓存方法签名:如果在完整转发中经常处理同一类消息,可以缓存方法签名,避免每次调用 methodSignatureForSelector:

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    static NSMutableDictionary *signatureCache;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        signatureCache = [NSMutableDictionary dictionary];
    });
    
    NSString *selString = NSStringFromSelector(aSelector);
    NSMethodSignature *signature = signatureCache[selString];
    if (!signature) {
        signature = [NSMethodSignature signatureWithObjCTypes:"v@:"];
        signatureCache[selString] = signature;
    }
    return signature;
}
  1. 避免频繁触发转发:如果一个方法经常被调用,最好不要依赖消息转发来处理它。考虑在 resolveInstanceMethod: 中动态添加IMP,这样后续调用就和正常方法一样快了。

5.3 调试消息转发

当遇到与消息转发相关的bug时,可以使用以下调试技巧:

  1. 使用instrumentObjcMessageSends:开启日志,查看消息转发的每一步。

  2. 添加日志输出:在转发方法中添加日志:

- (id)forwardingTargetForSelector:(SEL)aSelector {
    NSLog(@"📱 Forwarding %@ to another target", NSStringFromSelector(aSelector));
    // ...
}
  1. 使用断点调试:在 forwardInvocation: 中设置断点,检查 NSInvocation 的内容。

  2. 检查方法签名:常见的崩溃原因是 methodSignatureForSelector: 返回了不正确的签名。可以使用以下代码验证签名:

NSMethodSignature *sig = [self methodSignatureForSelector:@selector(someMethod:)];
NSLog(@"Signature: %s", [sig methodReturnType]); // 检查返回类型
for (NSUInteger i = 0; i < [sig numberOfArguments]; i++) {
    NSLog(@"Arg %lu: %s", i, [sig getArgumentTypeAtIndex:i]);
}

5.4 与其他动态特性的比较

消息转发与Objective-C的其他动态特性既有联系又有区别:

特性 目的 触发时机
消息转发 处理无法识别的消息 方法查找失败后
方法交换 交换两个方法的IMP 运行时主动执行
动态添加方法 为类添加新方法 运行时主动执行
KVO 监听属性变化 创建子类并重写setter

重要区别

  • 消息转发是被动的,只有在正常消息发送失败后才会触发
  • 方法交换、动态添加方法是主动的,我们可以在任何时候执行
  • KVO是利用Runtime创建子类并重写方法,本质上也是动态特性的一种应用

第六章:面试深度解析

消息转发是iOS面试中的高级话题。下面梳理一些常见的面试题和深度解析。

6.1 基础问题

Q1:OC中给nil对象发送消息会发生什么?

解析:给nil发送消息是安全的,不会崩溃。Runtime在 objc_msgSend 中会首先检查接收者是否为nil,如果是nil,直接返回。返回值的类型取决于方法声明的返回类型:

  • 如果返回对象类型,返回nil
  • 如果返回整型,返回0
  • 如果返回结构体,返回的结构体各字段都是0
  • 如果返回浮点类型,返回0.0

Q2:unrecognized selector sent to instance 这个异常是怎么产生的?

解析:当向一个对象发送它无法处理的消息,且消息转发机制也无法处理时,Runtime最终会调用 doesNotRecognizeSelector: 方法。NSObject 中该方法的默认实现就是抛出这个异常。也就是说,这个异常是消息转发流程失败的最后结果。

Q3:消息转发分哪几个阶段?每个阶段的作用是什么?

解析:消息转发分为三个阶段:

  1. 动态方法解析:调用 resolveInstanceMethod:/resolveClassMethod:,允许开发者动态添加方法实现。

  2. 快速消息转发:调用 forwardingTargetForSelector:,允许将消息转发给另一个对象。

  3. 完整消息转发:调用 methodSignatureForSelector: 获取方法签名,然后创建 NSInvocation 对象并调用 forwardInvocation:,允许修改消息内容或转发给多个对象。

6.2 进阶问题

Q4:如何在运行时动态添加方法?

解析:在 resolveInstanceMethod: 中使用 class_addMethod 函数:

void dynamicMethodIMP(id self, SEL _cmd) {
    NSLog(@"动态添加的方法被调用");
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(dynamicMethod)) {
        class_addMethod(self, sel, (IMP)dynamicMethodIMP, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

Q5:快速转发和完整转发有什么区别?如何选择?

解析:主要区别在于:

  1. 需要重载的方法数量:快速转发只需重载 forwardingTargetForSelector:,完整转发需要重载 methodSignatureForSelector:forwardInvocation: 两个方法。

  2. 功能强大程度:快速转发只能简单地改变消息接收者,不能修改消息内容;完整转发可以修改消息的参数、选择器、返回值等。

  3. 性能开销:快速转发性能更好,完整转发需要创建 NSInvocation 对象,开销较大。

选择建议

  • 如果只是想将消息转发给另一个对象,且不需要修改消息内容,优先使用快速转发
  • 如果需要修改消息内容、参数、返回值,或者需要将消息转发给多个对象,使用完整转发

Q6:消息转发可以用来实现多重继承吗?和真正的多重继承有什么区别?

解析:可以通过消息转发实现类似多重继承的效果。区别在于:

  • 真正的多重继承是将多个类的功能合并到一个对象中
  • 通过消息转发实现的"伪多继承",功能仍然分散在不同的对象中,只是通过转发机制让外部看起来像一个对象处理了所有消息

Q7:如果消息转发的方法本身也找不到实现会怎样?

解析:这是一个容易忽略的细节。如果消息转发的方法(如 forwardingTargetForSelector:)本身没有实现,Runtime也会按照同样的流程查找它的实现。如果找不到,同样会触发消息转发。但通常情况下,这些方法在 NSObject 中都有默认实现,所以不会出现这种情况。

Q8:如何调试消息转发过程?

解析:可以使用以下方法:

  1. 使用 instrumentObjcMessageSends(YES) 开启日志
  2. 查看 /tmp/msgSends- 目录下的日志文件
  3. 在转发方法中添加断点和日志输出
  4. 使用反汇编工具分析 __forwarding__ 函数

6.3 高级问题

Q9:消息转发和method swizzling有什么关系?能结合使用吗?

解析:消息转发和method swizzling是两种不同的动态特性,但可以结合使用。例如,可以实现一个通用的方法拦截机制:

// 1. 先将原方法的IMP替换为_objc_msgForward
Method method = class_getInstanceMethod(cls, originalSelector);
method_setImplementation(method, _objc_msgForward);

// 2. 再添加一个转发方法
class_addMethod(cls, @selector(customForward:), (IMP)customForwardIMP, "v@:@");

// 3. 交换forwardInvocation:方法
Method originalForwardMethod = class_getInstanceMethod(cls, @selector(forwardInvocation:));
Method swizzledForwardMethod = class_getInstanceMethod(cls, @selector(customForwardInvocation:));
method_exchangeImplementations(originalForwardMethod, swizzledForwardMethod);

这种技术被用于RxSwift等框架的方法拦截功能。

Q10:如何实现一个通用的消息转发中心,能够记录所有无法识别的消息?

解析:可以创建一个基类,所有需要日志功能的类都继承自这个基类:

@interface LoggingBase : NSObject
@property (nonatomic, strong) NSMutableArray *unrecognizedMessages;
@end

@implementation LoggingBase
- (instancetype)init {
    if (self = [super init]) {
        _unrecognizedMessages = [NSMutableArray array];
    }
    return self;
}

- (id)forwardingTargetForSelector:(SEL)aSelector {
    // 记录无法识别的消息
    NSString *message = [NSString stringWithFormat:@"%@: %@", self, NSStringFromSelector(aSelector)];
    [_unrecognizedMessages addObject:message];
    
    // 可以选择转发给默认处理对象
    return [DefaultHandler sharedHandler];
}

// 可以添加一个方法来导出日志
- (void)exportUnrecognizedMessages {
    NSLog(@"Unrecognized messages: %@", _unrecognizedMessages);
}
@end

Q11:消息转发机制在ARC下有什么特别需要注意的地方?

解析:ARC下使用消息转发时需要注意:

  1. 内存管理:在 forwardInvocation: 中处理对象参数时,ARC会自动处理内存管理,但要注意不要造成循环引用。

  2. 方法签名:方法签名的类型编码必须准确,特别是在有对象参数或返回值时。错误的类型编码可能导致ARC下的内存管理错误。

  3. 返回值处理:当从 forwardInvocation: 返回时,Runtime会根据方法签名自动处理返回值的retain/release。如果方法签名不准确,可能导致内存泄漏或崩溃。

  4. 使用 __unsafe_unretained:在某些情况下,可能需要使用 __unsafe_unretained 来避免ARC自动插入的retain/release操作干扰转发逻辑。

Q12:从源码层面分析,消息转发和消息发送的性能差异主要体现在哪些方面?

解析:从源码层面看,性能差异主要体现在:

  1. 正常消息发送:汇编实现,查找缓存后直接跳转,几条指令就能完成。

  2. 动态方法解析:需要调用Objective-C方法,但只执行一次,后续调用恢复正常。

  3. 快速转发:需要调用 forwardingTargetForSelector:,这是一个完整的Objective-C方法调用,涉及消息发送流程。但无需创建复杂的对象。

  4. 完整转发

    • 需要调用 methodSignatureForSelector: 获取签名
    • Runtime需要遍历方法签名,解析每个参数的类型
    • 创建 NSInvocation 对象需要分配内存
    • NSInvocation 需要拷贝参数值
    • 调用 forwardInvocation: 方法
    • 转发后需要处理返回值

这些步骤加起来,完整转发的性能开销可能是正常消息发送的几十倍甚至上百倍。


第七章:总结与展望

7.1 消息转发机制的核心价值

Objective-C的消息转发机制是其动态性的集中体现,它给了开发者三次机会来处理无法识别的消息:

  1. 动态方法解析:让我们能够在运行时动态添加方法实现
  2. 快速消息转发:让我们能够将消息简单地转发给其他对象
  3. 完整消息转发:让我们能够完全掌控消息的处理过程

这三次机会形成了一个从简单到复杂的递进结构,开发者可以根据需求选择合适的层次进行干预。

7.2 设计思想解读

消息转发机制的设计体现了几个重要的软件工程思想:

  1. 容错性:系统提供了容错机制,允许程序在出现问题时尝试恢复,而不是直接崩溃。

  2. 渐进式干预:提供了三个层次的干预机会,每个层次都有不同的复杂度和能力,开发者可以根据需要选择。

  3. 开闭原则:通过消息转发,我们可以在不修改原有类的情况下,扩展类的功能,符合开闭原则。

  4. 责任链模式:消息转发本质上是一个责任链模式的实现,每个阶段都有机会处理消息,如果处理不了就传递给下一阶段。

7.3 与其他语言动态特性的对比

与其他动态语言相比,Objective-C的消息转发机制有独特之处:

语言 类似特性 特点
Objective-C 消息转发 分三个阶段,功能强大,与Runtime紧密结合
Ruby method_missing 类似forwardInvocation:,但更简洁
Python getattr 属性访问的fallback机制
JavaScript Proxy 可以拦截对象的各种操作

其中,Ruby的 method_missing 和Objective-C的 forwardInvocation: 最为相似。不同之处在于,Objective-C提供了更细粒度的控制(三个阶段),而Ruby只提供了一个统一的入口。

7.4 未来展望

随着Swift的兴起,Objective-C的使用场景在减少,但消息转发机制的设计思想仍然值得学习:

  1. Swift的动态特性:Swift虽然强调静态类型安全,但也提供了反射机制和 @objc 动态特性。理解消息转发有助于理解Swift中与Objective-C交互的部分。

  2. 跨平台开发:像Flutter这样的跨平台框架,在实现平台通道时也借鉴了消息转发的思想。

  3. AOP编程:面向切面编程在现代开发中越来越重要,消息转发是实现AOP的基础技术之一。

7.5 最后的思考

消息转发机制是Objective-C Runtime皇冠上的明珠,它展示了动态语言的强大能力。掌握消息转发,不仅能帮助我们写出更健壮的代码,还能让我们更深入地理解Objective-C的设计哲学。

在实际开发中,我们应当合理使用消息转发机制:

  • 在需要的地方使用,但不要滥用
  • 优先考虑性能更好的方案(如快速转发优先于完整转发)
  • 做好日志和调试,确保转发逻辑正确

最终,消息转发机制体现了编程语言设计中的一个重要思想:给予开发者更多的控制权,同时也赋予更多的责任。当我们决定使用消息转发时,我们实际上是在说:"我知道这条消息可能无法被正常处理,但我有办法解决这个问题。"

这种思想超越了具体的编程语言,是每个优秀程序员都应该具备的能力——在系统无法自动处理的情况发生时,能够提供优雅的降级方案。


参考资料

  1. Apple官方文档:forwardInvocation:
  2. Objective-C Runtime源码 (objc4-818.2)
  3. 《Effective Objective-C 2.0》 - Matt Galloway
  4. 《Objective-C Runtime Programming Guide》 - Apple Inc.

OC消息转发机制

作者 小鸿是他
2026年2月5日 15:48

OC的消息转发机制(Message Forwarding)是 Objective-C 动态特性的核心之一。它允许对象在无法直接响应某个消息时,有机会将其转发给其他对象处理,而不是直接崩溃。

这个机制分为三个阶段,按顺序执行:


第一阶段:动态方法解析(Dynamic Method Resolution)

  • 方法名resolveInstanceMethod: (实例方法) 和 resolveClassMethod: (类方法)
  • 调用时机:当对象在自己的方法列表(objc_method_list)中找不到对应的方法实现时,会首先调用这个方法。
  • 作用:允许对象动态地添加新的方法实现。
  • 返回值:返回 YES 表示已成功添加方法,NO 表示未处理。
  • 关键点:这个阶段可以使用 class_addMethod 函数来添加方法。

示例代码:

// 假设有一个类 MyObject
@interface MyObject : NSObject
@end

@implementation MyObject

// 第一阶段:动态方法解析
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    // 检查是否是我们想动态添加的方法
    if (sel == @selector(someDynamicMethod)) {
        // 动态添加方法实现
        IMP newIMP = imp_implementationWithBlock(^{
            NSLog(@"This method was added dynamically!");
        });
        
        // 将新方法添加到类中
        class_addMethod([self class], sel, newIMP, "v@:");
        return YES; // 表示已处理
    }
    
    // 其他方法交给后续阶段处理
    return [super resolveInstanceMethod:sel];
}

// 原始方法(这里我们不定义,让其走转发流程)
// - (void)someDynamicMethod; // 这个方法在类中没有实现

@end

// 使用示例
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        MyObject *obj = [[MyObject alloc] init];
        
        // 调用动态添加的方法
        [obj someDynamicMethod]; // 输出: This method was added dynamically!
        
        // 如果调用一个不存在的方法,会进入第二阶段
        // [obj undefinedMethod]; // 会进入第二阶段
        
    }
    return 0;
}

第二阶段:备选接收者(Forwarding Target)

  • 方法名forwardingTargetForSelector:
  • 调用时机:如果第一阶段没有处理该方法,且对象实现了这个方法,系统会调用它。
  • 作用:允许对象将消息转发给另一个对象(备选接收者)。
  • 返回值:返回一个对象,该对象将接收后续的消息。如果返回 nil,则进入第三阶段。
  • 关键点:这个阶段是直接转发,不改变消息的 selector

示例代码:

@interface AnotherObject : NSObject
- (void)forwardedMethod;
@end

@implementation AnotherObject
- (void)forwardedMethod {
    NSLog(@"This method is forwarded to AnotherObject!");
}
@end

@interface MyObject : NSObject
@property (nonatomic, strong) AnotherObject *anotherObject; // 备选接收者
@end

@implementation MyObject

// 第二阶段:提供备选接收者
- (id)forwardingTargetForSelector:(SEL)aSelector {
    // 检查是否是特定方法,如果是,则转发给 anotherObject
    if (aSelector == @selector(forwardedMethod)) {
        return self.anotherObject; // 转发给 anotherObject
    }
    
    // 其他方法不转发,进入第三阶段
    return nil;
}

// 第一阶段:动态方法解析(这里不处理 forwardMethod)
// + (BOOL)resolveInstanceMethod:(SEL)sel { ... }

@end

// 使用示例
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        MyObject *obj = [[MyObject alloc] init];
        obj.anotherObject = [[AnotherObject alloc] init];
        
        // 调用一个不在 MyObject 中定义的方法,但会转发给 anotherObject
        [obj forwardedMethod]; // 输出: This method is forwarded to AnotherObject!
        
    }
    return 0;
}

第三阶段:完整的消息转发(Full Forwarding Mechanism)

  • 方法名

    • methodSignatureForSelector::获取方法签名(NSMethodSignature)。
    • forwardInvocation::实际转发 NSInvocation 对象。
  • 调用时机:如果前两个阶段都没有处理该消息,系统会进入这个阶段。

  • 作用:允许你完全控制消息的转发过程,包括方法签名和参数。

  • 关键点

    • 首先调用 methodSignatureForSelector: 获取方法签名,如果返回 nil,则消息转发失败。
    • 然后调用 forwardInvocation:,传入封装了消息的 NSInvocation 对象。
    • 这个阶段允许你修改参数、执行不同的逻辑、或者将消息转发给多个对象

示例代码:

@interface TargetObject : NSObject
- (void)targetMethod:(NSString *)param1 andNumber:(NSInteger)num;
@end

@implementation TargetObject
- (void)targetMethod:(NSString *)param1 andNumber:(NSInteger)num {
    NSLog(@"TargetObject received: %@, %@", param1, @(num));
}
@end

@interface MyObject : NSObject
@property (nonatomic, strong) TargetObject *targetObject;
@end

@implementation MyObject

// 第三阶段:完整转发机制
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    // 检查是否是我们想转发的方法
    if (aSelector == @selector(targetMethod:andNumber:)) {
        // 返回方法签名,用于后续的 invocation 构造
        return [NSMethodSignature signatureWithObjCTypes:"v@:@i"];
    }
    
    // 其他方法交给超类处理
    return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    // 检查 invocation 的 selector 是否是我们要处理的
    SEL selector = [anInvocation selector];
    if (selector == @selector(targetMethod:andNumber:)) {
        // 执行转发逻辑,例如调用 targetObject
        [anInvocation invokeWithTarget:self.targetObject];
        // 或者执行其他逻辑
        // NSLog(@"Forwarding via NSInvocation...");
    } else {
        // 如果不是我们处理的,调用超类的 forwardInvocation
        [super forwardInvocation:anInvocation];
    }
}

// 第一阶段和第二阶段:这里不处理特定方法,让其进入完整转发

@end

// 使用示例
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        MyObject *obj = [[MyObject alloc] init];
        obj.targetObject = [[TargetObject alloc] init];
        
        // 调用一个不在 MyObject 中定义的方法,会进入完整转发
        [obj targetMethod:@"Hello" andNumber:42]; // 输出: TargetObject received: Hello, 42
        
    }
    return 0;
}

总结

OC的消息转发机制是一个强大的特性,允许开发者在运行时灵活处理未知消息。它分为三个阶段:

  1. 动态方法解析:允许对象动态添加方法。
  2. 备选接收者:允许对象将消息转发给另一个对象。
  3. 完整转发机制:允许开发者完全控制消息的转发和执行过程。

关键理解点:

  • 顺序性:严格按照上述三个阶段进行。
  • 最终兜底:如果所有转发机制都没处理,会调用 -doesNotRecognizeSelector:,默认抛出异常。
  • 灵活性:可用于实现动态代理拦截器协议适配器等功能。
  • 性能考虑:消息转发会带来一定的性能开销,应谨慎使用。

这个机制是理解OC动态性、实现高级功能(如KVO、运行时、协议实现)的基础。

应用场景

消息转发机制(Message Forwarding)在实际开发中有许多重要的应用场景,它利用了Objective-C的动态特性,提供了强大的灵活性和扩展性。以下是一些关键的应用:

1. 拦截器/切面编程(Interceptor/AOP)

通过消息转发,可以实现类似AOP(面向切面编程)的功能,对方法调用前后进行增强。

应用场景:

  • 日志记录:自动记录方法调用、参数、返回值。
  • 性能监控:测量方法执行时间。
  • 权限检查:在方法执行前进行权限验证。
  • 缓存机制:将方法结果缓存起来。

示例:

@interface LoggingInterceptor : NSObject
@property (nonatomic, strong) id target;
@end

@implementation LoggingInterceptor

- (id)forwardingTargetForSelector:(SEL)aSelector {
    // 为所有方法添加日志记录
    NSLog(@"[LOG] Calling method: %@", NSStringFromSelector(aSelector));
    return self.target; // 转发给实际的目标对象
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    // 在调用前记录参数
    NSLog(@"[LOG] Parameters: %@", [self getInvocationArguments:anInvocation]);
    
    // 执行实际方法
    [anInvocation invokeWithTarget:self.target];
    
    // 在调用后记录返回值
    id returnValue;
    [anInvocation getReturnValue:&returnValue];
    NSLog(@"[LOG] Return value: %@", returnValue);
}

- (NSString *)getInvocationArguments:(NSInvocation *)invocation {
    // 获取参数信息(简化示例)
    return @"(arguments)";
}

@end

2. 动态方法注册(Dynamic Method Registration)

在运行时根据条件动态地注册或启用某些方法。

应用场景:

  • 功能开关:根据配置启用/禁用某些功能。
  • 插件系统:动态加载插件并注册其方法。
  • 条件编译:根据不同环境(Debug/Release)注册不同方法。

示例:

@interface ConditionalObject : NSObject
@property (nonatomic, assign) BOOL debugEnabled;
@end

@implementation ConditionalObject

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(debugLog:)) {
        if ([self debugEnabled]) {
            // 动态添加调试日志方法
            IMP debugIMP = imp_implementationWithBlock(^(__unsafe_unretained id self, NSString *message) {
                NSLog(@"DEBUG: %@", message);
            });
            class_addMethod([self class], sel, debugIMP, "v@:@");
            return YES;
        }
    }
    return [super resolveInstanceMethod:sel];
}

@end

3. 模拟多重继承(Multiple Inheritance Simulation)

虽然Objective-C不直接支持多重继承,但可以通过消息转发模拟类似效果。

应用场景:

  • 混合类:让一个类同时拥有多个协议的行为。
  • 组合模式:将多个对象的行为组合到一个类中。

示例:

@interface CompositeObject : NSObject
@property (nonatomic, strong) id<Printable> printer;
@property (nonatomic, strong) id<Serializable> serializer;
@end

@implementation CompositeObject

- (id)forwardingTargetForSelector:(SEL)aSelector {
    // 如果方法与打印相关,转发给printer
    if ([self printer] && [self printer respondsToSelector:aSelector]) {
        return self.printer;
    }
    
    // 如果方法与序列化相关,转发给serializer
    if ([self serializer] && [self serializer respondsToSelector:aSelector]) {
        return self.serializer;
    }
    
    return nil;
}

@end

4. 与KVO和运行时的结合

消息转发机制常与KVO、运行时(Runtime)特性结合使用,实现更高级的功能。

应用场景:

  • 自定义KVO:实现更灵活的观察者模式。
  • 运行时方法交换:在运行时动态替换方法实现。

示例(结合运行时):

@interface RuntimeSwapper : NSObject
@property (nonatomic, strong) id target;
@end

@implementation RuntimeSwapper

- (void)swizzleMethod:(SEL)originalSel withMethod:(SEL)swizzledSel {
    // 运行时方法交换
    Method originalMethod = class_getInstanceMethod([self.target class], originalSel);
    Method swizzledMethod = class_getInstanceMethod([self class], swizzledSel);
    
    // 交换方法实现
    method_exchangeImplementations(originalMethod, swizzledMethod);
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    // 在转发过程中,可以进行额外的处理
    // 例如:记录调用、修改参数等
    
    // 执行原始方法
    [anInvocation invokeWithTarget:self.target];
}

@end

5. 实现respondsToSelector:instancesRespondToSelector:的增强

通过消息转发机制,可以实现更复杂的响应判断逻辑。

示例:

@interface EnhancedObject : NSObject
@end

@implementation EnhancedObject

- (BOOL)respondsToSelector:(SEL)aSelector {
    // 先检查原生方法
    if ([super respondsToSelector:aSelector]) {
        return YES;
    }
    
    // 然后检查通过转发能处理的方法
    // 可以通过动态方法解析或转发机制来判断
    // 这里简化处理
    return NO;
}

@end

总结

消息转发机制在iOS开发中提供了强大的灵活性,使得开发者能够:

  • 增强现有功能:无需修改原始代码即可添加新行为。
  • 实现设计模式:如代理、装饰器、适配器等。
  • 提高代码复用性:通过通用转发逻辑处理多种情况。
  • 构建动态系统:根据运行时条件调整行为。
  • 实现高级架构:如插件系统、配置驱动API等。

注意事项:

  • 性能影响:消息转发会带来额外的开销,应谨慎使用。
  • 调试困难:转发链复杂时,调试和追踪问题会变得困难。
  • 文档重要性:使用消息转发的代码需要详细的文档说明其行为。
❌
❌