OC Runtime 底层类结构2020最新

PS:在跟着看今年的WWDC2020所有的session,发现了很多更新,例如 objc_setHook_setAssociatedObjectset入口函数做了一层下沉,以及等等等等,最大的一个更新就是出现了 class_rw_ext_t, 以前只有 ro rw,现在多了一个rwe。

runtime的代码,一点只用 API,不要写这些底层结构,但是一定要了解,因为你的API是随时可用的,如果你从 ro 里面读取 protocolspropertiesmethods,那样一旦底层结构变了,代码就得更新了。

再次梳理了目前最新的 OC 类结构。以便同事们参考。

整体代码基于目前官方最近开源的OC底层源码版本objc4_781,主要研究 objc-runtime-new.h 文件,runtime 这部分包:

  1. OC 的声明后的实现部分开源。
  2. 底层调用的 C 和 C++ 开源。
  3. 汇编语言.mm,Apple 对于一部分CC++无法实现的功能使用,例如msgSend

整个逻辑关系图如下

类结构大手稿

1. objc_object 「万物之源」

连类的结构都是基于 objc_object 的,所以说 OC 里面一切皆对象呢。

objc_object

/// Represents an instance of a class.
struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

1.1 id 和 isa

id 的本质其实就是一个 objc_object * 的结构体指针

/// A pointer to an instance of a class.
typedef struct objc_object *id;

在结构体内部只有一个 Class 结构的 isa

isa 是什么?isa 的本质是 isa_t,在另外一个文件 objc-private.h 中有 isa_t 的定义,是一个联合体,也叫共用体,可以参考这篇文章讲解共用体

union isa_t {
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    Class cls;
    uintptr_t bits;
#if defined(ISA_BITFIELD)
    struct {
        ISA_BITFIELD;  // defined in isa.h
    };
#endif
};

这个里面存储两个,Class clsuintptr_t bits。因为是联合体,所有实际只能有一个出现,ISA_BITFIELD是真正存储值的东西,它存储于文件 isa.h,根据平台不同而不同,下面是 arm64 版本的:

ISA_BITFIELD

# if __arm64__
#   define ISA_MASK        0x0000000ffffffff8ULL
#   define ISA_MAGIC_MASK  0x000003f000000001ULL
#   define ISA_MAGIC_VALUE 0x000001a000000001ULL
#   define ISA_BITFIELD\
      uintptr_t nonpointer        : 1; /*0:代表普通指针,1:表示优化过的,可以存储更多信息。*/\
      uintptr_t has_assoc         : 1; /*是否设置过关联对象。如果没设置过,释放会更快*/\
      uintptr_t has_cxx_dtor      : 1; /*是否有C++的析构函数*/\
      uintptr_t shiftcls          : 33; /* MACH_VM_MAX_ADDRESS 0x1000000000 内存地址值*/ \
      uintptr_t magic             : 6; /*用于在调试时分辨对象是否未完成初始化*/\
      uintptr_t weakly_referenced : 1; /*是否有被弱引用指向过*/\
      uintptr_t deallocating      : 1; /*是否正在释放*/\
      uintptr_t has_sidetable_rc  : 1; /*引用计数器是否过大无法存储在ISA中。如果为1,那么引用计数会存储在一个叫做SideTable的类的属性中*/\
      uintptr_t extra_rc          : 19; /*里面存储的值是引用计数器减1*/\

#   define RC_ONE   (1ULL<<45)
#   define RC_HALF  (1ULL<<18)

这个 ISA_MASK 这种的就是一个蒙版,isa 完整的值通过跟他进行与操作,就只留下了class的地址,(Class)(isa.bits & ISA_MASK)

image.png

话外一提,objc_object 的结构除了上面这个结构,根据这个结构存的值,提供了一整套的API方法,存储于 objc_private.h 文件中。

那么 Class 是什么呢?看2.0

2. class 结构体:objc_class

Classobjc_class * 结构体指针。

objc_class

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_rw_t *data() const {
        return bits.data();
    }
}

