说到对象,什么是对象?
由于文章的连贯性、强烈建议先看看之前的文章: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
美图欣赏,以上所说的都是为了能看懂这张图片:
由图可知:
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对象。
通过上图也已经有所提现了,再把上图做一个标识,如下:
接下来就是证明一下这 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 来查看看。比如想要查看 instance 的 isa 值,可以这么操作:
可以看出 obj 的 isa 就是其对应的 objCls 的值。同理可以查看一下 objCls 的 isa 的值是不是 objMetaCls。在操作的过程中会发现这样的提示:
我有一个解决方案,将 objCls 转成一个 NSObject 即可,如下:
// 将 Class 转成 NSObject
NSObject* clsObj = (NSObject*)objCls;
NSLog(@"%p", clsObj);
然后通过查询 clsObj 的 isa 的值,就是 objCls 的 isa 的值。
判断是否为元类的方法:
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 发现这样的结果:
结果不一样了!!!!
是的,是这样的。在一些系统下,做了一个转换,什么样的转换呢?先看一下结果:
厉害了,这是一个巧合吧,这个巧合不太巧合,是一个规律。在一些系统下的 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_MASK 与 ISA_MAGIC_VALUE。
关于 LLDB 的更多使用,可以参考:Xcode 常用 LLDB 指令