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

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

作者: laona | 来源:发表于2020-09-30 23:33 被阅读0次

    通过上一篇文章对isa的分析,我们知道了所有的对象都包含isa,并且isa存储了类的相关信息,所以这篇文章我们主要通过isa来引出类的底层结构以及一些信息

    代码分析

    创建对象

    LYPerson *person = [[LYPerson alloc] init];
    

    lldb分析

    (lldb) x/4gx person
    0x100513970: 0x001d80010000820d 0x0000000000000000
    0x100513980: 0x0000000000000000 0x0000000000000000
    (lldb) 
    

    通过之前的分析,我们知道所有对象的第一个属性都是isa,故上面的0x001d80010000820disa的地址,我们继续通过打印isa的地址来看下

    (lldb) po 0x001d80010000820d & 0x00007ffffffffff8ULL
    LYPerson
    (lldb) p/x 0x001d80010000820d & 0x00007ffffffffff8ULL
    (unsigned long long) $1 = 0x0000000100008208
    (lldb) po 0x0000000100008208
    LYPerson
    

    这里需要& 0x00007ffffffffff8ULL主要是因为isa只有中间的44(或33)位才是类信息,所以需要把其它位置为0,通过上面的地址打印我们得出结论:对象的isa指向类信息,那么我们继续来打印下类的isa看看是什么

    (lldb) x/4gx 0x0000000100008208
    0x100008208: 0x00000001000081e0 0x00007fff98b0e118
    0x100008218: 0x0000000100513990 0x0001802400000003
    (lldb) p/x 0x00000001000081e0 & 0x00007ffffffffff8ULL
    (unsigned long long) $5 = 0x00000001000081e0
    (lldb) po 0x00000001000081e0
    LYPerson
    

    这里我们发现类的isa只想的信息,打印出来也是LYPerson,通过内存地址我们可以肯定此处的LYPerson跟上一步的LYPerson不是同一个,但是类在内存中又只会存在一份,因此我们引出了元类的概念,至此,我们得出结论:类的isa指向元类信息.
    接下来我们给出类在内存中只存在一份的证明以及元类的说明

    //MARK:--- 分析类对象内存 存在个数
    void testClassNum(){
        Class class1 = [LYPerson class];
        Class class2 = [LYPerson alloc].class;
        Class class3 = object_getClass([LYPerson alloc]);
        NSLog(@"\n%p-\n%p-\n%p-\n%p", class1, class2, class3);
    }
    
    image.png
    • 元类是系统给的,其定义和创建都是由编译器完成,在这个过程中,类的归属来自于元类(类似于对象的归属是类)

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

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

    接下来我们继续分析下元类的isa

    (lldb) x/4gx 0x00000001000081e0
    0x1000081e0: 0x00007fff98b0e0f0 0x00007fff98b0e0f0
    0x1000081f0: 0x00000001006471f0 0x0003e03500000007
    (lldb) p/x 0x00007fff98b0e0f0 & 0x00007ffffffffff8ULL
    (unsigned long long) $7 = 0x00007fff98b0e0f0
    (lldb) po 0x00007fff98b0e0f0
    NSObject
    

    通过打印isa我们发现LYPerson的元类的isa指向了NSObject,那么此处的NSObject是不是就是我们的根类呢?我们来证明下

    (lldb) p/x NSObject.class
    (Class) $9 = 0x00007fff98b0e118 NSObject
    

    很明显,两个NSObject并不是同一个类,因此我们认定上面的NSObject根元类,这里我们得出结论:元类的isa指向根元类.接下来我们继续打印根元类的isa

    (lldb) x/4gx 0x00007fff98b0e0f0
    0x7fff98b0e0f0: 0x00007fff98b0e0f0 0x00007fff98b0e118
    0x7fff98b0e100: 0x00000001020081b0 0x0005e03100000007
    (lldb) p/x 0x00007fff98b0e0f0 & 0x00007ffffffffff8ULL
    (unsigned long long) $11 = 0x00007fff98b0e0f0
    (lldb) po 0x00007fff98b0e0f0
    NSObject
    

    这里我们发现根元类的isa与它本身的地址相同,由此我们得出结论:根元类的isa指向它本身

    isa & 继承链

    通过上面的分析再加上我们的继承关系,于是就有了下面这个著名的图


    image.png

    上面这幅图,对于isa的走位我们已经通过代码证明过了,没有任何问题.那么关于集成链有两点需要注意下

    • 根元类的父类为根类
    • 根类的父类为nil

    以上两点也说明了NSObject做为基类,是万物起源,我们可以看下底层编译代码

    struct NSObject_IMPL {
        Class isa;
    };
    
    // 结构体
    typedef struct objc_class *Class;
    

    Class分析

    源码分析

    通过上面的分析我们来到了我们非常熟悉的Class,接下来我们就来具体分析下Class的定义objc_class的源码实现

    struct objc_object {
        Class _Nonnull isa __attribute__((deprecated));
    };
    
    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
        //....方法部分省略,未贴出
    }
    

    通过源码我们可以发现objc_class中包含的元素主要有ISA,superclass,cache,bits,对与ISA我们已经了解了,superclass很明显是父类,也没什么好分析的,那么剩下的cachebits,cache主要是缓存数据,我们可以放到后面,这里主要还是分析下bits里面有哪些内容,那么如何获取到bits呢,这里我们补充下个知识点--内存偏移

    内存偏移

    偏移地址是指段内相对于段起始地址的偏移值,
    例如一个存储器的大小是1KB,可以把它分为4段,第一段的地址范围就是0—255,第二段的地址范围就是256-511,依次类推。

    我们拿数组来举例

    //数组指针
    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);
    0x7ffeefbff560 -- 0x7ffeefbff560 - 0x7ffeefbff564
    0x7ffeefbff560 -- 0x7ffeefbff564 - 0x7ffeefbff568
    

    通过上面我们可以得出:数组的首地址即位第一个元素的地址,第二个元素的地址即为第一个元素的地址加上元素类型所占的字节大小,int占用4个字节,即+4,若为double+8

    综上,内存偏移就可以理解为首地址加偏移量

    class_data_bits_t bits分析

    在了解了内存偏移后,我们来尝试下获取bits,首先ISAsuperclass都是Class类型,Class是结构体指针类型,占用8个字节,所以加一起是16个字节,接下来我们分析下cache,源码如下

    struct cache_t {
    #if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
        explicit_atomic<struct bucket_t *> _buckets; // 是一个结构体指针类型,占8字节
        explicit_atomic<mask_t> _mask; //是mask_t 类型,而 mask_t 是 unsigned int 的别名,占4字节
    #elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
        explicit_atomic<uintptr_t> _maskAndBuckets; //是指针,占8字节
        mask_t _mask_unused; //是mask_t 类型,而 mask_t 是 uint32_t 类型定义的别名,占4字节
        
    #if __LP64__
        uint16_t _flags;  //是uint16_t类型,uint16_t是 unsigned short 的别名,占 2个字节
    #endif
        uint16_t _occupied; //是uint16_t类型,uint16_t是 unsigned short 的别名,占 2个字节
    

    通过源码,我们得出cache占用16个字节,因此bits距首地址偏移32个字节,接下来我们通过lldb进行调试

    // 打印LYPerson的内存地址
    (lldb) x/4gx LYPerson.class
    0x100008200: 0x00000001000081d8 0x000000010034c140
    0x100008210: 0x000000010184dd90 0x0001802400000003
    // 首地址为0x100008200,偏移32位获取class_data_bits_t
    (lldb) p (class_data_bits_t *)0x100008220
    (class_data_bits_t *) $30 = 0x0000000100008220
    (lldb) 
    

    通过objc_class源码我们可以发现bits中存储的信息为class_rw_t结构体类型,可以通过data()方法获取.源码如下

    struct objc_class : objc_object {
        ...
        class_rw_t *data() const {
            return bits.data();
        }
        ...
    }
    

    我们继续通过lldb调试

    (lldb) p $30->data()
    (class_rw_t *) $31 = 0x0000000100645c60
    (lldb) p *$31
    (class_rw_t) $32 = {
      flags = 2148007936
      witness = 1
      ro_or_rw_ext = {
        std::__1::atomic<unsigned long> = {
          Value = 4295000216
        }
      }
      firstSubclass = nil
      nextSiblingClass = NSUUID
    }
    

    接下来通过查看class_rw_t的源码,我们发现,class_rw_t存储了属性列表,方法列表,协议,ro(class_ro_t类型)等信息,如下图

    image.png

    接下来,继续通过lldb调试

    // 获取属性列表
    (lldb) p $32.properties()
    (const property_array_t) $33 = {
      list_array_tt<property_t, property_list_t> = {
         = {
          list = 0x00000001000081a8
          arrayAndFlag = 4295000488
        }
      }
    }
    // 获取属性数组
    (lldb) p $33.list
    (property_list_t *const) $34 = 0x00000001000081a8
    // 打印具体值
    (lldb) p *$34
    (property_list_t) $35 = {
      entsize_list_tt<property_t, property_list_t, 0> = {
        entsizeAndFlags = 16
        count = 1
        first = (name = "name", attributes = "T@\"NSString\",&,N,V_name")
      }
    }
    
    (lldb) p $32.methods()
    (const method_array_t) $36 = {
      list_array_tt<method_t, method_list_t> = {
         = {
          list = 0x00000001000080e0
          arrayAndFlag = 4295000288
        }
      }
    }
    (lldb) p $36.list
    (method_list_t *const) $37 = 0x00000001000080e0
    (lldb) p *$37
    (method_list_t) $38 = {
      entsize_list_tt<method_t, method_list_t, 3> = {
        entsizeAndFlags = 26
        count = 5
        first = {
          name = "sayName"
          types = 0x0000000100003f78 "v16@0:8"
          imp = 0x0000000100003d60 (KCObjc`-[LYPerson sayName])
        }
      }
    }
    (lldb) p $38.get(0)
    (method_t) $39 = {
      name = "sayName"
      types = 0x0000000100003f78 "v16@0:8"
      imp = 0x0000000100003d60 (KCObjc`-[LYPerson sayName])
    }
    (lldb) p $38.get(1)
    (method_t) $40 = {
      name = "saySex"
      types = 0x0000000100003f78 "v16@0:8"
      imp = 0x0000000100003d90 (KCObjc`-[LYPerson saySex])
    }
    (lldb) p $38.get(2)
    (method_t) $41 = {
      name = ".cxx_destruct"
      types = 0x0000000100003f78 "v16@0:8"
      imp = 0x0000000100003e10 (KCObjc`-[LYPerson .cxx_destruct])
    }
    (lldb) p $38.get(3)
    (method_t) $42 = {
      name = "name"
      types = 0x0000000100003f8c "@16@0:8"
      imp = 0x0000000100003dc0 (KCObjc`-[LYPerson name])
    }
    (lldb) p $38.get(4)
    (method_t) $43 = {
      name = "setName:"
      types = 0x0000000100003f94 "v24@0:8@16"
      imp = 0x0000000100003de0 (KCObjc`-[LYPerson setName:])
    }
    (lldb) p $38.get(5)
    Assertion failed: (i < count), function get, file /Users/LY/Desktop/LY/objc4_debug/objc4-781/runtime/objc-runtime-new.h, line 438.
    error: Execution was interrupted, reason: signal SIGABRT.
    The process has been returned to the state before expression evaluation.
    (lldb) 
    

    通过上面获取到的properties我们得到了类的所有属性列表,但是却并没有找到对应的成员变量的列表,于是我们通过查看上面的ro的源码可以发现有一个ivars属性,同样的方式分析ro我们找到了所有的成员变量,这里不做赘述.大家可以自行尝试

    通过上面获取到的methods我们得到了类的所有实例方法列表,但是却并没有找到对应的类方法的列表,根据我们最开始探索的isa指向,我们知道对象的isa指向类,类的isa指向元类.那么现在对象方法存在了类中,会不会类方法存在元类中呢,于是我们通过上面的步骤对元类进行调试,发现类方法确实存在元类中,这里不做赘述.大家可以自行尝试

    通过上面的调试我们得出如下结论

    • 类中存储了类的所有属性列表,``方法列表,协议等信息在class_rw_t`中
    • 类的方法列表中除了实例方法还包括settergetter方法
    • 类方法并不存在类信息中而是存在元类中
    • 成员变量存在class_ro_tivars

    至此,对于类的结构以及类中的class_data_bits_t存储了哪些信息我们已经基本都了解了,这篇文章就先到这里了,后续我们再对类的cache进行分析.

    相关文章

      网友评论

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

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