这里先后存储的东西分四个:

  1. 继承来的 isa 结构的指针,占8字节
  2. superClass 的 Class 指针,占8字节
  3. cache类型,本篇跳过。
  4. class_data_bits_t类型的bits,下段落展开。

3. 类中数据 class_data_bits_t

class_data_bits_t 里存储着当前类的很多信息,通过 class_data_bits_t 进行蒙版运算 (bits & FAST_DATA_MASK) 可以算出 class_rw_t,简称就是 rw
下一段落重点讲解 class_rw_t

class_data_bits_t ==> class_rw_t ==> class_ro_t
class_data_bits_t ==> class_rw_t ==> class_rwe_t

这个关系是 2020 年新的变动,作为OC Swift的开发者一点要了解,具体看这段 WWDC Session

class_data_bits_t

struct class_data_bits_t {
    friend objc_class;
    // Values are the FAST_ flags above.
    uintptr_t bits;
public:

    class_rw_t* data() const {
        return (class_rw_t *)(bits & FAST_DATA_MASK);
    }
    void setData(class_rw_t *newData)
    {
        ASSERT(!data()  ||  (newData->flags & (RW_REALIZING | RW_FUTURE)));
         //这个过程不需要加锁处理,因为仅在实现或构造期间进行 Set 操作
         //使用 store-release fence 操作,类似一种轻量级的资源锁,
         //如下面的`atomic_thread_fence `,因为可能会同时存在数据的读写
        uintptr_t newBits = (bits & ~FAST_DATA_MASK) | (uintptr_t)newData;
        atomic_thread_fence(memory_order_release);
        bits = newBits;
    }

//获取类的ro数据,即使在构建过程中。
//这个需要修复,至少在没有编译器屏障的情况下,并不是真正安全的。
//在 实现类 更改数据字段的时候,可能没有内存屏障的。
    const class_ro_t *safe_ro() {
        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;
        }
    }
}

上面代码提到了内存屏障,那么什么是内存屏障,这个概念涉及到代码的乱序执行,扩展里面单独说。这个注释的意思是说,imageLoader的时候,还在实现这个类的过程中,不要读取ro数据,因为还没有构建完成。

ro rw rwe 是相互依存的表述类结构的结构,ro 是在内存中直接读取出来的,只读readonly的,其中包括了我们熟悉的ivar,所以说ivar是不可以动态添加的,就是这个原因。

3.1 类数据演变出:class_ro_t

class_ro_t

struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;
#ifdef __LP64__
    uint32_t reserved;
#endif

    const uint8_t * ivarLayout;
    
    const char * name;
    method_list_t * baseMethodList;
    protocol_list_t * baseProtocols;
    const ivar_list_t * ivars;

    const uint8_t * weakIvarLayout;
    property_list_t *baseProperties;

    // This field exists only when RO_HAS_SWIFT_INITIALIZER is set.
    _objc_swiftMetadataInitializer __ptrauth_objc_method_list_imp _swiftMetadataInitializer_NEVER_USE[0];

    _objc_swiftMetadataInitializer swiftMetadataInitializer() const {
        if (flags & RO_HAS_SWIFT_INITIALIZER) {
            return _swiftMetadataInitializer_NEVER_USE[0];
        } else {
            return nil;
        }
    }

    method_list_t *baseMethods() const {
        return baseMethodList;
    }

    class_ro_t *duplicate() const {
        if (flags & RO_HAS_SWIFT_INITIALIZER) {
            size_t size = sizeof(*this) + sizeof(_swiftMetadataInitializer_NEVER_USE[0]);
            class_ro_t *ro = (class_ro_t *)memdup(this, size);
            ro->_swiftMetadataInitializer_NEVER_USE[0] = this->_swiftMetadataInitializer_NEVER_USE[0];
            return ro;
        } else {
            size_t size = sizeof(*this);
            class_ro_t *ro = (class_ro_t *)memdup(this, size);
            return ro;
        }
    }
};

其中包含 const ivar_list_t * ivars 用来表示 ivar_list_tivar_t

