OC - Runtime - Class 结构 和 OC 消息机制
Runtime 源码中 Class 结构如下:
// Class 其实就是一个 struct objc_class *
typedef struct objc_class *Class;
// struct objc_class 继承 objc_object
struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
}
// objc_object 结构如下
struct objc_object {
isa_t isa;
}
所以Class本身结构如下:
struct objc_class {
isa_t isa;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
}
Class 内部 (bits & FAST_DATA_MASK) 可以得到 (class_rw_t *) 类型,
其内部结构如下
struct class_rw_t {
uint32_t flags;
uint32_t version;
const class_ro_t *ro;
method_array_t methods;
property_array_t properties;
protocol_array_t protocols;
Class firstSubclass;
Class nextSiblingClass;
char *demangledName;
}
其中结构 class_ro_t 结构如下:
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
// strong修饰的ivars
const uint8_t * ivarLayout;
const char * name;
method_list_t * baseMethodList;
protocol_list_t * baseProtocols;
const ivar_list_t * ivars;
// weak修饰的ivars
const uint8_t * weakIvarLayout;
property_list_t *baseProperties;
};
这几个类型关系如下图
class_rw_t 和 class_ro_t
class_rw_t 里面的 methods、properties、protocols 是二维数组、是可读可写的,包含了类的初始内容,分类的内容等
class_ro_t 里面同样也包含了 baseMethodList、baseProtocols、baseProperties 信息,他们是一维数组,是不可读写的,他们包含的是 Class 最原始的方法列表信息
method_t
在 class_rw_t 和 class_ro_t 中都包含 class 的各种方法协议信息,以方法为例,其类型为 method_t.
- method_t 是对方法/函数的封装,结构如下
struct method_t {
SEL name; // 函数名
const char *types; // 编码(返回值类型,参数类型)
IMP imp; // 指向函数的指针(函数地址)
}
-
IMP
是指向函数具体实现的指针。定义id (*IMP)(id, SEL, ...)
。
在 OC 底层的方法调用,都会转化为 C 语言的函数调用,所有的函数都有两个默认的参数 self、SEL 其他参数才是用户定义方法设置的参数,这也是在对象方法内我们能直接使用 self 的原因。
-
SEL
代表方法、函数名,定义typedef struct objc_selector *SEL;
,通常叫做选择器,底层结构和 char* 类似。可以通过@selector()
和sel_registerName
获得SEL
type encode
runtime 会对函数的返回值参数进行编码。具体来说,OC 对象的方法,在Runtime底层会转化为id (*IMP)(id, SEL, ...)
类型指针。
以get方法为例- (NSString *)name;
其编码为@16@0:8
以无返回值方法为例- (void)name;
其编码为v16@0:8
以上两个方法对应的转换为IMP函数:NSString* name(id self, SEL _cmd)
和 void name(id self, SEL _cmd)
其编码都是一样的。
编码的规则示例如下:
方法缓存 cache_t
Class 内部结构有个方法缓存cache_t
, 用散列表来缓存曾经调用过的方法,可以提高查找速度。
散列表有一个起始长度值,会使用使用到的@selector(sel) & _mask
来缓存具体的方法地址。
当列表的容量不够的时候会扩容,直接扩大为原来的2倍。
objc_msgSend执行流程
消息发送机制的执行流程主要分为3大阶段
- 消息发送阶段 -> 对象方法在Class中找,类方法在 MetaClass 中找,一层层往上找
- 动态方法解析 -> 系统找不到方法,会给一个机会添加动态方法
- 消息转发,如果此步再没有操作,就会报错
unrecognized selector sent to instance
objc_msgSend 函数在 Runtime 的代码中是直接使用混编语言实现的,可以从源码中查到其查找方法流程: 缓存 -> 自己的方法列表 -> 遍历父类的缓存 & 方法列表。整个流程如果都没有,就进入查找动态方法阶段。
如果都没找到,进入动态方法解析,动态方法解析流程如下。动态方法会根据 + (BOOL)resolveInstanceMethod:(SEL)sel;
查看内部有没有给对应的 selector 动态添加方法实现,如果有就重新走消息发送流程。否则进入下一阶段:消息转发。
如果动态解析还是什么都没有操作,就会进入消息转发阶段。消息转发阶段流程如下,这里是闭源的无法从源码上查看,但是可以从代码角度验证流程。
#pragma mark - 消息转发阶段 - 1
- (id)forwardingTargetForSelector:(SEL)aSelector
{
if (aSelector == @selector(test)) {
return [NSObject objectSpecifier];
}
return [super forwardingTargetForSelector:aSelector];
}
#pragma mark - 消息转发阶段 - 2
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
// 这里如果没有操作就会走找不到方法。
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
// 对 invocation 做需要做的事情
}
面试题
- 简述 OC 对象的消息机制。
1. OC 中的方法调用其实都是转化成了 objc_msgSend 函数的调用,给receiver发送一条消息
2. objc_msgSend 函数内部有3大阶段。
1. 消息查找阶段
2. 动态消息解析阶段
3. 消息转发阶段
- 消息转发机制流程
1. 调用 - (id)forwardingTargetForSelector:(SEL)aSelector 方法,如果有实现,且返回转发的对象,就将消息转发给该对象。反之,进入第二步骤
2. 调用 - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector 方法,如过有针对 aSelector 的方法签名就继续调用 - (void)forwardInvocation:(NSInvocation *)anInvocation 方法去实现任何自己想实现的逻辑。反之进入第三步
3. 调用 doesNotRecognizeedSelector 方法,向外抛出异常,经典的 unrecognized selector sent to instance 错误
- Runtime 是什么,项目中用到过吗?
是什么:
OC 是一门动态性比较强的语言,允许很多操作推迟到程序运行时候再进行。
OC 的动态性就是由 Runtime 来支撑和实现的,Runtime 是一套C语言API,封装了很多动态性相关的函数。
平时编写的OC代码,底层都是转换成了 Runtime API 进行调用。
具体应用:
1. 利用关联对象 (AssociatedObject) 给分类添加属性,创建便利分类,如按钮直接通过block处理事件
2. 遍历类的成员变量 (修改textfield的站位文字颜色、字典转模型、自动归档等)
3. 交换方法实现、例如交换系统方法,eg:自己写过的一个监听工具,hook系统实现,添加自己逻辑
4. 利用消息转发机制解决方法找不到的问题
- 参考孙源博客,中有几道面试题。
下面代码输出什么?
@implementation Son : Father
- (id)init {
self = [super init];
if (self) {
NSLog(@"%@", NSStringFromClass([self class]));
NSLog(@"%@", NSStringFromClass([super class]));
}
return self;
}
@end
// 答案: 都是 Son
// 原因: super 是编译器特性,会被转成 objc_msgSendSuper 函数,其真实接收者仍是 self,class 方法是在 NSObject 中实现的,最终在消息查找会走到基类的 class 方法,返回的是消息接收者的类型,即 self 的类型。
-
super
关键字详解
一个Person 类,其 init 方法如下:
- (instancetype)init
{
self = [super init];
return self;
}
// 使用 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc Person.m -o Person-arm64.cpp 转化为 C++ 代码如下
static instancetype _I_Person_init(Person * self, SEL _cmd) {
self = ((Person *(*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("Person"))}, sel_registerName("init"));
return self;
}
// [super init]; 简化如下
struct __rw_objc_super {
struct objc_object *object; // 真正的消息接受者
struct objc_object *superClass; // 接收者父类,作为第一个查找的类。
};
// __rw_objc_super 结构体
__rw_objc_super objc_super = (__rw_objc_super){
(id)self,
(id)class_getSuperclass(objc_getClass("Person"))
};
// 实际上转化为此类型函数指针 (Person *(*)(__rw_objc_super *, SEL))
objc_msgSendSuper(objc_super, sel_registerName("init"));
super
关键字总结
-
super
关键字会转化为objc_msgSendSuper()
,函数. - 其中会指定,消息接受者,从哪个类开始查找,和要发送的消息。
-
super
就是要在当前对象的父类开始查找消息的实现。
--- end ---