【OC】Runtime:类元数据、加载初始化与消息派发

Runtime:类元数据、加载初始化与消息派发

文章目录

源码阅读

objc_class 基本结构

上期我们详细讲解了关于 isa_t 的实现,这期我们对 objc_class 结构体展开讨论

objc 复制代码
struct objc_object {
private:
    isa_t isa;
}
// 源码简化模型
struct objc_class : objc_object {
    // Class ISA;
    Class superclass;
    cache_t cache;
    class_data_bits_t bits;
    // 省略其他成员函数、构造函数及辅助方法
};

可以看到 objc_class 本质也是继承自 objc_object,所以它相当于也有一个 isa_t isa;

其中 class_data_bits_t bits; 如下

objc 复制代码
struct class_data_bits_t {
    uintptr_t bits;

    class_rw_t *data() const;
    void setData(class_rw_t *newData);
    // 省略其他成员函数
};

data() 获取 bits 里的真实数据需要经过按位与 FAST_DATA_MASK,再转换类型为 class_rw_t 进行返回

这里要留意:指针类型转换本身不会创建 class_rw_t 对象, 它只是把提取出来的地址按 class_rw_t 类型使用

现代版本最值得注意的变化,是 class_rw_t 不再必然直接保存完整的方法、属性和协议数组,而是通过 ro_or_rw_ext 在只读元数据与扩展数据之间进行组织

objc 复制代码
struct class_rw_t {
    uint32_t flags; // 保存 Runtime 状态标志
    uint16_t witness; // 与 Runtime 内部的类布局索引机制有关

#if SUPPORT_INDEXED_ISA
    uint16_t index;  // 在启用 SUPPORT_INDEXED_ISA 时使用
#endif
    //   通过指针联合机制关联 class_ro_t 或 class_rw_ext_t
    explicit_atomic<uintptr_t> ro_or_rw_ext; // 

    Class firstSubclass;  // 指向第一个子类
    Class nextSiblingClass;  // 指向同一父类下的下一个兄弟类

  // 以下省略其他辅助方法
};

这里的核心是 ro_or_rw_ext,ro_or_rw_ext 通过封装的指针objc::PointerUnion 联合表示,关联的是 class_ro_t或 class_rw_ext_t

class_rw_ext_t 按需分配的扩展结构,包括方法、属性、协议列表等

objc 复制代码
struct class_rw_ext_t {
    const class_ro_t *ro; // 指回初始类元数据

    method_array_t methods; // 运行时使用的方法列表集合
    property_array_t properties;  // 运行时使用的属性列表集合
    protocol_array_t protocols;  // 运行时使用的协议列表集合

    char *demangledName;  // 保存反修饰后的类名等相关信息
    uint32_t version;  // 类版本信息
};

注意,method_array_t、property_array_t、protocol_array_t 是列表集合类型,不要把它们直接理解成简单的 C 数组

class_rw_ext_t 内部有一个 const 类型 class_ro_t ,用于存放类的初始元数据,例如编译产生的类名、基础方法、实例变量、基础协议和属性

objc 复制代码
struct class_ro_t {
    uint32_t flags;  // 编译期及类元数据相关的标志
    uint32_t instanceStart;  // 实例中该类自身实例变量区域的起始偏移
    uint32_t instanceSize;   // 实例布局相关的大小信息

#ifdef __LP64__
    uint32_t reserved;
#endif

    const uint8_t *ivarLayout;  // 强引用实例变量布局信息

    const char *name;  // 类名

    // 现代版本的基础方法列表表示
    // 应结合源码中的 PointerUnion / baseMethods() 理解

    const protocol_list_t *baseProtocols;  // 基础协议列表
    const ivar_list_t *ivars;   // 实例变量列表

    const uint8_t *weakIvarLayout;  // 弱引用实例变量布局信息
    const property_list_t *baseProperties;  // 基础属性列表

    // 具体版本中还有其他元数据字段和访问方法
};

对比旧版本 objc_class,class_rw_t,class_ro_t 三层架构新增 class_rw_ext_t 和 ro_or_rw_ext

旧版本默认所有类都有扩展数据的需求,class_rw_t 也必须为这些成员预留结构空间

但是 Apple 在 iPhone 系统上测量到,class_rw_t 结构总共占用了约 30 MB 内存;进一步观察发现,实际运行中只有大约 10% 的类真正需要修改方法列表

这就引出了一个问题:为什么要让所有类都承担一套完整的可变元数据管理结构,而其中大部分类根本用不到这些扩展能力?

