iOS开发之类的本质

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

#import 
#import 
#import 
#   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 _buckets; // 是一个结构体指针类型,占8字节
    explicit_atomic _mask; //是mask_t 类型,而 mask_t 是 unsigned int 的别名,占4字节
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    explicit_atomic _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开发之类的本质)