1、前文提要&基本知识

结合前文,如果完整阅读过前面几篇文章的读者,相信大家对 isaClass 已经不再陌生了。SO,今天我们的主题是根据 isa 走位和 Class 的继承关系图,来出两道经典面试题。首先还是祭出我们的镇店之宝

image.png

在开始本文之前,我们还是先对上图进行一些基本解释,图中实线是 super_class 指针,虚线是 isa 指针。

  1. Root class (class) 其实就是 NSObjectNSObject 是没有超类的,所以Root class(class)superclass 指向 nil
  2. 每个 Class 都有一个 isa 指针指向唯一的Meta class
  3. Root class(meta)superclass 指向 Root class(class),也就是NSObject,形成一个回路。
  4. 每个 Meta classisa 指针都指向 Root class (meta)

所以我们应该明白,类对象元类对象 是唯一的。在大家对上图有了进一步理解之后,现在各位同学开始坐稳系好安全带,我们准备开始面试,要芜湖起飞了。

关于元类究竟是什么,大家可以具体研究一下这两篇文章

  1. What is a meta-class in Objective-C?
  2. Classes and metaclasses

2、面试题[一] 类方法的归属问题

2.1 class_getInstanceMethod 方法

首先我们先上第一部分代码

  1. void zlInstanceMethod_classToMetaclass(Class pClass){
  2. const char *className = class_getName(pClass);
  3. Class metaClass = objc_getMetaClass(className);
  4. Method method1 = class_getInstanceMethod(pClass, @selector(sayHello));
  5. Method method2 = class_getInstanceMethod(metaClass, @selector(sayHello));
  6. Method method3 = class_getInstanceMethod(pClass, @selector(sayHappy));
  7. Method method4 = class_getInstanceMethod(metaClass, @selector(sayHappy));
  8. ZLLog(@"%s - %p-%p-%p-%p",__func__,method1,method2,method3,method4);
  9. }
  10. int main(int argc, const char * argv[]) {
  11. @autoreleasepool {
  12. ZLStudent *student = [ZLStudent alloc];
  13. Class pClass = object_getClass(student);
  14. zlInstanceMethod_classToMetaclass(pClass);
  15. }
  16. return 0;
  17. }

首先,这个方法 class_getInstanceMethod我们从字面上的意思去理解这个方法,应该是 获取某个类的实例方法,我们根据 isa 走位图,可以得出,实例方法存储在类里面,类方法存储在元类

我们把 class_getInstanceMethod 的源码实现贴出来

  1. /***********************************************************************
  2. * class_getInstanceMethod. Return the instance method for the
  3. * specified class and selector.
  4. **********************************************************************/
  5. Method class_getInstanceMethod(Class cls, SEL sel)
  6. {
  7. if (!cls || !sel) return nil;
  8. // This deliberately avoids +initialize because it historically did so.
  9. // This implementation is a bit weird because it's the only place that
  10. // wants a Method instead of an IMP.
  11. #warning fixme build and search caches
  12. // Search method lists, try method resolver, etc.
  13. lookUpImpOrForward(nil, sel, cls, LOOKUP_RESOLVER);
  14. #warning fixme build and search caches
  15. return _class_getMethod(cls, sel);
  16. }

所以这段代码的输出结果为

image.png

注意,如果我们将 say666 方法的实现注释掉的话,那么结果将会完全不同

image.png

输出结果为

image.png

我们通过 MachOView 进行查看编译出来的 MachO 可执行文件,也会发现不同的结果。

image.png

image.png

为什么会造成这种结果呢?大家可以先自行思考,具体的方法查找流程,我们在后面的文章中进行详细讲解。

2.2 class_getClassMethod 方法

接下来我们看另外一段代码的输出结果

  1. void zlClassMethod_classToMetaclass(Class pClass){
  2. const char *className = class_getName(pClass);
  3. Class metaClass = objc_getMetaClass(className);
  4. Method method1 = class_getClassMethod(pClass, @selector(sayHello));
  5. Method method2 = class_getClassMethod(metaClass, @selector(sayHello));
  6. Method method3 = class_getClassMethod(pClass, @selector(sayHappy));
  7. Method method4 = class_getClassMethod(metaClass, @selector(sayHappy));
  8. ZLLog(@"%s - %p-%p-%p-%p",__func__,method1,method2,method3,method4);
  9. }
  10. int main(int argc, const char * argv[]) {
  11. @autoreleasepool {
  12. ZLStudent *student = [ZLStudent alloc];
  13. Class pClass = object_getClass(student);
  14. zlClassMethod_classToMetaclass(pClass);
  15. }
  16. return 0;
  17. }

