从objc源码看世界--类结构

191 阅读4分钟
HXPerson *p = [[HXPerson alloc] init];
Class cls1 = p.class;
Class cls2 = object_getClass(p);
Class cls3 =   [HXPerson class];
Class cls4 = object_getClass(cls2);
Class cls5 = object_getClass(cls3);

打印上面代码,我们可以看到能打印对应的类名字 那这个HXPerson到底是怎么样的,让我们从objc源码来探究下类的结构。

struct objc_class : objc_object {
    // Class 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 superclass,cache_t cache,class_data_bits_t bits,接着我们通过x/6gx来打印这个类的结构,我们可以看到

类内存结构

我们打印第一个8字节,并得不到什么,打印第二个8字节发现是NSObject,也就是我们HXPersonsuperclass。那这样前面8自己是什么内容呢,我们通过注释可以了解到,superclass前面还有一个隐藏的变量就是ISA,那我们结合之前的学习,把这个内容和isa掩码做一个与操作。打印处理就是HXPerson。

接下来我们可以看到一个cache_t,和class_data_bits_t。这两个成员变量在内存中占了多少的空间呢,我们分别点进去看下对应的结构描述。

struct cache_t {  //以下描述都是在64位机器下
    struct bucket_t *_buckets; // 8字节,这是一个结构体指针,不是结构体。
    //不用看内部的构造。如果是结构体就要看内部构造,然后一一计算。
    //可以类比这里结构体的内存布局计算方式。
    mask_t _mask; //4字节,在64位机器下,是一个uint32_t
    mask_t _occupied; // 4字节,同上 
struct class_data_bits_t {

    // Values are the FAST_ flags above.
    uintptr_t bits; // 8字节,无符号长整形。

最后我们可以得到类的内存布局。

struct objc_class : objc_object {
    // Class ISA;  //8字节
    Class superclass; // 8字节
    cache_t cache;             // 16字节
    class_data_bits_t bits;    // 8字节 ?是否有问题

##cache_t

struct cache_t {
    struct bucket_t *_buckets;
    mask_t _mask;
    mask_t _occupied;

public:
    struct bucket_t *buckets();
    mask_t mask();
    mask_t occupied();
    void incrementOccupied();
    void setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask);
    void initializeToEmpty();

    mask_t capacity();
    bool isConstantEmptyCache();
    bool canBeFreed();

    static size_t bytesForCapacity(uint32_t cap);
    static struct bucket_t * endMarker(struct bucket_t *b, uint32_t cap);

    void expand();
    void reallocate(mask_t oldCapacity, mask_t newCapacity);
    struct bucket_t * find(cache_key_t key, id receiver);

    static void bad_cache(id receiver, SEL sel, Class isa) __attribute__((noreturn));
};
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__
    MethodCacheIMP _imp;
    cache_key_t _key;
#else
    cache_key_t _key;
    MethodCacheIMP _imp;
#endif

public:
    inline cache_key_t key() const { return _key; }
    inline IMP imp() const { return (IMP)_imp; }
    inline void setKey(cache_key_t newKey) { _key = newKey; }
    inline void setImp(IMP newImp) { _imp = newImp; }

    void set(cache_key_t newKey, IMP newImp);
};

cache_t,顾名思义是一个和缓存有关系的结构,初步一看是和方法的缓存有关系,接下来我们来探究下这个cache_t是怎么工作。

cache_t 方法缓存策略

我们通过incrementOccupied()找到调用方cache_fill_nolock(),接着找到再上一层cache_fill(),然后在找上一层,我们可以看到有几个方法,其中有一个方法特别吸引人,就是lookUpImpOrForward(),这样就和我们设想的对应上了,这个cache_t和方法缓存是有关系的.(相当于逆着找)

static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver)

这个方法是我们开始研究的主要对象.详细阅读代码后,我们可以整理一个流程.

方法缓存.jpg

class_data_bits_t

最后我们看一下类结构的最后一个元素class_data_bits_t,它是做什么用的呢.

struct class_data_bits_t {// class_rw_t * plus custom rr/alloc flags

    // Values are the FAST_ flags above.
    uintptr_t bits; // 8字节,无符号长整形。

我们大概浏览其内部,就是一个无符号长整形变量以及一些方法,大家都说class_data_bits_t存储类的方法、属性、协议等信息的地方,具体他是怎么样体现他的价值的呢?这些都是存在哪的呢? 乍一眼看去,这个bit和下面的第一个方法class_rw_t *data()好像有点关系,我们点进去看下这个class_rw_t:

struct class_rw_t {
    // Be warned that Symbolication knows the layout of this structure.
    uint32_t flags;
    uint32_t version;

    const class_ro_t *ro;

    method_array_t methods;
    property_array_t properties;
    protocol_array_t protocols;

    Class firstSubclass;
    Class nextSiblingClass;

    char *demangledName;

这样两者的关系好像就联系上了。我们试着用lldb打印下内存信息看。 把bits 强转类型为 class_data_bits_t *,获取 bits -> data() 返回的 class_rw_t * 并打印显示如下结果。

class_rw_t *

接着我们打印下class_rw_tro属性,里面包含ivarsproperties等信息

ro

接着我们来试着找一下里面的属性(HXPerson定义了一些name,age等属性),通过直接打印得到:

我们有那么多属性,其他的属性在哪呢? 我们看到ro.basePropertyies是一个property_list_t,我们来看下这个property_list_t是怎么实现的

struct property_list_t : entsize_list_tt<property_t, property_list_t, 0> {
};
struct entsize_list_tt {
    uint32_t entsizeAndFlags;
    uint32_t count;
    Element first;

    uint32_t entsize() const {
        return entsizeAndFlags & ~FlagMask;
    }
    uint32_t flags() const {
        return entsizeAndFlags & FlagMask;
    }

    Element& getOrEnd(uint32_t i) const { 
        assert(i <= count);
        return *(Element *)((uint8_t *)&first + i*entsize()); 
    }
    Element& get(uint32_t i) const { 
        assert(i < count);
        return getOrEnd(i);
    }

里面有个get方法,我们试着调用下看

我们把*号去掉,我们得到了age方法

同理,我们可以得到方法列表,协议列表等。

总结和问题

通过上述看源代码,lldb打印可以得到Class的大概结构,只是大概了解。当然在看源代码时还有很多细节疑问。类的结构还要再复习阅读。 1.cache_t章节:cache_getImp是怎么实现的。 2.cache_t章节:在开辟buckets为什么有(uintptr_t)(mask_t)(newCapacity-1) == newCapacity-1这样的判断。 3.cache_t章节:emptyBucketsForCapacity()空桶的判断有这么一道处理,还没太理解其中的用意。