OC对象的底层结构及isa、superClass详解

  • isa指针
    通过上一篇文章的分析我们已经知道了实例对象,类对象,元类对象的结构如上图所示,每个对象中都有 isa 指针,isa 指针有什么作用?他们之间的关系是怎样的呢?我们写一个Person类然后调用它的实例方法eat,看一下底层源码:
[person eat];
//本质
objc_msgSend(person, sel_registerName("eat"));

[person eat]本质就是向person对象发送一条eat消息,通过上面的结构图我们知道,对象方法是存放在类对象中的,并不在实例对象中,那实例对象是怎么拿到类对象中的对象方法的呢?同样,类方法也是如此怎么拿到存放在元类对象中的类方法的呢?.其实这就用到了isa指针,大家记住:实例对象的isa指针指向类对象,类对象的isa指针指向元类对象,元类对象的isa指针指向基类的元类对象

  • superClass
    superClass 只存在类对象和元类对象中,实例对象中没有superClass.类对象的 superClass 指向父类的类对象,父类的 superClass 指向基类的类对象,如果基类没有父类,superClass 为nil;元类的 superClass 指向父类的元类对象,父类的元类对象的 superClass 指向基类的元类对象,基类的元类对象的 superClass 指向基类.
    现在我们再添加一个Student类继承自Person类,如下:
// Person
@interface Person : NSObject <NSCopying>
{
int _age;
int _no;
int _height;
}

-(void)personInstanceMethod;
+(void)personClassMethod;

@end

@implementation Person

- (void)personInstanceMethod{
    
}

+ (void)personClassMethod{
    
}
@end

// Student
@interface Student : Person <NSCopying>
{
    int _sex;
}

-(void)studentInstanceMethod;
+(void)studentClassMethod;

@end

@implementation Student

- (void)studentInstanceMethod{
    
}

+ (void)studentClassMethod{
    
}
@end

然后再创建一个student对象,让student对象去调用Person的实例方法:

Student *student = [[Student alloc]init];
[student personInstanceMethod];

我们知道Person的实例方法是存放在Person的类对象中的,student对象如何找到这个方法呢?我们画一张图:


首先student根据isa找到Student class对象,然后再通过Student class对象的superClass找到父类Person class对象,再从Person class对象中找到Person的实例方法.如果父类Person class中依然没有,就依次往上找,直到NSObject,如果NSObject中还没有,就会报一个很经典的错误unrecognized selector sent to instance.
我们再改变一下,让改一下代码,让Student去执行Person的类方法:

[Student personClassMethod];

这次方法执行的查找顺序是怎样的呢?我就不画图了,其实跟[student personInstanceMethod]差不多.首先Student类对象先通过isa找到Student 元类对象,然后通过Student 元类对象superClass找到父类的元类对象,也就是Person 元类对象,然后在Person 元类对象找到类方法执行.
关于isa 和 superClass的关系,网上有人总结了一张图,可以很明显的展示,虚线表示 isa , 实现 表示 superClass:

我们参照这张图,对isa 和 superClass做一个总结:

  • instance 的 isa 指向 class , class 的 isa 指向 meta-class , meta-class 的 isa 指向基类的 meta-class.
  • class 的 superClass 指向父类
    如果没有父类, superClass 为 nil.
  • meta-class 的 superClass 指向父类的 meta-class
    基类的 meta-class 的 superClass 指向基类的 class.
  • instance 调用方法的轨迹
    通过 isa 找到 class ,如果方法不存在就通过 superClass 找父类
  • class 调用类的方法轨迹
    通过 isa 找到 meta-class,如果没有,通过 superClass 找到父类的 meta-class.

注意上图中有一根 superClass 线我用红色的⭕️标识出来了,这根线特别特殊:基类的元类对象的 superClass 指向基类.下面我们通过代码验证一下.
我们给NSObject创建一个分类NSObject+test,然后在分类的.m方法中写一个实例方法,打印输出调用者:

- (void)test{
    NSLog(@"NSObject 的对象方法test,调用者: %p",self);
}

然后在Person类的头文件中声明一个+ (void)test;方法,在让Person调用这个方法,如下:

NSLog(@"Student 地址: %p",[Student class]);
NSLog(@"Person 地址: %p",[Person class]);
[Student test];