因此新版把扩展部分单独分配,这就是 ro_or_rw_ext 存在的意义:用一个经过封装的关联入口,让 Runtime 可以选择直接关联 class_ro_t,或通过 class_rw_ext_t 访问扩展数据

它本身不负责保存方法列表;它让 Runtime 能够在不为所有类都分配扩展结构的前提下,仍然访问所需的元数据

其中method_t 表示一个方法,多个 method_t 组成一个 method_list_t,因为一个类的方法信息可能来自不同的列表;因此,Runtime 需要一个容器 method_array_t 来管理多张方法列表

objc 复制代码
// 源码简化模型
struct method_t {
    SEL name; // 函数名、方法名    
    const char *types;    // 编码(返回值类型、参数类型)
    IMP imp;    // 指向函数的指针(函数地址)
}
  • IMP 本质是函数指针,指向某个 Objective-C 方法的具体实现代码,可以简化理解为
objc 复制代码
typedef id (*IMP)(id self, SEL _cmd, ...);

我们可以通过 class_getMethodImplementation 函数获取 IMP

拿到 IMP 之后我们要先转换为与方法签名相匹配的函数指针类型后传入 self 和 _cmd 再调用

  • SEL 是 Runtime 用来标识一个方法的选择子,是方法的名字标签
objc 复制代码
typedef struct objc_selector *SEL;

在同一 Runtime 环境中,选择子经过注册后,相同方法名通常会对应同一个 SEL 标识

我们一般使用 @selector 或 sel_registerName() 获取方法的选择子

注意 SEL 不是字符串,如果想要转换成字符串可以用 sel_getName() 或 NSStringFromSelector();

  • types 保存的是方法的类型编码(Type Encoding),用字符串描述方法的返回值和参数类型

源代码是怎么变成可执行文件

  • 那 Objective-C 程序是怎么从一份 .m 源文件,到最终运行起来,中间编译器和 Runtime 分别做了什么?

编译器负责把 Objective-C 源代码转换成机器指令和 Runtime 能识别的元数据

Runtime 负责在程序运行时读取、组织这些元数据,并完成类的初始化、方法查找、消息发送等工作

class_ro_t 、class_rw_t 这些结构不是编译器直接把源代码 翻译成的结构体实例 这么简单;编译器会生成符合 ABI 约定的类元数据,链接器将相关内容组织进可执行文件,Runtime 再根据这些约定识别并处理它们

整个流程简化为:

  1. 编写好 Objective-C 源代码

  2. 进入 Clang 编译生成机器指令 + Objective-C 类元数据,写入 .o 文件

  3. 链接器链接将目标文件、库及元数据组织成 Mach-O 可执行文件

    以上整个编译阶段类对象和元类的初始结构就已经存在于编译产物中, Clang 不只是把 方法 编译成机器指令,它还会按照 Objective-C ABI 生成描述 类 的元数据,包括类对象的初始结构,元类对象的初始结构,class_ro_t 等静态类元数据,方法列表、属性信息、实例变量信息,类对象的 isa、superclass 等字段所需的初始引用或重定位信息

    也就是说,类对象、元类对象不是必须等到 Runtime 启动之后才凭空创建出来的

  4. 程序启动:dyld + Runtime 加载镜像、注册类、建立类之间的关系,按需实现类

    Runtime 阶段是让编译好的类结构真正能够工作,当 dyld 加载程序时,Runtime 会读取 Objective-C 元数据,注册类,并根据需要完成类的 realization(类实现化)

    在这个过程中,Runtime 可能会建立或修正类、父类、元类之间的引用关系,创建 class_rw_t 关联编译期已有的 class_ro_t,按需使用 class_rw_ext_t 管理扩展方法列表等信息,处理分类、方法列表以及其他运行时元数据,准备实例大小、继承关系等运行时信息等

  5. 执行程序代码创建实例对象、发送消息、执行 IMP

    当 执行 main()实例对象才被创建,并初始化实例对象的 isa,让它关联到 Person 类对象

  • 或许你会想编译器已经讲类元数据和方法实现以及分类的元数据这些都已经生成好了,分类的方法列表也已经作为元数据存在于编译产物中,还需要 Runtime 干什么?

但是有一个关键问题就是Person 和 Person (Study) 可能位于不同的 .o 文件,甚至不同的动态库里

编译器不能假设所有分类都已经出现,也不能保证将来不会有其他动态库加载新的分类

所以,Runtime 需要在加载相应镜像时,把分类中已经编译好的方法列表纳入目标类的方法查找体系