我们先把 class_getClassMethod 的源码实现贴出来

  1. Method class_getClassMethod(Class cls, SEL sel)
  2. {
  3. if (!cls || !sel) return nil;
  4. return class_getInstanceMethod(cls->getMeta(), sel);
  5. }

可以看到,其实 class_getClassMethod 方法最终也是调用到class_getInstanceMethod 方法里面来。只是传进去的 class 会变成传递 classMetaClass

所以,我们的最终输出结果为

image.png

这次相信有不少人都栽倒在第四个方法的输出结果这里

Method method4 = class_getClassMethod(metaClass, @selector(sayHappy));

为什么这个能正常输出结果呢,这就要根据源码来分析了,我们看到去查找方法的时候,传入的是 cls->getMeta(),我们跟进去看他究竟是怎样实现的

  1. // NOT identical to this->ISA when this is a metaclass
  2. Class getMeta() {
  3. if (isMetaClass()) return (Class)this;
  4. else return this->ISA();
  5. }

这样就非常清晰了,注释都已经提示我们了,如果我们传入的 classMetaClass 就直接返回。

那么为什么苹果这样设计呢,其实就是为了防止 无限递归,所以就知道为什么是这样的输出结果了。

2.3 class_getMethodImplementation 方法

我们看最后一段代码的输出结果

  1. void zlIMP_classToMetaclass(Class pClass){
  2. const char *className = class_getName(pClass);
  3. Class metaClass = objc_getMetaClass(className);
  4. IMP imp1 = class_getMethodImplementation(pClass, @selector(sayHello));
  5. IMP imp2 = class_getMethodImplementation(metaClass, @selector(sayHello));
  6. IMP imp3 = class_getMethodImplementation(pClass, @selector(sayHappy));
  7. IMP imp4 = class_getMethodImplementation(metaClass, @selector(sayHappy));
  8. ZLLog(@"%s - %p-%p-%p-%p",__func__,imp1,imp2,imp3,imp4);
  9. }
  10. int main(int argc, const char * argv[]) {
  11. @autoreleasepool {
  12. ZLStudent *student = [ZLStudent alloc];
  13. Class pClass = object_getClass(student);
  14. zlIMP_classToMetaclass(pClass);
  15. }
  16. return 0;
  17. }

按照惯例,继续大家思考十秒钟

然后我们也把源码贴出来,因为也是涉及到方法的查找流程,所以我们这里也暂时不详细展开分析先。

  1. /**
  2. * Returns the function pointer that would be called if a
  3. * particular message were sent to an instance of a class.
  4. *
  5. * @param cls The class you want to inspect.
  6. * @param name A selector.
  7. *
  8. * @return The function pointer that would be called if \c [object name] were called
  9. * with an instance of the class, or \c NULL if \e cls is \c Nil.
  10. *
  11. * @note \c class_getMethodImplementation may be faster than \c method_getImplementation(class_getInstanceMethod(cls, name)).
  12. * @note The function pointer returned may be a function internal to the runtime instead of
  13. * an actual method implementation. For example, if instances of the class do not respond to
  14. * the selector, the function pointer returned will be part of the runtime's message forwarding machinery.
  15. */
  16. IMP class_getMethodImplementation(Class cls, SEL sel)
  17. {
  18. IMP imp;
  19. if (!cls || !sel) return nil;
  20. imp = lookUpImpOrNil(nil, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER);
  21. // Translate forwarding function to C-callable external version
  22. if (!imp) {
  23. return _objc_msgForward;
  24. }
  25. return imp;
  26. }

苹果的描述也很有意思

Returns the function pointer that would be called if a particular message were sent to an instance of a class. class_getMethodImplementation may be faster than method_getImplementation(class_getInstanceMethod(cls, name)).

如果向一个类的实例发送一条消息,该函数会返回该条消息的 IMPclass_getMethodImplementation 可能比method_getImplementation 更高效。

The function pointer returned may be a function internal to the runtime instead of an actual method implementation. For example, if instances of the class do not respond to the selector, the function pointer returned will be part of the runtime’s message forwarding machinery.

返回的指针可能会是一个方法的 IMP,也可能是runtime 内部的一个函数。比如说,如果一个类的对象不能响应一个 selector,这个函数指针返回的就会是 runtime 里面消息转发机制的一部分。

现在让我们看一下输出结果

image.png

