OC底层原理--类归属与方法归属相关面试题分析

通过前面的分析,我们对于类的继承以及方法的存储位置有了一定的了解,那么这篇文章我们主要来分析两个比较常见的面试题,具体如下:

关于iskindOfClass & isMemberOfClass的理解

//-----使用 iskindOfClass & isMemberOfClass 类方法
BOOL re1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];
BOOL re2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];
BOOL re3 = [(id)[LYPerson class] isKindOfClass:[LYPerson class]];
BOOL re4 = [(id)[LYPerson class] isMemberOfClass:[LYPerson class]];     
NSLog(@" re1 :%hhd\n re2 :%hhd\n re3 :%hhd\n re4 :%hhd\n",re1,re2,re3,re4);

//------iskindOfClass & isMemberOfClass 实例方法
BOOL re5 = [(id)[NSObject alloc] isKindOfClass:[NSObject class]];       
BOOL re6 = [(id)[NSObject alloc] isMemberOfClass:[NSObject class]];     
BOOL re7 = [(id)[LYPerson alloc] isKindOfClass:[LYPerson class]];       
BOOL re8 = [(id)[LYPerson alloc] isMemberOfClass:[LYPerson class]];     
NSLog(@" re5 :%hhd\n re6 :%hhd\n re7 :%hhd\n re8 :%hhd\n",re5,re6,re7,re8);

既然是分析底层原理,那么在给出上面的结果前,我们先来通过源码来分析下isKindOfClassisMemberOfClass的类方法和实例方法分别做了哪些事情,具体源码如下:

+ (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;
}

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

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

源码分析

  • +(BOOL)isKindOfClass:(Class)cls

    依次将clsself(类)元类,父元类(如果有父类),根元类,根类,nil进行对比

  • -(BOOL)isKindOfClass:(Class)cls

    依次将clsself(对象),父类,根类,nil进行对比

  • +(BOOL)isMemberOfClass:(Class)cls

    self(类)元类与传入类cls进行对比

  • -(BOOL)isMemberOfClass:(Class)cls

    self(对象)与传入类cls进行对比

断点调试

通过上面的源码分析完,我们其实已经差不多可以得出结论了,但是为了验证一下,我们还是对源码进行断点调试一下(防止有坑)

果然,通过调试我们发现源码中isKindOfClass的类方法和实例方法并没有走到(确实有坑).既然没走,那我们就只能看下具体的堆栈信息,通过堆栈信息我们发现isKindOfClass实际走的是BOOL objc_opt_isKindOfClass(id obj, Class otherClass)方法,如下图:

image.png

接下来,我们来看下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);
}

通过注释其实我们也可以很清楚的知道,调用[obj isKindOfClass]的时候会改为调用objc_opt_isKindOfClass,这里之所以会这样也是跟我们最开始分析alloc方法时一样的道理,编译器llvm编译时做了相应的优化

所以isKindOfClass的实际流程图如下

image.png

至此,我们已经通过源码和实际的断点调试对iskindOfClass & isMemberOfClass进行了分析,那么对于最初我们给出的问题,你有答案了吗?让我们一起来看下吧

结果

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

    • NSObject类NSObject元类对比,不相等,继续下一步
    • NSObject类NSObject元类的父类即NSObject类对比,相等,返回YES
  • BOOL re2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];

    • NSObject类NSObject的元类对比,不相等,返回NO
  • BOOL re3 = [(id)[LYPerson class] isKindOfClass:[LYPerson class]];

    • LYPerson类LYPerson的元类对比,不相等,继续下一步
    • LYPerson类LYPerson元类的父类即NSObject元类对比,不相等,继续下一步
    • LYPerson类NSObject元类的父类即NSObject类对比,不相等,继续下一步
    • LYPerson类NSObject类的父类即nil对比,不相等,结束,返回NO
  • BOOL re4 = [(id)[LYPerson class] isMemberOfClass:[LYPerson class]];

    • LYPerson类LYPerson的元类对比,不相等,返回NO
  • BOOL re5 = [(id)[NSObject alloc] isKindOfClass:[NSObject class]];

    • NSObject类NSObject对象的类NSObject类对比,相等,返回YES
  • BOOL re6 = [(id)[NSObject alloc] isMemberOfClass:[NSObject class]];

    • NSObject类NSObject对象的类对比,相等,返回YES
  • BOOL re7 = [(id)[LYPerson alloc] isKindOfClass:[LYPerson class]];

    • LYPerson类LYPerson对象的类对比,相等,返回YES
  • BOOL re8 = [(id)[LYPerson alloc] isMemberOfClass:[LYPerson class]];

    • LYPerson类LYPerson对象的类对比,相等,返回YES

方法归属

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

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

