OC底层原理11:类 & isa 底层面试题分析
前言
本文的面试题主要涉及 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 | @interface ZJPerson : NSObject |
- 在
main
主函数,调用自定义的方法
1 | int main(int argc, const char * argv[]) { |
zjObjc_copyMethodList
:用于获取类的方法列表
1 | // 获取类的方法列表 |
查看一下打印结果:
1 | Method, name: sayHello |
下面我们来分析一下打印结果:
【zjObjc_copyMethodList函数】
这个函数的主要作用是打印 类
中存在的 方法
,由前面所知,实例方法
存储在 类中
,因此打印结果只有 Method, name: sayHello
zjInstanceMethod_classToMetaclass
:用于获取类和元类
的实例方法
1 | void zjInstanceMethod_classToMetaclass(Class pClass){ |
查看一下打印结果:
1 | zjInstanceMethod_classToMetaclass - 0x100003100-0x0-0x0-0x100003098 |
下面我们来分析一下打印结果:
【zjInstanceMethod_classToMetaclass函数】:用于获取 类和元类
的 类方法
在分析前先了解一下函数 class_getInstanceMethod
:作用是获取实例方法,根据官方文档的解释:
其大致含义就是:如果在传入的类或者类的父类中没有找到指定的实例方法,则返回NULL
从上面的代码可知传入的 pClass
是 ZJPerson 类
,metaClass 是 ZJPerson元类
,函数中4个打印结果分别是:
method1
的地址:0x100003100
传入的pClass
是ZJPerson
,查找的方法是sayHello实例方法
,由于ZJPerson中有该方法,所以返回的地址是0x100003100
method2
的地址:0x0
传入的metaClass
是ZJPerson元类
,查找的方法是sayHello实例方法
,由于ZJPerson中没有该方法,所以返回的地址是0x0
method3
的地址:0x0
传入的pClass
是ZJPerson
,查找的方法是sayHappy实例方法
,由于ZJPerson中没有该方法,所以返回的地址是0x0
method4
的地址:0x100003098
传入的metaClass
是ZJPerson元类
,查找的方法是sayHappy实例方法
,由于类ZJPerson中类方法sayHappys
是以实例方法
存储在元类中
的,因此元类ZJPerson
中有该方法,所以返回的地址是0x100003098
zjClassMethod_classToMetaclass
函数:获取类或类的父类中的类方法
1 | // 类和元类-类方法 |
查看一下打印结果:
1 | zjClassMethod_classToMetaclass-0x0-0x0-0x100003098-0x100003098 |
下面我们来分析一下打印结果:
class_getClassMethod
:用于获取 类或类的父类
的 类方法
,根据官方文档的解释:
其大致含义就是:如果在传入的类或者类的父类中没有找到指定的类方法,则返回NULL
从上面的代码可知传入的 pClass
是 ZJPerson 类
,metaClass
是 ZJPerson元类
,函数中4个打印结果分别是:
method1
的地址:0x0
传入的pClass
是ZJPerson
,查找的方法是sayHello类方法
,由于ZJPerson中没有该方法,所以返回的地址是0x0
method2
的地址:0x0
传入的metaClass
是ZJPerson元类
,查找的方法是sayHello类方法
,由于ZJPerson元类
中没有该方法,所以返回的地址是0x0
method3
的地址:0x100003098
传入的pClass
是ZJPerson
,查找的方法是sayHappy类方法
,由于ZJPerson中有该方法,所以返回的地址是0x100003098
method4
的地址:0x100003098
传入的metaClass
是ZJPerson元类
,查找的方法是sayHappy类方法
,由于类ZJPerson中类方法sayHappys
是以实例方法
存储在元类中
的,因此元类ZJPerson
中有该实例方法
,那么为什么会返回类方法sayHappy
的地址呢?
【问题】:ZJPerson元类
为什么会有 类方法sayHappy
?
我们查看一下源码:
1 | //获取类方法 |
由源码可知:class_getClassMethod
的实现是获取类的类方法,其本质就是 获取元类的实例方法
,最终还是会走到 class_getInstanceMethod
,但是在这里需要注意的一点是,在 getMeta
源码中,如果判断出 cls
是 元类
,那么就 不会
再继续往下 递归查找
,会直接返回 this
,其目的是为了 防止元类的无限递归查找
【结论】
由源码可知:
获取元类的类方法,本质是
获取元类的实例方法
方法method4:是会返回地址的
zjIMP_classToMetaclass
函数:用于获取类和元类
中的方法实现
1 | void zjIMP_classToMetaclass(Class pClass){ |
查看一下打印结果:
1 | 0x1000017c0-0x7fff72080580-0x7fff72080580-0x1000017f0 |
下面我们来分析一下打印结果:
class_getMethodImplementation
:用于获取 类或类的父类
的 方法实现
,根据官方文档的解释:
其大致含义就是:该函数在向类实例发送消息时会被调用,并返回一个指向 方法实现函数的指针
。这个函数会比 method_getImplementation(class_getInstanceMethod(cls, name))
更快。返回的函数指针可能是一个指向 runtime内部的函数
,而不一定是方法的实际实现。如果类实例无法响应 selector
,则返回的函数指针将是运行时 消息转发机制
的一部分
下面我们也可以通过这个方法的源码来印证上面的这个说法,
1 | IMP class_getMethodImplementation(Class cls, SEL sel) |
从上面的代码可知传入的 pClass
是 ZJPerson 类
,metaClass
是 ZJPerson元类
,函数中4个打印结果分别是:
imp1
的地址:0x1000017c0
传入的pClass
是ZJPerson
,查找的是sayHello函数指针
,由于ZJPerson中有该函数指针,所以返回的地址是0x1000017c0
imp2
的地址:0x7fff72080580
传入的metaClass
是ZJPerson元类
,查找的方法是sayHello类方法
,根据类方法存储在元类中
可知,sayHello
是一个实例方法,并不存储在元类中,也没有其任何实现,所以进行了消息转发
imp3
的地址:0x7fff72080580
传入的pClass
是ZJPerson
,查找的方法是sayHappy类方法
,sayHappy
是一个类方法,并不存储在类中,也没有其任何实现,所以进行了消息转发
imp4
的地址:0x1000017f0
传入的metaClass
是ZJPerson元类
,查找的方法是sayHappy类方法
,根据类方法存储在元类中
,可以在元类中查找到sayHappy
的具体实现,所以返回一个imp函数指针的地址
【总结】
class_getInstanceMethod
:获取实例方法
,如果指定的类或其父类
不包含带有指定选择器的实例方法,则为NULLclass_getClassMethod
:获取类方法
,如果指定的类或其父类不包含具有指定选择器的类方法,则为NULL。class_getMethodImplementation
:获取方法的具体实现,如果未查找到,则进行消息转发
【面试题】iskindOfClass & isMemberOfClass 的理解
iskindOfClass & isMemberOfClass
类方法调用
1 | BOOL re1 = [(id)[NSObject class] isKindOfClass:[NSObject class]]; |
查看打印结果:
1 | 2020-09-16 23:45:31.732090+0800 KCObjc[41895:1261834] re1 :1 |
iskindOfClass & isMemberOfClass
实例方法调用
1 | BOOL re5 = [(id)[NSObject alloc] isKindOfClass:[NSObject class]]; |
查看打印结果:
1 | 2020-09-16 23:45:31.733480+0800 KCObjc[41895:1261834] re5 :1 |
【问题】:那么是为什么呢?接下来我们通过 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,是
NSObject
和NSObject
的对比,使用+isKindOfClass
- NSObject(传入类,即
根类
)vs NSObject的元类(即根元类
) 相比 –不相等
- NSObject(传入类,即
根类
)vs 根元类的父类(即根类
)相比 –相等
- NSObject(传入类,即
[NSObject class] isKindOfClass:[NSObject class]]
内部调用分析图:
由上面的分析图可知:打印结果为 1
re2:0,是
NSObject
和NSObject
的对比,使用+isMemberOfClass
- NSObject (传入类,即
根类
) vs NSObject的元类(即根元类
)对比 –不相等
- NSObject (传入类,即
[NSObject class] isMemberOfClass:[NSObject class]
内部调用分析图:
re3:0,是
ZJPerson
与ZJPerson
的对比,使用+isKindOfClass
- ZJPerson(传入
类
)vs ZJPerson的元类(即元类ZJPerson
) 对比 –不相等
- ZJPerson(传入
类
)vs 元类ZJPerson的父类(即根元类
) 对比 –不相等
- ZJPerson(传入
类
)vs 根元类的父类(即根类
) 对比 –不相等
- ZJPerson(传入
类
)vs 根类的父类(即nil
) 对比 –不相等
- ZJPerson(传入
[ZJPerson class] isKindOfClass:[ZJPerson class]
内部调用分析图:
re4:0,是
ZJPerson
与ZJPerson
的对比,使用+isMemberOfClass
- ZJPerson(传入
类
)vs元类
对比 –不相等
- ZJPerson(传入
[ZJPerson class] isMemberOfClass:[ZJPerson class]
内部调用分析图:
【使用实例方法结果分析】
re5:1,是
NSObject对象
和NSObject
的对比,使用-isKindOfClass
- NSObject(传入
根类
)vs 对象的isa
(即NSObject根类
) 对比 –相等
- NSObject(传入
[NSObject alloc] isKindOfClass:[NSObject class]
内部调用分析图:
re6:1,是
NSObject对象
和NSObject
的对比,使用-isMemberOfClass
- NSObject(传入
根类
)vs 对象的类(即NSObject根类
) 对比 –相等
- NSObject(传入
[NSObject alloc] isMemberOfClass:[NSObject class]
内部调用分析图:
re7:1,是
ZJPerson对象
和ZJPerson
的对比,使用-isKindOfClass
- ZJPerson(传入
类
)vs 对象的isa
(即ZJPerson
) 对比 –相等
- ZJPerson(传入
[ZJPerson alloc] isKindOfClass:[ZJPerson class]
内部调用分析图:
re8:1,是
ZJPerson对象
和ZJPerson
的对比,使用-isMemberOfClass
- ZJPerson(传入
类
)vs 对象的类(即ZJPerson
) 对比 –相等
- 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.