OC类相关的经典面题分析

1、类存在几份?

由于类的信息在内存中永远只存在一份,所以 类对象只有一份,同样,元类对象也只有一份,在lldbpo类对象和元类对象的地址会输出相同的类名,因为po会调用类的description方法。

2、objc_object 与 对象的关系

objc_object是OC类的c/c++实现,没有直接的联系,编译器会在编译阶段将OC语法的类转译为c/c++objc_object结构体实现,objc_object结构体是OC类的底层模板。通过typedef struct objc_object *id;定义可以看到id这个可以指向任意OC类关键字实际是指向objc_object结构体的指针。

3、什么是 属性 & 成员变量 & 实例变量 ?
  • 属性在OC中是通过@property开头定义在类的头文件中,编译器会给这样声明的属性添加setget方法,另外在类内部也可以通过_ivarName的方式,即下划线加属性名的方式直接访问属性,而不必使用点语法,这样做效率更高,减少了setget方法的消息转发。
  • 成员变量在OC的类@implement{}中定义的,仅供类内部访问的变量,编译器不会生成setget方法。
  • 实例变量是对象类型的变量,是需要实例化的变量,是一种特殊的成员变量,例如 NSObject、UILabel、UIButton等。
4、如何证明元类中存放着类方法
//定义Person类
@interface Person : NSObject
- (void)sayHi;
+ (void)sayHaHa;
@end
//添加类方法和实例方法
#import "Person.h"
@implementation Person
- (void)sayHi{
    NSLog(@"Person say : Hi!!!");
}
+ (void)sayHaHa{
    NSLog(@"Person say : HaHa!!!");
}
@end

通过运行时方法class_copyMethodList可以获取一个类的实例的方法列表:

//打印方法名
void objc_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));
        
        CHLog(@"Method, name: %@", key);
    }
    free(methods);
}

main中运行以上代码:

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        Person *person = [Person alloc];
        Class pClass     = object_getClass(person);
        objc_copyMethodList(pClass);

    }
    return 0;
}

输出结果:

打印类实例方法.png

objc_copyMethodList法中我们传入了Person类对象,所以只会输出类的实例方法sayHi,符合预期,再看下面的方法输出:

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

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

这个方法分别针对Person类和元类调用class_getInstanceMethod方法,看看打印结果:

类元类获取方法地址.png

可以看到共打印了4个方法的地址,只有method1method4打印出了内容,其余都是0x0说明没找到对应的方法,这就很好的证明了实例方法sayHi可以在类对象pClass中找到,而类方法sayHaHa,只能在metaClass元类中找到,这4个方法都调用了class_getInstanceMethod,从方法名中可以判断是获取实例方法,进而说明对元类调用获取实例方法,就相当于获取一个类的类方法,因为类在内存中也是一个特殊的实例,元类中存储的就是这个特殊实例的实例方法。

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

    Method method3 = class_getClassMethod(pClass, @selector(sayHaHa));
    Method method4 = class_getClassMethod(metaClass, @selector(sayHaHa));
    
    CHLog(@"%s-%p-%p-%p-%p",__func__,method1,method2,method3,method4);
}

将上面方法中调用的class_getInstanceMethod换成class_getClassMethod,分别对类和元类进行测试,看看打印结果:

对类和元类调用获取类方法.png

打印结果只有method3method4打印出了地址,分析之前看看class_getClassMethod的源码实现:

//获取类方法
Method class_getClassMethod(Class cls, SEL sel)
{
    if (!cls  ||  !sel) return nil;

    return class_getInstanceMethod(cls->getMeta(), sel);
}
//获取一个类的元类,如果本身是元类,就直接返回自己,否则返回类的ISA()找到元类
Class getMeta() {
    if (isMetaClass()) return (Class)this;
    else return this->ISA();
}
  • method1是传入了Person类本身和实例方法sayHi,把参数带入到class_getClassMethod中分析,经过第一个if (!cls || !sel) return nil;条件判断,最终调用了class_getInstanceMethod传入了Person类的元类,sel参数是sayHi,这个方法在Person元类中是找不到的,因为它是一个实例方法,存放在类对象中,所以会返回0x0,找不到这个方法。
  • method2是传入了Person类的元类和实例方法sayHi,把参数带入到class_getClassMethod中分析,经过条件判断,最终调用了class_getInstanceMethod传入了Person类的元类,sel参数是sayHi,元类的getMeta()方法会返回自己,所以还是在Person元类中查找sayHi,因为它是一个实例方法,存放在类对象中,元类中没有这个方法,返回0x0,找不到这个方法。
  • method3是传入了Person类本身和类方法sayHaHa,把参数带入到class_getClassMethod中分析,最终调用了class_getInstanceMethod传入了Person类的元类,sel参数是sayHaHa,这个方法在Person元类中是存在的,因为sayHaHa是一个类方法,返回方法地址0x100003148
  • method4是传入了Person类的元类和类方法sayHaHa,把参数带入到class_getClassMethod中,最终调用了class_getInstanceMethod传入了Person类的元类,sel参数是sayHaHa,元类的getMeta()方法会返回自己,所以还是在Person元类中查找类方法sayHaHa,可以找到方法并返回方法地址0x100003148

