美文网首页runtime深度学习
runtime源码中的类和对象

runtime源码中的类和对象

作者: WeiHing | 来源:发表于2018-02-03 21:24 被阅读73次

    本文基于objc4-709源码进行分析。关于源码编译:objc - 编译Runtime源码objc4-706

    objc中的类和对象

    1.类和对象的结构概要

    NSObject是所有类的基类,NSObject在源码中的定义:

    在NSObject.mm文件中找到

    @interface NSObject <NSObject> {
        Class isa  OBJC_ISA_AVAILABILITY;
    }
    

    NSObject类的第一个成员变量就是Class类型的isa。

    Object.mm文件:

    typedef struct objc_class *Class; //类
    typedef struct objc_object *id; 
    
    @interface Object { 
        Class isa; 
    } 
    

    Class就是c语言定义的objc_class结构体类型的指针,objc中的类实际上就是objc_class。

    而id类型就是objc_object结构体类型的指针(就是我们平时经常用到的id类型),我们平时用的id可以用来声明一个对象,说明objc中的对象实际上就是objc_object。

    objc-runtime-new.h

    struct objc_class : objc_object {
        // Class ISA;
        Class superclass;           //父类的指针
        cache_t cache;             // formerly cache pointer and vtable 方法缓存
        class_data_bits_t bits;    // class
        ...
    }
    

    objc_class继承于objc_object,objc中的类也是一个对象。

    objc-private.h

    struct objc_object {
    private:
        isa_t isa;//objc_object唯一成员变量
    public:
        Class ISA();
        Class getIsa();
        void initIsa(Class cls /*nonpointer=false*/);
        
        ...
    private:
        void initIsa(Class newCls, bool nonpointer, bool hasCxxDtor);
    }
    
    union isa_t 
    {
        isa_t() { }
        isa_t(uintptr_t value) : bits(value) { }
    
        Class cls;
        uintptr_t bits;
        
        ...
    }
    

    objc_object是objc中对象的定义。isa 是 objc_object的唯一成员变量。我们经常说,所有的objc对象都包含一个isa指针,从源码上来看,现在准确说应该是isa_t结构体(isa指针应该是isa_t联合体中的Class cls指针)。当objc为一个对象分配内存,初始化实例变量后,在这些对象的实例变量的结构体中的第一个就是isa。

    为了方便阅读,我把objc_class写成:

    struct objc_class : objc_object {
        isa_t isa;
        Class superclass;           //父类的指针
        cache_t cache;             // formerly cache pointer and vtable 方法缓存
        class_data_bits_t bits;    // class
        ...
    }
    

    在objc中,对象的方法的实现不会存储在每个对象的结构体中,而是在相应的类里(如果每一个对象都要维护一个实例方法列表,那么开销太大了)。当一个实例方法被调用时,会通过对象的isa,在对应的类中找到方法的实现(具体是在class_data_bits_t结构体中查找,里面有一个方法列表)。

    同时我们还从源码中看到,objc_class结构体中还有一个Class类型的superclass成员变量,指向了父类。通过这个指针可以查找从父类继承的方法。

    然而,对于类对象来说,它的isa又是什么呢?objective-c里面有一种叫做meta class元类的东西。


    对象、类、元类关系图

    为了让我们能够调用类方法,类的isa“指针”必须指向一个类结构,并且该类结构必须包含我们可以调用的类方法列表。这就导致了元类的定义:元类是类对象的类。

    类方法调用时,通过类的isa“指针”在元类中获取方法的实现。元类中存储了一个类的所有类方法。

    从上图中总结以下几个信息:

    • root class(class)就是NSObject,由于它是基类,所以它没有父类。
    • 实例对象的isa指向其类,类的isa指向其元类。每个元类的isa都指向根元类root class(meta),根元类的isa指向自己。
    • 根元类的父类指针指向基类(NSObject)。

    What is a meta-class in Objective-C?

    ps:isa指针并不总是指向实例对象所属的类,不能依靠它来确定类型,而应用class方法。(KVO的实现中,将被观察的对象的isa指针指向一个中间类而非真实的类)。

    2.isa_t结构体的分析

    通过源码,我们可以知道isa_t实际上是一个union联合体。其中的方法、成员变量、结构体公用一块空间。取决于其中的结构体,最终isa_t共占64位内存空间

    union isa_t 
    {
        isa_t() { }
        isa_t(uintptr_t value) : bits(value) { }
    
        Class cls;//指针
        uintptr_t bits;
    
    #if SUPPORT_PACKED_ISA
    
    //-------------------arm64上的实现
    # if __arm64__
    #   define ISA_MASK        0x0000000ffffffff8ULL//ISA_MAGIC_MASK 和 ISA_MASK 分别是通过掩码的方式获取MAGIC值 和 isa类指针
    #   define ISA_MAGIC_MASK  0x000003f000000001ULL
    #   define ISA_MAGIC_VALUE 0x000001a000000001ULL
        struct {
            uintptr_t nonpointer        : 1;    //0:普通isa指针,1:优化的指针用于存储引用计数
            uintptr_t has_assoc         : 1;    //表示该对象是否包含associated object,如果没有,析构时会更快
            uintptr_t has_cxx_dtor      : 1;    //表示对象是否含有c++或者ARC的析构函数,如果没有,析构更快
            uintptr_t shiftcls          : 33;   // MACH_VM_MAX_ADDRESS 0x1000000000 类的指针
            uintptr_t magic             : 6;    //用于在调试时分辨对象是否未完成初始化(用于调试器判断当前对象是真的对象还是没有初始化的空间)
            uintptr_t weakly_referenced : 1;    //对象是否有过weak对象
            uintptr_t deallocating      : 1;    //是否正在析构
            uintptr_t has_sidetable_rc  : 1;    //该对象的引用计数值是否过大无法存储在isa指针
            uintptr_t extra_rc          : 19;   //存储引用计数值减一后的结果。对象的引用计数超过 1,会存在这个这个里面,如果引用计数为 10,extra_rc的值就为 9。
    #       define RC_ONE   (1ULL<<45)
    #       define RC_HALF  (1ULL<<18)
        };
    
    //-------------------__x86_64__上的实现
    # elif __x86_64__
    #   define ISA_MASK        0x00007ffffffffff8ULL
    #   define ISA_MAGIC_MASK  0x001f800000000001ULL
    #   define ISA_MAGIC_VALUE 0x001d800000000001ULL
        struct {
            uintptr_t nonpointer        : 1;
            uintptr_t has_assoc         : 1;
            uintptr_t has_cxx_dtor      : 1;
            uintptr_t shiftcls          : 44; // MACH_VM_MAX_ADDRESS 0x7fffffe00000
            uintptr_t magic             : 6;  //值为0x3b
            uintptr_t weakly_referenced : 1;
            uintptr_t deallocating      : 1;
            uintptr_t has_sidetable_rc  : 1;
            uintptr_t extra_rc          : 8;
    #       define RC_ONE   (1ULL<<56)
    #       define RC_HALF  (1ULL<<7)
        };
    
    # else
    #   error unknown architecture for packed isa
    # endif
    
    #endif
    
    ...略
    

    由于源码要在mac os下才能编译,因此接下来都基于x86_64分析,arm64其实大同小异。

    源码中遇到的编译宏的说明:

    SUPPORT_PACKED_ISA:表示平台是否支持在 isa 指针中插入除 Class 之外的信息。如果支持就会将 Class 信息放入 isa_t 定义的 struct 内(shiftcls),并附上一些其他信息,比如引用计数,析构状态;如果不支持,那么不会使用 isa_t 内定义的 struct,这时 isa_t 只使用 cls成员变量(Class 指针,经常说的“isa指针”就是这个)。在 iOS 以及 MacOSX 上,SUPPORT_PACKED_ISA 定义为 1(支持)。

    struct结构体的成员含义:

    参数                     含义
    nonpointer      0 表示普通的 isa 指针,1 表示使用优化,存储引用计数
    has_assoc       表示该对象是否包含 associated object 关联对象,如果没有,则析构时会更快
    has_cxx_dtor        表示该对象是否有 C++ 或 ARC 的析构函数,如果没有,则析构时更快
    shiftcls        类的指针
    magic           __x86_64__环境初始化值为 0x3b,用于在调试时分辨对象是否未完成初始化。
    weakly_referenced   表示该对象是否有过 weak 对象,如果没有,则析构时更快
    deallocating        表示该对象是否正在析构
    has_sidetable_rc    表示该对象的引用计数值是否过大无法存储在 isa 指针
    extra_rc        存储引用计数值减一后的结果。19位将保存对象的引用计数,这样对引用计数的操作只需要原子的修改这个指针即可,如果引用计数超出19位,才会将引用计数保存到外部表,而这种情况往往是很少的,因此效率将会大大提高。
    

    在初始化一节会对其中一些参数继续谈谈。

    初始化

    在我们调用alloc来实例化对象时,会调用到objc_object::initInstanceIsa(Class cls, bool hasCxxDtor) 方法。方法的具体实现:

    inline void 
    objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
    {
        initIsa(cls, true, hasCxxDtor);
    }
    
    inline void 
    objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor) 
    {     
        if (!nonpointer) {
            isa.cls = cls;
        } else {
            isa_t newisa(0);
    
    #if SUPPORT_INDEXED_ISA
            newisa.bits = ISA_INDEX_MAGIC_VALUE;
            newisa.has_cxx_dtor = hasCxxDtor;
            newisa.indexcls = (uintptr_t)cls->classArrayIndex();
    #else
    //__x86_64__和arm64会进入这个条件分支
            newisa.bits = ISA_MAGIC_VALUE;
            newisa.has_cxx_dtor = hasCxxDtor;
            newisa.shiftcls = (uintptr_t)cls >> 3;
    #endif
            isa = newisa;
        }
    }
    

    initInstanceIsa方法实际上调用了initIsa方法,并且传入nonpointer=true参数。

    由于isa_t是一个联合体,所以newisa.bits = ISA_MAGIC_VALUE;把isa_t中的struct结构体初始化为:

    objc-isa-isat-bits.png

    实际上只设置了nonpointer和magic的值。

    nonpointer

    nonpointer 表示是否对isa开启指针优化。

    先介绍一种称为Tagged Pointer的技术,这是苹果为64位设备提出的节省内存和提高执行效率的一种优化方案。

    假设我们要存储一个NSNumber对象,其值是一个整数。正常情况下,如果这个整数只是一个NSInteger的普通变量,那么它所占用的内存是与CPU的位数有关,在32位CPU下占4个字节,在64位CPU下是占8个字节的。而指针类型的大小通常也是与CPU位数相关,一个指针所占用的内存在32位CPU下为4个字节,在64位CPU下也是8个字节。

    如果没有Tagged Pointer对象,从32位机器迁移到64位机器中后,虽然逻辑没有任何变化,但这种NSNumber、NSDate一类的对象所占用的内存会翻倍。

    为了改进上面提到的内存占用和效率问题,苹果提出了Tagged Pointer对象。由于NSNumber、NSDate一类的变量本身的值需要占用的内存大小常常不需要8个字节,拿整数来说,4个字节所能表示的有符号整数就可以达到20多亿。所以我们可以将一个对象的指针拆成两部分,一部分直接保存数据,另一部分作为特殊标记,表示这是一个特别的指针,不指向任何一个地址。

    技术详细可以查看以下两个链接。

    深入理解Tagged Pointer

    64位与Tagged Pointer

    虽然Tagged Pointer专门用来存储小的对象,例如NSNumber和NSDate。但在isa指针优化上用到了tagged Pointer的概念。

    使用整个指针大小的内存来存储isa有些浪费,在arm64上运行的iOS只用了33位(和结构体中shiftcls的33无关,Mac OS用了47位),剩下的31位用于其他目的。

    nonpointer = 0 ,raw isa,表示isa_t不使用struct结构体,访问对象的 isa 会直接返回 cls 指针,cls 会指向对象所属的类的结构。这是在 iPhone 迁移到 64 位系统之前时 isa 的类型。

    nonpointer = 1,isa_t使用了struct结构体,此时 isa 不再是指针,不能直接访问 objc_object 的 isa 成员变量,但是其中也有 cls 的信息,只是其中关于类的指针都是保存在 shiftcls 中。

    bool hasNonpointerIsa();这个函数就是用来判断当前对象的isa是否启用tagged pointer的。

    magic

    用于判断当前对象是否已经完成初始化。

    has_cxx_dtor

    接着设置has_cxx_dtor,这一位表示当前对象是否有 C++ 或者 ObjC 的析构器,如果没有析构器就会快速释放内存。

    shiftcls

    newisa.shiftcls = (uintptr_t)cls >> 3; 将对象对应的类的指针存入结构体的shiftcls成员中。

    将cls(地址)右移三位的原因:字节对齐。类的指针按照8字节对齐。

    为什么需要字节对齐

    C语言字节对齐问题详解

    对象的内存地址必须对齐到字节的倍数,这样可以提高代码运行的性能,在 iPhone5s 中虚拟地址为 33 位,所以用于对齐的最后三位比特为 000,我们只会用其中的 30 位来表示对象的地址。

    将cls右移三位,可以将地址中无用的后三位清除减小内存的消耗。

    回过头来看,类的isa的shiftcls就指向元类了。

    ISA()

    由于开启了指针优化后,isa不再是指针,要获取类指针就要用到 ISA() 方法。

    #define ISA_MASK 0x00007ffffffffff8ULL
    
    inline Class 
    objc_object::ISA() 
    {
    #if SUPPORT_INDEXED_ISA
        if (isa.nonpointer) {
            uintptr_t slot = isa.indexcls;
            return classForIndex((unsigned)slot);
        }
        return (Class)isa.bits;
    #else
        return (Class)(isa.bits & ISA_MASK); //按位与运算获取类指针,__x86_64__是44位
    #endif
    }
    

    3.cache_t 结构体的分析

    typedef unsigned int uint32_t;
    typedef uint32_t mask_t; 
    typedef unsigned long uintptr_t;
    typedef uintptr_t cache_key_t;
    
    struct cache_t {
        struct bucket_t *_buckets;
        mask_t _mask;
        mask_t _occupied;
        ......
    };
    
    struct bucket_t {
    private:
        cache_key_t _key;
        IMP _imp;
        ......
    };
    

    cache 存放着实例方法的缓存,提高方法调用效率。当一个对象调用一个方法时,它会先从缓存里面找这个方法,如果没有找到才会去类的方法列表中去找。

    如果一个对象调用一个方法,首先根据对象的isa找到对应的类,再在类的方法列表中寻找这个方法,如果找不到就到父类中的方法列表查找,一旦找到就调用。如果没有找到,有可能转发消息,也可能忽略它。但这样效率太低了,有些方法会经常用到,那么每次调用都要走一遍以上流程,是不是很慢?用cache来缓存方法,优先在 cache 中查找,找到就调用没找到再走正常路子。
    cache_t中存储了一个bucket_t结构体指针和两个无符号整形变量。

    bucket_t结构体中存储了IMP函数指针和unsigned long。_buckets 用来存储Method的链表。

    _mask 分配用来缓存bucket的总数。

    _occupied 当前占用的bucket数。

    4.class_data_bits_t

    之前说过,实例方法被调用时,会通过其持有 isa 指针寻找对应的类,然后在其中的 class_data_bits_t 中查找对应的方法。相对来说 class_data_bits_t 会有点复杂,包含了 class_rw_t 、class_ro_t 等重要的结构体。这一节会对 class_data_bits_t 进行分析,介绍方法是如何在objc中存取的。

    struct class_data_bits_t {
    
        uintptr_t bits;
        
        class_rw_t* data() {
            return (class_rw_t *)(bits & FAST_DATA_MASK);
        }
        ......
    };
    
    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() { 
            return bits.data();
        }
        ......
    };
    

    class_data_bits_t 结构体只含有一个64bit的bits变量,它存储了类相关的信息,和不同的以'FAST_'开头的flag掩码做按位与运算。

    uintptr_t bits存储了一个指向 class_rw_t 结构体的指针和三个标志位。bits在64位兼容版中的内容:

    // 当前类是swift类
    #define FAST_IS_SWIFT           (1UL<<0)
    //当前类或者父类含有默认的 retain/release/autorelease/retainCount/_tryRetain/_isDeallocating/retainWeakReference/allowsWeakReference 方法
    #define FAST_HAS_DEFAULT_RR     (1UL<<1)
    // 当前类的实例需要 raw isa
    #define FAST_REQUIRES_RAW_ISA   (1UL<<2)
    // 数据指针
    #define FAST_DATA_MASK          0x00007ffffffffff8UL
    
    class_rw_t* data() {
            return (class_rw_t *)(bits & FAST_DATA_MASK);
        }
    

    在 objc_class 的 data 方法中,直接调用并返回了 class_data_bits_t 的data方法。bits 和 FAST_DATA_MASK做按位与运算,转换为 class_rw_t *并返回。

    Mac OS X 只使用 47 位内存地址,所以bits前 17 位空余出来。bits中最大的一块存储区域用于存储class_rw_t 指针,在 FAST_DATA_MASK 对应的[3,46]位数据段中。[3,46]一共是44位,有没有觉得44很眼熟?由于字节对齐,所以47位的后三位为0,可以用来做标志位,所以就有了另外三个标志位。

    除了 FAST_DATA_MASK 是用一段空间做存储外,其他宏都是用1bit。这些数据都有对应的getter、setter封装函数。比如:

    bool isSwift() {return getBit(FAST_IS_SWIFT);}
    
    void setIsSwift() {setBits(FAST_IS_SWIFT);}
    
    bool hasDefaultRR() {return getBit(FAST_HAS_DEFAULT_RR);}
    
    void setHasDefaultRR() {setBits(FAST_HAS_DEFAULT_RR);}
    

    在64非兼容版本下还有更多的宏定义,位于objc-runtime-new.h文件中,如有需要请自行阅读源码。

    class_rw_t

    从上面的分析中我们知道,class_data_bits_t 用了大段空间存储了 class_rw_t 指针。class_rw_t 提供了运行时对类扩展的能力,class_rw_t 结构体中保存了 objc 中的属性、方法、协议等信息。

    在 objc_class结构体中的注释写到 class_data_bits_t相当于 class_rw_t指针加上 rr/alloc 的标志。

    class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags

    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;//是计算机语言用于解决实体名称唯一性的一种方法,做法是向名称中添加一些类型信息,用于从编译器中向连接器传递更多语义信息。
        
        ......
    };
    

    关于32位 flags 标志,在源码中有这样一段注释:

    Values for class_rw_t->flags
    These are not emitted by the compiler and are never used in class_ro_t.
    Their presence should be considered in future ABI versions.

    它的值不是由编译器设置的,并且从不在class_ro_t中使用。未来的ABI版本应考虑到他们的存在。

    flags 标记了类的一些状态,与生命周期、内存管理有关,有些位目前还没有定义。


    class_rw_t 中的 methods、properties、protocols 存放了和类的方法、属性、协议有关的信息。

    method_array_t methods;
    property_array_t properties;
    protocol_array_t protocols;
    
    class method_array_t : public list_array_tt<method_t, method_list_t> 
    class property_array_t : public list_array_tt<property_t, property_list_t> 
    class protocol_array_t : public list_array_tt<protocol_ref_t, protocol_list_t> 
    

    method_array_t、property_array_t、protocol_array_t这三种类都继承自 list_array_tt<Element, List>这种结构。

    template <typename Element, typename List>
    class list_array_tt {
        struct array_t {
            uint32_t count;
            List* lists[0];//长度为0的数组,c99的写法,允许在运行期动态申请内存
        };
    }
    

    list_array_tt 存储一些元数据,是通过c++模板定义的容器类,提供了一些诸如count、迭代器iterator的方法和类。Element表示元数据的类型,比如 method_t 、 property_t 、 protocol_ref_t;List表示存储元数据的容器,理解成是用于存储元数据的一维数组,比如 method_list_t 、 property_list_t 。由于list_array_tt 存储了List指针数组,所以list_array_tt实际上可以看做是元数据的二维数组。list_array_tt 有三种状态:

    • 自身为空
    • List指针数组只有一个指向元数据数组的指针
    • List指针数组有多个指针

    一个类创建之初可能处于前两个状态,如果用category或者class_addMethod来添加方法,就变成第三个状态,而且是不可逆的回不去前两个状态

    可以对list_array_tt不断进行扩张。比如在通过category添加方法时,就调用到这个方法,把新的方法列表(相当于是装有一个category所有方法的容器)添加到二维数组中:

    void attachLists(List* const * addedLists, uint32_t addedCount) {
        if (addedCount == 0) return;
    
         //调用realloc将原空间扩展,把原数组复制到后面,新数组复制到前面
        if (hasArray()) {
            // many lists -> many lists
            uint32_t oldCount = array()->count;
            uint32_t newCount = oldCount + addedCount;
            setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
            array()->count = newCount;
            memmove(array()->lists + addedCount, array()->lists, 
                    oldCount * sizeof(array()->lists[0]));
            memcpy(array()->lists, addedLists, 
                   addedCount * sizeof(array()->lists[0]));
        }
        else if (!list  &&  addedCount == 1) {
            // 0 lists -> 1 list
            list = addedLists[0];
        } 
        
        //List指针数组只有一个指针时
        //malloc重新申请内存,最后一个位置留给原来元数据数组的指针
        else {
            // 1 list -> many lists
            List* oldList = list;
            uint32_t oldCount = oldList ? 1 : 0;
            uint32_t newCount = oldCount + addedCount;
            setArray((array_t *)malloc(array_t::byteSize(newCount)));
            array()->count = newCount;
            if (oldList) array()->lists[addedCount] = oldList;
            memcpy(array()->lists, addedLists, 
                   addedCount * sizeof(array()->lists[0]));
        }
    }
    

    无论是哪一种逻辑,新加的方法列表都会添加到二维数组前面。

    有一个名字很类似的结构体 class_ro_t,'rw' 和 ro' 相信很容易就理解是 'readwrite' 和 'readonly'的意思吧。

    class_rw_t 的结构体成员中还有一个指向常量的指针class_ro_t *ro。因此在编译期间类的结构中的 class_data_bits_t *data 指向的是一个 class_ro_t * 指针。class_ro_t也是一个结构体,下一节我们来讲讲它。

    class_ro_t

    class_rw_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;//ivar列表
    
        const uint8_t * weakIvarLayout;
        property_list_t *baseProperties;//属性列表
    
        method_list_t *baseMethods() const {
            return baseMethodList;
        }
    };
    

    在类进行初始化之前,通过object_class 的data()方法得到的实际是一个指向 class_ro_t 结构体的指针。

    instanceStart、instanceSize 两个成员变量的存在保证了objc2.0的ABI稳定性。Non Fragile ivars

    method_list_t、ivar_list_t、property_list_t 结构体都继承自 entsize_list_tt<Element, List, FlagMask> , protocol_list_t 的结构则相对简单很多。entsize_list_tt 是通过c++模板定义的容器类,提供了一些诸如count、get、迭代器iterator的方法和类,通过这些方法和类可以方便地遍历并获取容器内的数据。

    template <typename Element, typename List, uint32_t FlagMask>
    struct entsize_list_tt {
        uint32_t entsizeAndFlags;//总大小
        uint32_t count;//个数
        Element first;//第一个元数据
    };
    

    而在类进行初始化之时,会调用到一个 static Class realizeClass(Class cls) 方法(这个之后分析oc中的消息发送时会具体分析,这里只要知道会调用到这个方法即可)。

    realizeClass的实现节选:

    ro = (const class_ro_t *)cls->data();//强制转换
    if (ro->flags & RO_FUTURE) {
        // This was a future class. rw data is already allocated.
        rw = cls->data();
        ro = cls->data()->ro;
        cls->changeInfo(RW_REALIZED|RW_REALIZING, RW_FUTURE);
    } else {
        // Normal class. Allocate writeable class data.
        rw = (class_rw_t *)calloc(sizeof(class_rw_t), 1);//初始化一个 class_rw_t 结构体,分配可读写数据空间
        rw->ro = ro;//设置结构体 ro 的值以及 flag
        rw->flags = RW_REALIZED|RW_REALIZING;
        cls->setData(rw);//最后设置正确的 data
    }
    

    这里主要做了几个事:

    • 调用 object_class 的 data 方法,把得到的 class_rw_t 指针强制转换成 class_ro_t 指针。
    • 新初始化一个 class_rw_t ,分配可读写数据空间。
    • 将 class_ro_t 指针赋值给 class_rw_t->ro ,并设置 class_rw_t->flag 。
    • 设置 object_class 的正确 data 。

    也即由以下图1变成了图2:图片来源深入解析 ObjC 中方法的结构


    但是此时 class_rw_t 中的方法,属性以及协议列表均为空。最后在 realizeClass 方法中 调用了 methodizeClass 方法来将类 class_ro_t 的方法、属性和遵循的协议加载到 methods、 properties 和 protocols 列表中(用到了在 class_rw_t 一节最后提到的 attachLists 方法),但是class_ro_t其中的 ivar 不会被拷贝(不能在运行时给已有的类增加实例变量)。methodizeClass 方法节选:

    // Install methods and properties that the class implements itself.
    method_list_t *list = ro->baseMethods();
    if (list) {
        prepareMethodLists(cls, &list, 1, YES, isBundleClass(cls));
        rw->methods.attachLists(&list, 1);
    }
    
    property_list_t *proplist = ro->baseProperties;
    if (proplist) {
        rw->properties.attachLists(&proplist, 1);
    }
    
    protocol_list_t *protolist = ro->baseProtocols;
    if (protolist) {
        rw->protocols.attachLists(&protolist, 1);
    }
    

    可以这样说,经过realizeClass函数处理的类才成为了真正的类。

    5.验证一下

    一下这part的内容基于Draveness大大《深入解析 ObjC 中方法的结构》一文中验证部分进行复现,分析运行时初始化过程的内存变化,作为自己敲一下代码所做的记录。

    #import <Foundation/Foundation.h>
    @interface XXObject : NSObject
    @property (nonatomic, assign) CGFloat test;
    
    - (void)hello;
    @end
    
    
    #import "XXObject.h"
    @implementation XXObject
    - (void)hello {
        NSLog(@"Hello");
    }
    @end
    
    主程序代码:
    #import <Foundation/Foundation.h>
    #import "XXObject.h"
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            Class cls = [XXObject class];
            NSLog(@"%p", cls);
        }
        return 0;
    }
    

    由于类在内存中的位置是编译期就确定的,在之后修改代码,也不会改变内存中的位置。所以先跑一次代码获取XXObject 在内存中的地址。地址为:0x1000011e0

    在运行时初始化之前加入断点:


    然后通过lldb验证一下:

    (lldb) p (objc_class *)0x1000011e0
    (objc_class *) $0 = 0x00000001000011e0
    (lldb) p (class_data_bits_t *)0x100001200 //偏移32为获取class_data_bits_t指针
    (class_data_bits_t *) $1 = 0x0000000100001200
    (lldb) p $1->data()
    warning: could not find Objective-C class data in the process. This may reduce the quality of type information available.
    (class_rw_t *) $2 = 0x0000000100001158
    (lldb) p (class_ro_t *)$2   //强制转换类型
    (class_ro_t *) $3 = 0x0000000100001158
    (lldb) p *$3
    (class_ro_t) $4 = {
      flags = 128
      instanceStart = 8
      instanceSize = 16
      reserved = 0
      ivarLayout = 0x0000000000000000 <no value available>
      name = 0x0000000100000f6e "XXObject"
      baseMethodList = 0x00000001000010c8
      baseProtocols = 0x0000000000000000
      ivars = 0x0000000100001118
      weakIvarLayout = 0x0000000000000000 <no value available>
      baseProperties = 0x0000000100001140
    }
    

    可以看到name、baseMethodList、ivars、baseProperties是有值的,对于后两者有值的原因:因为XXObject类中我添加了@property (nonatomic, assign) CGFloat test;属性,而属性也在编译期自动生成了一个对应的ivar(带下划线前缀的原属性名的成员变量)。

    //获取ivars
    (lldb) p $4.ivars
    (const ivar_list_t *) $6 = 0x0000000100001118
    (lldb) p *$6
    //显示count=1只有一个ivar
    (const ivar_list_t) $7 = {
      entsize_list_tt<ivar_t, ivar_list_t, 0> = {
        entsizeAndFlags = 32
        count = 1
        first = {
          offset = 0x00000001000011b0
          name = 0x0000000100000f8b "_test"
          type = 0x0000000100000fb2 "d"
          alignment_raw = 3
          size = 8
        }
      }
    }
    (lldb) p $7.get(0)
    (ivar_t) $8 = {
      offset = 0x00000001000011b0
      name = 0x0000000100000f8b "_test"
      type = 0x0000000100000fb2 "d"
      alignment_raw = 3
      size = 8
    }
    
    //获取property
    (lldb) p $4.baseProperties
    (property_list_t *) $10 = 0x0000000100001140
    (lldb) p *$10
    (property_list_t) $11 = {
      entsize_list_tt<property_t, property_list_t, 0> = {
        entsizeAndFlags = 16
        count = 1
        first = (name = "test", attributes = "Td,N,V_test")
      }
    }
    
    

    查看 baseMethodList 中的内容:

    (lldb) p $4.baseMethodList
    (method_list_t *) $14 = 0x00000001000010c8
    (lldb) p *$14
    (method_list_t) $15 = {
      entsize_list_tt<method_t, method_list_t, 3> = {
        entsizeAndFlags = 24
        count = 3
        first = {
          name = "hello"
          types = 0x0000000100000f97 "v16@0:8"
          imp = 0x0000000100000e20 (debug-objc`-[XXObject hello] at XXObject.m:11)
        }
      }
    }
    (lldb) p $15.get(0)
    (method_t) $16 = {
      name = "hello"
      types = 0x0000000100000f97 "v16@0:8"
      imp = 0x0000000100000e20 (debug-objc`-[XXObject hello] at XXObject.m:11)
    }
    (lldb) p $15.get(1)
    (method_t) $17 = {
      name = "test"
      types = 0x0000000100000f9f "d16@0:8"
      imp = 0x0000000100000e50 (debug-objc`-[XXObject test] at XXObject.h:11)
    }
    (lldb) p $15.get(2)
    (method_t) $18 = {
      name = "setTest:"
      types = 0x0000000100000fa7 "v24@0:8d16"
      imp = 0x0000000100000e70 (debug-objc`-[XXObject setTest:] at XXObject.h:11)
    }
    

    结果显示 baseMethodList 中有三个方法,打印出来看,第一个是 XXObject 的 hello 方法,其余两个是 test 属性的对应getter和setter方法。所以也就不难理解为什么objc中的property = ivar + getter + setter了。

    关于"v16@0:8""d16@0:8""v24@0:8d16" 请见Type Encoding

    以上这些都是在编译器就生成了的。

    接着来看一下如果没有把class_rw_t强转成class_ro_t时的情形:先打印出来留到后面与初始化之后进行对比。

    (lldb) p *$2
    (class_rw_t) $5 = {
      flags = 128
      version = 8
      ro = 0x0000000000000010
      methods = {
        list_array_tt<method_t, method_list_t> = {
           = {
            list = 0x0000000000000000
            arrayAndFlag = 0
          }
        }
      }
      properties = {
        list_array_tt<property_t, property_list_t> = {
           = {
            list = 0x0000000100000f6e
            arrayAndFlag = 4294971246
          }
        }
      }
      protocols = {
        list_array_tt<unsigned long, protocol_list_t> = {
           = {
            list = 0x00000001000010c8
            arrayAndFlag = 4294971592
          }
        }
      }
      firstSubclass = nil
      nextSiblingClass = 0x0000000100001118
      demangledName = 0x0000000000000000 <no value available>
    }
    

    接着在 realizeClass 方法中的这个地方打一个断点:(直接和地址值进行比较是因为类在编译期就确定了在内存中的地址)


    判断当前类是否XXObject,此时还没有进行初始化,类结构体中的布局依旧是上面那样没有变化的。

    [图片上传失败...(image-5a0ad-1517664314598)]
    当这段代码运行完后,再来lldb打印一下:

    (lldb) p (objc_class *)0x1000011e0
    (objc_class *) $19 = 0x00000001000011e0
    (lldb) p (class_data_bits_t *)0x0000000100001200
    (class_data_bits_t *) $20 = 0x0000000100001200
    (lldb) p $20->data()
    (class_rw_t *) $21 = 0x0000000100f05030
    (lldb) p *$21
    (class_rw_t) $22 = {
      flags = 2148007936
      version = 0
      ro = 0x0000000100001158
      methods = {
        list_array_tt<method_t, method_list_t> = {
           = {
            list = 0x0000000000000000
            arrayAndFlag = 0
          }
        }
      }
      properties = {
        list_array_tt<property_t, property_list_t> = {
           = {
            list = 0x0000000000000000
            arrayAndFlag = 0
          }
        }
      }
      protocols = {
        list_array_tt<unsigned long, protocol_list_t> = {
           = {
            list = 0x0000000000000000
            arrayAndFlag = 0
          }
        }
      }
      firstSubclass = nil
      nextSiblingClass = nil
      demangledName = 0x0000000000000000 <no value available>
    }
    (lldb) p $22.ro
    (const class_ro_t *) $23 = 0x0000000100001158
    (lldb) p *$23
    (const class_ro_t) $24 = {
      flags = 128
      instanceStart = 8
      instanceSize = 16
      reserved = 0
      ivarLayout = 0x0000000000000000 <no value available>
      name = 0x0000000100000f6e "XXObject"
      baseMethodList = 0x00000001000010c8
      baseProtocols = 0x0000000000000000
      ivars = 0x0000000100001118
      weakIvarLayout = 0x0000000000000000 <no value available>
      baseProperties = 0x0000000100001140
    }
    

    可以看到,$3class_ro_t * 已经被设置成$22 class_rw_t中的 ro 成员了,但 class_rw_t 的 methods、properties、protocols 成员
    仍然为空。

    等到执行完 methodizeClass 方法:


    (lldb) p *$21
    (class_rw_t) $25 = {
      flags = 2148007936
      version = 0
      ro = 0x0000000100001158
      methods = {
        list_array_tt<method_t, method_list_t> = {
           = {
            list = 0x00000001000010c8
            arrayAndFlag = 4294971592
          }
        }
      }
      properties = {
        list_array_tt<property_t, property_list_t> = {
           = {
            list = 0x0000000100001140
            arrayAndFlag = 4294971712
          }
        }
      }
      protocols = {
        list_array_tt<unsigned long, protocol_list_t> = {
           = {
            list = 0x0000000000000000
            arrayAndFlag = 0
          }
        }
      }
      firstSubclass = nil
      nextSiblingClass = 0x00007fffcd723f50
      demangledName = 0x0000000000000000 <no value available>
    }
    

    class_ro_t 中 baseMethodList 中的方法被添加到 class_rw_t 中的 methods 数组中,class_ro_t 中 baseProperties 被添加到 class_rw_t 中的 properties。核心都是通过在 methodizeClass 方法中执行 attachLists 方法添加类的方法、属性、协议:

    auto rw = cls->data();
    auto ro = rw->ro;
    
    // Install methods and properties that the class implements itself.
    method_list_t *list = ro->baseMethods();
    if (list) {
        prepareMethodLists(cls, &list, 1, YES, isBundleClass(cls));
        rw->methods.attachLists(&list, 1);
    }
    
    property_list_t *proplist = ro->baseProperties;
    if (proplist) {
        rw->properties.attachLists(&proplist, 1);
    }
    
    protocol_list_t *protolist = ro->baseProtocols;
    if (protolist) {
        rw->protocols.attachLists(&protolist, 1);
    }
    

    总结

    • 类的方法、属性以及协议在编译期间存放到了“错误”的位置,直到 realizeClass 执行之后,才放到了 class_rw_t 指向的只读区域 class_ro_t,这样我们即可以在运行时为 class_rw_t 添加方法,也不会影响类的只读结构。

    • 在 class_ro_t 中的属性在运行期间就不能改变了,再添加方法时,会修改 class_rw_t 中的 methods 列表,而不是 class_ro_t 中的 baseMethodList

    6.对象的初始化

    创建一个 NSObject 对象的代码:[[NSObject alloc] init];

    alloc 方法的实现:

    + (id)alloc {
        return _objc_rootAlloc(self);
    }
    

    仅仅调用了一个私有方法_objc_rootAlloc

    id _objc_rootAlloc(Class cls)
    {
        return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
    }
    
    static ALWAYS_INLINE id
    callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
    {
        // 检查 cls 信息是否为 nil,如果为 nil,则无法创建新对象,返回 nil。
        if (slowpath(checkNil && !cls)) return nil;
    
    #if __OBJC2__
        //是否有自定义的allocWithZone方法
        if (fastpath(!cls->ISA()->hasCustomAWZ())) {
            //没有alloc / allocWithZone实现。
            
            //是否可以快速分配内存。这里好像是写死了返回false
            if (fastpath(cls->canAllocFast())) {
                bool dtor = cls->hasCxxDtor();//是否有析构器
                id obj = (id)calloc(1, cls->bits.fastInstanceSize());
                if (slowpath(!obj)) return callBadAllocHandler(cls);
                obj->initInstanceIsa(cls, dtor);
                return obj;
            }
            else {
                id obj = class_createInstance(cls, 0);// 创建对象的关键函数
                if (slowpath(!obj)) return callBadAllocHandler(cls);// 分配失败
                return obj;
            }
        }
    #endif
    
        // callAlloc 传入 allocWithZone = true
        if (allocWithZone) return [cls allocWithZone:nil];// 这里 cls 的 allocWithZone 方法里也是调用了 class_createInstance。
        return [cls alloc];
    }
    

    关于slowpathfastpath两个宏,实际上是__builtin_expect(),一个GCC的一个内建函数,用于分支预测优化。__builtin_expect — 分支预测优化。只需要知道if(fastpath(x))if(x)是相同意思就可以了。

    实际上我在执行[[XXObject alloc] init]这句代码时,if (fastpath(!cls->ISA()->hasCustomAWZ()))并没有进入这个条件分支:

    直接跳到这一步了

    所以也即相当于:

    static ALWAYS_INLINE id
    callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
    {
        return [cls allocWithZone:nil];
    }
    

    但很神奇的是,接下来并没有执行allocWithZone方法(打了断点但无事发生),这是一个让我觉得很迷的地方。最后落入的是这两个方法:

    id class_createInstance(Class cls, size_t extraBytes)
    {
        return _class_createInstanceFromZone(cls, extraBytes, nil);
    }
    
    static __attribute__((always_inline)) 
    id 
    _class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone, 
                                  bool cxxConstruct = true, 
                                  size_t *outAllocatedSize = nil)
    {
        if (!cls) return nil;
    
        assert(cls->isRealized());
    
        // Read class's info bits all at once for performance
        bool hasCxxCtor = cls->hasCxxCtor();// 是否有构造函数
        bool hasCxxDtor = cls->hasCxxDtor();// 是否有析构函数
        bool fast = cls->canAllocNonpointer();// 是否使用原始 isa 格式
    
        size_t size = cls->instanceSize(extraBytes);// 需要分配的空间大小,从 instanceSize 实现可以知道对象至少16字节
            if (outAllocatedSize) *outAllocatedSize = size;
    
        id obj;
        
        if (!zone  &&  fast) {//跑代码时会执行这个分支
            obj = (id)calloc(1, size);//分配空间
            if (!obj) return nil;
            obj->initInstanceIsa(cls, hasCxxDtor);//看到了一个熟悉的方法~
        } 
        else { //这里不执行
            if (zone) {
                obj = (id)malloc_zone_calloc ((malloc_zone_t *)zone, 1, size);
            } else {
                obj = (id)calloc(1, size);
            }
            if (!obj) return nil;
    
            // Use raw pointer isa on the assumption that they might be 
            // doing something weird with the zone or RR.
            obj->initIsa(cls);
        }
    
        if (cxxConstruct && hasCxxCtor) {
            obj = _objc_constructOrFree(obj, cls);
        }
    
        return obj;
    }
    

    在上面函数的实现中,执行了 objc_object::initInstanceIsa(Class cls, bool hasCxxDtor) 方法。在第二部分讲过,这个方法就是用来初始化isa_t结构体的。

    初始化 isa 之后,[NSObject alloc] 的工作算是做完了,下面就是 init 相关逻辑:

    - (id)init {
        return _objc_rootInit(self);
    }
    
    id _objc_rootInit(id obj)
    {
        return obj;
    }
    

    可以看到, init 实际上只是返回了当前对象。

    总结一下对象的初始化过程主要做了两个事:1.分配需要的内存空间 2.初始化isa_t结构体。

    参考文章:

    从 NSObject 的初始化了解 isa

    深入解析 ObjC 中方法的结构

    相关文章

      网友评论

        本文标题:runtime源码中的类和对象

        本文链接:https://www.haomeiwen.com/subject/udkpzxtx.html