OC底层原理11:类 & isa 底层面试题分析

张建 lol

前言

本文的面试题主要涉及 isa走位 & 继承关系 & 类结构 相关的面试题以及针对面试题的分析

【面试题】类存在几份?

由于 类的信息 在内存中永远 只存在一份,所以 类对象只有一份

【百度面试题】objc_object对象 的关系?

  • 所有的 对象 都是以 objc_object 为模板 继承 过来的

  • 所有的 对象 都是 来自NSObject(来自于OC端),但是真正到 底层 是一个 objc_object(C/C++) 结构体 类型的

【总结】objc_object对象 的关系是 继承 关系

【面试题】什么是 属性 & 成员变量 & 实例变量

  • 属性(property):在 OC 中是通过 @property 开头 定义,且是 带下划线成员变量 + setter + getter方法 的变量

  • 成员变量(ivar):在 OC 类的 {} 中定义的,且 没有下划线的变量

  • 实例变量:通过当前对象类型,具备实例化的变量,是一种 特殊的成员变量,例如 NSObject、UILabel、UIButton

【面试题】元类 中为什么会有 类对象类方法

在上一章 OC底层原理10:类 & 类结构分析 中,我们知道了 实例方法存储在类中类方法存储在元类中

为了探索我们的面试题现象,定义了以下几个方法,来探索方法的归属问题

  • 在ZJPerson中定义一个实例方法和一个类方法,并实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@interface ZJPerson : NSObject
- (void)sayHello;
+ (void)sayHappy;
@end

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

+ (void)sayHappy{
NSLog(@"ZJPerson say : Happy!!!");
}
@end
  • main 主函数,调用自定义的方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int main(int argc, const char * argv[]) {
@autoreleasepool {
ZJPerson * p = [ZJPerson alloc];
Class pClass = object_getClass(p);
// 获取类的方法
zjObjc_copyMethodList(pClass);
// 获取类和元类的实例方法
zjInstanceMethod_classToMetaclass(pClass);
// 获取类和元类的类方法
zjClassMethod_classToMetaclass(pClass);
// 获取类和元类的方法实现
zjIMP_classToMetaclass(pClass);
NSLog(@"Hello, World!");
}
return 0;
}
  • zjObjc_copyMethodList :用于获取 类的方法列表
1
2
3
4
5
6
7
8
9
10
11
12
13
// 获取类的方法列表
void zjObjc_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));

ZJLog(@"Method, name: %@", key);
}
free(methods);
}

查看一下打印结果:

1
Method, name: sayHello

下面我们来分析一下打印结果:

【zjObjc_copyMethodList函数】

这个函数的主要作用是打印 中存在的 方法,由前面所知,实例方法 存储在 类中,因此打印结果只有 Method, name: sayHello

  • zjInstanceMethod_classToMetaclass :用于获取 类和元类实例方法
1
2
3
4
5
6
7
8
9
10
11
12
13
void zjInstanceMethod_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));

ZJLog(@"%s - %p-%p-%p-%p",__func__,method1,method2,method3,method4);
}

查看一下打印结果:

1
zjInstanceMethod_classToMetaclass - 0x100003100-0x0-0x0-0x100003098

下面我们来分析一下打印结果:

【zjInstanceMethod_classToMetaclass函数】:用于获取 类和元类类方法

在分析前先了解一下函数 class_getInstanceMethod :作用是获取实例方法,根据官方文档的解释:

其大致含义就是:如果在传入的类或者类的父类中没有找到指定的实例方法,则返回NULL