Runtime 通常不是重新编译方法,而是管理和关联已有的方法列表,这就是为什么即使编译器已经做了大量工作,Runtime 仍然有事可做,Runtime 会在发现一个类定义后检查并注册它,解析类之间的关系、实现化类、处理分类,以及支持后续动态修改

类对象是类在运行时的表示,实例对象是根据类的布局在执行过程中创建的具体对象;编译器负责提供静态信息,Runtime 负责让这些信息在运行过程中发挥作用

接下来我们进入 Objective-C Runtime 的核心运行机制:消息发送(Message Sending)和消息转发(Message Forwarding)

消息发送

在 C语言 中函数调用的目标通常由编译器和链接器确定,但是 Objective-C 不一样,编译器通常并不会直接把它翻译成调用某个固定的函数

而是按照 Objective-C 的消息发送机制,生成类似下面的调用

objc 复制代码
sayHello(); // C
[p sayHello];  // OC
objc_msgSend(p, @selector(sayHello));  // OC 转换后

objc_msgSend 的核心任务就是根据接收者 p 和方法选择子 sayHello,在运行时找到真正需要执行的 IMP

我们简单看一下objc_msgSend 的完整工作流程

接收消息[p sayHello] ---> 通过接收者的 isa 找到类对象 ---> 查询方法缓存(在 cache_t 中查找 SEL) ---> if 缓存未命中 进入慢速方法查找 ---> 查找方法列表(先当前类,再沿 superclass 查找) ---> 找到 IMP(必要时填充缓存,执行实现)

如果是类方法就直接从元类里找,如果最终找不到方法,还会进入后续的动态方法解析、消息转发流程

这里 Objective-C 消息发送性能优化 的核心是:objc_msgSend 不等于每次都遍历方法列表,第一次调用时,可能需要经过慢速查找,后续调用如果命中方法缓存,就可以直接取得 IMP

其中关于isKindOfClass和isMemberOfClass函数等知识可以阅读isa指针详解

消息转发

上面提到发送消息会在相关的类对象中搜索方法列表,如果找不到则会沿着继承树向上一直搜索,如果找不到就会进行消息转发,其中消息转发包括三次机会:

  • 动态方法解析
  • 快速转发
  • 完整转发

动态方法解析

这是找不到方法后执行的第一阶段,会调用 +resolveInstanceMethod: 或者 +resolveClassMethod:,让你有机会提供一个函数实现,如果你添加了函数并返回 YES, 那运行时系统就会重新启动一次消息发送的过程

objc 复制代码
- (void)viewDidLoad {
    [super viewDidLoad];
    //执行foo函数
    [self performSelector:@selector(foo:)];
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(foo:)) {  //如果是执行foo函数,就动态解析,指定新的IMP
        class_addMethod([self class], sel, (IMP)fooMethod, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

void fooMethod(id obj, SEL _cmd) {
    NSLog(@"Doing foo");
}
// 输出:Doing foo 说明完成了消息转发

其中 class_addMethod 是用来动态添加方法的,它会将方法注册到类的方法结构中,Runtime 再重新执行 lookup,找到 fooImplementation 对应的 IMP,objc_msgSend 间接调用这个 IMP

objc 复制代码
static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
    runtimeLock.unlock();

    if (!cls->isMetaClass()) {
        resolveInstanceMethod(inst, sel, cls);
    } else {
        resolveClassMethod(inst, sel, cls);

        if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
            resolveInstanceMethod(inst, sel, cls);  // 进入动态方法解析
        }
    }

    return lookUpImpOrForwardTryCache(inst, sel, cls, behavior); // 返回 IMP
}

第一次发送 foo: 时会触发解析;解析成功后,后续调用通常直接命中缓存

如果成功添加了新的 IMP,新消息发送正常结束;如果仍然没有,就进入第二阶段快速转发

快速转发

Runtime 会调用 forwardingTargetForSelector: 把消息直接交给另一个对象;NSObject 默认实现返回 nil

objc 复制代码
@interface ViewController : NSObject
@property (nonatomic, strong) Person *person;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.person = [Person new];
    [self performSelector:NSSelectorFromString(@"foo")];
}

- (id)forwardingTargetForSelector:(SEL)sel {
    if (sel == @selector(foo)) {
        return self.person;
    }
    return [super forwardingTargetForSelector:sel];
}

@end
  
#import <Foundation/Foundation.h>
@interface Person : NSObject
- (void)foo;
@end

@implementation Person
- (void)foo {
    NSLog(@"Person 执行 foo");
}
@end
objc 复制代码
Person 执行 foo  // 输出

