美文网首页
类的探究分析

类的探究分析

作者: 冼同学 | 来源:发表于2021-06-23 09:50 被阅读0次

    准备工作

    内存偏移

    普通指针代码分析:

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
           int a = 10; 
            int b = 10; 
            int *a_p = &a;
            int *b_p = &b;
            NSLog(@"%d -- %p -- %p",a,&a,&a_p);
            NSLog(@"%d -- %p -- %p",b,&b,&b_p);
    }
    
    //打印结果
    内存偏移[86667:1854956] 10 -- 0x7ffeefbff47c -- 0x7ffeefbff470
    内存偏移[86667:1854956] 10 -- 0x7ffeefbff478 -- 0x7ffeefbff468
    
    • 好明显ab虽然值是一样的,但是它们的地址却不一样。这就是我们常说的深拷贝咯
    • ab的地址刚好相差了4个字节,这取决于a的的类型。
    • 地址大小比较:a > b > a_p > b_p,由于是局部变量,他们都存放在栈区
      注意:栈区的地址是由高到低的。
      对象指针代码分析:
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
           XXPerson *p1 = [LGPerson alloc];
            XXPerson *p2 = [LGPerson alloc];
            NSLog(@"%@ -- %p",p1,&p1);
            NSLog(@"%@ -- %p",p2,&p2);
    
    //打印结果
    内存偏移[86667:1854957] <LGPerson: 0x100796fb0> -- 0x7ffeefbff460
    内存偏移[86667:1854957] <LGPerson: 0x10078b730> -- 0x7ffeefbff458
    }
    
    • p1p2的内存地址是不一样的,alloc开辟的内存在堆区
    • &p1&p2的内存地址也不一样的,好明显是指向了两个不同的地址。
      注意:堆区的地址是由低到高的。
      数组指针代码分析:
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            int c[4] = {1,2,3,4};
            int *d   = c;
            NSLog(@"%p - %p - %p",&c,&c[0],&c[1]);
            NSLog(@"%p - %p - %p",d,d+1,d+2);
    
            for (int i = 0; i<4; i++) {
                int value =  *(d+i);
                NSLog(@"%d",value);
            }
    }
    //打印结果
    内存偏移[86667:1854956] 0x7ffeefbff490 - 0x7ffeefbff490 - 0x7ffeefbff494
    内存偏移[86667:1854956] 0x7ffeefbff490 - 0x7ffeefbff494 - 0x7ffeefbff498
    //循环打印结果
    内存偏移[86667:1854956] 1
    内存偏移[86667:1854956] 2
    内存偏移[86667:1854956] 3
    内存偏移[86667:1854956] 4
    
    • 数组的地址就是数组元素内存中的首地址&c,&c[0]指向同一个地址。
    • 数组中元素地址的间隔是元素数据类型决定的。
    • 数组里面的元素可以通过地址+n取址的方式取出来,如*(d+i);
    • 数组元素不相同用首地址+偏移量方式,根据当前变量的偏移值(需要前面类型大小相加)
      图解如下:
      数组地址偏移

    类isa走位的分析

    通过之前的文章可以知道对象本质是结构体,结构体的第一个成员变量就是isa。那么类的结构是什么有什么?类有isa指向嘛?如果有他们之间的关系是怎么样的?那么针对这些问题我们进行以下的分析。

    isa的走位图(官方的)

    isa走位图

    iOS不同架构下的isa掩码

    • 拿到类的信息我之前总结过有三种,用掩码来获取是比较快的而且直接的。
    • objc4的源码可以拿到:
      x86_64:define ISA_MASK 0x00007ffffffffff8ULL
      arm64:define ISA_MASK 0x0000000ffffffff8ULL
      arm64(simulators):define ISA_MASK 0x007ffffffffffff8ULL

    类对象的内存个数

    int main(int argc, char * argv[]) {
        @autoreleasepool {
            Class class1 = [MyPersion class];
            Class class2 = [MyPersion alloc].class;
            Class class3 = object_getClass([MyPersion alloc]);
            Class class4 = [MyPersion alloc].class;
            NSLog(@"\n-%p-\n-%p-\n-%p-\n-%p-",class1,class2,class3,class4);  
        }
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
    打印结果:
    2021-06-18 15:57:27.779872+0800 test[1355:490263] 
    -0x102e21818-
    -0x102e21818-
    -0x102e21818-
    -0x102e21818-
    

    得出结论:类的内存地址分配都是一样的,每个类只有一个内存块,这根对象的内存分配不一样。

    对象的类isa指向(元类的引出)

    我自己创建了一个persion类继承于NSObject类,代码分析如下:

    详细分析
    分析:
    • 0x0000000100ea1a30XXPersion类的地址,当我们找出XXPersion类的isa指向的类的地址是0x0000000100ea1a08,也同样与指向了XXPersion类。上面已经说到了类只会开辟一个内存空间,那么现在是不是矛盾了?还是其中有一个不是XXpersion类?
    • 其不然,0x0000000100ea1a30输出的是XXPersion类的地址,0x0000000100ea1a08指向是XXPersion元类

    根元类的引出

    参照以上的想法,元类会不会也有isa,isa指向是什么?很简单,那就实践实践一下呗!请以下操作:

    根元类引出
    分析:
    • 0x00000001e7afb260地址是XXPersion的元类isa指向的类地址,发现是NSObject类,然后再找NSObject类的isa指向哪个类,发现地址还是 0x00000001e7afb260,并指向了自己。

    总结:

    经过上面代码的层层分析,我们验证么isa的走位图的isa走位流程:objc(对象) --> class(类) --> metaClass(元类) --> rootMetaClass(根元类) --> rootMetaClass(根元类自己)。
    isa走位图:

    isa走位图

    类的继承链分析

    用oc代码看看继承链的原理,创建XXTeacher类继承于XXPerson类,如下图:

    继承链代码
    分析:NSObject的父类打印的结果是nilXXTeacher的元类的父类是XXPerson的元类(XXPerson的元类的地址和XXPerson类的地址不一样)。XXPerson的元类的父类是NSObject的元类,NSObject的元类的父类是NSObject(和NSObject类的地址一样)
    • XXTeacher 继承 XXPersonXXPerson 继承NSObjectNSObject的父类是nil
    • XXTeacher元类 继承 XXPerson元类,XXPerson 继承 根元类,根元类继承NSObject
      类之间的继承流程图如下:
      类之间的继承图

    官方isa走位图和继承图的还原:

    还原图

    类的结构分析

    查看objc4(818版本)的源码,找到了objc_class结构如下:

    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)
    #   if ISA_SIGNING_AUTH_MODE == ISA_SIGNING_AUTH
            if (superclass == Nil)
                return Nil;
    
     .........    //源码位置为objc-runtime-new.h文件第1688行-2173行
    //省略了好多代码
    

    分析

    • 类的结构中也有隐藏的isa,占用8个字节
    • Class superclass是类的父类,占用8个字节
    • cache_t cache是类的缓存空间,占用16个字节
    • class_data_bits_t保存类的数据,如属性方法等信息。

    探究cache_t内存大小

    我们在开发过程中看类主要看类的属性和方法,上面所述类的属性和方法都存放在class_data_bits_t结构体中,那么我们必须要知道class_data_bits_t结构体的地址,所以分析cache_t结构体内存大小是非常必要的。上代码:

    struct cache_t {
    private:
        explicit_atomic<uintptr_t> _bucketsAndMaybeMask;           //占用8个字节
        union {  //联合体大小只关心内存最大的成员
            struct {
                explicit_atomic<mask_t>    _maybeMask;   //占用4个字节
    #if __LP64__
                uint16_t                   _flags;    //占用2个字节,现在的objc版本只能进入这个判断
    #endif
                uint16_t                   _occupied;     //占用2个字节
            };
            explicit_atomic<preopt_cache_t *> _originalPreoptCache;  //占用8个字节
        };
    
    #if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
        // _bucketsAndMaybeMask is a buckets_t pointer
        // _maybeMask is the buckets mask
    
        static constexpr uintptr_t bucketsMask = ~0ul;
        static_assert(!CONFIG_USE_PREOPT_CACHES, "preoptimized caches not supported");
       .........忽略与结构体内存无关的代码
      //源码位置为objc-runtime-new.h文件第338行-550行
    

    分析:

    • 结构体中static修饰的静态变量、调用的方法已经其他相关的运算都不占用结构体的内存,所以我省略了好多的代码。
    • typedef unsigned long uintptr_t是无符号长整形,占用8个字节。
    • preopt_cache_t *是结构体指针,占用8个字节。
    • uint16_t是无符号16位整形,占用2个字节。
    • mask_tuint32_t类型的,占用4个字节。
      cache_t的内存大小为:uintptr_t内存大小8个字节+union内存大小8个字节 = 16字节

    分析class_data_bits_t bits结构体

    综上所述,class_data_bits_t bits结构体记录的是类的属性、成员变量以及方法。所以必须要了解结构体里面有什么哦!上代码:

    struct class_data_bits_t {
        friend objc_class;
    
        // Values are the FAST_ flags above.
        uintptr_t bits;
    private:
        bool getBit(uintptr_t bit) const
        {
            return bits & bit;
        }
    
        // Atomically set the bits in `set` and clear the bits in `clear`.
        // set and clear must not overlap.
        void setAndClearBits(uintptr_t set, uintptr_t clear)
        {
            ASSERT((set & clear) == 0);
            uintptr_t newBits, oldBits = LoadExclusive(&bits);
          //此处省略部分代码
    public:
    
        class_rw_t* data() const {
            return (class_rw_t *)(bits & FAST_DATA_MASK);
        }
        void setData(class_rw_t *newData)
        {
       //此处省略部分代码
    

    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 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中,在XXPerson类中添加属性和方法 以及成员变量,请继续往下看验证流程。

    获取类的属性、方法操作流程分析

    获取类的属性

    1.创建好XXPersion类,如图所示:

    XXPerson类
    2.断点,进行lldb调试,如图所示:
    获取属性1
    获取属性2
    步骤:
    • x/4gx XXPerson.class格式化输出XXPerson.class,拿到类的首地址:0x0000000100004530
    • p/x 0x0000000100004530 + 0x20首地址偏移32个字节(ISA8字节、superclass8字节、cache16字节),拿到类对象属性地址
    • p (class_data_bits_t *)0x0000000100004550将地址转化成class_data_bits_t类型,为了使用class_data_bits_t的函数
    • p $21->data() 使用class_data_bits_tdata()函数,拿到class_rw_t类型的地址
    • p $22->properties()通过properties()函数获取XXPerson的成员变量
    • p $23.listp $24.ptr解析出property_list_t的地址
    • p *$25 通过取地址的方式获取成员变量property_list_t
    • p $26.get(0)p $26.get(1)通过c++函数单个获取类的成员变量namenickNameage
      注意:
    • 最后获取属性的get()方法是迭代器,系统自带的方法。
    • 发现属性列表中只有3个,那么定义的hobby成员变量去哪里了?
    • 方法列表的方法也是同样获取的,请接着往下操作。

    获取类的方法

    获取类方法1
    获取属性方法2
    步骤:NSObject.class -> class_data_bits_t -> class_rw_t -> method_array_t -> method_list_t -> method_t-> big

    成员变量与类方法的获取

    成员变量获取

    观察发现class_rw_t还有一个获取class_ro_t *的方法,会不会在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
    
        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;
    //省略了部分代码(objc-runtime-new.h文件第1037行-1171行)
    }
    

    分析:

    • class_ro_t是结构体类型,有一个ivar_list_t * ivars变量
    • ivar_list_t * ivars变量存储着类的成员变量。
    • 系统会给属性自动生成一个带_属性名变量,存储在class_ro_t中的变量列表里
      lldb调试
      类成员变量获取
      步骤:
    • x/4gx LGPerson.class 格式化输出LGPerson.class,获取到首地址
    • p/x 0x100008408 + 0x20 首地址偏移32字节(ISA8字节、superclass8字节、cache_t16字节),拿到包含类属性方法成员变量的对象class_data_bits_t的地址
    • p (class_data_bits_t *)$1 将地址转换为class_data_bits_t,为了使用class_data_bits_t的函数
    • p $2->data()使用class_data_bits_t的data()函数,拿到class_rw_t类型的地址
    • p $3->ro 使用class_rw_tro函数,拿到class_ro_t类型的地址
    • p *$4lass_ro_t类型地址的值,拿到了class_ro_t对象
    • p $5.ivars 使用ivars函数获取class_ro_t对象的ivars,得到了指向ivar_list_t地址的指针
    • p *$6 通过取地址的方式获实例变量数组ivar_list_t,entsize_list_tt<ivar_t, ivar_list_t, 0, PointerModifierNop> = (entsizeAndFlags = 32, count = 4),可以看到有4ivars
    • 通过c++函数get()单个获取类的实例方法:
      p $7.get(0):
      (ivar_t) $8 = {
      offset = 0x0000000100008370
      name = 0x0000000100003eec "hobby"
      type = 0x0000000100003f60 "@"NSString""
      alignment_raw = 3
      size = 8
      }
      ivars获取流程源码:NSObject.class -> class_data_bits_t -> class_rw_t -> class_ro_t -> ivar_list_t -> ivar_t

    类方法获取

    对象的方法是存储在类中,那么类方法可能存储在元类中。那么实践一下咯。
    lldb调试

    类方法获取
    步骤:
    • x/4gx XXPerson.class格式化打印类XXPerson,得到类的首地址
    • p/x 0x00000001000083e0 & 0x00007ffffffffff8isa指针和ISA_MASK做与操作,拿到XXPerson的metaClass(元类)
    • x/4gx 0x00000001000083e0,格式化打印XXPersonmetaClass(元类),拿到元类的首地址
    • p/x 0x1000083e0 + 0x20,将元类的首地址偏移32个字节(ISA8字节、superclass8字节、cache_t16字节),那多元类的class_data_bits_t对象地址
    • p (class_data_bits_t *)0x0000000100008400将地址转化为class_data_bits_t对象,方便调用函数
    • p $3->data()调用class_data_bits_tdata函数,拿到class_rw_t对象
    • p $4->methods()获取class_rw_tmethods方法列表
    • p $5.listp $5.ptr拿到指向method_list_t地址的指针
    • p *$7取地址,拿到了method_list_t对象,count1entsize_list_tt<method_t, method_list_t, 4294901763, method_t::pointer_modifier>= (entsizeAndFlags = 27, count = 1), 有一个类方法
      通过c++函数get()big()单个获取类的类方法:
      p $7.get(0).big():
      (method_t::big) $9 = {
      name = "sayNB"
      types = 0x0000000100003f74 "v16@0:8"
      imp = 0x0000000100003ce0 (KCObjcBuild+[XXPerson sayNB]) } **类方法获取流程:NSObject.class->metaClass->class_data_bits_t->class_rw_t->method_array_t->method_list_t->method_t->big`*

    总结:

    附上类探究的整个流程图:

    总流程
    学习过程的确艰辛!但是学到知识让我非常兴奋!通过对类结构的深入学习,使得我对底层的理解更加深刻了,自身的专业知识又有了进一步的提升。让我们继续加油吧💪🏻!

    相关文章

      网友评论

          本文标题:类的探究分析

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