我们来分析一下,Person的头文件中声明了一个test方法,并没有在实现文件中实现,而NSObject+test.m文件中实现了一个对象方法test,如果我们执行[Student test];会执行成功么?
运行一下看看:

2019-02-27 15:23:19.834089+0800 OC对象的分类_01[2112:300025] Student 地址: 0x100001478
2019-02-27 15:23:19.834469+0800 OC对象的分类_01[2112:300025] Person 地址: 0x100001428
2019-02-27 15:23:19.834505+0800 OC对象的分类_01[2112:300025] NSObject 的对象方法test,调用者: 0x100001478

会发现并没有报unrecognized selector sent to instance这个错误,居然调用成功了!很奇怪,为什么我们使用Student调用➕方法,最后却执行了NSObject的➖方法呢?这就是我在上图中用红⭕️标识的哪条线的作用.

我们分析一下它的方法调用轨迹:
1: 首先Student通过isa指针找到Studentmeta-class,在Studentmeta-class中查找test方法,结果没找到.
2: 又通过Studentmeta-class中的superClass找到他的父类的元类对象,也就是Personmeta-class,结果又没找到
3: 继续通过Personmeta-class中的superClass找到NSObjectmeta-class,结果还没找到
4: 最后NSObjectmeta-classsuperClass指向了基类NSObject,它又去NSObject中查找,结果找到了- (void)test,就执行了.

大家可能会疑惑,为什么调用的类方法,最后却执行了对象方法?
因为OC调用方法的本质就是发送消息,[Student test]本质就是objc_msgSend(objc_getClass("Student"), sel_registerName("test"));它只知道去执行test,并不关心是加号方法还是减号方法.

我们一直在说实例对象的isa指针指向类对象,类对象的isa指针指向元类对象,也就是说实例对象的isa地址和类对象的地址相同,类对象的isa地址和元类的地址相同,我们来验证一下.
1: 分别创建Person的实例对象,类对象,元类对象:

//实例对象
Person *person = [[Person alloc]init];
//类对象
Class personClass = [person class];
//元类对象
Class personMetaClass = object_getClass(personClass);

2: 通过命令行打印person->isapersonClass地址


person->isa地址是: 0x001d800100001429
personClass地址是: 0x0000000100001428
咦~怎么不一样呢?
这里需要注意一下,在 arm64 位之前,实例对象的 isa 地址和类对象的地址就是相同的,但是 arm64 位之后,实例对象的 isa 地址需要按位与ISA_MASK后才能得到类对象的地址,我们在object之isa指针详解篇幅中详细讲解过,有兴趣的同学可以看一下.
ISA_MASK是什么呢?打开 runtime 源码搜索一下就能看到:

# if __arm64__
#   define ISA_MASK        0x0000000ffffffff8ULL
#   define ISA_MAGIC_MASK  0x000003f000000001ULL
#   define ISA_MAGIC_VALUE 0x000001a000000001ULL

# elif __x86_64__
#   define ISA_MASK        0x00007ffffffffff8ULL
#   define ISA_MAGIC_MASK  0x001f800000000001ULL
#   define ISA_MAGIC_VALUE 0x001d800000000001ULL

如果是 arm64 位系统,ISA_MASK就是0x0000000ffffffff8ULL,如果是x86_64系统ISA_MASK就是0x00007ffffffffff8ULL,因为我们是在mac 上运行,所以我们就使用x86_64的:

计算结果

可以发现,通过位运算后得到的地址0x0000000100001428personClass地址:0x0000000100001428就是同一个地址!
下面验证类对象的 isa 指针指向元类对象
我们打印personClass 的 isa,会发现报错,因为系统没有暴露isa这个成员:
image.png

为了解决这个问题,我们定义一个和class结构相同的hh_objc_class:

struct objc_class {
    Class _Nonnull isa;
    Class _Nullable super_class
}

然后把Class personClass类型转换为struct hh_objc_class类型:

struct hh_objc_class *hh_personClass = (__bridge struct hh_objc_class *)(personClass);

然后再打印:
p/x hh_personClass->isa发现结果为:0x001d800100001401
p/x personMetaClass的结果为:0x0000000100001400
同样在按位或运算:


按位或运算后的结果和p/x personMetaClass的结果:0x0000000100001400相同,也印证了我们之前的结论.