注意图中红色标注的地址出现了2次:0x7fff6b57d580,这个是在调用class_getMethodImplementation()方法时,无法找到对应实现时返回的相同的一个地址,无论该方法是在实例方法或类方法,无论是否对一个实例调用该方法,返回的地址都是相同的,但是每次运行该程序时返回的地址并不相同。至于为什么会这样,我们也在后面的文章进行详细解释。

3、[面试题二] isKindOfClassisMemberOfClass面试题

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

我们先来分析一下源码这两个函数的对象实现

  1. + (Class)class {
  2. return self;
  3. }
  4. - (Class)class {
  5. return object_getClass(self);
  6. }
  7. inline Class
  8. objc_object::ISA()
  9. {
  10. return (Class)(isa.bits & ISA_MASK);
  11. }
  12. + (BOOL)isMemberOfClass:(Class)cls {
  13. return self->ISA() == cls;
  14. }
  15. - (BOOL)isMemberOfClass:(Class)cls {
  16. return [self class] == cls;
  17. }
  18. BOOL
  19. objc_opt_isKindOfClass(id obj, Class otherClass)
  20. {
  21. if (slowpath(!obj)) return NO;
  22. Class cls = obj->getIsa();
  23. if (fastpath(!cls->hasCustomCore())) {
  24. for (Class tcls = cls; tcls; tcls = tcls->superclass) {
  25. if (tcls == otherClass) return YES;
  26. }
  27. return NO;
  28. }
  29. }

首先题目中 NSObjectZLStudent 分别调用了 class 方法。

3.1 re1 结果分析

  • + (BOOL)isKindOfClass:(Class)cls 方法,会先调用 objc_opt_isKindOfClass(id obj, Class otherClass) ,为什么会调用这个呢,因为是 LLVM 优化的结果,然后会调用 ISA() 方法中获得 MetaClass 的指针。
  • 接着在 isKindOfClass 中有一个循环,先判断class 是否等于 MetaClass,不等就继续循环判断是否等于 superclass,不等再继续取 superclass,如此循环下去。
  • [NSObject class] 执行完之后调用isKindOfClass,第一次判断先判断 NSObjectNSObjectMetaClass 是否相等,从之前的 isa 的走位图上我们也可以看出,NSObjectMetaClass 与本身不等。接着第二次循环判断 NSObjectMetaClasssuperclass是否相等。还是从那张图上面我们可以看到:Root class(meta)superclass 就是 Root class(class),也就是 NSObject 本身。所以第二次循环相等,于是 re1 输出应该为YES

3.2 re3 结果分析

  • [ZLStudent class] 执行完之后调用 objc_opt_isKindOfClass ,然后开始循环遍历,第一次 for 循环,ZLStudentMetaClass[ZLStudent class] 不等,第二次 for 循环,ZLStudent(MetaClass)superclass 指向的是 NSObject(MetaClass), 和 [ZLStudent class] 不相等。第三次 for 循环,NSObject(MetaClass)superclass 指向的是 NSObject(Class),和 [ZLStudent class] 不相等。第四次循环,NSObject(Class)superclass 指向 nil, 和 [ZLStudent class] 不相等。第四次循环之后,退出循环,所以 re3 输出为 NO

3.3 re2 结果分析

  • + (BOOL)isMemberOfClass:(Class)cls 方法内部,会先调用 ISA() 方法中获得 MetaClass 的指针,然后直接比较当前类的 MetaClass 是否等于 [NSObject class],所以 re2 输出为 NO

3.4 re4 结果分析

  • 比较的是 ZLStudent(MetaClass)[ZLStudent class],所以 re4 输出为 NO

3.5 re5 结果分析

  • 此时应该输出 YES,因为在 objc_opt_isKindOfClass 函数中,判断 NSObjectisa 指向是否是自己的类NSObject,第一次 for 循环就能输出 YES 了。

3.6 re7 结果分析

  • 同上,第一次 for 循环就输出 YES

3.7 re6 结果分析

  • 因为在 - (BOOL)isMemberOfClass:(Class)cls 函数中,获取当前类的 class[NSObject class] 进行比较,输出结果为 YES

3.8 re8 结果分析

  • 同理,输出结果为 YES

最终全部输出结果为:

image.png

4、练习题

最后再给大家一份练习题,以下这段代码的运行结果是什么?会崩溃吗?

  1. @interface ZLTeacher : NSObject
  2. + (void)sayByebye;
  3. @end
  4. @implementation ZLTeacher
  5. - (void)sayByebye {
  6. }
  7. @end
  8. int main(int argc, const char * argv[]) {
  9. @autoreleasepool {
  10. [ZLTeacher sayByebye];
  11. }

5、参考资料