[iOS]OC底层探索——类的结构

203 阅读6分钟

前言

前文主要对对象的原理进行了分析,本文将结合源码探究OC类的内存结构。

isa的走位

isa的走位可见经典图,可结合object_getClass, class_getSuperclass函数自行验证一下。

isa流程图.png 这里引入了一个概念——元类metaClass,对象的isa指向类,类的isa指向元类,那么元类的作用是什么?

我个人理解是为了存放类方法,OC中调用对象方法总的是消息发送机制,如下代码,调用对象objfunc函数其实就是向对象objclass发送消息。而类方法我们知道是直接通过类进行调用,那么自然需要有一个类的类,即metaclass来存放类方法。一些具体过程可见:metaclass作用——属性,类方法与实例方法

[obj func]; --> (void (*)()objc_msgSend)([obj class], @selector(func));

类的结构体

OC底层探索——对象原理之ISA中曾追踪到,OC类的源码层结构体即objc_class,结构如下:

struct objc_class : objc_object {
    ...
    // isa_t isa; 继承自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
    ...
}

struct objc_object {
    private:
        isa_t isa;
    ...
}

这里使用的源码版本是objc4-818.2,有一些老版本乃至objc2的类结构还在广为流传,大家要注意留意区别。

lldb调试验证

定义一对继承的类,直接读取子类的对应内存信息,发现确实符合上面源码的前两个成员变量isasuperclassimage.png

cache_t

cache_t在后面消息发送时会有更详细的讲解,本文主要以了解其结构和大小。

源码定义

struct cache_t {
private:
    explicit_atomic<uintptr_t> _bucketsAndMaybeMask;
    union {
        struct {
            explicit_atomic<mask_t>    _maybeMask;
#if __LP64__
            uint16_t                   _flags;
#endif
            uint16_t                   _occupied;
        };
        explicit_atomic<preopt_cache_t *> _originalPreoptCache;
    };
}

#if __LP64__
typedef uint32_t mask_t;  // x86_64 & arm64 asm are less efficient with 16-bits
#else
typedef uint16_t mask_t;
#endif

cache_t的结构体大小

由源码可见,cache_t主要包含一个uintptr_t与一个联合体,联合体内是一个大小为8字节的结构体和一个指针,所以联合体大小为8字节,综上cache_t的大小为16字节。

_bucketsAndMaybeMask

_bucketsAndMaybeMask,顾名思义,既是bucket_t的指针,也是一个掩码,两者合用8字节的内存大小。其中对于内存的使用规则在不同架构下有不同的规范,对于arm64&LP64架构对应的标准CACHE_MASK_STORAGE_HIGH_16是我们重点关注的。

#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    // _bucketsAndMaybeMask is a buckets_t pointer in the low 48 bits
    // _maybeMask is unused, the mask is stored in the top 16 bits.

    // How much the mask is shifted by.
    static constexpr uintptr_t maskShift = 48;

    // Additional bits after the mask which must be zero. msgSend
    // takes advantage of these additional bits to construct the value
    // `mask << 4` from `_maskAndBuckets` in a single instruction.
    static constexpr uintptr_t maskZeroBits = 4;

    // The largest mask value we can store.
    static constexpr uintptr_t maxMask = ((uintptr_t)1 << (64 - maskShift)) - 1;
    
    // The mask applied to `_maskAndBuckets` to retrieve the buckets pointer.
    static constexpr uintptr_t bucketsMask = ((uintptr_t)1 << (maskShift - maskZeroBits)) - 1;
    
    // Ensure we have enough bits for the buckets pointer.
    static_assert(bucketsMask >= MACH_VM_MAX_ADDRESS,
            "Bucket field doesn't have enough bits for arbitrary pointers.");

如上图定义,此时低48位存储bucket_t的指针,高16位存储mask

bucket_t

bucket_t的源码定义如下:

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
...
}

bucket_t中存储了类对象的方法编号_sel,和方法指针_imp

lldb调试验证

  1. 打印出Foo类的地址,偏移16位(前16位是isasuperclass)即cache_t的地址。 image.png

  2. 打印cache_t image.png

  3. 打印bucket_t *,寻找其中是否含有[Foo test]函数的缓存。 image.png

综上,cache_t的内存结构通过lldb调试成功验证。

class_data_bits_t

源码分析

首先直接看class_data_bits_t的定义,其只有一个成员变量bits,可我们期望找的是方法,属性等定义,我们继续看一下其内部的方法,关注到两个关键词class_rw_tclass_ro_t,通过自己看定义或网上搜索这两个类,我们可以发现其就是用来存放类的属性、方法等信息的结构体。

struct class_data_bits_t {
    friend objc_class;
    // Values are the FAST_ flags above.
    uintptr_t bits;
public:
    // !!重点--获取class_rw_t!!
    class_rw_t* data() const {
        return (class_rw_t *)(bits & FAST_DATA_MASK);
    }
    ...
    // !!重点--获取class_ro_t!!
    const class_ro_t *safe_ro() const {
        class_rw_t *maybe_rw = data();
        if (maybe_rw->flags & RW_REALIZED) {
            // maybe_rw is rw
            return maybe_rw->ro();
        } else {
            // maybe_rw is actually ro
            return (class_ro_t *)maybe_rw;
        }
    }

class_ro_t

ro代表只读readonly,其中存储了当前类在编译期就已经确定的成员变量、属性、方法以及遵循的协议等信息。这一点从下面源码定义中也可以较清晰的看出。

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;
};

