内省和自省是一个概念,下文使用内省替代;
什么是内省(Introspection)?
在计算机科学中,内省是指计算机程序在运行时(Runtime)检查对象(Object)类型的一种能力,通常也可以称作“运行时类型检查"。一些编程语言如C++、Java、Ruby、PHP、Objective-C、Perl等等具有这种特性。
Objective-C 内省
Objective-C 中的内省方法都定义在NSObject协议中。
除此之外还有些人认为 + instancesRespondToSelector:或者 + conformsToProtocol:也是内省方法,个人认为这两个不能算是内省方法,虽然它们可以做到 respondsToSelector 和 conformsToProtocol一样的效果,但由于它们是类方法,也就是说它们的方法调用者并不是一个实例对象,所以不能将其归到内省方法中。
isKindOfClass
直接来看runtime源码吧,isKindOfClass最终调用就是 NSObject.mm 中的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->superclass) {
if (tcls == otherClass) return YES;
}
return NO;
}
#endif
return ((BOOL(*)(id, SEL, Class))objc_msgSend)(obj, @selector(isKindOfClass:), otherClass);
}
在当前环境下,执行的都是 Objective-C 2.0 的代码。关于 runtime 中的宏 请看这里
先看这一句 if (slowpath(!obj)) return NO;,表示如果 obj 是个 nil 的话,直接返回 NO。
Class cls = obj->getIsa(); 通过实例对象的 isa 指针获取它的 Class 对象 cls。fastpath(!cls->hasCustomCore()) 表示是否自己实现了 new/self/class/respondsToSelector/isKindOfClass 的实例方法,没有实现的话就会执行 if 内部的代码。
这个 for 循环比较简单,就是从 obj对应的 Class 对象沿着继承关系开始寻找和 otherClass 相同的 Class 对象。顺手提一句 == 大家都知道是内存地址的比较,tcls == otherClass相等,说明他们的内存是相同的,也从侧面说明 Class 对象也是对象,存在于内存中。
isMemberOfClass
稍微修改一下代码
CZPerson *objc = [[CZPerson alloc] init];
[objc isMemberOfClass:[NSObject class]];
在来看一下调用过程
这和 isKindOfClass 有点区别,使用的是消息发送,说明它底层是调用了 Objective-C 方法。 在NSObject.mm 中有 isMemberOfClass 的默认实现
- (BOOL)isMemberOfClass:(Class)cls {
return [self class] == cls;
}
从这里看出 isMemberOfClass 逻辑比较简单,就是比较两个 Class 对象的内存地址是否相同,也就是调用者的Class和参数Class必须是同一个。
respondsToSelector
respondsToSelector 指的是对象是否 实现 了某个方法,如果只是声明没有实现的话,返回值为 NO。看下源码更加直观。
CZPerson *objc = [[CZPerson alloc] init];
[objc respondsToSelector:@selector(testMethod)];
再来看一下调用过程
我们来看一下 NSObject.mm 中的 objc_opt_respondsToSelector实现
// Calls [obj respondsToSelector]
BOOL
objc_opt_respondsToSelector(id obj, SEL sel)
{
#if __OBJC2__
if (slowpath(!obj)) return NO;
Class cls = obj->getIsa();
if (fastpath(!cls->hasCustomCore())) {
return class_respondsToSelector_inst(obj, sel, cls);
}
#endif
return ((BOOL(*)(id, SEL, SEL))objc_msgSend)(obj, @selector(respondsToSelector:), sel);
}
这里的逻辑和 isKindOfClass 有点类似,就不做赘述了,直接说结果。如果对象没有重写 respondsToSelector ,那么就调用 class_respondsToSelector_inst 返回结果。
// inst is an instance of cls or a subclass thereof, or nil if none is known.
// Non-nil inst is faster in some cases. See lookUpImpOrForward() for details.
NEVER_INLINE __attribute__((flatten)) BOOL
class_respondsToSelector_inst(id inst, SEL sel, Class cls)
{
// Avoids +initialize because it historically did so.
// We're not returning a callable IMP anyway.
return sel && cls && lookUpImpOrNilTryCache(inst, sel, cls, LOOKUP_RESOLVER);
}
IMP lookUpImpOrNilTryCache(id inst, SEL sel, Class cls, int behavior)
{
return _lookUpImpTryCache(inst, sel, cls, behavior | LOOKUP_NIL);
}
graph TD
class_respondsToSelector_inst --> lookUpImpOrNilTryCache --> _lookUpImpTryCache --> ...
看到这里,就可以解释了上述说法了
respondsToSelector指的是对象是否 实现 了某个方法,如果只是声明没有实现的话,返回值为NO
IMP 粗略的就可以理解为是函数的实现。
关于更多如何寻找IMP的细节,也就是_lookUpImpTryCache的实现过程,请参考另一篇objc_MsgSend的过程解读, 这也是消息机制中的常用函数。
conformsToProtocol
CZPerson *objc = [[CZPerson alloc] init];
[objc conformsToProtocol:@protocol(NSObject)];
也比较简单,就是调用 NSObject.mm 中的 conformsToProtocol 默认实现。
- (BOOL)conformsToProtocol:(Protocol *)protocol {
if (!protocol) return NO;
for (Class tcls = [self class]; tcls; tcls = tcls->getSuperclass()) {
if (class_conformsToProtocol(tcls, protocol)) return YES;
}
return NO;
}
从代码中可以看出,只要本类或者继承关系中某个类遵守了协议,就会返回YES。
接着看一下核心代码 class_conformsToProtocol
/***********************************************************************
* class_conformsToProtocol
* fixme
* Locking: read-locks runtimeLock
**********************************************************************/
BOOL class_conformsToProtocol(Class cls, Protocol *proto_gen)
{
protocol_t *proto = newprotocol(proto_gen);
if (!cls) return NO;
if (!proto_gen) return NO;
mutex_locker_t lock(runtimeLock);
checkIsKnownClass(cls);
ASSERT(cls->isRealized());
for (const auto& proto_ref : cls->data()->protocols()) {
protocol_t *p = remapProtocol(proto_ref);
if (p == proto || protocol_conformsToProtocol_nolock(p, proto)) {
return YES;
}
}
return NO;
}
这里新进行了加锁,runtimeLock 是一把读写锁。checkIsKnownClass(cls); 检查是否是已知的类型,如果不是则会报错,关于怎么判断是已知类型,请参考后面补充
for (const auto& proto_ref : cls->data()->protocols()) ,我相信有很多人看不懂这句代码,参考一下,菜鸟教程 - 基于范围的for循环(C++11),这句意思是循环取出protocols中的 protocol 指针。remapProtocol(proto_ref); 表示返回一个指向protocol_t的指针,这里是因为可能重新分配过内存,所以要取最新的值。protocol_conformsToProtocol_nolock 这里的话就是循环加递归的去检查是否相等。