OC方法isKindOfClass趣探

我们先来看一下下面的一个面试题:

1
2
3
4
5
6
7
8
9
10
11
BOOL re1 = [(id)[NSObject class] isKindOfClass:[NSObject class]]; //
BOOL re2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]]; //
BOOL re3 = [(id)[LLYModel class] isKindOfClass:[LLYModel class]]; //
BOOL re4 = [(id)[LLYModel class] isMemberOfClass:[LLYModel class]]; //
NSLog(@" re1 :%hhd\n re2 :%hhd\n re3 :%hhd\n re4 :%hhd\n",re1,re2,re3,re4);
BOOL re5 = [(id)[NSObject alloc] isKindOfClass:[NSObject class]]; //
BOOL re6 = [(id)[NSObject alloc] isMemberOfClass:[NSObject class]]; //
BOOL re7 = [(id)[LLYModel alloc] isKindOfClass:[LLYModel class]]; //
BOOL re8 = [(id)[LLYModel alloc] isMemberOfClass:[LLYModel class]]; //
NSLog(@" re5 :%hhd\n re6 :%hhd\n re7 :%hhd\n re8 :%hhd\n",re5,re6,re7,re8);

请问输出分别是多少呢?大家可以自己试一试,答案分别是:

1
2
3
4
5
6
7
8
9
re1 :1
re2 :0
re3 :0
re4 :0
re5 :1
re6 :1
re7 :1
re8 :1

我们直接看源码进行分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//直接判断当前类的元类是否和后面的类相等。
+ (BOOL)isMemberOfClass:(Class)cls {
return self->ISA() == cls;
}
//直接判断是不是同一个类对象
- (BOOL)isMemberOfClass:(Class)cls {
return [self class] == cls;
}
//使用当前类对象的元类的继承链和后面的类对象进行比较,只要后面的类对象在当前元类的继承链中,既判断相等。
+ (BOOL)isKindOfClass:(Class)cls {
for (Class tcls = self->ISA(); tcls; tcls = tcls->superclass) {
if (tcls == cls) return YES;
}
return NO;
}
//使用当前实例的类对象的继承链和后面的类对象进行比较,只要后面的类在当前类的继承链中,既判断是相等。
- (BOOL)isKindOfClass:(Class)cls {
for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
if (tcls == cls) return YES;
}
return NO;
}

总结一下上面的判断逻辑大概是这样:

isKindOfClass会使用继承链比较,实例方法使用类对象的继承链,类方法使用元类对象的继承链。
isMemberOfClass直接比较,实例方法使用类对象,类方法使用元类对象。

看到这里,不得不再次祭出下面这张图了:

我们来瞧瞧元类对象的继承链,根元类的superclass指针指向的是根类,这也就是

1
BOOL re1 = [(id)[NSObject class] isKindOfClass:[NSObject class]]; //

这里为什么返回1的原因。

有了以上结论,我们再来Debug一下runtime的代码,看一下这几个函数内部的运行逻辑是否和我们上面的结论一致。

isMemberOfClass lldb分析

先来看看类方法:

1
2
3
4
5
6
7
8
9
10
11
BOOL re2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]]; //
(lldb) x/4gx self //类对象
0x10034c140: 0x000000010034c0f0 0x0000000000000000
0x10034c150: 0x0000000100888a40 0x0001801000000003
(lldb) x/4gx cls //类对象
0x10034c140: 0x000000010034c0f0 0x0000000000000000
0x10034c150: 0x0000000100888a40 0x0001801000000003
(lldb) x/4gx 0x000000010034c0f0(ISA) //元类对象也是根源类
0x10034c0f0: 0x000000010034c0f0 0x000000010034c140
0x10034c100: 0x0000000100739650 0x0004e03100000007

这里就很清晰,类对象只有一个,因为他们的首地址和内部变量都相同 ,根元类对象也只有一个,但是和类对象首地址不同,ISA指针相同,这又证明了上图的内容,即根源类的ISA指向自己。根元类和类对象不相等,返回0。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
BOOL re4 = [(id)[LLYModel class] isMemberOfClass:[LLYModel class]]; //
(lldb) x/4gx self //类对象
0x100008408: 0x00000001000083e0 0x000000010034c140
0x100008418: 0x0000000100346430 0x0000802400000000
(lldb) x/4gx cls //类对象
0x100008408: 0x00000001000083e0 0x000000010034c140
0x100008418: 0x0000000100346430 0x0000802400000000
(lldb) x/4gx 0x00000001000083e0 //元类对象
0x1000083e0: 0x000000010034c0f0 0x000000010034c0f0
0x1000083f0: 0x00000001007396d0 0x0002e03500000003
(lldb) x/4gx 0x000000010034c0f0 //根源类对象
0x10034c0f0: 0x000000010034c0f0 0x000000010034c140
0x10034c100: 0x0000000100739650 0x0004e03100000007
(lldb)