现在来验证一下类对象的 superClass 指向父类对象也就是说Student classsuperClass地址和Person class的地址相同.
还是之前代码,我们直接打印p/x hh_studentClass->super_classp/x personClass,结果如图:

通过打印的结果可以看出,superClassisa不同,superClass并没有& ISA_MASK,而是直接相等.

我们一直在说属性信息、协议信息、成员变量信息、对象方法存放在类对象中,类方法存放在元类对象中,但是一直没有亲眼看到,接下来我们就来验证一下,亲眼看看.
首先我们知道,类对象和元类对象的结构是一模一样的,因为他们都是class类型.所以我们点击进入class类型,发现它是这样:

struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    Class _Nullable super_class                              OBJC2_UNAVAILABLE;
    const char * _Nonnull name                               OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */

可以看到这个结构体中的确是有method_list,ivar_list,protocol_list等等,完全符合我们之前的说法,但是请注意:#if !__OBJC2__,也就是说这段代码的执行有一个前提不是objc2.0才会执行,而现在我们用的都是OC2.0了,所以这段代码是不会执行的.也就是说这段代码已经过时了OBJC2_UNAVAILABLE.所以我们需要从 runtime 的源码中查看.
打开 runtime 源码搜索struct objc_class找到struct objc_class : objc_object {这个结构体,我把主要成员挑出来,如下:

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() { 
        return bits.data();
    }
}

我们主要研究class_rw_t,rw_tread_write_table的缩写,意思是可读可写的表.bits.data()返回class_rw_t.class_rw_t里面存放的什么信息呢?我们点击进入class_rw_t看看:

struct class_rw_t {
    // Be warned that Symbolication knows the layout of this structure.
    uint32_t flags;
    uint32_t version;

    const class_ro_t *ro;

    method_array_t methods;//方法信息
    property_array_t properties;//属性信息
    protocol_array_t protocols;//协议信息
}

可以看到我们想看的方法,属性,协议信息.只是还没看到成员变量信息. class_rw_t中有一个class_ro_t,ro_treadOnly_table的缩写,意思是只读的表.我们点击进入const class_ro_t看看里面存放哪些信息:

struct class_ro_t {
    const char * name;//类名
    method_list_t * baseMethodList;
    protocol_list_t * baseProtocols;
    const ivar_list_t * ivars;//成员变量
}

我画了一张图,能一目了然展示Class的内存结构:

Class类和元类内存结构图

这就是class类型在 runtime 中的源码,到这里我们已经看到了所有我们想到看的信息,但是这只是在源码上证明了结论,最好我们能把代码跑起来,在内存中看看是否是这样,要想做到这种效果,就要借助李哥用 c++ 仿写的一个class类型:MJClassInfo.h文件.
步骤:
1: 导入MJClassInfo.h文件到我们工程中,这是编译会出错,我们把main.m后缀改为main.mm,让编译器编译OC 和 C++ 文件.
2: 创建Student classPerson class然后转换为mj_objc_class类型

mj_objc_class *studentClass = (__bridge mj_objc_class *)[Student class];
mj_objc_class *personClass = (__bridge mj_objc_class *)[Person class];

3: 调用mj_objc_classdata()方法,返回class_rw_t,这是李哥封装的方法,在data()内部调用bits.data()和runtime源码实现一致.

class_rw_t *studentClass_rw_t_Data = studentClass->data();
class_rw_t *personClass_rw_t_Data = personClass->data();

4:打断点,查看class_rw_t内存数据:


这样我们也从内存上查验了class的内存结构,meta-class的查验方法和class一样,我就不写步骤了,直接上图大家撸一眼:

大总结:

1: 对象的 isa 指针指向哪里?

  • instance 对象的 isa 指针指向 class 对象
  • class 对象的 isa 指针指向 meta-class对象
  • meta-class 对象的 isa 指向基类的 meta-class 对象

2: OC的类信息存放在哪里?

  • 属性信息,协议信息,成员变量信息,对象方法存放在类对象中.
  • 类方法存放在 meta-class 对象中.
  • 成员变量的具体值存放在 instance 对象.

3: 类对象和元类对象什么时候加载?什么时候释放?

  • 当程序执行main函数后就会加载类信息,此时加载到内存;
  • 程序运行过程中一直存在,直到程序退出时销毁.

4: 实例对象中的成员变量和类对象中的成员变量有什么不同?

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

推荐阅读更多精彩内容