美文网首页
OC底层原理07-类的结构分析

OC底层原理07-类的结构分析

作者: Gomu_iOS | 来源:发表于2020-09-14 16:38 被阅读0次

    一、类的本质

    《OC底层原理04-对象的本质》那篇文章中,我们讲到了如何将.m文件编译成.cpp文件查看底层结构,这里就不作过多赘述

    1.1 在cpp文件找查找Class的定义

    typedef struct objc_class *Class;
    
    • main.cpp中,找到了底层关于Class的定义,类是一个objc_class类型的结构体

    1.2 接着进入objc4源码查找objc_class,源码相关可以查阅《OC底层原理02-iOS_objc4-781.2 最新源码编译调试》

    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();
        }
        //: 由于代码量过大,这里只展示关键代码,源码请自行查阅
    }
    
    • objc_class是一个结构体,所以进一步再次说明类也是一个结构体
    • objc_class继承于objc_object类也是对象,万物皆对象
    • objc_class继承了objc_objectisa属性,所以类的地址第一位仍然存的是isa
    • superclassobjc_object类型的结构体指针
    • cache:缓存
    • bits:存储属性,实例方法,协议

    总结:类的本质是一个objc_class类型的结构体

    二、 探索objc_class中的bits

    类不能像对象那样直接断点调试打印,只有先从源码入手,分析源码,并且引入二种分析方法:

    • 地址平移:通过首地址+前面属性所占内存平移到我们需要的存储属性(bits)的内存地址处 (文章结尾拓展有讲到)
    • *():输出指针类型的对象

    2.1 通过阅读源码定位bits内存地址

    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();
        }
        //: 由于代码量过大,这里只展示关键代码,源码请自行查阅
    }
    
    2.1.1 第一位属性:isa 的内存大小
    • 由于objc_class继承于objc_object,继承属性isa,占8字节
    2.1.2 第二位属性:superclass 的内存大小
    typedef struct objc_class *Class;
    
    • Class是一个objc_class结构体superclassClass指针结构体指针大小为8,指向NSObject类,占8字节
    2.1.3 第三位属性:cache 的内存大小
    struct cache_t {
    #if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
        explicit_atomic<struct bucket_t *> _buckets;
        explicit_atomic<mask_t> _mask;
    #elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
        explicit_atomic<uintptr_t> _maskAndBuckets;
        mask_t _mask_unused;
    #if __LP64__
        uint16_t _flags;
    #endif
        uint16_t _occupied;
    //: 由于代码量过大,这里只展示关键代码,源码请自行查阅
    
    • cache_t结构体中有大量static类型的方法和变量,还有大量其他方法
    • static申明的变量和方法不计入结构体内存,不会存在结构体中。
    • 方法也不会存在结构体中,也不计入结构体内存
    • cache_t大小由_buckets_mask_flags_occupied来决定
    • _buckets是一个结构体指针<struct bucket_t *>,占8字节
    • _mask源码声明:typedef uint32_t mask_tuint32_t源码声明:typedef unsigned int uint32_t,占4字节
    • _flags_occupied都是uint16_tuint16_t源码声明:typedef unsigned short uint16_t,各占2字节
    • 所以计算出cache占16字节,8+4+2+2
    2.1.4 结论
    • 只要把GomuPerson首地址平移32位(isa[8位]+superclasss[8位]+ cache[16位]),就能拿到研究对象bits的指针地址

    2.2 通过lldb调试拿到bits中的方法、属性、协议

    2.2.2 准备工作
    GomuPerson.h
    @protocol TestProtocol <NSObject>
    
    @end
    
    @interface GomuPerson : NSObject
    {
        NSString *hobby;
    }
    @property (nonatomic, strong) NSString *name;
    @property (nonatomic, strong) NSString *sex;
    
    - (void)sayNO;
    + (void)sayLove;
    
    GomuPerson.m
    - (void)sayNO{}
    + (void)sayLove{}
    
    • objc4工程中创建类GomuPerson
    • GomuPerson.h中创建属性namesex,协议TestProtocol,实例属性hobby,实例方法sayNO,类方法sayLove
    • GomuPerson.m中实现实例方法sayNO、类方法sayLove
    2.2.2 获取类GomuPerson的首地址
    //: 方法一
    (lldb) p/x GomuPerson.class
    (Class) $0 = 0x0000000100002320 GomuPerson
    //: 得到地址 `0x0000000100002488 `
    //: 方法二
    (lldb) x/4gx person
    0x1010297b0: 0x001d800100002325 0x0000000000000000
    0x1010297c0: 0x0000000000000000 0x0000000000000000
    (lldb) p/x 0x001d800100002325 & 0x00007ffffffffff8ULL
    (unsigned long long) $2 = 0x0000000100002320
    //: 得到地址 `0x0000000100002320 `
    
    • 得到GomuPerson的首地址0x0000000100002320
    2.2.2 把GomuPerson的首地址平移32位
    (lldb) p/x 0x0000000100002320 + 32
    (long) $3 = 0x0000000100002340
    
    • 得到bits的地址0x0000000100002340
    2.2.3 根据源码拿到bits.data()
    //: 源码
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
    class_rw_t *data() const {
        return bits.data();
    }
    
    //: lldb拿data()
    //: 强转(class_data_bits_t *)类型
    (lldb) p (class_data_bits_t *)$3
    (class_data_bits_t *) $4 = 0x0000000100002340
    //: 取出data(),指针取值用->
    (lldb) p $4->data()
    (class_rw_t *) $5 = 0x00000001006b0460
    //: 去指针化
    (lldb) p *($5)
    (class_rw_t) $6 = {
      flags = 2148007936
      witness = 0
      ro_or_rw_ext = {
        std::__1::atomic<unsigned long> = 4294975768
      }
      firstSubclass = nil  //: 子类
      nextSiblingClass = NSUUID
    }
    
    2.2.4 在源码中查看class_rw_t
    struct class_rw_t {
        //: 由于代码量过大,这里只展示关键代码,源码请自行查阅
        //: 获取ro()
        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;
            }
            return v.get<const class_ro_t *>();
        }
        //: 获取方法
        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 *>()->methods;
            } else {
                return method_array_t{v.get<const class_ro_t *>()->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 *>()->properties;
            } else {
                return property_array_t{v.get<const class_ro_t *>()->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 *>()->protocols;
            } else {
                return protocol_array_t{v.get<const class_ro_t *>()->baseProtocols};
            }
        }
    }
    
    • class_rw_t源码中,找到methods()properties()protocols()
    2.2.5 获取方法列表methods()中存的方法
    //: 获取methods()
    (lldb) p $6.methods()
    (const method_array_t) $7 = {
      list_array_tt<method_t, method_list_t> = {
         = {
          list = 0x0000000100002160
          arrayAndFlag = 4294975840
        }
      }
    }
    
    //: 获取methods()中的list
    (lldb) p $7.list
    (method_list_t *const) $8 = 0x0000000100002160
    //: 去指针化
    (lldb) p *($8)
    (method_list_t) $9 = {
      entsize_list_tt<method_t, method_list_t, 3> = {
        entsizeAndFlags = 26
        count = 6
        first = {
          name = "sayNO"
          types = 0x0000000100000f4c "v16@0:8"
          imp = 0x0000000100000da0 (GomuTest`-[GomuPerson sayNO])
        }
      }
    }
    
    // count = 6 ,我们打印一下这6个元素
    (lldb) p $9.get(0)
    (method_t) $10 = {
      name = "sayNO"
      types = 0x0000000100000f4c "v16@0:8"
      //: 拿到 sayNO
      imp = 0x0000000100000da0 (GomuTest`-[GomuPerson sayNO])
    }
    (lldb) p $9.get(1)
    (method_t) $11 = {
      name = "sex"
      types = 0x0000000100000f60 "@16@0:8"
      //: 系统自动生成get方法
      imp = 0x0000000100000e00 (GomuTest`-[GomuPerson sex])
    }
    (lldb) p $9.get(2)
    (method_t) $12 = {
      name = "setSex:"
      types = 0x0000000100000f68 "v24@0:8@16"
      //: 系统自动生成set方法
      imp = 0x0000000100000e20 (GomuTest`-[GomuPerson setSex:])
    }
    (lldb) p $9.get(3)
    (method_t) $13 = {
      name = ".cxx_destruct"
      types = 0x0000000100000f4c "v16@0:8"
      //: 系统自动生成c++函数
      imp = 0x0000000100000e50 (GomuTest`-[GomuPerson .cxx_destruct])
    }
    (lldb) p $9.get(4)
    (method_t) $14 = {
      name = "name"
      types = 0x0000000100000f60 "@16@0:8"
      //: 系统自动生成get方法
      imp = 0x0000000100000db0 (GomuTest`-[GomuPerson name])
    }
    (lldb) p $9.get(5)
    (method_t) $15 = {
      name = "setName:"
      types = 0x0000000100000f68 "v24@0:8@16"
      //: 系统自动生成set方法
      imp = 0x0000000100000dd0 (GomuTest`-[GomuPerson setName:])
    }
    
    • 实例方法(sayNo)确定存在bits里面
    • 除了我们自定义的实例方法,系统在编译中自动帮我们实现了属性getset方法([GomuPerson sex]、[GomuPerson setSex:]、[GomuPerson name]、[GomuPerson name])
    • 除此之外,系统还实现了.cxx_destructc++的方法,因为OC是底层是封装的c++,所以会默认添加
    • 类方法sayLove没有存在bits
    • 系统在编译中没有给实例属性hobby生成getset方法
    2.2.6 获取属性列表properties()中存的属性
    //: 获取properties()
    (lldb) p $6.properties()
    (const property_array_t) $16 = {
      list_array_tt<property_t, property_list_t> = {
         = {
          list = 0x0000000100002260
          arrayAndFlag = 4294976096
        }
      }
    }
    
    //: 获取properties()中的list
    (lldb) p $16.list
    (property_list_t *const) $17 = 0x0000000100002260
    //: 去指针化
    (lldb) p *($17)
    (property_list_t) $18 = {
      entsize_list_tt<property_t, property_list_t, 0> = {
        entsizeAndFlags = 16
        count = 2
        first = (name = "name", attributes = "T@\"NSString\",&,N,V_name")
      }
    }
    
    // count = 2 ,我们打印一下这2个元素
    (lldb) p $18.get(0)
    (property_t) $19 = (name = "name", attributes = "T@\"NSString\",&,N,V_name")
    (lldb) p $18.get(1)
    (property_t) $20 = (name = "sex", attributes = "T@\"NSString\",&,N,V_sex")
    (lldb) 
    
    • properties()中只存了属性namesex
    • 实例属性hobby没有存在properties()
    2.2.7 获取协议列表protocols()中存的协议
    (lldb) p $6.protocols()
    (const protocol_array_t) $7 = {
      list_array_tt<unsigned long, protocol_list_t> = {
         = {
          list = 0x0000000000000000
          arrayAndFlag = 0
        }
      }
    }
    (lldb) p $7.list
    (protocol_list_t *const) $6 = 0x0000000000000000
    //: $6 是个空,全为0
    
    • 协议也没有存在protocols

    2.3 探索实例属性、类方法、协议存在哪

    2.3.1 实例属性存储探索
    //: 拿到`GomuPerson`首地址
    (lldb) p/x GomuPerson.class
    (Class) $0 = 0x0000000100002320 GomuPerson
    //: 地址平移32位
    (lldb) p/x 0x0000000100002320 + 32
    (long) $1 = 0x0000000100002340
    //: 强转成(class_data_bits_t *)类型
    (lldb) p (class_data_bits_t *)$1
    (class_data_bits_t *) $2 = 0x0000000100002340
    //: 取data()
    (lldb) p $2->data()
    (class_rw_t *) $3 = 0x00000001007646a0
    // 去指针化
    (lldb) p *($3)
    (class_rw_t) $4 = {
      flags = 2148007936
      witness = 0
      ro_or_rw_ext = {
        std::__1::atomic<unsigned long> = 4294975768
      }
      firstSubclass = nil
      nextSiblingClass = NSUUID
    }
    // 拿到ro(),同上面的`methods()`
    (lldb) p $4.ro()
    (const class_ro_t *) $5 = 0x0000000100002118
    // 去指针化
    (lldb) p *$5
    (const class_ro_t) $6 = {
      flags = 388
      instanceStart = 8
      instanceSize = 32
      reserved = 0
      ivarLayout = 0x0000000100000f03 "\x03"
      name = 0x0000000100000ef8 "GomuPerson"
      baseMethodList = 0x0000000100002160
      baseProtocols = 0x0000000000000000
      ivars = 0x00000001000021f8
      weakIvarLayout = 0x0000000000000000
      baseProperties = 0x0000000100002260
      _swiftMetadataInitializer_NEVER_USE = {}
    }
    //: 拿到ivars
    (lldb) p $6.ivars
    (const ivar_list_t *const) $7 = 0x00000001000021f8
    //: 去指针化
    (lldb) p *$7
    (const ivar_list_t) $8 = {
      entsize_list_tt<ivar_t, ivar_list_t, 0> = {
        entsizeAndFlags = 32
        count = 3
        first = {
          offset = 0x0000000100002290
          name = 0x0000000100000f0d "hobby"
          type = 0x0000000100000f54 "@\"NSString\""
          alignment_raw = 3
          size = 8
        }
      }
    }
    // 由于 ivar_list_t 是一个数组,所以直接get
    (lldb) p $8.get(0)
    (ivar_t) $9 = {
      offset = 0x0000000100002290
      //: 找到实例属性`hobby `
      name = 0x0000000100000f0d "hobby"
      type = 0x0000000100000f54 "@\"NSString\""
      alignment_raw = 3
      size = 8
    }
    (lldb) p $8.get(1)
    (ivar_t) $10 = {
      offset = 0x0000000100002298
      //: 系统自动给属性生成`_name `实例属性
      name = 0x0000000100000f13 "_name"
      type = 0x0000000100000f54 "@\"NSString\""
      alignment_raw = 3
      size = 8
    }
    (lldb) p $8.get(2)
    (ivar_t) $11 = {
      offset = 0x00000001000022a0
      //: 系统自动给属性生成`_sex`实例属性
      name = 0x0000000100000f19 "_sex"
      type = 0x0000000100000f54 "@\"NSString\""
      alignment_raw = 3
      size = 8
    }
    
    • 实例属性hobby存在ro
    • 系统在编译中还是给属性自动生成实例属性_name_sex
    2.3.2 类方法存储探索
    2.3.2.1 查看源码
    struct category_t {
        const char *name;
        classref_t cls;
        struct method_list_t *instanceMethods;
        struct method_list_t *classMethods;
        struct protocol_list_t *protocols;
        struct property_list_t *instanceProperties;
        // Fields below this point are not always present on disk.
        struct property_list_t *_classProperties;
       
        method_list_t *methodsForMeta(bool isMeta) {
            if (isMeta) return classMethods;
            else return instanceMethods;
        }
    
        property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
        
        protocol_list_t *protocolsForMeta(bool isMeta) {
            if (isMeta) return nullptr;
            else return protocols;
        }
    };
    
    • instanceMethodsclassMethods都是method_list_t类型
    • 发现methodsForMeta这个方法,如果是元类,则返回类方法列表,如果不是元类,则返回实例方法列表
    • 猜想,类方法存在元类
    2.3.2.1 通过lldb调试元类,验证猜想
    //: 拿到`GomuPerson`内存地址
    (lldb) x/4gx GomuPerson.class
    0x100002320: 0x00000001000022f8 0x0000000100334140
    0x100002330: 0x000000010032e410 0x0000802c00000000
    //: isa & mask
    (lldb) p/x 0x00000001000022f8 & 0x00007ffffffffff8ULL
    (unsigned long long) $1 = 0x00000001000022f8
    //: 打印$1,GomuPerson的元类还是GomuPerson类型
    (lldb) po $1
    GomuPerson
    //: 平移32位
    (lldb) p/x $1 + 32
    (unsigned long long) $2 = 0x0000000100002318
    //: 强转成(class_data_bits_t *)类型
    (lldb) p (class_data_bits_t *)$2
    (class_data_bits_t *) $3 = 0x0000000100002318
    //: 取data()
    (lldb) p $3->data()
    (class_rw_t *) $4 = 0x000000010104bee0
    //: 去指针化
    (lldb) p *$4
    (class_rw_t) $5 = {
      flags = 2684878849
      witness = 1
      ro_or_rw_ext = {
        std::__1::atomic<unsigned long> = 4294975664
      }
      firstSubclass = nil
      nextSiblingClass = 0x00007fff8c84bc60
    }
    //: 拿到methods()
    (lldb) p $5.methods()
    (const method_array_t) $6 = {
      list_array_tt<method_t, method_list_t> = {
         = {
          list = 0x00000001000020f8
          arrayAndFlag = 4294975736
        }
      }
    }
    //: 取出methods()里面的list
    (lldb) p $6.list
    (method_list_t *const) $7 = 0x00000001000020f8
    (lldb) p *$7
    (method_list_t) $8 = {
      entsize_list_tt<method_t, method_list_t, 3> = {
        entsizeAndFlags = 26
        count = 1
        first = {
          name = "sayLove"
          types = 0x0000000100000f4c "v16@0:8"
          //: 找到类方法sayLove
          imp = 0x0000000100000d90 (GomuTest`+[GomuPerson sayLove])
        }
      }
    }
    
    2.3.3 协议的存储探索

    暂时没探索到,后面找机会补起
    三、拓展知识

    3.1 内存平移

    3.1.1 普通指针
    int num1 = 20;
    int num2 = 20;
    int num3 = 20;
    NSLog(@"%d---%p",num1,&num1);
    NSLog(@"%d---%p",num2,&num2);
    NSLog(@"%d---%p",num3,&num3);
    
    //: 打印
    20---0x7ffeefbff57c
    20---0x7ffeefbff580
    20---0x7ffeefbff584
    
    • num1num2num3都指向10,这个10系统编译中就已经存到了某段内存中,num1num2num3的地址却不一样,这就叫值拷贝,也叫浅拷贝
    • num1num2num3地址之间相差4字节,内存连续
    • 如下图


      image.png
    3.1.2 对象指针
    GomuPerson *person1 = [GomuPerson alloc];
    GomuPerson *person2 = [GomuPerson alloc];
    GomuPerson *person3 = [GomuPerson alloc];
            
    NSLog(@"%p---%p",person1,&person1);
    NSLog(@"%p---%p",person2,&person2);
    NSLog(@"%p---%p",person3,&person3);
    
    //: 打印
    0x102230b50---0x7ffeefbff570
    0x102234b00---0x7ffeefbff578
    0x102234b20---0x7ffeefbff580
    
    • person1person2person3指针,指向各自[GomuPerson alloc]开辟的内存,&person1&person2&person3是指向person1person2person3对象指针的地址,这个指针就是二级指针
    • 如下图


      image.png
    3.1.3 数组指针
    int arr[4] = {1,2,3,4};
    int *d = arr;
    NSLog(@"%p -- %p - %p", &arr, &arr[0], &arr[1]);
    NSLog(@"%p -- %p - %p", d, d+1, d+2);
    
    //: 打印
    0x7ffeefbff570 -- 0x7ffeefbff570 - 0x7ffeefbff574
    0x7ffeefbff570 -- 0x7ffeefbff574 - 0x7ffeefbff578
    
    • &arr&arr[0]d都是取的第一个地址,说明数组的首地址就是第一个元素的地址
    • 通过地址平移d+1,我们取到了arr[1],数组指针地址平移,按照数组下标平移,内存中就是按照元素类型所占内存进行平移0x7ffeefbff574 -> 0x7ffeefbff578,因为是int类型所以平移4
    • 依次类推,结构体中也可以用地址平移的方式去拿不能直接拿到的属性

    相关文章

      网友评论

          本文标题:OC底层原理07-类的结构分析

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