3.1.1ivar_list_tivar_t

ivar_list_t 通过 entsize_list_tt 存储了 ivar_t

entsize_list_tt 是提供遍历器的通用容器,详细见我博客的另一篇文章,Apple源码用到的一些数据结构

ivar_list_t

struct ivar_list_t : entsize_list_tt<ivar_t, ivar_list_t, 0> {
    bool containsIvar(Ivar ivar) const {
        return (ivar >= (Ivar)&*begin()  &&  ivar < (Ivar)&*end());
    }
};

ivar_t

struct ivar_t {
#if __x86_64__
    // *offset was originally 64-bit on some x86_64 platforms.
    // We read and write only 32 bits of it.
    // Some metadata provides all 64 bits. This is harmless for unsigned 
    // little-endian values.
    // Some code uses all 64 bits. class_addIvar() over-allocates the 
    // offset for their benefit.
#endif
    int32_t *offset;
    const char *name;
    const char *type;
    // alignment is sometimes -1; use alignment() instead
    uint32_t alignment_raw;
    uint32_t size;

    uint32_t alignment() const {
        if (alignment_raw == ~(uint32_t)0) return 1U << WORD_SHIFT;
        return 1 << alignment_raw;
    }
};

3.2 类数据演变出:class_rw_t

rw 是可读可写的意思,runtime 动态增加方法等就是通过对 rw 进行修改写的操作完成的。

这里先看他的结构定义,对全局有了解。这里后续章节「。。。」详细展开更细致的研究。

class_rw_t

struct class_rw_t {
    uint32_t flags;
    uint16_t witness;
#if SUPPORT_INDEXED_ISA
    uint16_t index;
#endif
    Class firstSubclass;
    Class nextSiblingClass;
public:
    class_rw_ext_t *deepCopy(const class_ro_t *ro);
    const method_array_t methods();
    const property_array_t properties();
    const protocol_array_t protocols();
};

3.3 类数据演变出:class_rw_ext_t

三者的演变关系,我粘贴一段类构造过程的源码:如下:

// at file `objc-runtime-new.mm` static void methodizeClass(Class cls, Class previously)
    auto rw = cls->data();
    auto ro = rw->ro();
    auto rwe = rw->ext();

但是 ro 其实才是一直不变的,也叫 clean memory,RW 是跟随runtime去操作变化的,叫做 Dirty Memory。

rwe 是什么其实很简单,2020年苹果出了iOS14,其SDK里面把rw中不常用的一部分砍了出去,变成了RWE,官方说,用 Mac 的 MailApp 做测试,Dirty Memory从80MB节约到了40MB。

class_rw_ext_t

struct class_rw_ext_t {
    const class_ro_t *ro;
    method_array_t methods;
    property_array_t properties;
    protocol_array_t protocols;
    char *demangledName;
    uint32_t version;
};

4. method_array_t、 method_list_t、 method_t

接下来三部分是:

  1. 类的 rw rwe 中的 method_array_t
  2. 类的 rw rwe 中的 property_array_t
  3. 类的 rw rwe 中的 protocol_array_t

这三个的继承数据结构都是:list_array_tt,详情可见我博客的另一篇文章,Apple源码用到的一些数据结构

通过 rw rwe 中的 method_array_t 获取 methods,它的类型定义如下:

method_array_t

class method_array_t : 
    public list_array_tt<method_t, method_list_t> 
{
    typedef list_array_tt<method_t, method_list_t> Super;

 public:
    method_array_t() : Super() { }
    method_array_t(method_list_t *l) : Super(l) { }

    method_list_t * const *beginCategoryMethodLists() const {
        return beginLists();
    }
    
    method_list_t * const *endCategoryMethodLists(Class cls) const;

    method_array_t duplicate() {
        return Super::duplicate<method_array_t>();
    }
};

其中获取到的一个或多个 method_list_t,具体原理移步到上面文章中。

method_list_t