lldb验证ivar

示例代码

@interface Father : NSObject
{
    int ivarInt;
    NSString *ivarString;
}

@property (nonatomic, assign) int age;

- (void)instanceFunc;
+ (void)classFunc;

@end

从lldb中逐步获取class_ro_t中的成员变量如下。因为我们上节知道cache_t占16字节,所以我们可以直接计算出class_data_bits_t的内存地址。 image.png 这里补充一个知识点,每个成员变量还专门存取了一个type,OC中变量类型的符号编码可见: Type Encodings

class_rw_t

rw代表可读可写readwrite,其存储了类中的属性、方法还有遵循的协议等信息。其内部包含class_ro_t的指针,可以理解为类的动态信息结构体。当一个类第一次被使用时,运行时会为其分配额外的内存,即class_rw_t。 2020WWDC有相关优化可见:developer.apple.com/videos/play…

源码中定义如下,先过滤出了重点关注的内容。

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
    // 只读或可读可写的指针(原子类型)
    explicit_atomic<uintptr_t> ro_or_rw_ext;
    Class firstSubclass; // 首个子类
    Class nextSiblingClass; // 下一个兄弟类
...
public:
    const method_array_t methods() const {
        auto v = get_ro_or_rwe();
        if (v.is<class_rw_ext_t *>()) {
            return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->methods;
         } else {
            return method_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseMethods()};
        }
    }

    const property_array_t properties() const {
        auto v = get_ro_or_rwe();
        if (v.is<class_rw_ext_t *>()) {
            return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->properties;
        } else {
            return property_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseProperties};
        }
    }

    const protocol_array_t protocols() const {
        auto v = get_ro_or_rwe();
        if (v.is<class_rw_ext_t *>()) {
            return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->protocols;
        } else {
            return protocol_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseProtocols};
        }
    }
};

从源码定义中,我们看到了三个关键方法methods(), properties(), protocols(),结合其实现发现class_rw_t中并没有直接存储(旧版runtime是直接存储)属性、方法、协议等信息,而是用指针ro_or_rw_ext指向了这些信息的地址。

我们点开其中property_array_t的定义往下稍微找一下,能找到属性的最下层定义property_t

// 结构体组的封装定义
class property_array_t : 
    public list_array_tt<property_t, property_list_t, RawPtr>
{
    typedef list_array_tt<property_t, property_list_t, RawPtr> Super;

 public:
    property_array_t() : Super() { }
    property_array_t(property_list_t *l) : Super(l) { }
};

// 属性的结构体定义
struct property_t {
    const char *name;
    const char *attributes;
};

lldb验证property

实例代码

@interface Father : NSObject
{
    NSString *instanceName;
}

@property (nonatomic, assign) int age;
@property (nonatomic, strong) NSString *name1;
@property (nonatomic, strong) NSString *name2;

- (void)test;
+ (void)test0;
@end

下图展示了lldb中一步一步获取类的属性的过程。 image.png

lldb验证method

实例代码

@interface Father : NSObject
- (void)instanceFunc;
+ (void)classFunc;
@end

下图展示了lldb中逐步获取类的方法的过程。 image.png 从上图发现Father类只有两个方法,类方法classFunc并没有在这里,回顾上文,可以知道是因为我们查找的是Father的类信息而不是其metaclassimage.png 由图可见类方法确实存在了类对应的元类中。

ro_or_rw_ext

回过头来看class_rw_t中最关键的变量无疑是explicit_atomic<uintptr_t> ro_or_rw_ext;,首先它无疑是个指针类型,其次它故名思义,有可能是两种类型(分别是class_ro_tclass_rw_ext_t)的指针。从下面源码也可见其定义。

// 自定义的指针联合体
using ro_or_rw_ext_t = objc::PointerUnion<const class_ro_t, class_rw_ext_t, PTRAUTH_STR("class_ro_t"), PTRAUTH_STR("class_rw_ext_t")>;

const ro_or_rw_ext_t get_ro_or_rwe() const {
    return ro_or_rw_ext_t{ro_or_rw_ext};
}

const property_array_t properties() const {
    // 将ro_or_rw_ext类型转为联合指针ro_or_rw_ext_t
    auto v = get_ro_or_rwe();
    // 判断其指针类型后再使用
    if (v.is<class_rw_ext_t *>()) {
        return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->properties;
    } else {
        return property_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseProperties};
    }
}

class_rw_ext_t

struct class_rw_ext_t {
    DECLARE_AUTHED_PTR_TEMPLATE(class_ro_t)
    class_ro_t_authed_ptr<const class_ro_t> ro;
    method_array_t methods;
    property_array_t properties;
    protocol_array_t protocols;
    char *demangledName; // 解构的名字
    uint32_t version;
};

class_rw_ext_t的结构定义还是比较直观的,但我们有一个疑问,为什么这些变量不直接存储在class_rw_t中,而要专门定一个更小的类型?

答案是为了节省内存,只有在类的属性、方法等发生改变时,才会初始化class_rw_ext_t存储变化后的属性、方法等;否则直接访问class_ro_t来查找属性、方法等,有点懒加载的味道。更多可见:WWDC20中objc增加的结构class_rw_ext_t