iOS底层之类和对象的经典面试题分析

类的方法的归属

一、class_getInstanceMethod

看以下代码

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        BKPerson *person = [BKPerson alloc];
        Class pClass     = object_getClass(person);
        logInstanceMethodFromClass(pClass);
    }
    return 0;
}

void logInstanceMethodFromClass(Class pClass){
    
    const char *className = class_getName(pClass);
    Class metaClass = objc_getMetaClass(className);
    
    Method method1 = class_getInstanceMethod(pClass, @selector(sayDad));
    Method method2 = class_getInstanceMethod(metaClass, @selector(sayDad));

    Method method3 = class_getInstanceMethod(pClass, @selector(sayHappy));
    Method method4 = class_getInstanceMethod(metaClass, @selector(sayHappy));
    
    BKLog(@"%s - %p-%p-%p-%p",__func__,method1,method2,method3,method4);
}

可以看出来打印的是什么结果吗?
首先,Class pClass = object_getClass(person);获取到了BKPerson类,传入logInstanceMethodFromClass方法中,
const char *className = class_getName(pClass);这一步获取到BKPerson类的类名的常量字符串“BKPerson”,用以传入Class metaClass = objc_getMetaClass(className);获取到BKPerson类的元类。
它的源码定义是:

Class objc_getMetaClass(const char *aClassName)
{
    Class cls;

    if (!aClassName) return Nil;

    cls = objc_getClass (aClassName);
    if (!cls)
    {
        _objc_inform ("class `%s' not linked into application", aClassName);
        return Nil;
    }

    return cls->ISA();
}

也就是如果aClassNamenil是就返回Nil,否则,objc_getClass获取到当前类放在cls,如果cls是空的话,就返回Nil,否则cls->ISA()返回类对象的isa保存的元类。如果不懂的,可以看下这篇文章iOS底层之类的结构分析对ISA的解释。
这一步相信大部分人都能看出来获取到了元类。

重点在于class_getInstanceMethod这个方法做了什么。
我们先看看这个sayDadsayHappy方法的定义。

@interface BKPerson : NSObject

- (void)sayDad;
+ (void)sayHappy;

@end

@implementation BKPerson

- (void)sayDad{
    NSLog(@"BKPerson say : Dad~");
}

+ (void)sayHappy{
    NSLog(@"BKPerson say : I'm happy");
}

@end

区别是- (void)sayDad;是一个类的实例方法,+ (void)sayHappy;是类的类方法。
我们再从class_getInstanceMethod的方法的源码看下,这个方法是怎么实现的。

Method class_getInstanceMethod(Class cls, SEL sel)
{
    if (!cls  ||  !sel) return nil;

    // This deliberately avoids +initialize because it historically did so.

    // This implementation is a bit weird because it's the only place that 
    // wants a Method instead of an IMP.

#warning fixme build and search caches
        
    // Search method lists, try method resolver, etc.
    lookUpImpOrForward(nil, sel, cls, LOOKUP_RESOLVER);

#warning fixme build and search caches

    return _class_getMethod(cls, sel);
}

if (!cls || !sel) return nil;如果传入的类或者方法编号为空,则返回nil,显然我们这里不是空的。重点在于这一句return _class_getMethod(cls, sel);,点进入是:

static Method _class_getMethod(Class cls, SEL sel)
{
    mutex_locker_t lock(runtimeLock);
    return getMethod_nolock(cls, sel);
}

static method_t *
getMethod_nolock(Class cls, SEL sel)
{
    method_t *m = nil;

    runtimeLock.assertLocked();

    // fixme nil cls?
    // fixme nil sel?

    ASSERT(cls->isRealized());

    while (cls  &&  ((m = getMethodNoSuper_nolock(cls, sel))) == nil) {
        cls = cls->superclass;
    }

    return m;
}

可以看到重点在于这一行代码while (cls && ((m = getMethodNoSuper_nolock(cls, sel))) == nil) { cls = cls->superclass; },查看条件语句的方法:

static method_t *
getMethodNoSuper_nolock(Class cls, SEL sel)
{
    runtimeLock.assertLocked();

    ASSERT(cls->isRealized());
    // fixme nil cls? 
    // fixme nil sel?

    auto const methods = cls->data()->methods();
    for (auto mlists = methods.beginLists(),
              end = methods.endLists();
         mlists != end;
         ++mlists)
    {
        //  getMethodNoSuper_nolock is the hottest
        // caller of search_method_list, inlining it turns
        // getMethodNoSuper_nolock into a frame-less function and eliminates
        // any store from this codepath.
        method_t *m = search_method_list_inline(*mlists, sel);
        if (m) return m;
    }

    return nil;
}

可以看到条件只要是 类存在 并且 在类的方法列表里没找到这个方法编号时,则会查找其父类的方法列表,循环下去,直到找到时返回方法,或者找不到时返回nil,又或者找到根类NSObject的父类nil,则不满足条件,也返回nil

而这里我们需明确的一点是类的实例方法是在类的方法列表里,而类对象的方法(类方法)是在元类的方法列表里,所以class_getInstanceMethod方法只能从类的方法列表,或者父类的方法列表里查找,只能找到类的实例方法,而无法查找到类方法。

这时候我们看回这道题。

1.
Method method1 = class_getInstanceMethod(pClass, @selector(sayDad));
  1. 由于sayDad为实例方法,pClassBKPerson,所以可以找到方法的地址。
2.
Method method2 = class_getInstanceMethod(metaClass, @selector(sayDad));
  1. 由于sayDad为实例方法,metaClassBKPerson的元类,所以无法在元类里找到实例方法的地址。地址为空。
3.
 Method method3 = class_getInstanceMethod(pClass, @selector(sayHappy));
  1. 由于sayHappy为类方法,pClassBKPerson,所以无法在类里找到类方法的地址。地址为空
4.
Method method4 = class_getInstanceMethod(metaClass, @selector(sayHappy));
  1. 由于sayHappy为类方法,metaClassBKPerson的元类,所以在元类里可以找到类方法的地址。

打印这4个方法的地址

0x1000031b0-0x0-0x0-0x100003148

二、class_getClassMethod

void logClassMethodFromClass(Class pClass){
    
    const char *className = class_getName(pClass);
    Class metaClass = objc_getMetaClass(className);
    
    Method method1 = class_getClassMethod(pClass, @selector(sayDad));
    Method method2 = class_getClassMethod(metaClass, @selector(sayDad));

    Method method3 = class_getClassMethod(pClass, @selector(sayHappy));
    // 元类 为什么有 sayHappy 类方法 0 1
    //
    Method method4 = class_getClassMethod(metaClass, @selector(sayHappy));
    
    BKLog(@"%s-%p-%p-%p-%p",__func__,method1,method2,method3,method4);
}

如果是执行以上代码,那么打印的结果是什么呢?

我们来看class_getClassMethod的定义

Method class_getClassMethod(Class cls, SEL sel)
{
    if (!cls  ||  !sel) return nil;

    return class_getInstanceMethod(cls->getMeta(), sel);
}

上个题目我们分析了class_getInstanceMethod,那么重点在于cls->getMeta()做了什么事。
查看源码

Class getMeta() {
        if (isMetaClass()) return (Class)this;
        else return this->ISA();
    }

可以看到当这个类是元类时,则返回自己,否则返回类的isa指向的元类。
对返回的元类使用class_getInstanceMethod查找类的实例方法列表,而我们知道类的类方法是在其元类的实例方法列表里。说明class_getClassMethod,只要传入的参数是BKPerson的元类,和BKPerson的类方法时,才能找到方法地址。

看回上面的题可知

1.
Method method1 = class_getClassMethod(pClass, @selector(sayDad));
  1. pClass是BKPerson类,getMeta返回的是BKPerson类的元类。 sayDad是BKPerson的实例方法,所以在BKPerson的元类方法列表里是找不到BKPerson的实例方法的,返回方法地址为空。
2.
Method method2 = class_getClassMethod(metaClass, @selector(sayDad));
  1. metaClass是BKPerson的元类,getMeta返回的是自己, sayDad是BKPerson的实例方法,所以在元类metaClass的实例方法列表里是找不到BKPerson的实例方法的,返回方法地址为空。
3.
Method method3 = class_getClassMethod(pClass, @selector(sayHappy));
  1. pClass是BKPerson类,getMeta返回的是BKPerson类的元类。 sayHappyBKPerson的类方法,存储在其元类的实例方法列表中,所以可以找到方法的地址。
4.
Method method4 = class_getClassMethod(metaClass, @selector(sayHappy));
  1. metaClass是BKPerson类的元类,getMeta返回的是自己。 sayHappy是元类的实例方法,可以找到方法地址。

打印结果为:

0x0-0x0-0x100003148-0x100003148

三、class_getMethodImplementation

执行以下代码

void logClass_IMPFromClass(Class pClass){
    
    const char *className = class_getName(pClass);
    Class metaClass = objc_getMetaClass(className);

    // - (void)sayDad;
    // + (void)sayHappy;
    IMP imp1 = class_getMethodImplementation(pClass, @selector(sayDad));
    IMP imp2 = class_getMethodImplementation(metaClass, @selector(sayDad));

    IMP imp3 = class_getMethodImplementation(pClass, @selector(sayHappy));
    IMP imp4 = class_getMethodImplementation(metaClass, @selector(sayHappy));

    NSLog(@"%p-%p-%p-%p",imp1,imp2,imp3,imp4);
    NSLog(@"%s",__func__);
}

那么打印结果又是如何?

class_getMethodImplementation这个方法在苹果文档里是这样的

class_getMethodImplementation

大概意思为class_getMethodImplementation可能比method_getImplementation(class_getInstanceMethod(cls, name))快。

返回的函数指针可以是运行时内部的函数,而不是实际的方法实现。例如,如果类的实例不响应选择器,则返回的函数指针将成为运行时消息转发机制的一部分。

查看class_getMethodImplementation源码

IMP class_getMethodImplementation(Class cls, SEL sel)
{
    IMP imp;

    if (!cls  ||  !sel) return nil;

    imp = lookUpImpOrNil(nil, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER);

    // Translate forwarding function to C-callable external version
    if (!imp) {
        return _objc_msgForward;
    }

    return imp;
}

如果查找函数的实现地址没有找到,则会通过消息转发机制返回函数指针。
那么上面的题目可以解答:

1.
IMP imp1 = class_getMethodImplementation(pClass, @selector(sayDad));
  1. pClass为BKPerson,sayDad为其实例方法,所以可以找到其方法实现,直接返回函数指针地址。
2.
IMP imp2 = class_getMethodImplementation(metaClass, @selector(sayDad));
  1. metaClass为BKPerson的元类,sayDad为BKPerson的实例方法,所以无法在元类中找到其方法实现,通过消息转发返回。
3.
IMP imp3 = class_getMethodImplementation(pClass, @selector(sayHappy));
  1. pClass为BKPerson,sayHappy为其类方法,存储在其元类的实例方法列表中。所以无法在BKPerson方法列表找到其实现,通过消息转发返回。
4.
IMP imp4 = class_getMethodImplementation(metaClass, @selector(sayHappy));
  1. metaClass为BKPerson的元类,sayHappy为BKPerson的元类的实例方法,所以可以在元类的方法列表中找到其方法实现,直接返回函数指针地址。
    所以打印结果为:
0x100001d10-0x7fff66fe8580-0x7fff66fe8580-0x100001d40

通过上面获取方法地址的3个函数,可以得出总结:

  1. class_getInstanceMethod方法只能从类的方法列表,或者父类的方法列表里查找到,只可能找到当前类的实例方法,而无法查找到类方法。
  2. class_getClassMethod方法只能从当前类的元类的方法列表中查找类方法,而无法找到当前类的实例方法。
  3. class_getMethodImplementation方法查找函数的实现地址,如果没有找到,则会通过消息转发机制返回。

类和对象的归属:iskindOfClass & isMemberOfClass

BOOL cls1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];       //
BOOL cls2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];     //
BOOL cls3 = [(id)[BKPerson class] isKindOfClass:[BKPerson class]];       //
BOOL cls4 = [(id)[BKPerson class] isMemberOfClass:[BKPerson class]];     //
NSLog(@"\n cls1 :%hhd\n cls2 :%hhd\n cls3 :%hhd\n cls4 :%hhd\n",cls1,cls2,cls3,cls4);

BOOL obj1 = [(id)[NSObject alloc] isKindOfClass:[NSObject class]];       //
BOOL obj2 = [(id)[NSObject alloc] isMemberOfClass:[NSObject class]];     //
BOOL obj3 = [(id)[BKPerson alloc] isKindOfClass:[BKPerson class]];       //
BOOL obj4 = [(id)[BKPerson alloc] isMemberOfClass:[BKPerson class]];     //
NSLog(@"\n obj1 :%hhd\n obj2 :%hhd\n obj3 :%hhd\n obj4 :%hhd\n",obj1,obj2,obj3,obj4);

比较上面的方法,得出打印的信息。

首先可以发现,上面4个方法是类方法,下面4个方法是实例方法。

从源码出发

  1. + (BOOL)isKindOfClass
+ (BOOL)isKindOfClass:(Class)cls {
    for (Class tcls = self->ISA(); tcls; tcls = tcls->superclass) {
        if (tcls == cls) return YES;
    }
    return NO;
}

可以看出,这个for循环语句里,逻辑是这样的:
Class tcls = self->ISA()获取当前类的isa指向的元类,赋值到tcls;
tcls不为空时,如果tcls == clstrue,返回YES,否则判断tcls的父类是否等于cls,等于返回YES,不等于则继续判断其父类直到根类NSObject,再到nil时则不满足循环条件,返回NO

