iOS-浅谈OC中的Class对象
目录
- Class对象
----class
的结构
----class_rw_t
的结构
----class_ro_t
的结构结
---metho_t
的结构
---Type Encoding
cache_t
方法缓存
----方法调用的问题思考
----cache_t结构
----向缓存中存入方法
----从缓存中查找方法
----补充:散列表索引号计算
----实战拓展+方法/缓存方法调用过程解析
----方法查找过程
-
Class的结构
下面我们就通过查看objc源码,发现class对象
底层为objc_class
结构体。最终的Class的数据底层结构如下图:
在前面文章提到,OC对象有三种数据类型,里面就包含了Class对象。
下面是具体分析
struct objc_class : objc_object {
// Class ISA; // 继承来的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() const {
return bits.data();
}
// .... more
// .... more
// .... more 代码经过精简展示
}
从中我们看到Class结构体
中包含了isa共用体
、superclass指针
、方法缓存cache
和bits
。
struct class_data_bits_t {
friend objc_class;
// Values are the FAST_ flags above.
uintptr_t bits;
private:
bool getBit(uintptr_t bit) const
{
return bits & bit;
}
class_rw_t* data() const {
return (class_rw_t *)(bits & FAST_DATA_MASK);
}
// .... more 代码经过精简展示
};
由位运算知识点可知,bits
保存了Class
的一些信息,通过bits & FAST_DATA_MASK
可以获取类的class_rw_t
信息。
struct class_rw_t {
// Be warned that Symbolication knows the layout of this structure.
uint32_t flags;
uint16_t witness;
#if SUPPORT_INDEXED_ISA
uint16_t index;
#endif
Class firstSubclass;
Class nextSiblingClass;
const class_ro_t *ro() const { } // 里面内容暂时省略
const method_array_t methods() const { } // 里面内容暂时省略
const property_array_t properties() const { } // 里面内容暂时省略
const protocol_array_t protocols() const { } // 里面内容暂时省略
// .... more 代码经过精简展示
};
class_rw_t
包含了方法列表、属性列表、协议列表等
信息。
还包含了一个class_ro_t
结构体,里面保存了class的类名、成员变量等
初始信息。
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
#ifdef __LP64__
uint32_t reserved;
#endif
union {
const uint8_t * ivarLayout;
Class nonMetaclass;
};
explicit_atomic<const char *> name;
void *baseMethodList;
protocol_list_t * baseProtocols;
const ivar_list_t * ivars;
const uint8_t * weakIvarLayout;
property_list_t *baseProperties;
// .... more 代码经过精简展示
};
-
class_rw_t的结构
实际上class_rw_t
内部保存的是method_array_t
、property_array_t
、protocol_array_t
类型。和method_list_t*
意义相同,是二维数组。
class_rw_t
内部的methods、properties、protocols
都是二维数组,是可读可写的,包含了类的初始内容、分类的内容。- 比如
methods
内部保存了method_list_t
对象>method_list_t
内部保存了真正的方法_method_t
,类初始方法列列表会排在最后,后编译的分类中的方法会保存在methods
前面。
-
class_ro_t的结构
class_ro_t
里面的baseMethodList、baseProtocols、ivars、baseProperties
是一维数组,是只读的,包含了类的初始内容。
-
metho_t的结构
struct method_t {
SEL name; // 函数名
const char *types; // 编码(返回值类型、参数类型)
IMP imp; //指向函数的指针()
};
IMP代表函数的具体实现
types包含了函数返回值、参数编码的字符串。
SEL代表方法\函数名,一般叫做选择器,底层结构跟char *类似
可以通过@selector()和sel_registerName()获得。
可以通过sel_getName()和NSStringFromSelector()转成字符串。
不同类中相同名字的方法,所对应的方法选择器是相同的。
-
Type Encoding
iOS中提供了一个叫做@encode
的指令,可以将具体的类型表示成字符串编码,可以通过打印查看:
NSLog(@"@encode(int) = %s", @encode(int));
NSLog(@"@encode(void) = %s", @encode(void));
NSLog(@"@encode(id) = %s", @encode(id));
NSLog(@"@encode(SEL) = %s", @encode(SEL));
@encode(int) = i
@encode(void) = v
@encode(id) = @
@encode(SEL) = :
比如下面两个方法,test
方法对应metho_t
中的types值就是v16@0:8
,testAge:height:
方法对应metho_t
中的types值就是v24@0:8i16i20
。
// v16@0:8
- (void)test;
// v24@0:8i16i20
- (void)testAge:(int)age height:(int)height;
test方法默认会带有两个参数:
- (void)testSelf:(id)self _cmd:(SEL)_cmd;
也可以简写成v@:
方法缓存cache_t
-
方法调用的问题思考
Person *per = [[Person alloc] init ];
[per run];
[per run];
- instance对象的方法在类对象中存储着。class对象的方法在元类对象中存储着。
- instance对象调用对象方法的轨迹:
isa找到class,方法不存在,就通过superclass找父类。- class对象调用类方法的轨迹:
isa找到meta-class,方法不存在,就通过superclass找父类的meta-class,
如果基类中也不存在,则去基类的class对象中查找同名对象方法。
如果一直调用run
方法,每次按照这种调用,效率是很低的。所以设计了一种cache
结构来优化性能。
-
cache_t结构
cache_t
是用散列表来缓存曾经调用过的方法
,可以提高方法的查找速度,避免多次执行方法查找逻辑。
散列表(Hash table,也叫哈希表):是根据关键码值(Key value)而直接进行访问的数据结构。
它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
代码如下:
struct cache_t {
bucket_t *_buckets; // 散列表
mask_t _mask; // 散列长度-1
mask_t _occupied; // 已缓存的方法数量
};
struct bucket_t {
cache_key_t _key; // SEL作为key
IMP _imp; // 函数内存地址
};
- 例如散列表_buckets初始长度是10,那么_mask是9,_occupied
可能
是3。
-
向缓存中存入方法
按照上面思考的逻辑,第一次执行run
方法的时候,系统会把run
方法缓存到_buckets
数组中,类型是bucket_t
:
key = @selector(run)
_img = run的地址
- 在查找方法时,通过
sel(方法名)
作为key
,找到对应的bucket_t
,从而找到_imp(实现)
进行调用。- 补充:
SEL
=@selector(run)
-
从缓存中查找方法
_buckets
散列表(哈希)索引计算方法:sel & mask
。- 比如调用了方法
@selector(run)
,需要先【计算出散列表索引】,再将bucket_t
插入到索引位置。_buckets
其他位置如果没有信息会留空,是以空间换时间的方案。
举例:@selector(run) & _mask = 6
如果多个方法,同理在相应序号插入方法即可:@selector(eat) & _mask = 2
则在序号2插入该方法即可。
-
补充:散列表索引号计算
static inline mask_t cache_hash(SEL sel, mask_t mask)
{
uintptr_t value = (uintptr_t)sel;
#if CONFIG_USE_PREOPT_CACHES
value ^= value >> 7;
#endif
return (mask_t)(value & mask);
}
如果不同的方法通计算出来的索引值相同,就需要解决哈希冲突,苹果的方法时让索引-1:
static inline mask_t cache_next(mask_t i, mask_t mask) {
return i ? i-1 : mask;
}
写入:
计算出来的索引位置已经保存了bucket_t,则让索引-1,如果还不行继续-1,索引小于0时,将索引设置成mask(_buckets长度-1)从最后的位置继续向前遍历,直到找到空位置,进行插入操作。
读取:
计算出来的索引位置对应的bucket_t中的SEL如果和传入的SEL不一致,就让索引-1查找,如果还不相同,则继续-1,索引小于0时将索引设置成mask(_buckets长度-1),从最后位置继续向前遍历,直到找到SEL相同的bucket_t。
扩容:
当_buckets空间不够时会进行扩容操作(原有空间大小*2),会更新mask的值,并且清空_buckets。
-
实战拓展+方法/缓存方法调用过程解析
我们创建三个类:Person、Student、GoodStudent
继承关系如下代码所示,分别实现方法:run、studentRun、goodStudentRun
。
#import <Foundation/Foundation.h>
@interface Person : NSObject
- (void)run;
@end
#import "Person.h"
@interface Student : Person
- (void)studentRun;
@end
#import "Student.h"
@interface GoodStudent : Student
- (void)goodStudentRun;
@end
Person *per = [[Person alloc] init ];
[per run];
[per run];
方法/缓存方法调用过程:
- 第一次调用
run
方法的时候,通过instance对象per
的isa
指针找到Person类对象
。- 先在
cache
这个散列表中查找有无该方法缓存,通过sel(方法名)
作为key
,找到对应的bucket_t
,结果肯定空的的,该方法不存在。- 然后通过
bits
在class_rw_t
的properties
方法列表中查找run
方法,结果找到了,进行方法执行。- 然后把相关信息规整成
bucket_t
,存入Person类对象
的cache
散列表中。- 第二次调用
run
方法的时候,直接在Person类对象
的cache
这个散列表中找到缓存方法并调用即可。
Student *stu = [[Student alloc] init ];
[stu run];
[stu run];
子类调用父类方法
的过程:
- 调用过程和上面类似,instance对象
stu
在自己class方法也就是Student对象
中查找有无run
方法,结果是没有的。- 通过
superclass指针
去父类中进行查找,顺序是先查父类的cache
,再查父类的properties
。- 因为上面
[per run];
执行过,所以在父类的cache
中有缓存。- 在父类的
cache
中查找到run
方法,直接调用执行。- 查然后在自己class对象的
cache
中也会缓存一份。- 当再此执行
[stu run];
的时候,直接在自己的class对象的cache
中查找调用即可。
-
方法查找过程
在 isa
、superclass
中说明了OC方法的调用轨迹,这里做一下补充,在查找过程中需要先查找方法缓存,过程如下: