Objective-C 中的对象 & isa & superclass & 元类(metaClass)

说到对象,什么是对象?

由于文章的连贯性、强烈建议先看看之前的文章:Objective-C 中类的数据结构Objective-C 中实例所占内存的大小

在面向对象编程中,有两个重要的概念:对象。在实际的内存中 又是以什么样的形式存在的呢?万物皆屌丝,不对、是万物皆对象。

一、对象

在 OC 中什么是对象?有很长一段时间坚信认为 +alloc 出来的才是对象。如果说平时这么说还行,一旦放到面试的时候这样来回答、那恐怕就不行,因为这个答案是不全的。主要分为以下三种:

  • 1、instance 对象,也称实例对象。
  • 2、class 对象,也称类对象。
  • 3、meta-class 对象,也称元类对象。

没错,就是这三个对象。突然感觉哪里不对劲,不是说好的 block 也是一种特殊的对象么?对,也没有错,但是今天暂时不讨论这个 block 对象。
接下来、将会讨论这些对象中都包含了什么样的信息,这些信息都是如何关联起来的,成员变量是存在哪里的,类方法与实例方法是存在哪里的,如何找到 superclass 的。。。。。关于这些问题,将会一一的梳理一遍。

1.1 instance 对象

1.1.1 什么是 instance 对象

总而言之就是通过 +alloc 之后的的都是 instance 对象。但是这里还需要强调一点的是: instance 对象并不仅仅是 NSObject 的对象,还有一个代理类 NSProxy,也是能创建 instance 对象。

关于 NSProxy,大家应该高度重视,当面试官问你在 OC 中有什么代理类的时候,别说不知道什么代理类,就知道代理协议(delegate)。如果你说不知道,那有一点可以很肯定,YYKit 那么优秀的框架都不去学习一下,对于大厂来说,恐怕会遭到鄙视的。具体的可以参考 YYKit 中的 YYWeakProxy。看完 YYWeakProxy 之后,你还会学到另一个技能:如何处理 OC 中定时器的循环引用。这两个问题在面试中,含金量都不低。在接下来的介绍中,不再提及 NSProxy 类。

代码中是如何获取一个对象的:

// 创建一个对象
NSObject* obj = [NSObject alloc];

1.1.2 instance 对象中的信息

instance 对象中都包含什么呢?通过前面的两篇文章 Objective-C 中类的数据结构Objective-C 中实例所占内存的大小得知,仅有成员变量,没有其它的,其中他们的成员变量是有继承关系的。比如 Person 类继承于 NSObject,那么 Person 的 instance 对象中就会继承 NSObject 中的所有成员变量。

1.2 class 对象

1.2.1 获取 class 对象

class 对象是怎么被创建的?毕竟在开发过程中也没有见过通过 +alloc 的方式创建之。但是知道怎么去获取一个 class 对象,见下面的代码:

{
    // 创建对象
    ClsObject* cObj = [[ClsObject alloc] init];
    
    // 获取 class 的 所有方法
    [self fetchClassWiothCObj:cObj];
}

// 获取 class 的 所有方法
- (void)fetchClassWiothCObj:(ClsObject*)cObj {
    Class obcCls1 = [cObj class];
    Class obcCls2 = [ClsObject class];
    Class obcCls3 = object_getClass(cObj);
    
    NSLog(@"%@, %@, %@", NSStringFromClass(obcCls1), NSStringFromClass(obcCls2), NSStringFromClass(obcCls3));
    // 打印结果: ClsObject, ClsObject, ClsObject
    
    NSLog(@"%p, %p, %p", obcCls1, obcCls2, obcCls3);
    // 打印结果: 0x10f52dd30, 0x10f52dd30, 0x10f52dd30
}

以上代码中的 ClsObject 是一个直接继承于 NSObject 的 Class。

没错,不管是通过什么方法获取的 class 对象都是一样的,包括地址。说明在一个项目中一个 Class 仅有一个对象。但是上面的三种获取 class 对象的方式有什么不一样呢?
第一种与第二种是通过方法获取的,直接获取的是当前 instance 的 Class,但是第三种方式不一样,这种方式是获取当前 instance 的 isa 的值。

可以这样做一个实验,给上面的 cObj 做一个 KVO 监听,我们再看一下打印结果,会发现打印的结果变成了这样的:

// 打印结果:ClsObject, ClsObject, NSKVONotifying_ClsObject
// 打印结果:0x102e5ee10, 0x102e5ee10, 0x60000011a820