从上面的代码可知传入的 pClassZJPerson 类,metaClass 是 ZJPerson元类,函数中4个打印结果分别是:

  • method1的地址:0x100003100
    传入的 pClassZJPerson,查找的方法是sayHello实例方法,由于ZJPerson中有该方法,所以返回的地址是 0x100003100

  • method2的地址:0x0
    传入的 metaClassZJPerson元类,查找的方法是sayHello实例方法,由于ZJPerson中没有该方法,所以返回的地址是 0x0

  • method3的地址:0x0
    传入的 pClassZJPerson,查找的方法是sayHappy实例方法,由于ZJPerson中没有该方法,所以返回的地址是 0x0

  • method4的地址:0x100003098
    传入的 metaClassZJPerson元类,查找的方法是sayHappy实例方法,由于类ZJPerson中类方法sayHappys是以 实例方法 存储在 元类中的,因此元类ZJPerson 中有该方法,所以返回的地址是 0x100003098

  • zjClassMethod_classToMetaclass函数:获取类或类的父类中的类方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 类和元类-类方法
void zjClassMethod_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));

ZJLog(@"%s-%p-%p-%p-%p",__func__,method1,method2,method3,method4);
}

查看一下打印结果:

1
zjClassMethod_classToMetaclass-0x0-0x0-0x100003098-0x100003098

下面我们来分析一下打印结果:

class_getClassMethod:用于获取 类或类的父类类方法,根据官方文档的解释:

其大致含义就是:如果在传入的类或者类的父类中没有找到指定的类方法,则返回NULL

从上面的代码可知传入的 pClassZJPerson 类metaClassZJPerson元类,函数中4个打印结果分别是:

  • method1 的地址:0x0 传入的 pClassZJPerson,查找的方法是sayHello类方法,由于ZJPerson中没有该方法,所以返回的地址是 0x0

  • method2 的地址:0x0 传入的 metaClassZJPerson元类,查找的方法是sayHello类方法,由于 ZJPerson元类中没有该方法,所以返回的地址是 0x0

  • method3 的地址:0x100003098 传入的 pClassZJPerson,查找的方法是 sayHappy类方法,由于ZJPerson中有该方法,所以返回的地址是 0x100003098

  • method4的地址:0x100003098 传入的 metaClassZJPerson元类,查找的方法是 sayHappy类方法,由于类ZJPerson中 类方法sayHappys 是以 实例方法 存储在 元类中的,因此 元类ZJPerson 中有该 实例方法,那么为什么会返回 类方法sayHappy 的地址呢?

【问题】ZJPerson元类 为什么会有 类方法sayHappy

我们查看一下源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//获取类方法
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 判断是否是元类,是元类就直接返回,反之,继续找isa指向
Class getMeta() {
if (isMetaClass()) return (Class)this;
else return this->ISA();
}

由源码可知:class_getClassMethod 的实现是获取类的类方法,其本质就是 获取元类的实例方法,最终还是会走到 class_getInstanceMethod,但是在这里需要注意的一点是,在 getMeta 源码中,如果判断出 cls元类,那么就 不会 再继续往下 递归查找,会直接返回 this ,其目的是为了 防止元类的无限递归查找

【结论】
由源码可知:

  • 获取元类的类方法,本质是 获取元类的实例方法

  • 方法method4:是会返回地址的

  • zjIMP_classToMetaclass 函数:用于获取 类和元类 中的方法实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void zjIMP_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__);
}

查看一下打印结果:

1
0x1000017c0-0x7fff72080580-0x7fff72080580-0x1000017f0

下面我们来分析一下打印结果:

class_getMethodImplementation :用于获取 类或类的父类方法实现,根据官方文档的解释:

其大致含义就是:该函数在向类实例发送消息时会被调用,并返回一个指向 方法实现函数的指针。这个函数会比 method_getImplementation(class_getInstanceMethod(cls, name)) 更快。返回的函数指针可能是一个指向 runtime内部的函数,而不一定是方法的实际实现。如果类实例无法响应 selector ,则返回的函数指针将是运行时 消息转发机制 的一部分

下面我们也可以通过这个方法的源码来印证上面的这个说法,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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;
}

