本文详细整理了 Cocoa 的 Runtime 系统的知识,它使得 Objective-C 如虎添翼,具备了灵活的动态特性,使这门古老的语言焕发生机。主要内容如下:
- 引言
- 简介
- Runtime 基础数据结构
- 消息
- 动态方法解析
- 消息转发
- 健壮的实例变量 (Non Fragile ivars)
- Objective-C Associated Objects
- Method Swizzling
- 总结
引言
[receiver message] 会被编译器转化为:
objc_msgSend(receiver, selector)
如果消息含有参数,则为:
objc_msgSend(receiver, selector, arg1, arg2, ...)
执行以上代码会出现以下几种情况:
- 消息的接收者能够找到对应的 selector,直接执行了接收者这个对象的特定方法;
- 消息被转发;
- 临时向接收者动态添加这个 selector 对应的实现内容;
- 代码崩溃。
在编译阶段确定了要向接受者发送message这条消息,而 receive 将要如何响应这条消息,需要根据运行时发生的情况来决定。
Runtime 基础数据结构
objc_msgSend:方法,它的真身是这样的:
id objc_msgSend ( id self, SEL op, ... );
id
objc_msgSend 第一个参数类型为id,它是一个指向类实例的指针
typedef struct objc_object *id;
struct objc_object {
private:
isa_t isa;
public:
// ISA() assumes this is NOT a tagged pointer object
Class ISA();
// getIsa() allows this to be a tagged pointer object
Class getIsa();
}
objc_object 结构体包含一个 isa 指针,类型为 isa_t 联合体。根据 isa 就可以顺藤摸瓜找到对象所属的类。isa 这里还涉及到 tagged pointer 等概念。因为 isa_t 使用 union 实现,所以可能表示多种形态,既可以当成是指针,也可以存储标志位。
PS: isa 指针不总是指向实例对象所属的类,不能依靠它来确定类型,而是应该用 class 方法来确定实例对象的类。因为KVO的实现机理就是将被观察对象的 isa 指针指向一个中间类而不是真实的类,这是一种叫做 isa-swizzling 的技术.
image.png当你观察一个对象时,一个新的类会被动态创建。这个类继承自该对象的原本的类,并重写了被观察属性的 setter 方法。重写的 setter 方法会负责在调用原 setter 方法之前和之后,通知所有观察对象:值的更改。最后通过 isa 混写(isa-swizzling) 把这个对象的 isa 指针 ( isa 指针告诉 Runtime 系统这个对象的类是什么 ) 指向这个新创建的子类,对象就神奇的变成了新创建的子类的实例。我画了一张示意图,如下所示:
Class
Class 其实是一个指向 objc_class 结构体的指针:
typedef struct objc_class *Class;
而 objc_class 包含很多方法,主要都为围绕它的几个成员做文章:
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
class_rw_t *data() {
return bits.data();
}
... 省略其他方法
}
因为类也是一个对象,那它也必须是另一个类的实列,这个类就是元类 (metaclass)。元类保存了类方法的列表。当一个类方法被调用时,元类会首先查找它本身是否有该类方法的实现,如果没有,则该元类会向它的父类查找该方法,直到一直找到继承链的头。
元类 (metaclass) 也是一个对象,那么元类的 isa 指针又指向哪里呢?为了设计上的完整,所有的元类的 isa 指针都会指向一个根元类 (root metaclass)。根元类 (root metaclass) 本身的 isa 指针指向自己,这样就行成了一个闭环。上面提到,一个对象能够接收的消息列表是保存在它所对应的类中的。
image.png
有趣的是根元类的超类是 NSObject,而 isa 指向了自己,而 NSObject 的超类为 nil,也就是它没有超类。
cache_t
struct cache_t {
struct bucket_t *_buckets;
mask_t _mask;
mask_t _occupied;
... 省略其他方法
}
_buckets 存储 IMP,_mask 和 _occupied 对应 vtable。
cache 为方法调用的性能进行优化,通俗地讲,每当实例对象接收到一个消息时,它不会直接在isa指向的类的方法列表中遍历查找能够响应消息的方法,因为这样效率太低了,而是优先在 cache 中查找。Runtime 系统会把被调用的方法存到 cache 中(理论上讲一个方法如果被调用,那么它有可能今后还会被调用),下次查找的时候效率更高。
bucket_t 中存储了指针与 IMP 的键值对:
struct bucket_t {
private:
cache_key_t _key;
IMP _imp;
public:
inline cache_key_t key() const { return _key; }
inline IMP imp() const { return (IMP)_imp; }
inline void setKey(cache_key_t newKey) { _key = newKey; }
inline void setImp(IMP newImp) { _imp = newImp; }
void set(cache_key_t newKey, IMP newImp);
};
消息
Objc
中发送消息是用中括号([])
把接收者和消息括起来,而直到运行时才会把消息与方法实现绑定。
objc_msgSend
函数
消息发送的步骤:
1、检测这个selector
是不是要忽略的。比如 Mac OS X
开发,有了垃圾回收就不理会 retain
, release
这些函数了。
2、检测这个target
是不是 nil
对象。ObjC
的特性是允许对一个nil
对象执行任何一个方法不会 Crash
,因为会被忽略掉。
3、如果上面两个都过了,那就开始查找这个类的 IMP
,先从cache
里面找,完了找得到就跳到对应的函数去执行。
4、如果 cache
找不到,就去类的方法列表中找。
5、如果分发表找不到就到超类的方法列表去找,一直找,直到找到NSObject
类为止。
6、如果还找不到就要开始进入动态方法解析了,后面会提到。
方法中的隐藏参数
当objc_msgSend
找到方法对应的实现时,它将直接调用该方法实现,并将消息中所有的参数都传递给方法实现,同时,它还将传递两个隐藏的参数:
- 接收消息的对象(也就是
self
指向的内容); - 方法选择器(
_cmd
指向的内容)。
当方法中的super
关键字接收到消息时,编译器会创建一个objc_super
结构体:
struct objc_super { id receiver; Class class; };
面试题1
@implementation Son : Father
- (instancetype)init
{
self = [super init];
if (self) {
NSLog(@"%@", NSStringFromClass([self class]));
NSLog(@"%@", NSStringFromClass([super class]));
}
return self;
}
@end
以上代码输出结果是什么呢?
使用 clang
重写命令:
clang -rewrite-objc Son.m
重写后,代码如下所示
static instancetype _I_Son_init(Son * self, SEL _cmd) {
self = ((Son *(*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("Son"))}, sel_registerName("init"));
if (self) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_x__wh4dgr654mx__qc0b9bqyg5w0000gn_T_Son_f67def_mi_0, NSStringFromClass(((Class (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("class"))));
NSLog((NSString *)&__NSConstantStringImpl__var_folders_x__wh4dgr654mx__qc0b9bqyg5w0000gn_T_Son_f67def_mi_1, NSStringFromClass(((Class (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("Son"))}, sel_registerName("class"))));
}
return self;
}
[self class] => objc_msgSend(self, sel_registerName("class"))
把 self 做为第一个参数传递进去。
[super class] => objc_msgSendSuper((__rw_objc_super){self, class_getSuperclass(objc_getClass("Son"))}, sel_registerName("class"));
而在调用 [super class]时,会转化成 objc_msgSendSuper函数。看下函数定义:
id objc_msgSendSuper(struct objc_super *super, SEL op, ...)
第一个参数是 objc_super 这样一个结构体,其定义如下:
struct objc_super {
/// Specifies an instance of a class.
__unsafe_unretained _Nonnull id receiver;
/// Specifies the particular superclass of the instance to message.
#if !defined(__cplusplus) && !__OBJC2__
/* For compatibility with old objc-runtime.h header */
__unsafe_unretained _Nonnull Class class;
#else
__unsafe_unretained _Nonnull Class super_class;
#endif
/* super_class is the first class to search */
};
#endif
结构体有两个成员,第一个成员是 receiver, 类似于上面的 objc_msgSend函数第一个参数self 。第二个成员是记录当前类的父类是什么。
所以,当调用 [self class] 时,实际先调用的是 objc_msgSend函数,第一个参数是 Son当前的这个实例,然后在 Son 这个类里面去找 - (Class)class这个方法,没有,去父类 Father里找,也没有,最后在 NSObject类中发现这个方法。而 - (Class)class的实现就是返回self的类别,故上述输出结果为 Son。
objc Runtime开源代码对- (Class)class方法的实现:
- (Class)class {
return object_getClass(self);
}
而当调用 [super class]时,会转换成objc_msgSendSuper函数。第一步先构造 objc_super 结构体,结构体第一个成员就是 self 。第二个成员是 (id)class_getSuperclass(objc_getClass(“Son”)) , 实际该函数输出结果为 Father。第二步是去 Father这个类里去找- (Class)class,没有,然后去NSObject类去找,找到了。最后内部是使用 objc_msgSend(objc_super->receiver, @selector(class))去调用,此时已经和[self class]调用相同了,故上述输出结果仍然返回 Son。
简而言之:因为super为编译器标示符,向super发送的消息被编译成objc_msgSendSuper,但仍以self作为reveiver。
面试题2
BOOL res1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];
BOOL res2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];
BOOL res3 = [(id)[Father class] isKindOfClass:[Father class]];
BOOL res4 = [(id)[Father class] isMemberOfClass:[Father class]];
NSLog(@"%s %s %s %s",res1?"YES":"NO",res2?"YES":"NO",res3?"YES":"NO",res4?"YES":"NO");
以上代码输出的结果是什么呢?
我们使用clang -rewrite-objc main.m重写,可获得如下代码:
BOOL res1 = ((BOOL (*)(id, SEL, Class))(void *)objc_msgSend)((id)((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("class")), sel_registerName("isKindOfClass:"), ((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("class")));
BOOL res2 = ((BOOL (*)(id, SEL, Class))(void *)objc_msgSend)((id)((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("class")), sel_registerName("isMemberOfClass:"), ((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("class")));
BOOL res3 = ((BOOL (*)(id, SEL, Class))(void *)objc_msgSend)((id)((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Father"), sel_registerName("class")), sel_registerName("isKindOfClass:"), ((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Father"), sel_registerName("class")));
BOOL res4 = ((BOOL (*)(id, SEL, Class))(void *)objc_msgSend)((id)((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Father"), sel_registerName("class")), sel_registerName("isMemberOfClass:"), ((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Father"), sel_registerName("class")));
NSLog((NSString *)&__NSConstantStringImpl__var_folders_x__wh4dgr654mx__qc0b9bqyg5w0000gn_T_father_15bb14_mi_0,res1?"YES":"NO",res2?"YES":"NO",res3?"YES":"NO",res4?"YES":"NO");
isKindOfClass的实现是通过比较isa指针是否指向同一个类(元类),如果不是就比较父类的isa指针,直到isa指向了NSObject的isa,如果还未相等就返回NO。
isMemberOfClass的实现是通过比较isa指针是否指向同一个类(元类),如果相同就返回YES。
网友评论