类对象存储着isa、superclass、属性、对象方法、协议、成员变量等信息。元类对象存储着isa、superclass、类方法等信息。类对象和元类对象存储的信息有点相似,而且两者都是objc_class
类型,因此可以把元类对象称作是类对象的一种特殊对象。
一、Class
Class的结构:
1 | struct objc_class { |
bits & FAST_DATA_MASK
的结果指向结构体class_rw_t
:
1 | struct class_rw_t { |
结构体class_ro_t
的定义:
1 | struct class_ro_t { |
疑问:结构体class_rw_t
中的methods
、properties
、protocols
和结构体class_ro_t
中的baseMethodList
、baseProperties
、baseProtocols
有什么区别呢?
1.1. class_rw_t
class_rw_t
(rw代表可读可写,readwrite)里面的methods
、properties
、protocols
是二维数组,是可读可写的,包含了类的初始内容、分类的内容。
methods
数组里面存储的元素是method_list_t
类型的数组,method_list_t
数组中的元素是method_t
。而且分类方法在前面,类方法列表放在后面(优先执行分类方法的原因)。
properties
和protocols
的存储原理同methods
。
1.2. class_ro_t
class_ro_t
(ro代表只读,readonly)里面的baseMethodList
、baseProtocols
、ivars
、baseProperties
是一维数组,是只读的,包含了类的初始内容。
method_list_t
数组中的元素是method_t
,里面的方法是类的初始方法。
class_rw_t
中的methods
是从class_ro_t
中的baseMethodList
复制过去的。objc_class
中的变量bits最开始指向的是class_ro_t
,后面创建class_rw_t
后,把class_rw_t
中的变量ro指向class_ro_t
,并让objc_class
中的变量bits重新指向class_rw_t
。
1.3. method_t
method_t
是对方法\函数的封装,包含函数名、返回值类型、参数类型、函数地址。
1 | struct method_t { |
IMP
代表函数的具体实现:
1 | typedef if _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...); |
SEL
代表方法\函数名,一般叫做选择器,底层结构跟char *
类似(可以认为SEL就是一个名字)。
1 | typedef struct objc_selector *SEL; |
获取SEL
:可以通过@selector()
和sel_registerName()
获得。
1 | // C |
SEL
转成字符串:可以通过sel_getName()
和NSStringFromSelector()
把SEL
转成字符串。
1 | // C |
不同类中相同名字的方法,所对应的方法选择器是相同的(本质都是字符串),而且方法选择器的地址是唯一的。
1 | NSLog(@"%p, %p, %p", @selector(test), @selector(test), sel_registerName("test")); |
types
包含了函数返回值、参数编码的字符串
- (void)test;
方法会有两个默认参数:(id)self
和(SEL)_cmd
(注意:任意OC方法都会携带这两个默认参数)。
test方法中的types存储的是v16@0:8
,v
代表返回值类型是void
,@
代表第一个参数是id
类型,:
代表第二个参数SEL
类型。
返回值类型后面的数字代表方法所有参数占用的字节数,由于默认有id
和SEL
两个类型参数(都是指针类型,指针在64bit环境占用8个字节),所以一共占用16个字节。
参数类型后面的数字代表参数从哪个位置开始计算最终的参数内存地址,0
代表从第0个字节开始就是参数self
的内存地址,8
代表从第8个字节开始就是参数_cmd
的内存地址。从这也可以看出,后面参数类型的数字代表前面参数占用的字节总和。
返回值 | 参数1 | 参数2 | …… | 参数n |
---|
在OC中,程序运行时会把对应的类型转成字符编码,就像上面的v
、@
、:
,具体参考下面的1.4章节。
如- (int)test:(int)age height:(float)height;
方法中的types存储的是i24@0:8i16f20
,i
代表返回值类型是int
,@
和:
分别代表第一和第二个参数类型,后面的i
代表第三个参数是int
类型,f
代表第四个参数类型是float
。方法中所有参数类型一共占用id(8) + SEL(8) + int(4) + float(4) = 24
个字节(也可以直接用最后一个参数类型后面的数字加上参数类型所占用的字节得出总占用字节数,20 + float(4) = 24
)。
综上可以断定,有了method_t
就可以知道一个方法的名字、返回值类型、参数及参数类型是什么了。
1.4. TypeEncoding
iOS中提供了一个叫做@encode
的指令,可以将具体的类型表示成字符串编码。
1 | NSLog(@"%s", @encode(int)); // 输出:i |
1.5. 方法缓存
Class内部结构中有个方法缓存(cache_t
),用散列表(哈希表)来缓存曾经调用过的方法,可以提高方法的查找速度。
1 | struct cache_t { |
数组_buckets
中存储的元素是结构体bucket_t
:
1 | struct bucket_t { |
缓存查找函数(objc-cache.mm):
1 | bucket_t * cache_t::find(cache_key_t k, id receiver) |
每次查找缓存方法是通过遍历的方式在_buckets
中查找。比如,DBPerson类中有个- (void)test;
方法,在_buckets
中存储的就是[bucket_t(@selector(test), _imp)]
,计算@selector(test) & _mask
的哈希值(哈希值对应的是散列表中的索引),然后按照索引位置存储到_buckets
中。取值时到_buckets
里面遍历是否有和刚才的哈希值一致的索引值,找到索引对应的bucket_t
,找出key对应的IMP,最终执行对应的方法。
在objc源码中,如果存储缓存方法时遇到计算的索引位置已经有方法,并且两个key(方法名)不一致,会让索引减1后继续和
_mask
按位与计算,直到找到两个key一致的方法。散列表扩容时会把缓存都清掉,之前缓存的方法都不存在了,然后重新开始缓存。可以查阅相关资料了解散列表的原理,有可能不同平台计算索引的方法不一样,但原理都是一样的(空间换时间)。
方法查找过程:先从类对象中的缓存中查找方法,如果缓存中没有找到方法,就从bits
中查找方法,找到后就把方法实体返回,并且在cache
中缓存。如果在本类中没有找到方法,就到父类的cache
和bits
中查找,找到后会在自己的cache
中缓存(哪个类调用方法,就把方法缓存到哪个类中)。