1. Class
结构的本质
上一章对isa
结构的本质做了探究,下面探究Class
的内部结构。

- class对象和meta-class对象结构相似,meta-class对象是特殊的class对象;
- 关于存储方法:
class对象存储对象方法,meta-class对象存储类方法。
(1) Class
的结构
由OC源码可以找出Class
的结构:

由Class
的结构可知:
- 通过
bits & FAST_DATA_MASK
可以得到class_rw_t
结构体;class_rw_t
结构体中存放方法列表、属性列表、协议列表等,并包括一个指向class_ro_t
结构体的指针ro
;(rw
即readwrite
可读可写)class_ro_t
结构体中存放类的初始内容,包括方法列表、属性列表、协议列表等,并包括成员变量列表。(ro
即readonly
只读)
(2) class_rw_t
结构
class_rw_t
结构体中存放方法列表(method_array_t
类型)、属性列表 (property_array_t
类型)、协议列表(protocol_array_t
类型),接下来分析三者的结构:

通过以上源码可知:
method_array_t
、property_array_t
、protocol_array_t
结构相同;
下面只对method_array_t
的内部结构进行分析,property_array_t
和protocol_array_t
的可以类推。

由method_array_t
内部结构可知:
method_array_t
是一个二维数组,每个元素是method_list_t
;
method_list_t
是一维数组,每个元素是method_t
;
class_rw_t
里面的methods
、properties
、protocols
是二维数组,是可读可写的,包含了类的初始内容、分类的内容。
(3) class_ro_t
结构
class_ro_t
结构体中存放方法列表(method_list_t
类型)、属性列表 (property_list_t
类型)、协议列表(protocol_list_t
类型),所以method_list_t
的内部结构如下:

由method_list_t
内部结构可知:
method_list_t
是一维数组,每个元素是method_t
;
class_ro_t
里面的baseMethodList
、baseProtocols
、ivars
、baseProperties
是一维数组,是只读的,包含了类的初始内容。
(4) class_rw_t
结构与class_ro_t
结构
class_rw_t
结构体通过attachList
函数内的memmove
和memcpy
操作将所有分类的对象方法附加到类对象的方法列表中。
通过源码进入realizeClass
函数,查看class_rw_t
与class_ro_t
两者之间的关系:

由realizeClass
函数可知:
类的初始信息(方法、属性、协议、成员变量等)存放在
class_ro_t
中;
程序运行时,class_ro_t
中的列表和分类中的列表合并起来存放在class_rw_t
中。
(5) method_t
结构
由以上分析可知:
class_rw_t
结构体和class_ro_t
结构体的方法列表,其最小单位都是method_t
结构体。
通过源码进入method_t
结构体:

由源码可知:method_t
结构体包含三个成员变量,接下对三者进行分析:
1> SEL name;
函数名
SEL
代表方法/函数名,即选择器,底层结构和char
指针类似;
通过@selector()
或sel_registerName()
获得;
通过sel_getName()
或NSStringFromSelector()
将SEL
转成字符串;- 不同类中相同名字的方法,所对应的方法选择器是相同的。
验证如下:
int main(int argc, const char * argv[]) {
@autoreleasepool {
SEL sel1 = @selector(init);
SEL sel2 = sel_registerName("init");
NSLog(@"%p %p", sel1, sel2);
const char *str1 = sel_getName(sel1);
NSString *str2 = NSStringFromSelector(sel2);
NSLog(@"%s %@", str1, str2);
}
return 0;
}

打印的地址与方法名都相同,验证成功。
2> const char *types;
编码(返回值类型,参数类型)
再Person
类中写一个方法:
// TODO: ----------------- Person类 -----------------
@interface Person : NSObject
@end
@implementation Person
- (int)testHeight:(float)height age:(int)age {
return 0;
}
@end
将Person.m
文件转为C++语言,找到_class_ro_t
结构体,因为它存着类的初始信息:

由以上源码可知:
方法名为:testHeight:age:
;
编码types
为:"i24@0:8f16i20"
;
函数地址为:_I_Person_testHeight_age_
。
编码types
为:"i24@0:8f16i20"
,采用了iOS的@encode的指令,该指令将具体的类型表示成字符串编码。部分编码如下:

我们通过该表进行分析:
// 函数默认会带有self和_cmd两个参数
- (int)testHeight:(float)height age:(int)age;
types - i24@0:8f16i20
types - i 24 @ 0 : 8 f 16 I 20
int id SEL float int
返回值 self _cmd height age
// 分析数字意义:
24 - 所有参数占24个字节
0 - self是从第0个字节开始存储,id类型占8个字节
8 - _cmd是从第8个字节开始存储,SEL类型占8个字节
16 - height是从第16个字节开始存储,float类型占4个字节
20 - age是从第20个字节开始存储,int类型占4个字节
3> IMP imp;
指向函数的指针(函数地址)
IMP
的内部实现如下:
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...);
IMP
代表函数的具体实现,存储着函数地址。
2. 方法缓存cache_t

如果调用对象方法:
- 首先通过instance的
isa
指向Class,
然后在Class对象中class_rw_t
结构体的methods
数组中查找方法; - 如果Class对象中找不到该方法,
需要通过superclass
指针找到父类的Class对象,
然后在父类的Class对象中class_rw_t
结构体的methods
数组中查找方法 - 如果父类的Class对象中找不到该方法,再次通过
superclass
指针找上一级的父类,如此循环。
如果一个方法需要调用许多次的话,需要循环遍历多次以上步骤;
iOS采用方法缓存cache_t
,用散列表(哈希表)缓存曾经调用过的方法,可以提高方法的查找速度。
调用方法时优先去缓存中查找此方法,缓存中没有再去类对象中查找,然后将其加入缓存中方便下次调用。
(1) cache_t
缓存方式
由OC源码可以找出cache_t
的结构:

由cache_t
结构可知:
-
cache_t
结构体存储_buckets
、_mask
、_occupied
;
1>_buckets
散列表,存放方法选择器充当的Key
值,以及函数的内存地址IMP
;
2>_mask
散列表的长度减1,任何数通过与_mask
进行按位与运算之后获得的值都会小于等于_mask
,不会出现数组溢出的情况;
3>_occupied
已经缓存的方法数量。
cache_t
结构可以归纳如下:

接下来查看OC内部如何处理缓存:

网友评论