美文网首页IOS开发知识点
IOS底层(九): 类相关: 类结构分析

IOS底层(九): 类相关: 类结构分析

作者: ShawnAlex | 来源:发表于2021-04-14 17:50 被阅读0次

    OC底层源码/原理合集

    建议先看下
    IOS底层(三): alloc相关1.初探 alloc, init, new源码分析
    IOS底层(八): alloc相关: isa与类关联源码分析

    首先看个例子:

    exp

    XXX & 0x00007ffffffffff8ULL这块先解释下, 之前我们讲过ISA源码

    #   define ISA_MASK        0x00007ffffffffff8ULL  // x86_64下
    
    inline Class 
    objc_object::ISA() 
    {
        ASSERT(!isTaggedPointer()); 
    #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);
    #endif
    }
    

    这里 isa.bits & ISA_MASK 是返回类信息

    我们分解看下上面例子

    • x/4gx test读一下内存段, 第一个是isa, 我们p/x一下 isa & ISA_MASK, 可看到有0x0000000100008388, 我们接下来po一下, po 0x0000000100008388 没问题返回自定义类SATest
      (① x/4gx 读内存段, 打印内存情况, 第一个是isa指针地址
      ② p/x isa & ISA_MASK是获取类信息, 返回的是类的指针地址, 此时类是SATest
      ③ po 是打印类信息 (都是lldb调试命令))

    • 既然是类信息, 我们再做一次x/4gxp/x isa & ISA_MASKpo可看到po 0x0000000100008360(类isa指针地址)也为SATest

    • 既然是类信息, 我们再再做一次x/4gxp/x isa & ISA_MASKpo可看到0x000000010036a140(类isa指针地址)NSObject

    其实第二次时候就应该有疑问, 为什么还是SATest, 而第三次是NSObject

    第二次中 0x0000000100008360是之前isa中获取的isa的指针地址, 即SATest类的类, 在Apple中我们称SATest类的类元类, 之后的NSObject也称为元类, 不过由于它源自根类, 所以也可以成为根元类

    (下面图片是新走一遍的结果, 之前不小心给关了, 打印地址可能有点差别, 原理不变)


    元类探索

    元类

    1. 首先了解一点, 对象isa指向的, 类也是一个对象( 所以有个流传, 万物皆对象:) ), 这个对象我们一般称为类对象, 其isa位域指向苹果定义的元类

    2. 元类系统给的, 其定义创建都是由编译器完成, 归属来自于元类

    3.元类类对象, 每个都有一个独一无二的元类用来存储类方法相关信息

    4.元类本身是没有名称的, 由于与关联, 所以使用了同类名一样的名称

    由上面例子也可以得到关系
    对象元类NSObject, NSObject元类指向自身

    总结
    元类走位图
    • 对象isa(也称类对象)
    • isa 指向元类
    • 元类isa 指向NSObject (根元类)
    • NSObject的isa指向本身

    扩展个问题, 刚才看到自定义的一个类, 有元类有根元类依次循环, 那么岂不是, 创建一个类, 系统会自动帮我们创建多个NSObject?

    我们可以这样验证下

            Class cls1 = [SATest class];
            Class cls2 = [SATest alloc].class;
            Class cls3 = object_getClass([SATest alloc]);
            NSLog(@"cls1: %p", cls1);
            NSLog(@"cls2: %p", cls2);
            NSLog(@"cls3: %p", cls3);
    
    验证类1

    可看到打印地址只有一个, 所以NSObject只有一份, 或者说NSObject(根元类)在内存中只有一份。(类的信息在内存中永远只存在一份, 类对象只有一份)

    或者通过lldb验证也可以, 看下下面图片即可(根元类只是指向自己)

    验证类2

    isa走势关系图

    isa走势关系图

    (留意下虚线是isa, 实线是superclass)

    • isa走势:

    实例对象(Instance) isa指向 → Class(类) isa 指向 → meta(元类) isa 指向 → root (meta 根元类), 而root (meta 根元类) isa 指向 → NSObject

    父类实例对象也是一样走势

    自定义类打印 NSObject打印

    如果是类NSObject, 会省去一个元类指向

    • superclass走势:

    子类(SubClass) 继承 → 父类(SuperClass) →继承 → 根类(Root Class)
    根类即NSObject无继承

    子元类(meta SubClass) 继承 → 父元类(meta SuperClass) →继承 → 根元类(meta Root Class) 继承 → NSObject

    • 实例对象没有继承关系, 类才会有继承关系, NSObject继承nil
    isa走势关系图标注

    objc_class & objc_object

    既然提到了类和对象, 我们看下objc_classobjc_object源码

    
    1. typedef struct objc_class *Class;
    
    2. struct objc_class {
        Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
    
    #if !__OBJC2__
        Class _Nullable super_class                              OBJC2_UNAVAILABLE;
        const char * _Nonnull name                               OBJC2_UNAVAILABLE;
        long version                                             OBJC2_UNAVAILABLE;
        long info                                                OBJC2_UNAVAILABLE;
        long instance_size                                       OBJC2_UNAVAILABLE;
        struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;
        struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE;
        struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;
        struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
    #endif
    
    } OBJC2_UNAVAILABLE;
    /* Use `Class` instead of `struct objc_class *` */
    
    // objc-runtime-new.h
    3. struct objc_class : objc_object {
      objc_class(const objc_class&) = delete;
      objc_class(objc_class&&) = delete;
      void operator=(const objc_class&) = delete;
      void operator=(objc_class&&) = delete;
        // 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 getSuperclass() const {
    #if __has_feature(ptrauth_calls)
    
    // objc.h
    
    1. struct objc_object {
        Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
    };
    
    // objc-private.h
    
    2. struct objc_object {
    private:
        isa_t isa;
    
    public:
    
        // ISA() assumes this is NOT a tagged pointer object
        Class ISA(bool authenticated = false);
    
        // rawISA() assumes this is NOT a tagged pointer object or a non pointer ISA
        Class rawISA();
    
        // getIsa() allows this to be a tagged pointer object
        Class getIsa();
        
        uintptr_t isaBits() const;
    
        // initIsa() should be used to init the isa of new objects only.
        // If this object already has an isa, use changeIsa() for correctness.
        // initInstanceIsa(): objects with no custom RR/AWZ
        // initClassIsa(): class objects
        // initProtocolIsa(): protocol objects
        // initIsa(): other objects
        void initIsa(Class cls /*nonpointer=false*/);
        void initClassIsa(Class cls /*nonpointer=maybe*/);
        void initProtocolIsa(Class cls /*nonpointer=maybe*/);
        void initInstanceIsa(Class cls, bool hasCxxDtor);
    
        // changeIsa() should be used to change the isa of existing objects.
        // If this is a new object, use initIsa() for performance.
        Class changeIsa(Class newCls);
    ...
    
    

    首先可以看到objc_classobjc_object都有一个 isa 指针, 这个isa来自于objc_object(struct objc_object { Class _Nonnull isa OBJC_ISA_AVAILABILITY; };), 且在新版objc-runtime-new.h里面可看到objc_class是继承于objc_object

    总结:

    • Class底层是objc_class (typedef struct objc_class *Class; 结构体类型 ), 所有类都是以objc_class为模板继承过来的。

    • objc_object 与对象的关系是 继承关系, 所有的对象都是以objc_object为模板继承过来的, (但是真正到底层的是一个objc_object(C/C++)结构体类型)

    • 结构体类型objc_class 继承自objc_object, 其中objc_object(结构体), 因为是继承关系且有一个isa属性, 所以objc_class也拥有了isa属性 ( 其实的对象, , 元类 都有isa属性)

    • NSObject中的isa在底层是由Class 定义的, 其中class的底层编码来自 objc_class类型, 所以NSObject也拥有了isa属性

    • NSObject 是一个类,用它初始化一个实例对象objc,objc 满足 objc_object 的特性(即有isa属性),主要是因为isa 是由 NSObject 从objc_class继承过来的,而objc_class继承自objc_object,objc_object 有isa属性。所以对象都有一个 isaisa表示指向,来自于当前的objc_object(对象, 类, 元类都有isa)

    • objc_object根对象,所有的对象都有这样一个特性 objc_object,即拥有isa属性

    • 在结构层面可以通俗的理解为上层OC 与 底层的对接:
      ① 下层是通过结构体定义的 模板,例如 objc_class、objc_object
      ② 上层是通过底层的模板创建的 一些类型,例如CATest

    objc_class、objc_object、isa、object、NSObject关系图

    类结构分析

    首先还是先看一个例子

    例子1: 普通指针
    普通指针

    定义2个变量a, b = 10, 打印两个变量值以及内存地址

    普通指针地址指向
    • a 和 b 为变量都指向10, 10是系统开辟的固定内存空间, 其他需要10的值的变量都可以指向内存固定生成的10

    • a 和 b 地址不一样, 这是一种拷贝, 属于值拷贝, 也成深拷贝, 可发现a, b地址相差 4 个字节,这取决于a、b的类型

    例子2: 对象指针
    对象指针
    • &p1/&p2 是二级指针, 指向对象的指针地址(0x7ffeefbff478, 0x7ffeefbff480 为对象指针)

    • p1/p2 是一级指针, 指向的 [SATest alloc] 开辟空间的内存地址

    • SATest[SATest alloc]创建内存空间, [SATest alloc]开辟空间的 isa指向SATest

    对象指针地址指向
    例子3: 数组指针
    数组指针
    • &arr == &arr[0] == 首地址, 其实他都是取的首地址, 数组地址其实就是数组第一个元素地址即数组名为首地址

    • &arr[0]与%arr[1]相差4字节, 取决于数据类型

    • 数组类型指针可以通过首地址+偏移量得到其他元素(偏移量为数组下标)

    • 移动的字节数 等于 偏移量 * 数据类型字节数, 这个根据&arr[0], &arr[1]看出, 两者相差4

    有了上面的概念, 便于我们理解之后的探索


    bits探索

    再回头看下objc_class源码(在 objc-runtime-new.h)

    objc-runtime-new.h
    
    struct objc_class : objc_object {
      objc_class(const objc_class&) = delete;
      objc_class(objc_class&&) = delete;
      void operator=(const objc_class&) = delete;
      void operator=(objc_class&&) = delete;
        // 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();
        }
    
    ...
    }
    

    抛去那些删除的, ISA8字节, superclassClass类型占8字节(这里的Class是由objc_object定义的,是一个指针), bits 只有首地址经过上面3个属性的内存大小总和的平移,才能获取到bits(bits有所以我们想要的信息), 那么我们接下来看下cache

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

    先介绍几个定义

    • uintptr_t定义 typedef unsigned long uintptr_t;, long 8字节
    • mask_t定义typedef uint32_t mask_t;, typedef unsigned int uint32_t;, int类型占4字节
    • uint32_t定义 typedef unsigned int uint32_t;, int类型占4字节
    • uint16_t定义typedef unsigned short uint16_t; short类型占2字节

    拆开看一下

    占8字节
    explicit_atomic<uintptr_t> _bucketsAndMaybeMask; // long类型占8字节
    
    占8字节
            struct {
                explicit_atomic<mask_t>    _maybeMask; // int 4字节
    #if __LP64__
                uint16_t                   _flags; // short 占2字节
    #endif
                uint16_t                   _occupied; // short 占2字节
            };
    

    接下来看下 originalPreoptCache

    看下 preopt_cache_t
    explicit_atomic<preopt_cache_t *> _originalPreoptCache;
    
    /* dyld_shared_cache_builder and obj-C agree on these definitions */
    struct preopt_cache_entry_t {
        uint32_t sel_offs;
        uint32_t imp_offs;
    };
    
    /* dyld_shared_cache_builder and obj-C agree on these definitions */
    struct preopt_cache_t {
        int32_t  fallback_class_offset;
        union {
            struct {
                uint16_t shift       :  5;
                uint16_t mask        : 11;
            };
            uint16_t hash_params;
        };
        uint16_t occupied    : 14;
        uint16_t has_inlines :  1;
        uint16_t bit_one     :  1;
        preopt_cache_entry_t entries[];
    
        inline int capacity() const {
            return mask + 1;
        }
    };
    
    

    最后可计算出cache类的内存大小16字节, 那么总共需要偏移 8 + 8 + 16 = 32个字节。即首地址平移32位得到 bits(bit主要储存相关类信息)

    创建2个类, SAPerson继承NSObject, SAStudent继承SAPerson

    查看bit信息
    • p/x SAPerson.class 获取 SAPerson类首地址得到 0x0000000100008260 SAPerson

    • x/4gx 0x0000000100008260 打印首地址isa指针的内存信息,

    • 将得到0x100008260平移32位得0x100008280(此处为16进制, 逢16进1, 32则相当于进2个16即 8260变为8280)

    • bits是这个类型class_data_bits_t bits, 我们这边为了读取数据转一下

    • p $1 -> data通过bits地址得到bits数据, 这里的data看源码可知是class_rw_t类型的。留意下指针函数用->((XXXX *)结构), 结构体用.

     class_rw_t *data() const {
            return bits.data();
    }
    
    • p *$2打印bits中数据信息

    由于bits里面数据类型是class_rw_t, 为了方便探索类里面的属性, 方法等, 我们接下来看一下class_rw_t源码

    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;
    ...
        const class_ro_t *ro() const {
            auto v = get_ro_or_rwe();
            if (slowpath(v.is<class_rw_ext_t *>())) {
                return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->ro;
            }
            return v.get<const class_ro_t *>(&ro_or_rw_ext);
        }
        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};
            }
        }
    };
    

    通过查看class_rw_t源码可看到, 首先class_rw_t也是个结构体类型, 结构体中有提供相应的方法去获取 properties 属性列表method 方法列表protocols协议列表

    那么我们在SAPerson定义几个属性, 方法

    #import <Foundation/Foundation.h>
    
    NS_ASSUME_NONNULL_BEGIN
    
    @interface SAPerson : NSObject {
        NSString *hobby;
    }
    
    @property (nonatomic, copy) NSString *sa_name;
    @property (nonatomic, assign) NSInteger sa_age;
    
    - (void)sayYes;
    + (void)sayNo;
    
    
    @end
    
    属性列表

    bits 数据信息 $3在之前的例子我上面已经讲过了, 我们从读bits数据信息$3之后开始

    • p $3.properties()获得的属性列表的list结构, 其中list中的ptr就是属性数组的参数指针地址。(p $3.properties()命令中的propertoes方法是由class_rw_t提供的, 方法中返回的实际类型为property_array_t)

    • p *$4.list.ptr读一下指针地址, 可获取内存信息, count = 2, 也符合我们建的2个属性

    • p $5.get(0)可获取到sa_name对应属性(property_t) $6 = (name = "sa_name", attributes = "T@\"NSString\",C,N,V_sa_name")

    • p $5.get(1)可获取到sa_age属性(property_t) $7 = (name = "sa_age", attributes = "Tq,N,V_sa_age")

    • p $6.get(2)数组越界, 因为我们只建立了2个属性

    我们接下来看下方法


    方法列表
    • p $3.methods()获得的方法列表的list结构, 接下来跟读属性类型, 依次读取指针地址, 读取列表对应项

    • p $5.get(0).name读取出方法名

    • .cxx_destruct由于底层是C++, 系统默认添加的方法

    • 可以看到, 有自定义的方法sayYes, 系统自动生成的2个属性的set, get方法, 方法列表也有数组越界, 例如count = 6, p $5.get(6).name读的时候也能看见数组越界报错

    当然你要读协议列表的list结构, 那里就p $3.protocols()即可

    其实到这里我们会有疑问, 属性, 方法``协议打印出来了没问题, 但是成员变量hobby类方法sayNo并没有打印出来。我们再返回看一下struct class_rw_t , 里面除了methodspropertiesprotocols,还有一个ro方法 constclass_ro_t *ro(), 看下ro`底层

    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;
        // With ptrauth, this is signed if it points to a small list, but
        // may be unsigned if it points to a big list.
        void *baseMethodList;
        protocol_list_t * baseProtocols;
        const ivar_list_t * ivars;
    
        const uint8_t * weakIvarLayout;
        property_list_t *baseProperties;
    
      ...
    };
    

    可看到const ivar_list_t * ivars;, 有一个ivars属性, 我们仿照下上面也读一下ro

    ro

    可看到成员变量hoppy储存在ivar_list_t里面

    总结

    • 通过XXXX {}定义的成员变量,会存储在类的bits属性中,通过bits --> data() -->ro() --> ivars获取成员变量列表,除了包括成员变量,还包括属性的成员变量

    • 通过@property定义的属性,也会存储在bits属性中,通过bits --> data() --> properties() --> list获取属性列表,其中只包含property属性

    接下来我们看下类方法存在哪里


    类方法
    • x/4g SAPerson.class获取类的内存信息, 以4片段打印

    • p/x 0x00000001000083b8 & 0x00007ffffffffff8UL 获取元类的首地址

    • p (class_data_bits_t *)0x00000001000083d8转成class_data_bits_t型便于我们获取bits信息, 同时别忘了平移32位, 元类的首地址平移32位得到bits信息

    • p $2->data()通过元类地址获取bits信息

    • p *$3打印bits数据

    • p $4.methods()获取元类bits中的方法数组

    • p $5.list.ptr/p *$6(直接p *$5.list.ptr也可以) 获取元类方法数组中的方法列表

    • p $7.get(0).name读出方法名, 可看到类方法是储存在元类中的

    总结

    • 类的实例方法储存在类的bits属性中。 系统自动生成自定义属性@propertyset, get方法, 也是存在这里。

    • 类的类方法储存在元类的bits属性中。

    相关文章

      网友评论

        本文标题:IOS底层(九): 类相关: 类结构分析

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