为什么元类的getMeta()会返回自己,这个是为了防止无限递归,因为对一个类调用class_getClassMethod会顺着类的isa找到类的元类,而对元类调用class_getClassMethod的话,如果不返回元类自身,还是沿着元类的isa查找,最终会找到根元类,所有元类的isa都指向根元类,而根元类的isa指向了自己,就会在这里形成死循环,为了打破这个循环,就在元类的getMeta()方法中直接返回自己终止isa的循环。

再来看看下面的方法:

void imp_ClassToMetaclass(Class pClass){
    const char *className = class_getName(pClass);
    Class metaClass = objc_getMetaClass(className);
    // - (void)sayHi;
    // + (void)sayHaHa;
    IMP imp1 = class_getMethodImplementation(pClass, @selector(sayHi));
    IMP imp2 = class_getMethodImplementation(metaClass, @selector(sayHi));

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

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

结果打印:0x100001d10-0x7fff6a7d6580-0x7fff6a7d6580-0x100001d40,四个imp都会打印出来,不太符合直觉,从元类里面获取实例方法的实现也拿到了地址,从类里面获取类方法的实现也能拿到地址,看看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);
    //没有找到,则进行消息转发
    if (!imp) {
        return _objc_msgForward;
    }
    return imp;
}

从源码中可以知道由于消息转发机制的处理,即使类或者元类中找不到对应的类方法或者实例方法,最终通过消息转发,也能获取到对应方法的实现!

5、分析下面代码中iskindOfClass 、 isMemberOfClass 的打印结果:
void isKindAndisMember() {
    //-----调用类的 iskindOfClass & isMemberOfClass
    BOOL re1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];       //
    BOOL re2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];     //
    BOOL re3 = [(id)[Person class] isKindOfClass:[Person class]];       //
    BOOL re4 = [(id)[Person class] isMemberOfClass:[Person class]];     //
    NSLog(@"\nClass isKindAndisMember\n 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)[Person alloc] isKindOfClass:[Person class]];       //
    BOOL re8 = [(id)[Person alloc] isMemberOfClass:[Person class]];     //
    NSLog(@"\nInstance isKindAndisMember\n re5 :%hhd\n re6 :%hhd\n re7 :%hhd\n re8 :%hhd\n",re5,re6,re7,re8);
}

看下打印结果:

isKindisMember结果.png

分析前,先看看iskindOfClassisMemberOfClass的源码:

//isKindOfClass类方法
+ (BOOL)isKindOfClass:(Class)cls {
    for (Class tcls = self->ISA(); tcls; tcls = tcls->superclass) {
        if (tcls == cls) return YES;
    }
    return NO;
}
//isKindOfClass实例方法
- (BOOL)isKindOfClass:(Class)cls {
    for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
        if (tcls == cls) return YES;
    }
    return NO;
}
//isMemberOfClass类方法
+ (BOOL)isMemberOfClass:(Class)cls {
    return self->ISA() == cls;
}
//isMemberOfClass实例方法
- (BOOL)isMemberOfClass:(Class)cls {
    return [self class] == cls;
}

