Demo: https://github.com/iOSlixiang/RuntimeTest.git
objc4-818.2 源码: https://github.com/iOSlixiang/objc4-818.2.git
Class
Objective-C类是由Class类型来表示的,它实际上只是objc_class的结构体指针。
typedef struct objc_class *Class;
typedef struct objc_object *id;
@interface Object {
Class isa;
}
@interface NSObject <NSObject> {
Class isa OBJC_ISA_AVAILABILITY;
}
在2006年苹果发布Objc 2.0,objc_class定义没有发生变化,但是结构体发生了变化。
在Objc2.0之前,objc_class源码
// 一个类的实例的结构体
struct objc_object {
Class isa OBJC_ISA_AVAILABILITY;
};
// 类结构体
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE; // 父类
const char *name OBJC2_UNAVAILABLE; // 类名
long version OBJC2_UNAVAILABLE; // 类的版本信息,默认为0
long info OBJC2_UNAVAILABLE; // 类信息,供运行期使用的一些位标识
long instance_size OBJC2_UNAVAILABLE; // 该类的实例变量大小
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE; // 该类的成员变量链表
struct objc_method_list **methodLists OBJC2_UNAVAILABLE; // 方法定义的链表
struct objc_cache *cache OBJC2_UNAVAILABLE; // 方法缓存
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; // 协议链表
#endif
} OBJC2_UNAVAILABLE;
在Objc2.0
,objc_class继承于objc_object。在objc_class中也会包含isa_t类型的结构体isa。 Objective-C 中类也是一个对象。
struct objc_object {
private:
isa_t isa;
}
//继承
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
}
// isa是一个联合体
union isa_t
{
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
}
把源码的定义转化成类图,就是上图的样子。
从上述源码中,我们可以看到,Objective-C 对象都是 C 语言结构体实现的,在objc2.0中,所有的对象都会包含一个isa_t类型的结构体。
objc_object被源码typedef成了id类型,这也就是我们平时遇到的id类型。这个结构体中就只包含了一个isa_t类型的结构体。
bjc_class继承于objc_object。所以在objc_class中也会包含isa_t类型的结构体isa。至此,可以得出结论:Objective-C 中类也是一个对象。在objc_class中,除了isa之外,还有3个成员变量,一个是父类的指针,一个是方法缓存,最后一个这个类的实例方法链表。
object类和NSObject类里面分别都包含一个objc_class类型的isa。
上图的左半边类的关系,右边是isa的结构体
isa指针
- 联合体:一种特殊的数据类型,其目的是节省内存。联合体内部可以定义多种数据类型,但是同一时间只能表示某一种数据类型,且所有的数据类型共享同一段内存。联合体的内存大小等于所定义的数据类型中占用内存的最大者。
- 互斥赋值/共用内存:允许装入该“联合”所定义的任何一种数据成员,但同一时间只能表示一种数据成员,采用了覆盖的技术;
- union所占内存长度:union 变量所占用的内存长度等于最长的成员的内存长度;
union isa_t { //联合体
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
//提供了cls 和 bits ,两者是互斥关系
Class cls;
uintptr_t bits;
#if defined(ISA_BITFIELD)
struct {
ISA_BITFIELD; // defined in isa.h
};
#endif
};
isa_t
类型使用联合体
的原因也是基于内存优化
的考虑,这里的内存优化是指在isa指针中通过char + 位域(
即二进制中每一位均可表示不同的信息)的原理实现。通常来说,isa指针
占用的内存大小是8
字节,即64
位,已经足够存储很多的信息了,这样可以极大的节省内存,以提高性能
从isa_t
的定义中可以看出:
-
提供了两个成员,
cls
和bits
,由联合体的定义所知,这两个成员是互斥
的,也就意味着,当初始化isa指针时,有两种初始化方式-
通过
cls
初始化,bits无默认值
-
通过
bits
初始化,cls无默认值
-
-
还提供了一个结构体定义的·位域·,用于存储类信息及其他信息,结构体的成员·ISA_BITFIELD·,这是一个宏定义,有两个版本
__arm64__
(对应ios 移动端) 和__x86_64__
(对应macOS)
元类(Meta Class)
在上面我们提到,所有的类自身也是一个对象,我们可以向这个对象发送消息(即调用类方法)。如:
NSArray *array = [NSArray array];
这个例子中,+array消息发送给了NSArray类,而这个NSArray也是一个对象。既然是对象,那么它也是一个objc_object指针
,它包含一个指向其类的一个isa指针
。那么这些就有一个问题了,这个isa指针指向什么呢?为了调用+array方法,这个类的isa指针必须指向一个包含这些类方法的一个objc_class结构体。这就引出了meta-class的概念
meta-class是一个类对象的类。
当我们向一个对象发送消息
时,runtime会在这个对象所属的这个类的方法列表中查找方法;而向一个类发送消息
时,会在这个类的meta-class的方法列表中查找。
meta-class之所以重要,是因为它存储着一个类的所有类方法
。每个类都会有一个单独的meta-class,因为每个类的类方法基本不可能完全相同。
再深入一下,meta-class也是一个类,也可以向它发送一个消息,那么它的isa又是指向什么呢
?为了不让这种结构无限延伸下去,Objective-C的设计者让所有的meta-class的isa指向基类的meta-class
,以此作为它们的所属类。即,任何NSObject继承体系下的meta-class都使用NSObject的meta-class作为自己的所属类
,而基类的meta-class的isa指针是指向它自己
。这样就形成了一个完美的闭环
。
通过上面的描述,再加上对objc_class结构体中super_class指针的分析,我们就可以描绘出类及相应meta-class类的一个继承体系
了,如下图所示:
图中实线是 super_class指针,虚线是isa指针。
- Root class (class)其实就是NSObject,
NSObject
是没有超类的,所以Root class(class)的superclass指向nil。 - 每个Class都有一个isa指针指向唯一的Meta class
- Root class(meta)的superclass指向
Root class(class)
,也就是NSObject,形成一个回路。 - 每个Meta class的isa指针都指向Root class (meta)。
对于NSObject继承体系来说,其实例方法
对体系中的所有实例、类和meta-class都是有效的;而类方法
对于体系内的所有类和meta-class都是有效的。
cache_t
struct cache_t {
struct bucket_t *_buckets;
mask_t _mask;
mask_t _occupied;
}
typedef unsigned int uint32_t;
typedef uint32_t mask_t; // x86_64 & arm64 asm are less efficient with 16-bits
typedef unsigned long uintptr_t;
typedef uintptr_t cache_key_t;
struct bucket_t {
private:
cache_key_t _key;
IMP _imp;
}
根据源码,我们可以知道cache_t中存储了一个bucket_t的结构体,和两个unsigned int的变量。
mask:分配用来缓存bucket的总数。
occupied:表明目前实际占用的缓存bucket的个数。
bucket_t的结构体中存储了一个unsigned long和一个IMP。IMP是一个函数指针,指向了一个方法的具体实现。
cache_t中的bucket_t *_buckets其实就是一个散列表,用来存储Method的链表。
Cache的作用主要是为了优化方法调用的性能。当对象receiver调用方法message时,首先根据对象receiver的isa指针查找到它对应的类,然后在类的methodLists中搜索方法,如果没有找到,就使用super_class指针到父类中的methodLists查找,一旦找到就调用方法。如果没有找到,有可能消息转发,也可能忽略它。但这样查找方式效率太低,因为往往一个类大概只有20%的方法经常被调用,占总调用次数的80%。所以使用Cache来缓存经常调用的方法,当调用方法时,优先在Cache查找,如果没有找到,再到methodLists查找。
class_data_bits_t
// class_data_bits_t相当于 class_rw_t指针加上 rr/alloc 的标志。
struct class_data_bits_t {
// Values are the FAST_ flags above.
uintptr_t bits;
}
// 返回 class_rw_t *指针的快捷方法
class_rw_t *data() {
return bits.data();
}
// class_ro_t 在编译期产生,它是类中的可读信息。
// class_rw_t 在运行时产生,它是类中的可读写信息。
struct class_rw_t {
uint32_t flags; // 标记
uint32_t version; // 版本
// 类中的只读信息
const class_ro_t *ro;
/*
这三个都是二位数组,是可读可写的,包含了类的初始内容、分类的内容。
methods中,存储 method_list_t ----> method_t
二维数组,method_list_t --> method_t
这三个二位数组中的数据有一部分是从class_ro_t中合并过来的。
*/
method_array_t methods; // 方法数组
property_array_t properties; // 属性数组
protocol_array_t protocols; // 协议数组
Class firstSubclass; //为某个类第一次创建的时候去寻找父类的时候绑定给父类的。
Class nextSiblingClass; // 下一个相同父类的类
char *demangledName;
}
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart; // 开始位置
uint32_t instanceSize; // 所占空间大小
#ifdef __LP64__
uint32_t reserved;
#endif
/* ivar 布局, 在编译期这里就固定了,
它标示ivars的内存布局,在运行时不能改变,
这也是为什么我们在运行时不能动态给类添加成员变量的原因*/
const uint8_t * ivarLayout;
const char * name; //名字
method_list_t * baseMethodList; //方法链表
protocol_list_t * baseProtocols; //协议链表
const ivar_list_t * ivars; //成员变量链表
const uint8_t * weakIvarLayout; // weak 成员变量的内存布局
property_list_t *baseProperties; //属性链表
method_list_t *baseMethods() const {
return baseMethodList;
}
};
Objc的类的属性、方法、以及遵循的协议在obj 2.0的版本之后都放在class_rw_t中。class_ro_t是一个指向常量的指针,存储来编译器决定了的属性、方法和遵守协议。rw-readwrite,ro-readonly
在编译期类的结构中的 class_data_bits_t *data指向的是一个 class_ro_t *指针。
在运行时调用 realizeClass方法,会做以下3件事情:
- 从 class_data_bits_t调用 data方法,将结果从 class_rw_t强制转换为 class_ro_t指针
- 初始化一个 class_rw_t结构体
- 设置结构体 ro的值以及 flag
最后调用methodizeClass方法,把类里面的属性,协议,方法都加载进来。
整个流程 objc_readClassPair --> readClass --> realizeClass --> methodizeClass
网友评论