从上面的代码可知传入的 pClassZJPerson 类metaClassZJPerson元类,函数中4个打印结果分别是:

  • imp1的地址:0x1000017c0 传入的 pClassZJPerson,查找的是sayHello函数指针,由于ZJPerson中有该函数指针,所以返回的地址是 0x1000017c0

  • imp2的地址:0x7fff72080580 传入的 metaClassZJPerson元类,查找的方法是 sayHello类方法,根据 类方法存储在元类中 可知,sayHello是一个实例方法,并不存储在元类中,也没有其任何实现,所以进行了 消息转发

  • imp3的地址:0x7fff72080580 传入的 pClassZJPerson,查找的方法是 sayHappy类方法sayHappy 是一个类方法,并不存储在类中,也没有其任何实现,所以进行了 消息转发

  • imp4的地址:0x1000017f0 传入的 metaClassZJPerson元类,查找的方法是 sayHappy类方法,根据 类方法存储在元类中,可以在元类中查找到sayHappy 的具体实现,所以返回一个 imp函数指针的地址

【总结】

  • class_getInstanceMethod:获取 实例方法,如果指定的 类或其父类 不包含带有指定选择器的实例方法,则为NULL

  • class_getClassMethod:获取 类方法,如果指定的类或其父类不包含具有指定选择器的类方法,则为NULL。

  • class_getMethodImplementation:获取方法的具体实现,如果未查找到,则进行 消息转发

【面试题】iskindOfClass & isMemberOfClass 的理解

  • iskindOfClass & isMemberOfClass 类方法调用
1
2
3
4
5
BOOL re1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];       
BOOL re2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];
BOOL re3 = [(id)[ZJPerson class] isKindOfClass:[ZJPerson class]];
BOOL re4 = [(id)[ZJPerson class] isMemberOfClass:[ZJPerson class]];
NSLog(@" re1 :%hhd\n re2 :%hhd\n re3 :%hhd\n re4 :%hhd\n",re1,re2,re3,re4);

查看打印结果:

1
2
3
4
2020-09-16 23:45:31.732090+0800 KCObjc[41895:1261834]  re1 :1
re2 :0
re3 :0
re4 :0
  • iskindOfClass & isMemberOfClass 实例方法调用
1
2
3
4
5
6
BOOL re5 = [(id)[NSObject alloc] isKindOfClass:[NSObject class]];       
BOOL re6 = [(id)[NSObject alloc] isMemberOfClass:[NSObject class]];
BOOL re7 = [(id)[ZJPerson alloc] isKindOfClass:[ZJPerson class]];
BOOL re8 = [(id)[ZJPerson alloc] isMemberOfClass:[ZJPerson class]];

NSLog(@" re5 :%hhd\n re6 :%hhd\n re7 :%hhd\n re8 :%hhd\n",re5,re6,re7,re8);

查看打印结果:

1
2
3
4
5
2020-09-16 23:45:31.733480+0800 KCObjc[41895:1261834]  re5 :1
re6 :1
re7 :1
re8 :1
Program ended with exit code: 0

【问题】:那么是为什么呢?接下来我们通过 objc4 源码来分析一下:

  • 要想分析源码,我们需要深入理解 isa 流程图,如下:

注:NSObject类 和 NSObject根元类 不相等

  • 接下来我们查一下 isKindOfClass 调用的源码:

    • 查看 调用的源码:
    1
    2
    3
    4
    5
    6
    + (BOOL)isKindOfClass:(Class)cls {
    for (Class tcls = self->ISA(); tcls; tcls = tcls->superclass) {
    if (tcls == cls) return YES;
    }
    return NO;
    }
    • 查看 实例对象 调用的源码:
    1
    2
    3
    4
    5
    6
    - (BOOL)isKindOfClass:(Class)cls {
    for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
    if (tcls == cls) return YES;
    }
    return NO;
    }
  • 我们再查看一下 isMemberOfClass 调用的源码:

    • 查看 调用的源码:
    1
    2
    3
    + (BOOL)isMemberOfClass:(Class)cls {
    return self->ISA() == cls;
    }
    • 查看 实例对象 调用的源码:
    1
    2
    3
    4
    - (BOOL)isMemberOfClass:(Class)cls {
    return [self class] == cls;
    }

