NSObject底层探究之-cache(二)

306 阅读3分钟

调试工程环境

  • 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实现相关的数据结构的关联关系

ClassStructure-cache.001.png

的数据结构中包含了成员变量结构体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中查看相关的数据结构,如下图:

Class-cache.png

3.然后单步走完剩下的两行成员变量赋值,总所周知,成员变量赋值的本质就是调用相关setter方法,接下来我们探究一下这两个setter方法有没有被插入到cache_t中。 查看类所在的内存数据,找到cache所存储的数据: cache-buckets.png

4.查看cache所在的内存数据,找到hash表buckets的首地址,然后利用指针平移操作获取各bucket元素:

buckets_bucket.png

注意:因为是hash表,所以元素bucket的位置一般是不连续的,上例中的两个bucket元素的位置连续只是巧合,所在指针平移获取bucket元素的过程中偏移位置要在0-maybeMask范围内尝试获取。

总结

OC对象调用方法之后会将SEL-IMP对应关系实体保存在hash表中,而该对象所属中有结构体cache成员结构体cache中存有指向该hash表的地址,这一串关联关系,为消息的快速查找提供了底层实现逻辑。