Runtime 简介
因为Objective-C是一门动态语言,这意味着它不仅需要一个编译器,也还需要一个运行时的系统来动态的创建类和对象、进行消息转递和转发。这就是Objective-C Runtime 系统存在的意义,它是整个Objc 运行框架的一块基石。
Runtime 基础数据结构
在我们调用方法是系统会自动为我们转换成 Objc_magSend:函数
id objc_msgSend ( id self,SEL op,...)
id
objc_msgSend 第一个参数的类型为 id ,它是一个指向类实例的指针:
typedef struct objc_object *id;
objc_object 是什么? 参考objc-private.h部分源码:
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_t 使用 union 实现,所以可能表示多种形态,即可以当成多种形态 即可以当成指针 也可以存储标志位。
PS: isa 指针不总是指向实例对象的所属类,不能依靠它来确定类型 而是应该用 class 方法来确定实例对象的类。KVO的实现机制就是将被观察者对象的 isa 指针指向了一个中间类而不是真实的类,这种叫做 isa—swizzling 的技术。KVO
SEL
Objc_magSend 函数第二个参数类型为 SEL , 它是 selector 在objc 中表示类型。selector 是方法选择器,可以理解区分方法的ID,而ID的数据结构SEL:
typedef struct objc_selector *SEL
@selector( ) 基本可以等同于C语言的函数指针,只不过C语言中可以把函数名直接赋给一个函数指针,而Objc 的类不能直接应用函数指针。这样只能做一个@selector语法来取 它的结果是一个SEL类型。
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();
}
... 省略其他方法
}
objc_class 继承于 objc_object,也就是说一个Objc 类本身同时也是一个对象,为了处理类和对象的关系,runtime库创建了一种叫做元类(Meta Class),类对象所属的类型就叫做元类,它用来表述对象本身所具备的元数据。类方法就定义于此处,因为这些方法可以理解成类对象的实例方法。每个类仅有一个类对象。而每个类仅有一个与之相关的元类。当你发出一个类似 [NSObject alloc] 的消息时,实际上是把这个消息发送给一个类对象,这个类对象必须是一个元类的实例,而这个元类同时也是一个根元类 (root meta class) 的实例。 所有的元类最终指向根元类为其超类。所有的元类方法列表都能够响应消息的类方法。当 [NSObject alloc] 这条消息发送给对象的时候 objc_Send() 会去它的元类里面去查找能够响应的消息的方法。
上图实线是 superclass 的指针 ,虚线是isa 指针。
1.root class 其实就是NSObject NSObject 是没有超类的,所以 root class(class)和superrclass 指向 nil。
2.每个Class都有一个 isa 指针 指向唯一的Meta Class。
3. Root class(meta)的superclass指向Root class,也就是NSObject 形成一个回路。
4.每个Meta class的isa指针都指向Root class (meta)。
cache_t
struct cache_t {
struct bucket_t *_buckets; // 存储Method的链表
mask_t _mask; // 缓存的bucket的总数
mask_t _occupied; // 表明目前实际占用的缓存bucket的个数
... 省略其他方法
}
cache 为了方法的调用的性能进行了优化。每当实例对象接收到一个消息时,它不会直接在isa 指向的类的方法列表中遍历查找能够响应消息的方法,因为这样效率太低了 而是在cache 中查找。Runtime 系统会把被调用方法存到 cache 中,下次查找的时候效率更高。
class_data_bits_t
objc_class 中复杂的是 bits class_data_bits_t 结构体所包含的信息太多了,主要包含 class_rw_t,retain/release/autorelease/retainCount 和 alloc 等信息,很多存取方法也是围绕它展开。
struct class_data_bits_t {
// Values are the FAST_ flags above.
uintptr_t bits;
class_rw_t* data() {
return (class_rw_t *)(bits & FAST_DATA_MASK);
}
... 省略其他方法
}
Ivar
Ivar 是一种代表类种实例变量的类型。
typedef struct ivar_t *Ivar;
struct ivar_t {
int32_t *offset;
const char *name;
const char *type;
// alignment is sometimes -1; use alignment() instead
uint32_t alignment_raw;
uint32_t size;
uint32_t alignment() const {
if (alignment_raw == ~(uint32_t)0) return 1U << WORD_SHIFT;
return 1 << alignment_raw;
}
};
objc_property_t
objc_property_t 是表示Objective-C声明的属性的类型,其实际是指向objc_property结构体的指针,其定义如下:
typedef struct objc_property *objc_property_t;
objc_property_attribute_t
objc_property_attribute_t 定义了属性的特性(attribute),它是一个结构体,定义如下:
typedef struct {
const char *name; // 特性名
const char *value; // 特性值
}
Method
Method 是一种代表类中的某个方法的类型。
typedef struct method_t *Method;
struct method_t {
SEL name;
const char *types;
IMP imp;
struct SortBySELAddress :
public std::binary_function<const method_t&,
const method_t&, bool>
{
bool operator() (const method_t& lhs,
const method_t& rhs)
{ return lhs.name < rhs.name; }
};
};
方法名类型为 SEL,前面提到过相同名字的方法即使在不同类中定义,它们的方法选择器也相同。
方法类型 types 是个char指针,其实存储着方法的参数类型和返回值类型。
imp 指向了方法的实现,本质上是一个函数指针,后面会详细讲到。
IMP
IMP 定义:
typedef void (*IMP)(void /* id, SEL, ... */ );
它就是一个函数指针,这是由编译器生成的。当你发起一个 ObjC 消息之后,最终它会执行的那段代码,就是由这个函数指针指定的。而 IMP 这个函数指针就指向了这个方法的实现。既然得到了执行某个实例某个方法的入口,我们就可以绕开消息传递阶段,直接执行方法。
你会发现 IMP 指向的方法与 objc_msgSend 函数类型相同,参数都包含 id 和 SEL 类型。每个方法名都对应一个 SEL 类型的方法选择器,而每个实例对象中的 SEL 对应的方法实现肯定是唯一的,通过一组 id 和 SEL 参数就能确定唯一的方法实现地址;反之亦然。
消息实现
objc_msgSend 函数
在前面简单对 objc_msgSend 进行了一点介绍。看起来像是objc_msgSend 返回了数据,其实 objc_msgSend 从不返回数据而是你的方法被调用后返回的数据。消息发送过程:
1.检测这个 selector 是不是要忽略的。
2.检测这个 target 是不是 nil 对象。ObjC 的特性是允许对一个 nil 对象执行任何一个方法不会 Crash,因为会被忽略掉。
3.如果上面两个都过了,那就开始查找这个类的 IMP,先从 cache 里面找,完了找得到就跳到对应的函数去执行。
4.如果 cache 找不到就找一下Class中的方法列表。
5.如果分发表找不到就到超类的分发表去找,一直找,直到找到NSObject类为止。
6.如果还找不到就要开始进入消息转发。
方法中的隐藏参数
我们经常在方法中使用 self 关键字来引用实例本身,但没有想过为什么 self 就能取到调用当前方法的对象。其实self 的内容是在方法运行时被偷偷的动态转入的。
当 objc_msgSend 找到方法对应的实现时,它将直接调用该方法实现,并将消息中所有的参数转递给方法实现,同时还将两个隐藏的参数:
1.接收消息的对象 (self 指向的内容)
2.方法选择器 (_cmd 指向的内容)
在这两个参数中,self 更有用。实际上,它是在方法实现中访问消息接收者对象的实例变量的途径。
而当方法中的super关键字接收到消息时,编译器会创建一个objc_super结构体:
struct objc_super { id receiver; Class class; };
这个结构体指明了消息应该被传递给特定超类的定义。但receiver仍然是self本身,这点需要注意,因为当我们想通过[super class]获取超类时,编译器只是将指向self的id指针和class的SEL传递给了objc_msgSendSuper函数,因为只有在NSObject类才能找到class方法,然后class方法调用object_getClass(),接着调用objc_msgSend(objc_super->receiver, @selector(class)),传入的第一个参数是指向self的id指针,与调用[self class]相同,所以我们得到的永远都是self的类型。
下面这道题考察的点就是上面所说的
下面代码输出什么?🤔
@implementation Son : Father
- (id)init {
self = [super init];
if (self)
{
NSLog(@"%@", NSStringFromClass([self class]));
NSLog(@"%@", NSStringFromClass([super class]));
}
return self;
}
@end
引用文献:
神经病院 Objective-C Runtime 入院第一天—— isa 和 Class
玉令天下的博客-Objective-C Runtime