下面开始分析:

  • res1=[(id)[NSObject class] isKindOfClass:[NSObject class]];
    NSObject调用isKindOfClass,首先会调用+ (BOOL)isKindOfClass:(Class)cls类方法,其过程是for循环中tcls初始等于self->ISA(),也就是NSObject的元类,第一次循环相当于比较NSObject类和NSObject的元类是否地址相同,显然不相等,进入下一循环,此时tcls等于它的父类tcls->superclassNSObject元类就是根源类,根源类的superclass指向NSObject类,此时tcls就变成NSObject类,再次比较tcls和传入的cls,这两个值现在都是NSObject类,所以是相等的,返回YES, 所以res1打印结果就是1
  • re2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];
    NSObject调用isMemberOfClass,会调用+ (BOOL)isMemberOfClass:(Class)cls类方法,最终比较的是self->ISA()和传入的cls,也就是NSObject的元类和NSObject类,显然实不相等的,所以返回NOre2打印为0
  • re3 = [(id)[Person class] isKindOfClass:[Person class]];
    Person调用isKindOfClass,首先会调用+ (BOOL)isKindOfClass:(Class)cls类方法,for循环中tcls初始等于self->ISA(),也就是Person的元类,相当于比较Person类和Person的元类是否地址相同,不相等,进入下一循环,此时tcls等于它的父类tcls->superclass,也就是NSObject元类,是根源类,和Person类也不相等,继续向上寻找superclass,下一个循环中tcls就变成了NSObject类,根源类的superclassNSObject类,和Person类也不相等,继续再向上找superclassNSObject类的superclassnil,和Person类也不相等,最后由于tcls变成了nil循环终止,返回NO,所以打印re3也是0
  • re4 = [(id)[Person class] isMemberOfClass:[Person class]];
    Person调用isMemberOfClass,会调用Person类的+ (BOOL)isMemberOfClass:(Class)cls类方法,最终比较的是self->ISA()和传入的cls,也就是Person的元类和Person类,显然实不相等的,所以返回NOre4打印为0
  • re5 = [(id)[NSObject alloc] isKindOfClass:[NSObject class]];
    NSObject的实例调用isKindOfClass,会调用NSObject类的- (BOOL)isKindOfClass:(Class)cls实例方法,for循环中tcls初始等于[self class],也就是NSObject类,传入的参数cls[NSObject class],也是NSObject类,所以比较tclscls是相等的,返回YES,打印re51
  • re6 = [(id)[NSObject alloc] isMemberOfClass:[NSObject class]];
    NSObject的实例调用isMemberOfClass,会调用NSObject类的- (BOOL)isMemberOfClass:(Class)cls实例方法,比较的是[self class]和传入的[NSObject class]是否相等,显然是相等的,都是NSObject类,打印re61
  • re7 = [(id)[Person alloc] isKindOfClass:[Person class]];
    Person的实例调用isKindOfClass,会调用Person类的- (BOOL)isKindOfClass:(Class)cls实例方法,for循环中tcls初始等于[self class],也就是Person类,传入的参数cls[Person class],也是Person类,所以比较tclscls是相等的,返回YES,打印re71
  • re8 = [(id)[Person alloc] isMemberOfClass:[Person class]];
    Person的实例调用isMemberOfClass,会调用Person类的- (BOOL)isMemberOfClass:(Class)cls实例方法,比较的是[self class]和传入的[Person class]是否相等,显然是相等的,都是Person类,打印re81
坑点:

上面的分析似乎顺利成章,看不出破绽,都是对着isKindOfClassisMemberOfClass的代码实现一步一步分析的,而且分析的结果也是正确的,但是如果我们用断点调试的话就会发现这里面是有坑的:

断点.png

断点2.png

分别在以上位置下断点,单步运行就会发现不论是实例方法isKindOfClass还是类方法isKindOfClass都没进到对应的断电中,而isMemberOfClass都可以进入对应的方法,这个结果是不是很意外,难道我们上面分析的isKindOfClass流程都是错的吗,系统没有进入对应的isKindOfClass方法,那最终调用的又是那个方法呢?用汇编方式来看看最终调用的方法是什么:
汇编视图debug.png

如上图在Debug菜单选择Always Show Disassembly显示整个方法的汇编代码:
汇编分析.png

从图中标记1的红框可以看到,实际的isKindOfClass方法调用已经变成了objc_opt_isKindOfClass,而标记2的红框显示isMemberOfClass依然是消息转发objc_msgSend方式去调用,所以isMemberOfClass可以进入对应的方法断点,而isKindOfClass已经直接调用objc_opt_isKindOfClass的实现了,这里应该是llvm做的编译优化。在runtime源码中搜索一下objc_opt_isKindOfClass方法看看是否能找到这个方法:
objc_opt_isKindOfClass搜索.png

很容易就搜索到了,声明在objc-internal.h中,实现在NSObject.mm中,点击查看方法实现:

// 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]方法最终所执行的实现,在这个方法中打断点可以看到,不论是调用实例的isKindOfClass还是类的isKindOfClass,都会进入这个方法,这个方法和之前我们认为要走的+ (BOOL)isKindOfClass:(Class)cls- (BOOL)isKindOfClass:(Class)cls的实现基本是一致的,略有不同,分析一下执行过程:

  • re1re1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];
    进入objc_opt_isKindOfClass(id obj, Class otherClass)方法后,objNSObject类对象,后面然后通过Class cls = obj->getIsa();取得NSObject类的元类,之后进入for循环,这里就和+ (BOOL)isKindOfClass:(Class)cls的流程一致了,最终通过根源类的superclass判断相等
    同理,re3 = [(id)[Person class] isKindOfClass:[Person class]];的过程也是如此,此处就不再赘述了。
    再看实例对象调用objc_opt_isKindOfClass(id obj, Class otherClass)的情况:
  • re5re5 = [(id)[NSObject alloc] isKindOfClass:[NSObject class]];
    此时objc_opt_isKindOfClass方法的obj参数变成了NSObject的实例,通过Class cls = obj->getIsa();取得NSObject类对象,进入for循环,此时tclsNSObject类对象,另一个参数otherClass也是NSObject类对象,这两个必然是相等的,返回YES, 所以re5打印是1
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260