void lyClassMethod_classToMetaclass(Class pClass){
    
    const char *className = class_getName(pClass);
    Class metaClass = objc_getMetaClass(className);
    
    Method method5 = class_getClassMethod(pClass, @selector(sayHello));
    Method method6 = class_getClassMethod(metaClass, @selector(sayHello));

    Method method7 = class_getClassMethod(pClass, @selector(sayHappy));
    // 元类 为什么有 sayHappy 类方法 0 1
    //
    Method method8 = class_getClassMethod(metaClass, @selector(sayHappy));
    
    NSLog(@"%s-%p-%p-%p-%p",__func__,method5,method6,method7,method8);
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        LYPerson *person = [LYPerson alloc];
        Class pClass     = object_getClass(person);
        lyInstanceMethod_classToMetaclass(pClass);
        lyClassMethod_classToMetaclass(pClass);
        NSLog(@"Hello, World!");
    }
    return 0;
}

同样的,我们还是先来看下源码实现

源码分析

@interface LYPerson : NSObject
- (void)sayHello;
+ (void)sayHappy;
@end

@implementation LYPerson

- (void)sayHello{
    NSLog(@"LYPerson say : Hello!!!");
}

+ (void)sayHappy{
    NSLog(@"LYPerson say : Happy!!!");
}

@end

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);
}


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

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


// NOT identical to this->ISA when this is a metaclass
Class getMeta() {
    if (isMetaClass()) return (Class)this;
    else return this->ISA();
}

根据源码我们发现获取类方法的本质其实就是获取元类的实例方法

那么根据源码我们来分析下上面的题目,如下:

  • Method method1 = class_getInstanceMethod(pClass, @selector(sayHello));

    传入的pClassLYPerson类,需要去获取selName = sayHello的实例方法,由于我们知道实例方法存在类中,所以此处返回值method1是有值

  • Method method2 = class_getInstanceMethod(metaClass, @selector(sayHello));

    传入的metaClassLYPerson元类,需要去获取selName = sayHello的实例方法,其查找的顺序为元类 --> 根元类 --> 根类 --> nil,直到最后也没有找到,所以class_getInstanceMethod返回NULL,其method2的地址为0x0,表示未找到,所以此处返回值method2是没有值

  • Method method3 = class_getInstanceMethod(pClass, @selector(sayHappy));

    传入的pClassLYPerson类,需要去获取selName = sayHappy的实例方法,查找顺序为LYPerson类 --> 根类 --> nil,也没有找到sayhello实例方法,返回NULL,所以method3的地址为0x0,表示未找到,所以此处返回值method3是没有值

  • Method method4 = class_getInstanceMethod(metaClass, @selector(sayHappy));

    传入的metaClassLYPerson元类,需要去获取selName = sayHappy的实例方法,首先在LYPerson元类中查找,发现有sayHappy的实例方法,主要是因为类对象的类方法存储在元类中,类方法在元类中是实例方法,然后返回查找到的实例方法,所以method4的地址为0x100003148,表示找到了指定的实例方法,所以此处返回值method4是有值

  • Method method5 = class_getClassMethod(pClass, @selector(sayHello));

    传入的pClassLYPerson类,需要去获取selName = sayHello的类方法,首先判断 LYPerson类 是否是元类,此时不是,返回LYPerson的元类,然后在元类中查找 sayhello实例方法。查找顺序如下:元类 --> 根元类 --> 根类 --> nil,最后返回NULL,所以此处返回值method5是没有值

  • Method method6 = class_getClassMethod(metaClass, @selector(sayHello));

    传入的metaClassLYPerson元类,需要去获取selName = sayHello的类方法,首先判断 LYPerson元类 是否是元类,此时是,直接返回元类,然后在元类中查找 sayhello实例方法,发现并没有找到,返回NULL,所以此处返回值method6是没有值

  • Method method7 = class_getClassMethod(pClass, @selector(sayHappy));

    传入的pClassLYPerson类,需要去获取selName = sayHappy的类方法,首先判断 LYPerson类 是否是元类,此时不是,返回LYPerson的元类,然后在元类中查找 sayHappy实例方法,发现有这个实例方法,直接返回找到的实例方法,所以此处返回值method7是有值

  • Method method8 = class_getClassMethod(metaClass, @selector(sayHappy));

    传入的metaClassLYPerson元类,需要去获取selName = sayHappy的类方法,首先判断 LYPerson元类 是否是元类,此时是,直接返回元类,然后在元类中查找 sayHappy实例方法,发现有这个实例方法,直接返回找到的实例方法,所以此处返回值method5是有值

从上面的分析结果中,我们就发现了一个问题 method8也不为NULL,此时就很疑惑:元类中为什么会有 sayHappy 类方法?

主要还是因为class_getClassMethod方法在元类的判断导致的,这是苹果人为制造的 递归终止条件,目的就是防止无限次递归

具体代码如下:

Class getMeta() {
    if (isMetaClass()) return (Class)this;
    else return this->ISA();
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,482评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,377评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,762评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,273评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,289评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,046评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,351评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,988评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,476评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,948评论 2 324
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,064评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,712评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,261评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,264评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,486评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,511评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,802评论 2 345