是的,第三个值变了,变成了 NSKVONotifying_ClsObject。同时还发现,所有同一个 Class 的 instance 注册的 KVO 的 NSKVONotifying_ 的 class 对象的值也是一样的。

1.2.2 class 对象中的信息

  • 1、isa
  • 2、superclass
  • 3、属性 property
  • 4、instance 方法
  • 5、协议 protocal
  • 6、成员变量,这里的成员变量信息并不是一个 instance 中成员变量的值,而是指在这个 Class 中有哪些成员变量,是 NSSting 的,还是 int 类型的。
  • 7、其它
    。。。。。。。

1.3 meta-class 对象

1.3.1 获取 meta-class 对象

同理在开发中是不会手动去 +alloc 一个元类对象,可以通过 object_getClass 函数获取 class 对象的 isa 类获取之。代码如下:

// 获取元类对象
- (void)metaClass:(ClsObject*)cObj {
    // 获取一个对象的 isa
    Class obcISA = object_getClass(cObj);
    // 获取元类对象
    Class metaClass = object_getClass(obcISA);
    NSLog(@"%p, %@", metaClass, NSStringFromClass(metaClass));
}

会发现,元类还是当前的 Class,但是是另一个对象地址。
其次,不管是 class 对象还是元类对象,其类型都是 Class,说明在内存结构上是一致的。但是其包含的信息含义是不一样,其用途也不一样。

1.3.2 meta-class 对象中的信息

  • 1、isa
  • 2、superclass
  • 3、类方法信息
  • 4、其它

1.4 对象总结

  • 1、总共有三种对象:instance 对象、class 对象与 meta-class 对象
  • 2、成员变量的值都存于 instance 对象中。
  • 3、属性、instance (实例)方法、协议 protocol、成员变量都存于 class 对象中。
  • 4、类方法都存于 meta-class 对象中。
对象的信息分布

二、关于 isa

以上的三种对象是如何关联起来的呢?是通过 isa 关联的:

instance 对象的 isa 的值是 class 对象,class 对象的 isa 的值是 meta-class对象。

尽然实例方法是存在 class 对象中,那么当给一个 instance 对象发送消息的时候,是如何找到具体的方法实现的呢?

当调用实例方法的时候, 通过 instance 对象中的 isa 找到 class,找到对应的实例方法的实现。

同理,类方法的调用也是一样:

当调用类方法的时候,通过 class 对象的 isa 指针找到 meta-class,并找到对应的方法实现。

不管是调用 Class 方法还是对象方法都是消息发送,这里有一个面试题是这样问的:OC 中的消息发送的本质是什么?在之前我是这样的回答的:通过 SEL 去找对应的 IMP 实现,首先是从当前 Class(meta-class) 寻找,如果一旦找不到就会到父类寻找,当所有的都没有找到那么会启动消息转发机制,一旦找到了、那么会将当前的 SEL 与 IMP 缓存起来方便下一次查询。之前一直以为这样的回答够完美的了,但是现在看来需要再加一点专业术语会更加的完善。消息转发的本质是通过 isa 查找对应的 IMP 实现。然后加上之前的回答即可。
为什么要强调这一点呢?难道在 OC 中还有不需要 isa 直接发送消息的??是的、有一个方法被调用就没有通过 isa 的查询,那就是 +load 方法。在很久之前也一直有一个疑问:为什么在分类中重写了 +load 方法之后,原生 Class 的+load 方法还能被调用。原来是因为 +load 方法的调用逻辑是在 dyld 加载阶段,一旦检测到当前的 Class 或者其分类重写了 +load 直接通过 IMP 地址进行调用。所以这种情况就不会出现原生 Class 的 IMP 后移从而导致没有机会被调用的情况。

三、关于 superclass

superclass 指针 是相对于 class 对象meta-class 对象 来说的。这个指针有什么作用呢?
定义两个 Class:Person 继承于 NSObject,Student 继承于 Person。现在有一个场景,通过 Student 的 instance 对象调用 Person 中实现的实例方法,具体的调用过程如下:

通过 Student 类的 instance 对象 的 isa 找到对应 Student 类的 class 对象,但是没有找到相关的实现,系统会继续到 superclass 中找,于是会到 Person 类的 class 对象 中找到具体的实现,并调用。

类方法的调用,也是一样。

四、 isa 与 superclass

美图欣赏,以上所说的都是为了能看懂这张图片:


class.png

由图可知:

1、isa

  • 1、instance 的 isa 指向 class
  • 2、class 的 isa 指向 meta-class
  • 3、meta-class 的 isa 指向基类的 meta-class

