美文网首页iOS底层原理
iOS开发之类的本质

iOS开发之类的本质

作者: 爱看书de图图 | 来源:发表于2020-09-18 10:49 被阅读0次

      我们这里讨论类的结构,我们先定义2个类StrudentPersonStrudent继承自PersonPerson继承自NSObject

    #import <Foundation/Foundation.h>
    #import <objc/runtime.h>
    #import <malloc/malloc.h>
    #   define ISA_MASK        0x00007ffffffffff8ULL
    @interface Person : NSObject{
        NSString *nickname;
    }
    @property (nonatomic, copy)NSString *name;
    -(void)eat;
    +(void)drink;
    @end
    
    @implementation Person
    -(void)eat{
        NSLog(@"eat");
    }
    +(void)drink{
        NSLog(@"drink");
    }
    @end
    
    @interface Student : Person
    @end
    @implementation Student
    @end
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            // insert code here...
            Student *student = [[Student alloc]init];
            Person *person = [[Person alloc]init];
            NSLog(@"%@ - %@",student,person);
         }
        return 0;
    }
    

    我们先用lldb调试,看看类的在内存中的地址。


    我们可以看到,p/x 0x001d8001000022e5 & 0x00007ffffffffff8ULLp/x 0x00000001000022b8 & 0x00007ffffffffff8ULL打印的结果一致。这是为什么呢?因为0x00000001000022e0是示例对象isa经过掩码计算后得出的类对象的地址,而0x00000001000022b8是类对象的isa经过掩码计算后的元类对象的地址,元类是iOS底层一个抽象的概念,由编译器自动完成,所以两个结果是相同的。
    元类

    1.实例对象中存放成员变量,实例对象的isa指向类对象
    2.类对象中存放实例方法,类对象的isa指向元类对象
    3.元类对象存放类方法,元类对象的isa指向根元类NSObject

    我们可以继续往下进行lldb的调试,得到根类NSObjectlldb的说明和我们上面的一样。


    然后我们打印NSObject,这里的两个地址不一样,为什么?难道是因为底层有另外一个NSObject对象吗?我们接下来验证一下。
    image.png
            Class class1 = [Person class];
            Class class2 = [Person alloc].class;
            Class class3 = object_getClass([Person alloc]);
            Class class4 = [Person alloc].class;
            
            NSLog(@"%p",class1);
            NSLog(@"%p",class2);
            NSLog(@"%p",class3);
            NSLog(@"%p",class4);
    

    打印结果:


    这里说明在内存中,所有的类对象只会创建一份,为什么NSObject对象的地址会不一样呢。我们继续lldb调试。
    这里一样了,因为我们刚才不一样的原因是一个是NSObject对象,一个是NSObject的元类对象。大家明白了吗?然后我们看看经典的isa走位图,这个图片网上都有,因为很经典,所以大家都在用。 isa流程图.png
    objc_class & objc_object

      为什么对象,类,元类,都有isa呢?我们查看源码,里面有一个类型。

    struct objc_object {
        Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
    };
    

      然后我们查看发现有一个继承自他的类

    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
    

      说白了,我们的NSObject对象只是OC帮我们封装后的记过,在底层C/C++的实现里,是没有对象的概念的,在底层类都是struct objc_class类型的,然后继承自objc_object(结构体)。
    上面我们说到了,实例方法存在类对象里,类方法存在元类里,那么我们怎么验证呢?


    看上方源码里的类结构,第一个是被注释掉的//Class ISA,因为我们是有了继承的ISA,第二个是superclass(即NSObject),如果是那么我们打印的第二串地址里0x00000001000022d0,应该存放的是我们的父类信息。看下图,我们得到了验证结果,是这样的。接下来我们先补充一段内存偏移的知识,这样我们才能一步步拿到类后面的信息。
    内存偏移
            int a = 10;
            int b = 10;
            NSLog(@"%d----%p",a,&a);
            NSLog(@"%d----%p",b,&b);
    
    输出结果:

    ab的内存地址差了4个字节。我们再来看数组指针,

    //数组指针
        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);
    
    输出结果:

    从打印结果我们知道:

    • &c&c[0]都是取 首地址,即数组名等于首地址,所以相同。
    • &c&c[1]相差4个字节,地址之间相差的字节数,主要取决于存储的数据类型
    • 可以通过 首地址+偏移量取出数组中的其他元素,其中偏移量是数组的下标,内存中首地址实际移动的字节数等于 偏移量 x 数据类型字节数

    所以刚才我们打印出来的NSObject就是根据内存偏移得出来的,那么接下来我们想要知道类里面的bits信息,我们只需要知道cache的大小,然后让内存偏移就行了。刚才的结果可不是蒙的哦~

    计算cache类的内存大小

    进入cachecache_t的定义(只贴出了结构体中非static修饰的属性,主要是因为static类型的属性不存在结构体的内存中),有如下几个属性

    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个字节
    

    计算前两个属性的内存大小,有以下两种情况,最后的内存大小总和都是12字节
    【情况一】if流程

    • buckets类型是struct bucket_t *,是结构体指针类型,占8字节
    • maskmask_t类型,而mask_tunsigned int的别名,占4字节

    【情况二】elseif流程

    • _maskAndBuckets是uintptr_t类型,它是一个指针,占8字节
    • _mask_unusedmask_t类型,而mask_tuint32_t类型定义的别名,占4字节
    • _flagsuint16_t类型,uint16_tunsigned short的别名,占 2个字节
    • _occupieduint16_t类型,uint16_tunsigned short的别名,占 2个字节
      所以最后计算出cache类的内存大小 = 12 + 2 + 2 = 16字节。
      接下来我们就来重点了,获取bits。所有的内容我们只需要首地址偏移32字节即可。然后看lldb调试(下图)。(bits的类型class_data_bits_t

      注:
    • x/4gx我们拿到Person类的首地址0x100002138+32 = 0x100002158(16进制)
    • p $1->data()是因为OC底层有提供bitsdata()方法,我们可以看到方法列表的类型是class_rw_t(class_rw_t类型图如下)
    class_rw_t

    我们继续lldb调试,打印其中的属性列表,方法列表。


    但是属性好像只有一个@"nickname",但是我们看看我们定义的属性。
    @interface Person : NSObject{
        NSString *name;
    }
    @property (nonatomic, copy)NSString *nickname;
    -(void)eat;
    +(void)run;
    @end
    

    明明有两个,那么name这个成员变量跑哪里去了呢?为什么property_list中只有属性,没有成员变量呢?

    探索成员变量的存储位置

    在刚才我们查看class_rw_t的类型的时候,我们发现了methods(),properties(),protocols(),然后在网上,我们还有一个类型没有注意到class_ro_t(如下图)


    那么我们是不是就可以猜测,这里存放的是成员变量呢?我们继续lldb调试。

    在里面,我们成功找到了name。知道了属性和成员变量的存储位置,那么接下来我们探讨方法的存储。
    探索方法列表methods_list

    刚才我们lldb调试的是properities(),这次我们用methods()


    我们成功的找到了实例方法,eat(),我们继续往下看看方法列表里都放了什么方法。go on lldb

    我们找到了很多方法,比如eat(),cxx_destruct(),nicknamegettersetter方法。但是好像没有我们上面自己定义的类方法,run()。所以他应该不存在这里。很简单,我们验证我们上面的说法,究竟是不是放在元类里呢?继续lldb呗?还能咋地?

    OK,看到了没,类方法已经被我们找到了。接下来我们来总结一下。
    总结:
    • objc_object是我们OC底层实现对象的基类,里面重要的数据类型就是,Class ISA,Class superclass,cache_t cache,class_data_bits_t bits,重要的信息比如属性列表,方法列表,协议列表都放在bits这里。

    • 通过{}定义的属性没有setget方法,存放在bits --> data() -->ro() --> ivars获取成员变量列表

    • 通过@ property定义的属性,存放在bits --> data() -->() --> list获取成员属性列表

    • 方法在底层的类型是class_rw_t类型,在class_rw_t的实现内部,我们又发现了类方法的类型是class_ro_t的类型。

    • 类的实例方法存储在类的bits属性中,通过bits --> methods() --> list获取实例方法列表,例如Person类的实例方法eat就存储在Person类的bits属性中,类中的方法列表除了包括实例方法,还包括属性的set方法和get方法

    • 类的类方法存储在元类的bits属性中,通过元类bits --> methods() --> list获取类方法列表,例如Person中的类方法run就存储在Person类的元类(名称也是Person)的bits属性中

    相关文章

      网友评论

        本文标题:iOS开发之类的本质

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