// entsize 中的2个字符位用来做 fixup 标记。
struct method_list_t : entsize_list_tt<method_t, method_list_t, 0x3> {
    bool isUniqued() const;
    bool isFixedUp() const;
    void setFixedUp();

    uint32_t indexOfMethod(const method_t *meth) const {
        uint32_t I = 
            (uint32_t)(((uintptr_t)meth - (uintptr_t)this) / entsize());
        ASSERT(i < count);
        return I;
    }
};

通过遍历器遍历 method_t

method_t 就是表示一个方法或者函数(包含:函数名、返回值、参数、函数体)

method_t

struct method_t {
    SEL name; // 方法名称
    const char *types; // 编码(返回值类型、参数类型)
    MethodListIMP imp; // 方法的地址/实现

    struct SortBySELAddress :
        public std::binary_function<const method_t&,
                                    const method_t&, bool>
    {
        bool operator() (const method_t& lhs,
                         const method_t& rhs)
        { return lhs.name < rhs.name; }
    };
};

SortBySELAddress()方法是方便函数进行排序,其实在 methodList中 函数是根据SEL name这个对象的内存地址进行排序的,后续寻找方法的时候,会用二分查找增加查找效率。

这里也是今年的一个变动,获取方法的 MethodListIMP 直接从sel里面内存平移8个字节就可以得到。

4.1 method_t 方法的数据结构

SEL是一个指向objc_selector 结构体的指针:
typedef struct objc_selector *SEL;

获取一个SEL的方法

    SEL sel1 = @selector(selector);
    SEL sel2 = sel_registerName("selector");
    SEL sel3 = NSSelectorFromString(@"selector");
    
    // SEL 转换成字符串
    char *string1 = sel_getName(sel1);
    NSString *string2 = NSStringFromSelector(sel1);

IMP 是指向方法实现提的函数指针,调用一个方法,本质上就是一个方法寻址的过程,通过SEL寻找IMP。
method_t,就是在两者之间做映射关系

#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ ); 
#else
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...); 
#endif

4.2 Type Encodings

Type Encodings 就是一个编码,runtime 中对于 method_t 的返回值和参数,使用了Type Encodings 字符串来进行描述:
@encode()指令可以将类型转换为 Type Encodings 字符串编码:

char *buf1 = @encode(int **);
char *buf2 = @encode(struct key);
char *buf3 = @encode(Rectangle);

OC 方法都有两个隐式参数,方法调用者 (id)self 和方法名 (SEL) _cmd ,所以我们才能在方法中使用 self_cmd

-(void)test,它的编码为 “v16@0:8”,可以简写为 “v@:”
v:代表返回值类型为 void
16:代表所有参数所占的总字节数
@:代表参数 1 类型为 id
0:代表参数 1 从第几个字节开始存储
::代表参数 2 类型为 SEL
8:代表参数 2 从第几个字节开始存储

具体这里在官网上有详细解释,See Type Encodings in Objective-C Runtime Programming Guide

5. property_array_t、 property_list_t、 property_t

property_array_t

class property_array_t : 
    public list_array_tt<property_t, property_list_t> 
{
    typedef list_array_tt<property_t, property_list_t> Super;

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

    property_array_t duplicate() {
        return Super::duplicate<property_array_t>();
    }
};

property_list_t

struct property_list_t : entsize_list_tt<property_t, property_list_t, 0> {
};

property_t

struct property_t {
    const char *name;
    const char *attributes;
};

6. protocol_array_t、 protocol_list_t、 protocol_t

protocol_array_t

class protocol_array_t : 
    public list_array_tt<protocol_ref_t, protocol_list_t> 
{
    typedef list_array_tt<protocol_ref_t, protocol_list_t> Super;

 public:
    protocol_array_t() : Super() { }
    protocol_array_t(protocol_list_t *l) : Super(l) { }

    protocol_array_t duplicate() {
        return Super::duplicate<protocol_array_t>();
    }
};

protocol_list_t

struct protocol_list_t {
    // count is pointer-sized by accident.
    uintptr_t count;
    protocol_ref_t list[0]; // variable-size

