iOS开发之类的本质

  我们这里讨论类的结构,我们先定义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属性中

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,755评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,369评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,799评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,910评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,096评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,159评论 3 411
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,917评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,360评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,673评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,814评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,509评论 4 334
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,156评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,882评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,123评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,641评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,728评论 2 351