调试工程环境
- macOS 10.15.5
- x86_64
前言
OC方法调用都是通过消息发送(objc_msgSend(id self,SEL _cmd))实现的,而消息发送需要通过消息主体SEL找到消息实现IMP,调用IMP来完成消息发送,这就涉及到了消息的快速查找、慢速查找等流程,NSObject的cache就是为消息快速查找服务的。那接下来就对cache的底层一探究竟。
核心数据结构源码片段
struct objc_class : objc_object {
//Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits;
}
struct cache_t {
//成员变量
private:
explicit_atomic<uintptr_t> _bucketsAndMaybeMask;
union {
struct {
explicit_atomic<mask_t> _maybeMask;
uint16_t _flags;
uint16_t _occupied;
};
explicit_atomic<preopt_cache_t *> _originalPreoptCache;
};
}
struct bucket_t {
//成员变量
private:
// IMP-first is better for arm64e ptrauth and no worse for arm64.
// SEL-first is better for armv7* and i386 and x86_64.
#if __arm64__
explicit_atomic<uintptr_t> _imp;
explicit_atomic<SEL> _sel;
#else
explicit_atomic<SEL> _sel;
explicit_atomic<uintptr_t> _imp;
#endif
//方法
inline SEL sel() const { return _sel.load(memory_order_relaxed); }
inline IMP imp(UNUSED_WITHOUT_PTRAUTH bucket_t *base, Class cls) const {
uintptr_t imp = _imp.load(memory_order_relaxed);
if (!imp) return nil;
#if CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_PTRAUTH
SEL sel = _sel.load(memory_order_relaxed);
return (IMP)
ptrauth_auth_and_resign((const void *)imp,
ptrauth_key_process_dependent_code,
modifierForSEL(base, sel, cls),
ptrauth_key_function_pointer, 0);
#elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_ISA_XOR
return (IMP)(imp ^ (uintptr_t)cls);
#elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_NONE
return (IMP)imp;
#else
#error Unknown method cache IMP encoding.
#endif
}
}
类中cache实现相关的数据结构的关联关系
类的数据结构中包含了成员变量结构体cache_t,cache_t的数据结构包含了指向hash表指针buckets,hash表buckets的各个元素为存储SEL-IMP对应关系的结构体bucket_t,bucket_t提供了获取sel和imp的方法sel()、imp(UNUSED_WITHOUT_PTRAUTH bucket_t *base, Class cls)。
各数据结构注解
Class
isa: isa指针,包含指向元类的指针、引用计数等信息superclass: 指向父类的指针cache:方法缓存hash表的指针、容量、已缓存方法个数等信息bits: 成员变量等额外信息
cache_t
_bucketsAndMaybeMask:方法缓存hash表的地址信息,hash表的元素为SEL-IMP对应关系实体_maybeMask:容量信息,等于实际容量-1_flags:未知_occupied:已缓存方法的个数
bucket_t
_sel:方法编号SEL(SEL本质就是const char *,可以在函数_dyld_get_objc_selector()处得到验证)_imp:方法对应的函数实现地址IMP
小试弯刀
1.关键代码
@interface TYPerson : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic) int age;
@property (nonatomic) BOOL height;
@property (nonatomic) BOOL ch1;
@property (nonatomic) BOOL ch2;
@property (nonatomic) BOOL ch3;
@property (nonatomic) BOOL ch4;
@property (nonatomic, copy) NSString *nickName;
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
//ISA_MASK
//arm64e 0x007ffffffffffff8ULL
//arm64 0x0000000ffffffff8ULL
//x86_64 0x00007ffffffffff8ULL
TYPerson *p = [TYPerson alloc];
p.name = @"Telly";
p.nickName = @"TY";
}
return 0;
}
2.在main()函数p.name = @"Telly"设置断点,在lldb中查看相关的数据结构,如下图:
3.然后单步走完剩下的两行成员变量赋值,总所周知,成员变量赋值的本质就是调用相关setter方法,接下来我们探究一下这两个setter方法有没有被插入到cache_t中。
查看类所在的内存数据,找到cache所存储的数据:
4.查看cache所在的内存数据,找到hash表buckets的首地址,然后利用指针平移操作获取各bucket元素:
注意:因为是hash表,所以元素bucket的位置一般是不连续的,上例中的两个bucket元素的位置连续只是巧合,所在指针平移获取bucket元素的过程中偏移位置要在0-maybeMask范围内尝试获取。
总结
OC对象调用方法之后会将SEL-IMP对应关系实体保存在hash表中,而该对象所属类中有结构体cache成员,结构体cache中存有指向该hash表的地址,这一串关联关系,为消息的快速查找提供了底层实现逻辑。