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)
    {
        // <rdar://problem/46904873> 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
原因是编译器优化了执行的效率。

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

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