【源码分析总结】

  • isKindOfClass

    • 类方法:元类(isa) --> 根元类(父类) --> 根类(父类) --> nil(父类)传入类的对比

    • 实例方法:对象的类 --> 父类 --> 根类 --> nil传入类的对比

  • isMemberOfClass

    • 类方法: 类的元类传入类 对比

    • 实例方法:对象的父类传入类 对比

由上面的源码我们知道了具体调用的源码,由此我们来具体分析为什么打印1000 1111这个结果:

【使用类方法结果分析】

  • re1:1,是 NSObjectNSObject 的对比,使用 +isKindOfClass

    • NSObject(传入类,即 根类)vs NSObject的元类(即 根元类) 相比 – 不相等
    • NSObject(传入类,即 根类)vs 根元类的父类(即 根类)相比 – 相等

[NSObject class] isKindOfClass:[NSObject class]] 内部调用分析图:

由上面的分析图可知:打印结果为 1

  • re2:0,是 NSObjectNSObject 的对比,使用 +isMemberOfClass

    • NSObject (传入类,即 根类) vs NSObject的元类(即 根元类)对比 – 不相等

[NSObject class] isMemberOfClass:[NSObject class] 内部调用分析图:

  • re3:0,是 ZJPersonZJPerson 的对比,使用 +isKindOfClass

    • ZJPerson(传入 )vs ZJPerson的元类(即 元类ZJPerson) 对比 – 不相等
    • ZJPerson(传入 )vs 元类ZJPerson的父类(即 根元类) 对比 – 不相等
    • ZJPerson(传入 )vs 根元类的父类(即 根类) 对比 – 不相等
    • ZJPerson(传入 )vs 根类的父类(即 nil) 对比 – 不相等

[ZJPerson class] isKindOfClass:[ZJPerson class] 内部调用分析图:

  • re4:0,是 ZJPersonZJPerson 的对比,使用 +isMemberOfClass

    • ZJPerson(传入 )vs 元类 对比 – 不相等

[ZJPerson class] isMemberOfClass:[ZJPerson class] 内部调用分析图:

【使用实例方法结果分析】

  • re5:1,是 NSObject对象NSObject 的对比,使用 -isKindOfClass

    • NSObject(传入 根类)vs 对象的 isa(即 NSObject根类) 对比 – 相等

[NSObject alloc] isKindOfClass:[NSObject class] 内部调用分析图:

  • re6:1,是 NSObject对象NSObject 的对比,使用 -isMemberOfClass

    • NSObject(传入 根类)vs 对象的类(即 NSObject根类) 对比 – 相等

[NSObject alloc] isMemberOfClass:[NSObject class]内部调用分析图:

  • re7:1,是 ZJPerson对象ZJPerson 的对比,使用 -isKindOfClass

    • ZJPerson(传入 )vs 对象的 isa (即 ZJPerson) 对比 – 相等

[ZJPerson alloc] isKindOfClass:[ZJPerson class]内部调用分析图:

  • re8:1,是 ZJPerson对象ZJPerson 的对比,使用 -isMemberOfClass

    • ZJPerson(传入 )vs 对象的类(即 ZJPerson) 对比 – 相等

[ZJPerson alloc] isMemberOfClass:[ZJPerson class]

  • Post title:OC底层原理11:类 & isa 底层面试题分析
  • Post author:张建
  • Create time:2020-09-26 23:37:09
  • Post link:https://redefine.ohevan.com/2020/09/26/OC底层原理/OC底层原理11:类 & isa 底层面试题分析/
  • Copyright Notice:All articles in this blog are licensed under BY-NC-SA unless stating additionally.