iOS面试 - 方法归属&isKindOfClass

iOS开发底层探究之路

本篇文章将从几个面试题出发,探究方法的归属以及isa与Superclass。

objc_object 与对象的关系,objc_object 与 NSObject的关系

  • 所有对象在底层都是以objc_object为模版继承来的。
  • 所有对象都是继承自NSObject(根类),而在底层中NSObject 是一个objc_objectC/C++)结构体。
    所以结论:objc_object对象之间是继承关系。

属性、成员变量、实例变量

  • 属性:带下划线的成员变量 + setter + getter,以@property 开头定义的变量
  • 成员变量:定义在类.h文件的{}中的,不带下划线的变量
  • 实例变量:特殊的成员变量,经过实例化的成员变量对象,例如UIButtonUILabel
  • 成员变量中除去基本数据类型及{}中定义的NSString类型变量,剩下的都是实例化后的实例变量,实例变量可理解为是拥有属性对象

方法的归属分析

下面通过一个例子来探究实例方法及类方法的归属问题:

#import <Foundation/Foundation.h>
@interface LGPerson : NSObject
- (void)sayHello;
+ (void)sayHappy;
@end
#import "LGPerson.h"
@implementation LGPerson
- (void)sayHello{
    NSLog(@"LGPerson say : Hello!!!");
}
+ (void)sayHappy{
    NSLog(@"LGPerson say : Happy!!!");
}
@end

定义继承于NSObject的LGPerson类,并添加一个实例方法sayHello,一个类方法sayHappy,下面通过几个打印情况来分析:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        // 0x0000000100000000
        // LGTeacher *teacher = [LGTeacher alloc];

        LGPerson *person = [LGPerson alloc];
        Class pClass     = object_getClass(person);
        lgObjc_copyMethodList(pClass);

        lgInstanceMethod_classToMetaclass(pClass);
        lgClassMethod_classToMetaclass(pClass);
        lgIMP_classToMetaclass(pClass);
        NSLog(@"Hello, World!");
    }
    return 0;
}

方法从上而下的定义如下面:

#ifdef DEBUG
#define LGLog(format, ...) printf("%s\n", [[NSString stringWithFormat:format, ## __VA_ARGS__] UTF8String]);
#else
#define LGLog(format, ...);
#endif

void lgObjc_copyMethodList(Class pClass){
    unsigned int count = 0;
    Method *methods = class_copyMethodList(pClass, &count);
    for (unsigned int i=0; i < count; i++) {
        Method const method = methods[i];
        //获取方法名
        NSString *key = NSStringFromSelector(method_getName(method));
        
        LGLog(@"Method, name: %@", key);
    }
    free(methods);
}

void lgInstanceMethod_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));
    
    LGLog(@"%s - %p-%p-%p-%p",__func__,method1,method2,method3,method4);
}

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

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

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

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

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

下面我们先给出打印结果,然后逐个分析:


方法归属结果打印

lgObjc_copyMethodList

object_getClass 获取当前实例对象person类对象
此方法参数为Class类型的pClass,一个类对象class_copyMethodList 获取当前类对象中的方法列表,遍历此类对象所有的实例方法,根据打印情况只打印sayHello实例方法,可以验证类对象里面存放实例方法,类方法不存放在类对象中。

lgInstanceMethod_classToMetaclass

方法中的metaClass为当前方法参数类对象pClass的元类,由objc_getMetaClass 根据类获取到元类,class_getInstanceMethod 获取实例方法的源码分析后可知,在传入类及传入类的父类一级一级找下去直到找到返回,没找到返回null,下面分别分析几个打印情况:

  • method1:0x1000031b0

因为sayHello是实例方法,当前pClass刚好也是存放sayHelloLGPerson 类,所以能找到方法并打印

  • method2:0x0

因为metaClass为元类,元类LGPerson)--->根元类NSObject)--->NSObject--->nil,发现找不到当前的sayHello方法,说明返回null,打印为0x0

  • method3:0x0

当前需要找方法sayHappyLGPerson)--->根类NSObject)--->nil,没找到sayHappy方法,所以返回null,打印0x0

  • method4:0x100003148

寻找类方法sayHappymetaClass为当前类的元类,类方法存在类的元类中,所以此时在metaClass 中就找到sayHppy方法,即可返回并打印0x100003148

lgClassMethod_classToMetaclass

跟上面一样,方法参数pClass类对象,metaClass为当前类的元类class_getClassMethod 获取类方法源代码如下:

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

    return class_getInstanceMethod(cls->getMeta(), sel);
}
Class getMeta() {
        if (isMetaClass()) return (Class)this;
        else return this->ISA();
    }

class_getClassMethod 源码可见,获取类的类方法就是去获取当前类的元类的实例方法getMeta()就是返回当前类的元类(如果getMeta()方法当前调用者为元类时,直接返回自己,不是的话就返回当前调用者的元类),下面分别分析打印情况:

  • method1:0x0

sayhello实例方法,class_getClassMethod当前传入为类,不是元类,拿到元类,然后class_getInstanceMethod 方法寻找,元类 ---> 根元类--->NSObject ---> nil,没找到sayHello,返回null,打印0x0

  • method2:0x0

class_getClassMethod当前传入为metaClass元类,直接进行class_getInstanceMethod 方法查找当前元类--->根元类---> NSObject--->nil,没找到实例方法sayHello方法,所以返回null,打印0x0

  • method3:0x100003148

当前查找方法为类方法sayHappy,在pClass类中寻找,所以class_getClassMethod 中要拿到元类,然后进行查找,发现在元类中找到类方法sayHappy,所以能打印方法地址0x100003148

  • method4: 0x100003148

class_getClassMethod 传入即为类的元类,所以直接进行class_getInstanceMethod 方法查找,在当前元类找到了sayHappy类方法,所以返回找到的方法

lgIMP_classToMetaclass

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

如果在当前类对象里没找到对应的方法实现,就会触发底层的消息转发,所以打印结果是四个都有值,而不是0x0

  • method1:0x100001d10

sayHello 实例方法,在当前pClass类中能找到当前方法,所以method1返回的是sayHello方法地址指针

  • method2:0x7fff6cd17580

metaClass 元类中寻找实例方法sayHello,是找不到的,所以进行了底层的消息转发,返回的不是sayHello的实现地址

  • method3:0x7fff6cd17580

sayHappy 类方法,因为类方法是存储在元类里的,所以在类pClass中找不到sayHappy方法实现地址,所以触发了底层消息转发

  • method4:0x100001d40

metaClass元类 中找到了sayHappy方法实现,所以返回的就是真正的sayHappy实现地址

isKindOfClass & isMemberOfClass

同样,利用上面的类LGPerson进行下面探究:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        BOOL re1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];       //
        BOOL re2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];     //
        BOOL re3 = [(id)[LGPerson class] isKindOfClass:[LGPerson class]];       //
        BOOL re4 = [(id)[LGPerson class] isMemberOfClass:[LGPerson 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)[LGPerson alloc] isKindOfClass:[LGPerson class]];       //
        BOOL re8 = [(id)[LGPerson alloc] isMemberOfClass:[LGPerson class]];     //
        NSLog(@" re5 :%hhd\n re6 :%hhd\n re7 :%hhd\n re8 :%hhd\n",re5,re6,re7,re8);
    }
    return 0;
}
isKind&isMember结果

首先理清一下isKindOfClass 方法及isMemberOfClass 方法:

  • isKindOfClass方法:
     + (BOOL)isKindOfClass:(Class)cls {
    // 类 vs 元类
    // 根元类 vs NSObject
    // NSObject vs NSObject
    // LGPerson vs 元类 (根元类) (NSObject)
    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 比较线路:元类 ---> 根元类 ---> NSObject ---> nil 与当前cls类比较。实例方法-isKindOfClass比较路线:对象的类 ---> 父类 ---> 根类 ---> nilcls比较。

注意(有坑!!!)


你以为就是这样了?当然不是,isKindOfClass 方法在llvm编译器在编译期做了编译优化,isKindOfClass 在底层走的方法是objc_opt_isKindOfClass ,打开源码,搜索objc_opt_isKindOfClass 打断点调试,发现调用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);
}

可见,无论是实例方法还是类方法调用,比较的对象链:元类 ---> 根元类 ---> NSObject ---> nil ,也就是说isKindOfClass 就是遵循isa走向的规则


  • isMemberOfClass方法:
    + (BOOL)isMemberOfClass:(Class)cls {
        return self->ISA() == cls;
    }
    
     - (BOOL)isMemberOfClass:(Class)cls {
         return [self class] == cls;
     }
    
    类方法+isMemberOfClass 判断的是当前是否是元类,实例方法-isMemberOfClass 判断是否是当前对象的类

类方法re1re2re3re4
实例方法re5re6re7re8

  • re1: 1

首先拿到前者NSObject 的元类(根元类),与后者NSObject类比较,不相等,找根元类的父类,也就是NSObject类,比较结果为相等,所以返回YES,打印1

  • re2: 0

NSObject 元类与NSObject类当然不相等,所以返回NO

  • re3: 0

LGPerson 类 与 LGPerson 元类不想等,与 LGPerson 根元类不想等,与 NSObject类不想等,与 nil不想等,所以返回NO

  • re4: 0

LGPerson 类与LGPerson 元类不相等,返回NO

  • re5: 1

NSObject实例对象的类NSObject 当然与NSObject 类相等,返回YES

  • re6: 1

NSObject实例对象的类NSObject 当然与NSObject 类相等

  • re7: 1

LGPerson 实例对象的类LGPersonLGPerson 类相等

  • re8: 1

LGPerson 实例对象的类LGPersonLGPerson 类相等

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