2、superclass

  • 1、class 的 superclass 指向父类的 class,如果没有父类,superclass 为 nil
  • 2、neta-class 的 superclass 指向父类的 meta-class,基类的 meta-class 的 super 指向基类的 class

3、 方法调用轨迹

instance 对象: isa 找到class,方法如果不存在,就通过 superclass找父类。
class 对象: isa 找到meta-class,方法如果不存在,就通过 superclass 找父类。

五、isa、class 与元类(metaClass)的关系求证

上面说到这样的一句:

instance 对象的 isa 的值是 class 对象,class 对象的 isa 的值是 meta-class对象。

通过上图也已经有所提现了,再把上图做一个标识,如下:

image.png

接下来就是证明一下这 5 条线的正确性。具体代码如下:

// instance
HGObject* obj = [[HGObject alloc] init];
// class 第一根线
Class objCls = object_getClass(obj);
// metaClass 第二根线
Class objMetaCls = object_getClass(objCls);
// rootMetaCls (元类的父元类) 第三根线
Class rootMetaCls0 = class_getSuperclass(objMetaCls);
// 与元类的 Class 第四根线
Class rootMetaCls1 = object_getClass(objMetaCls);
// 根元类的 Class
Class rootMetaCls = object_getClass(rootMetaCls0);
NSLog(@"\ninstance = %p\nobjCls = %p \nobjMetaCls = %p\nrootMetaCls0 = %p\nrootMetaCls1 %p\nrootMetaCls = %p", obj, objCls, objMetaCls, rootMetaCls0, rootMetaCls1, rootMetaCls);

其中 HGObject 是直接继承 NSObject 的类。打印结果:

instance = 0x604000012430
objCls = 0x10619eea8 
objMetaCls = 0x10619ee80
rootMetaCls0 = 0x107147e58
rootMetaCls1 0x107147e58
rootMetaCls = 0x107147e58

注意一下后面的三个值, 都是一样的。到现在应该已经理清了。但是在上面的代码中,没有看到 isa 相关的,我们仅仅是获取了对应对象的 类型(Class)而已。现在想要看看具体的 isa 的值打的是多少,在代码中是很难看到的,如果一定要在代码中查看,那也是可以的。接下来使用 LLDB 来查看看。比如想要查看 instanceisa 值,可以这么操作:

image.png

可以看出 objisa 就是其对应的 objCls 的值。同理可以查看一下 objClsisa 的值是不是 objMetaCls。在操作的过程中会发现这样的提示:

image.png

我有一个解决方案,将 objCls 转成一个 NSObject 即可,如下:

// 将 Class 转成 NSObject
NSObject* clsObj = (NSObject*)objCls;
NSLog(@"%p", clsObj);

然后通过查询 clsObjisa 的值,就是 objClsisa 的值。

判断是否为元类的方法:

if (class_isMetaClass(objMetaCls)) {
    NSLog(@"是元类");
} else {
    NSLog(@"不是");
}
重点的问题来了!!!!!!!

以上的操作, 都是在 iOS 项目中的模拟器的结果,但是如果换成 Mac 项目,获取换成真机,结果就不一样了。比如,我将上面的代码放到 Mac 中(仅仅是部分代码), 如下:

// insert code here...
HGObject* obj = [[HGObject alloc] init];
// class 第一根线
Class objCls = object_getClass(obj);
// 打印
NSLog(@"\nobj = %p\nobjCls = %p", obj, objCls);

打印结果是这样的:

obj = 0x10050d8d0
objCls = 0x100001140

但是查看 isa 发现这样的结果:

image.png

结果不一样了!!!!
是的,是这样的。在一些系统下,做了一个转换,什么样的转换呢?先看一下结果:


image.png

厉害了,这是一个巧合吧,这个巧合不太巧合,是一个规律。在一些系统下的 isa 要做一个与运算才能得到真实类型的具体值。面使用的值是 0x00007ffffffffff8,这是在 MAC 上的,但是在真机上是不一样的。具体的定义,可以在源码中找到(已经删除其它定义):

# 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
# else
#   error unknown architecture for packed isa
# endif

// SUPPORT_PACKED_ISA
#endif

在上面使用的就是 ISA_MASK 的值,在这路也可以看出。在其它的地方也会用到同样的转换:ISA_MAGIC_MASKISA_MAGIC_VALUE

关于 LLDB 的更多使用,可以参考:Xcode 常用 LLDB 指令

谢谢!

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

推荐阅读更多精彩内容