    size_t byteSize() const {
        return sizeof(*this) + count*sizeof(list[0]);
    }

    protocol_list_t *duplicate() const {
        return (protocol_list_t *)memdup(this, this->byteSize());
    }

    typedef protocol_ref_t* iterator;
    typedef const protocol_ref_t* const_iterator;

    const_iterator begin() const {
        return list;
    }
    iterator begin() {
        return list;
    }
    const_iterator end() const {
        return list + count;
    }
    iterator end() {
        return list + count;
    }
};

protocol_t

struct protocol_t : objc_object {
    const char *mangledName;
    struct protocol_list_t *protocols;
    method_list_t *instanceMethods;
    method_list_t *classMethods;
    method_list_t *optionalInstanceMethods;
    method_list_t *optionalClassMethods;
    property_list_t *instanceProperties;
    uint32_t size;   // sizeof(protocol_t)
    uint32_t flags;
    // Fields below this point are not always present on disk.
    const char **_extendedMethodTypes;
    const char *_demangledName;
    property_list_t *_classProperties;

    const char *demangledName();

    const char *nameForLogging() {
        return demangledName();
    }

    bool isFixedUp() const;
    void setFixedUp();

    bool isCanonical() const;
    void clearIsCanonical();

#   define HAS_FIELD(f) (size >= offsetof(protocol_t, f) + sizeof(f))

    bool hasExtendedMethodTypesField() const {
        return HAS_FIELD(_extendedMethodTypes);
    }
    bool hasDemangledNameField() const {
        return HAS_FIELD(_demangledName);
    }
    bool hasClassPropertiesField() const {
        return HAS_FIELD(_classProperties);
    }

#   undef HAS_FIELD

    const char **extendedMethodTypes() const {
        return hasExtendedMethodTypesField() ? _extendedMethodTypes : nil;
    }

    property_list_t *classProperties() const {
        return hasClassPropertiesField() ? _classProperties : nil;
    }
};

6.isa 走位图

这个图解释网上太多了,没什么好说的,放这里,跳过不讲了。

isa 走位图

拓展研究

内存屏障 memory barrier

上面源码中 class_ro_t *safe_ro() 的注释里提到了一个的 compiler barrier,编译器屏障,这个是为了解决代码执行问题的。

指令重排是指:导致的代码乱序执行。举例来说:如果第一个语句涉及内存读取,第二个语句只是简单的指令计算,那么CPU为了提高效率,完全可能在第一个语句的内存读取完成之前,先执行完了第二个语句。

上下两条指令没有相关性,就可能发生。如果涉及一个值,就有可能发现。
另外一个概念叫做 as if serial,重排序的原则是看上去像是顺序执行的,不影响单线程的最终一致性。

但是多线程是不保证一致性的。所以重排序是CPU的公共特性。

然后就出现了,内存屏障 memory barrier
屏障上下的指令不会发生重排序,在汇编层面,通过 lock 汇编指令完成,lock是锁总线,CPU访问内存的总线,等指令访问完了,其他CPU才能去访问。

lock 本身的前面的所有指令全部不能越过 lock 重排序。

拓展阅读:Linux内核同步机制之(三):memory barrier

GCD 里面的 barrier 栅栏函数,等待所有位于栅栏函数之前的操作执行完毕后执行,跟这个命名类似,有相同也有不同,相同就是都是为了保证因为多线程和并发导致的前后指令/任务的乱序。

内存屏障是指令级别的,在CPU汇编指令级别上的。栅栏函数是GCD级别的,GCD又是iOS系统级别的机制。

References

  1. objc4_781 官方源码
  2. 简图记录-android fence机制
  3. Fence和非原子操作的ordering
  4. C语言共用体(C语言union用法)详解
  5. 「注意这个已经过时了,新版本不是这样的」补充git上找到的一个 o_t c_t ro rw 的连通图,缺点是没有rwe,名字我自己瞎起的,我后面画图会参考这个。


    o_t c_t ro rw 连通图
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343