因为是自定义的类,所以会有一个元类对象,元类对象isa指向上面的根元类,元类和类对象不相等,返回0,没有问题。

然后我们来看看实例方法,实例对象的内存分析和类对象有一点区别,请注意!!!

1
2
3
4
5
6
7
8
9
10
11
12
13
BOOL re6 = [(id)[NSObject alloc] isMemberOfClass:[NSObject class]]; //
(lldb) x/4gx self //实例对象
0x100629c30: 0x001d80010034c141 0x0000000000000000
0x100629c40: 0x00007fff7ba35c58 0x00000000e642baab
(lldb) x/4gx cls //类对象
0x10034c140: 0x000000010034c0f0 0x0000000000000000
0x10034c150: 0x0000000100629c80 0x0001801000000007
(lldb) p/x 0x001d80010034c141 & 0x00007ffffffffff8ULL //这里是重要,实例对象的isa指针是nonpointer_isa 如果想要拿到类对象信息,需要带个面具。!!!
(unsigned long long) $40 = 0x000000010034c140
(lldb) x/4gx 0x000000010034c140 //类对象
0x10034c140: 0x000000010034c0f0 0x0000000000000000
0x10034c150: 0x0000000100629c80 0x0001801000000007

在分析前,我们先回到源码部分

1
2
3
- (BOOL)isMemberOfClass:(Class)cls {
return [self class] == cls;
}

这里的

1
[self class]

怎么理解呢?

实际上分两种情况:

  • 当self是实例对象时,class是实例方法,返回isa指针,也就是类对象
  • 当self是类或者元类对象时,class是类方法,返回自己。

所以上面我需要拿到self的isa指向的内存,但是self的isa不能直接打印内存,因为nonpointer_isa对isa进行了优化,存储很多其他的信息,我们需要先使用面具过滤掉其他不需要的信息,才能拿到isa中存储的类对象地址。从lldb打印情况可以看到,self实例对象的isa指向的就是类对象,所以这里返回1.

re8的分析过程和re6相似,就不再重复劳动了。

特殊的isKindOfClass

为什么要加上【特殊】这个前缀呢,原因很简单,isKindOfClass里面的断点根本没有进去,可见runtime内部对这个方法的调用可能走的就不是我们上面的实现,这种情况我们之前在alloc的分析过程中也遇到过,所以不要慌。经过在源码中一番搜索,我们最终找到下面这个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 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);
}

内部的判断逻辑和我们上面看到的源码一样,所有我们可以在这个函数中进行debug了,不过这个debug过程已经不是重点了,重点是为什么会走到这个函数里面,上面的函数又是什么时候调用,因为有了之前的经验,我们直接去llvm中看能否寻找的一些答案。

果不其然,我们在llvm的源码中发现了这个:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// This is the table of ObjC "accelerated dispatch" functions. They are a set
// of objc methods that are "seldom overridden" and so the compiler replaces the
// objc_msgSend with a call to one of the dispatch functions. That will check
// whether the method has been overridden, and directly call the Foundation
// implementation if not.
// This table is supposed to be complete. If ones get added in the future, we
// will have to add them to the table.
const char *AppleObjCTrampolineHandler::g_opt_dispatch_names[] = {
"objc_alloc",
"objc_autorelease",
"objc_release",
"objc_retain",
"objc_alloc_init",
"objc_allocWithZone",
"objc_opt_class",
"objc_opt_isKindOfClass",
"objc_opt_new",
"objc_opt_respondsToSelector",
"objc_opt_self",
};

在注释中,我们发现了【加速派发】,【不常重写】等字样,这里我们大胆猜测就是llvm在底层为了优化方法的调用速度,对下面列表中的方法做了优化,会优先进到这些优化方法中去,如果我们重写isKindOfClass等方法,可能才会进入之前我们看到的函数中去。

ok 来尝试一下我们的猜测,很简单,我们只需要重写一下:

1
2
3
4
5
6
7
+ (BOOL)isKindOfClass:(Class)aClass {
return [super isKindOfClass:aClass];
}
- (BOOL)isKindOfClass:(Class)aClass {
return [super isKindOfClass:aClass];
}

果然,在我们重写之后,之前的断点就生效了,完美!!!

源码的探索过程还是比较有趣的,不过如果你找不到正确的方法的话,可能跟到一个方法后就很难再深入进入,导致直接放弃,打击学习的积极性,所以还是要多看多学,不断积累才行啊。