相当于执行了 [self.person foo]

快速转发的优势在于:

  • 不需要 NSInvocation;
  • 转发速度较快;
  • 只能替换接收者,不能修改参数和返回值;
  • 不要返回 self,否则可能造成递归。

如果快速转发返回 nil,就进入最后完整转发阶段

完整转发

完整转发接管整个消息调用,Runtime 会调用 methodSignatureForSelector:,该函数负责告诉告诉 runtime 这个 selector 的参数类型和返回值类型是什么

objc 复制代码
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
    if (sel == @selector(foo:)) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:@"];
    }
    return [super methodSignatureForSelector:sel];
}

其中签名非常重要,因为 NSInvocation 需要依据它正确读取参数和返回值

比如 "v@:@" 拆看看就是:v 表示返回 void,@ 表示 self,: 表示 _cmd,@ 表示一个对象参数

接下来会调用以下函数,这里可以真正接管消息

objc 复制代码
- (void)forwardInvocation:(NSInvocation *)invocation {
    SEL selector = invocation.selector;
  // invocation.selector 获取原始消息的 selector
  // 判断 person 是否实现了这个 selector
    if ([self.helper respondsToSelector:selector]) {
        [invocation invokeWithTarget:self.helper];
        return;
    }

    [self doesNotRecognizeSelector:selector];
}

由于NSInvocation 中包含原始 selector,所有参数,返回值存储位置,调用目标等消息

因此完整转发可以做到修改参数,修改接收者,选择不同对象,记录调用,代理远程对象,统一处理多个 selector

如果最终还是失败就会调用- (void)doesNotRecognizeSelector:(SEL)sel;

我们来看一个demo

objc 复制代码
@interface ViewController ()
  
@property (nonatomic, strong) Person *person;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.person = [Person new];
    [self performSelector:@selector(foo:) withObject:@"hello"];
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    return [super resolveInstanceMethod:sel];
}

- (id)forwardingTargetForSelector:(SEL)sel {
    return nil;
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
    if (sel == @selector(foo:)) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:@"];
    }
    return [super methodSignatureForSelector:sel];
}

// Foundation 根据这个签名,随后创建一个 NSInvocation
- (void)forwardInvocation:(NSInvocation *)invocation {
    if ([self.person respondsToSelector:invocation.selector]) {
        [invocation invokeWithTarget:self.person];
        return;
    }
    [self doesNotRecognizeSelector:invocation.selector];
}

@end
  
@interface Person : NSObject
- (void)foo:(NSString *)text;
@end

@implementation Person

- (void)foo:(NSString *)text {
    NSLog(@"Person 执行 foo: %@", text);
}

@end
objc 复制代码
Person 执行 foo: hello
对比 动态解析 快速转发 完整转发
主要方法 +resolveInstanceMethod: -forwardingTargetForSelector: -methodSignatureForSelector: + -forwardInvocation:
是否添加 IMP 是 否 否
是否修改方法列表 是 否 否
后续调用是否受影响 受影响 不受影响 不受影响
是否更换接收者 不一定 可以 可以
是否修改 selector 不适合 不方便 可以
是否修改参数 不适合 不方便 可以
是否修改返回值 不适合 不方便 可以
是否需要 NSInvocation 否 否 是
性能 最好 较好 较慢
适合场景 动态补方法 简单代理 复杂代理/消息拦截
相关推荐
Felven1 小时前
C. Removing Smallest Multiples
c语言·开发语言
云和数据.ChenGuang1 小时前
光哥说Python 循环导入(Circular Import)深度解析:成因、排查与解决方案
开发语言·前端·人工智能·python·langchain
星夜离尘561 小时前
MATLAB 工程实验:滞回加持续时间报警,把 UNKNOWN 留在状态机里
开发语言·matlab
fengci.1 小时前
BUUCTF(困难部分)
android·开发语言·前端·学习·php
songgeb1 小时前
iOS App支持设置RTL语言的实践
ios·swift·ui kit
Highcharts.js1 小时前
BI可视化图表“维护难”问题的解决方案
开发语言·前端·javascript·编辑器·ecmascript·bi·highcharts
软泡芙2 小时前
【iOS】swift启动屏显示黑屏
ios
yixun_gdas2 小时前
政企网站错敏巡查:错敏都藏在哪些位置?
开发语言·前端·javascript·内容运营
小羊没烦恼!2 小时前
.NET 11 Runtime Async 详解
java·运维·开发语言·javascript·python