父类的继承关系是:父类->根类NSObject->nil

所以,只要self的元类(或其self的元类的父类),等于cls则为真,否则为假。

对比顺序是:当前类的元类->当前类的元类的父类->NSObject->nil == 传入类?

  1. + (BOOL)isMemberOfClass
+ (BOOL)isMemberOfClass:(Class)cls {
    return self->ISA() == cls;
}

只有self当前类的元类是cls,则为真,否则为假。

对比顺序是:当前类的元类 == 传入类?

  1. - (BOOL)isKindOfClass
- (BOOL)isKindOfClass:(Class)cls {
    for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
        if (tcls == cls) return YES;
    }
    return NO;
}

这个for循环语句里,逻辑是这样的:
Class tcls = [self class]获取当前对象的类tcls
如果类tclsnil,则返回NO;
如果类不为nil,当类tcls等于cls类,返回YES,否则,将tcls的父类赋值给tcls,重新执行以上逻辑。
所以,只要self当前对象的类(或其父类)是cls类,则为真,否则为假。

对比顺序是:当前对象的类->父类->根类NSObject->nil == 传入类?

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

只有self当前对象的类是cls,则为真,否则为假。

对比顺序是:当前对象的类 == 传入类?

+ (Class)class- (Class)class也是由区别的

+ (Class)class {
    return self;
}

- (Class)class {
    return object_getClass(self);
}

要注意+ (Class)class返回的是自身。- (Class)class返回的是当前对象的类。

回到题干

  1. 类的对比为
BOOL cls1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];       //

左边:NSObject的元类是根元类NSObject,根元类NSObject的父类是NSObject
右边:NSObject自身;
所以结果为1

BOOL cls2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];     //

左边:NSObject的元类是根元类NSObject
右边: NSObject自身;
结果为0

BOOL cls3 = [(id)[BKPerson class] isKindOfClass:[BKPerson class]];       //

左边:BKPerson的元类是元类BKPerson,元类BKPerson的父类是根元类NSObject,根元类NSObject的父类是NSObjectNSObject的父类是nil
右边: BKPerson自身;
都不等于,结果为0

BOOL cls4 = [(id)[BKPerson class] isMemberOfClass:[BKPerson class]];     //

左边:BKPerson的元类是元类BKPerson
右边: BKPerson自身;
不等于,结果为0

NSLog(@"\n cls1 :%hhd\n cls2 :%hhd\n cls3 :%hhd\n cls4 :%hhd\n",cls1,cls2,cls3,cls4);
打印结果为
 cls1 :1
 cls2 :0
 cls3 :0
 cls4 :0
  1. 而对象的对比为
BOOL obj1 = [(id)[NSObject alloc] isKindOfClass:[NSObject class]];       //

左边:NSObject对象的类是NSObject
右边: NSObject自身;
等于,结果为1

BOOL obj2 = [(id)[NSObject alloc] isMemberOfClass:[NSObject class]];     //

左边:NSObject对象的类是NSObject
右边: NSObject自身;
等于,结果为1

BOOL obj3 = [(id)[BKPerson alloc] isKindOfClass:[BKPerson class]];       //

左边:BKPerson对象的类是BKPersonBKPerson类的父类是NSObjectNSObject的父类是nil
右边: BKPerson自身;
等于,结果为1

BOOL obj4 = [(id)[BKPerson alloc] isMemberOfClass:[BKPerson class]]; 

左边:BKPerson对象的类是BKPerson
右边: BKPerson自身;
等于,结果为1

NSLog(@"\n obj1 :%hhd\n obj2 :%hhd\n obj3 :%hhd\n obj4 :%hhd\n",obj1,obj2,obj3,obj4);
打印结果为
 obj1 :1
 obj2 :1
 obj3 :1
 obj4 :1

然而,我们看源码以为代码执行过程真的是按照以上的方法执行的,这却是一个很大的坑点。虽然最后的结果是正确的。但是通过跟进程序的执行,可以发现,无论- (BOOL)isKindOfClass还是+ (BOOL)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);
}

主要也是执行for循环里的语句,如果传入的obj是类的实例对象,则对比类(或父类、NSObjectnil)与传入的类是否一致,如果传入的obj是类,则获取到返回类的元类(或者元类的父类、根元类NSObject、或NSObject、或nil)。

为什么会走这个方法呢?而不走isKindOfClass
原因是编译器优化了执行的效率。

以上,如有疑虑或欠妥之处,欢迎指出!

你可能感兴趣的:(iOS底层之类和对象的经典面试题分析)