isMemberOfClass与isKindOfClass的底层实现
在我们平常工作中,isMemberOfClass 和 isKindOfClass 出现的频次算比较高的,光看文档,我们知道 isMemberOfClass 用来判断一个对象是否为某个类的实例,而 isKindOfClass 用来判断一个对象是否为某个类的实例或者继承自该类的子类的实例。那么在 Objc 的底层是如何实现这一功能的呢,让我们一起来围观一下。
准备
需要一份 Objc 的源码,官方源码)(需要自己踩坑编译)
快速通道(已编译通过,附带demo)。
本文中使用的源码版本为:objc4-818.2 。
运行示例代码
打开编译好的 Ojbc 工程,工程目录如下图:
工程目录:
如图中所示,我们已经添加了一个新的taget :TestObjc,接下来的实例代码也是写在它的main.m文件中。
在 main.m 文件中添加调试代码:
void testKindOf(){
BOOL ret1 = [NSObject.class isMemberOfClass:NSObject.class];
BOOL ret2 = [TestObject.class isMemberOfClass:TestObject.class];
BOOL ret3 = [[NSObject alloc] isMemberOfClass:NSObject.class];
BOOL ret4 = [[TestObject alloc] isMemberOfClass:TestObject.class];
NSLog(@"isMemberOfClass== %d - %d - %d - %d",ret1,ret2,ret3,ret4);
BOOL ret5 = [NSObject.class isKindOfClass:NSObject.class];
BOOL ret6 = [TestObject.class isKindOfClass:TestObject.class];
BOOL ret7 = [[NSObject alloc] isKindOfClass:NSObject.class];
BOOL ret8 = [[TestObject alloc] isKindOfClass:TestObject.class];
NSLog(@"isKindOfClass == %d - %d - %d - %d",ret5,ret6,ret7,ret8);
}我们再 ret1 处添加一个断点,并在 源码 NSObject.mm 中给 isKindOfClass 和 isMemberOfClass 方法实现中添加断点,然后运行代码。
运行代码之后,我们发现除了类方法 + (BOOL)isMemberOfClass:(Class)cls 和实例方法 - (BOOL)isMemberOfClass:(Class)cls 被调用外,isKindOfClass 的两个方法并没有被调用。why ?
汇编+断点调试
通过勾选 xcode 的菜单栏 Debug -> Debug Workflow -> Always Show Disassembly 开启汇编调试,我们看到了熟悉的函数调用逻辑。
汇编调试:
如图中所示,程序并没有走 isKindOfClass 方法的实现,而是走了 objc_opt_isKindOfClass 函数,于是我们在源码中搜索找到该函数,并添加断点。而 isMemberOfClass 则通过 objc_msgSend 进行消息转发而被调用的,相对简单一些。
下面我们先来看看 isMemberOfClass 方法的源码实现:
类方法:+isMemberOfClass
当执行 [NSObject.class isMemberOfClass:NSObject.class];
这行代码时,调用的就是这个类方法。
+ (BOOL)isMemberOfClass:(Class)cls {
return self->ISA() == cls;
}方法内部调用了底层 objc_object.h 的 ISA() 函数,该函数经过层层调用(看流程图),最终来到 isa_t::getClass() 函数中返回一个 Class ,我们知道 Class 的本质是 objc_class 结构体的指针,因此当"=="运算符的两边都是同一个地址时,运算结果为真。所以我们稍微修改一下代码,然后用 LLDB 的 x 指令进行调试:
调试画面:
如上图所示,我们看到,指针 acls 的值为:0x10036a0f0 ,而指针cls 的值为: 0x10036a140 ,并不是同一个地址,因此 ret 的值为 NO 。
虽然两个地址值不相同,但他们都指向了同一个地址 0x10036a0f0 ,和 acls 的值相同。而我们通过调用 object_getClass(cls) 方法获取 cls 的元类,发现依然是 0x10036a0f0 。类、元类、根元类 的补充在文章末尾。
当运行到下一行代码 [TestObject.class isMemberOfClass:TestObject.class]; 时,我们再跳入 +isMemberOfClass 方法去调试:
调试画面:
我们发现 acls 的值还是 0x100008160 ,而 cls 的值为 0x100008188 ,两者依然不同,但都指向同一个地址 0x10036a0f0 (NSObject)。
综上所述,我们可以判断 self->ISA(); 获取到的是元类。而上述的两行代码都是拿元类与类比较,因此结果都是 NO。
+isMemberOfClass 的流程图:
流程图:
实例方法:-isMemberOfClass
- (BOOL)isMemberOfClass:(Class)cls {
return [self class] == cls;
}先上流程图:
流程图:
通过对比流程图,我们发现-isMemberOfClass 也是调用了底层的 ISA()函数,不同的是返回了对象的类,而不是元类。(可以使用 class_isMetaClass 函数来判断 Class 是否为元类)
[[NSObject alloc] isMemberOfClass:NSObject.class];
[[TestObject alloc] isMemberOfClass:TestObject.class];因此,在运行这两行代码时,其结果都为 YES 。
接着,我们再转移到 isKindOfClass 方法,它同样有类方法和实例方法,来看看底层是如何实现的。
类方法:+isKindOfClass
在运行 [NSObject.class isKindOfClass:NSObject.class]; 这行代码时,我们使用单步调试发现,代码执行并不会走入 +isKindOfClass方法的实现,而是走了objc_opt_isKindOfClass 函数,这是因为底层对+isKindOfClass方法作了优化,将其IMP指向了objc_opt_isKindOfClass 函数,函数的具体实现如下:
// Calls [obj isKindOfClass]
BOOL
objc_opt_isKindOfClass(id obj, Class otherClass)
{
#if __OBJC2__
if (slowpath(!obj)) return NO;
Class cls = obj->getIsa();
if (fastpath(!cls->hasCustomCore())) {
for (Class tcls = cls; tcls; tcls = tcls->getSuperclass()) {
if (tcls == otherClass) return YES;
}
return NO;
}
#endif
return ((BOOL(*)(id, SEL, Class))objc_msgSend)(obj, @selector(isKindOfClass:), otherClass);
}NSObject.mm文件中+isKindOfClass方法的源码如下:
+ (BOOL)isKindOfClass:(Class)cls {
for (Class tcls = self->ISA(); tcls; tcls = tcls->getSuperclass()) {
if (tcls == cls) return YES;
}
return NO;
}经过对比发现,该方法 Objective-c2.0版本除了代码优化,核心的内容并没有变化。
由于是类调用的 +isKindOfClass,根据上述的内容,可知 getIsa() 函数或者ISA()函数返回的都是元类。由此可见,[A isKindOfClass:B],实际上是获取 A 的元类以及遍历元类的父类,与 B 相比较。而根据类、元类的关系图(见“补充知识点部分”),我们知道元类的最终父类依然是 NSObject ,所以[NSObject.class isKindOfClass:NSObject.class];这行代码的结果就是 YES。
再看下一行代码:[TestObject.class isKindOfClass:TestObject.class];,TestObject 的元类及其父类,都不会与 TestObject 相等,因此结果为 NO。
实例方法:-isKindOfClass
通过单步调试,我们看到 -isKindofClass还是会调用 objc_opt_isKindOfClass 函数,只不过getIsa() 函数或者ISA()函数返回的都是实例对象的类。
那么[a isKindOfClass:B],实际上是获取实例对象a的类及其父类,与B类相比较。所以
[[NSObject alloc] isKindOfClass:NSObject.class];
[[TestObject alloc] isKindOfClass:TestObject.class];这两行代码结果都是 YES。
补充知识点
类、元类、根元类
Class (类)对于我们来说很熟悉,想要创建实例对象就需要先定义一个类。而在 OC Runtime 中类是一个 objc_class 类型的对象 :typedef struct objc_class *Class;,我们通常称之为类对象 ,这个类对象的类被称为元类(meta class),而元类的类就是根元类,这个根元类就是 NSObject ,他们之间通过 isa 指针来联系起来,如下图:
类、元类、根元类:
关于 \_OBJC2_ 宏定义
\_OBJC2_ 是 objective-c 2.0 版本的宏定义,在宏定义的作用域里面的是2.0版本的代码,它是用来兼容iOS低版本系统的,Stack Overflow 有一个很好的解释,如下图:
OBJC2:
总结
1、类和实例对象对底层 getIsa() 函数或者ISA()函数 的调用结果是不同的:类调用会返回元类,而对象调用会返回类。
2、对 isKindOfClass类似的底层方法调用,并不一定会直接执行方法的 IMP ,因为底层优化会修改IMP的指向。比如调用 isKindOfClass 会执行 objc_opt_isKindOfClass 函数,而不是走isKindOfClass 的IMP实现。