学习MJ的视频课程,整理总结知识点–class、super
isKindOfClass 和 isMemberOfClass
isKindOfClass
和isMemberOfClass
的源码如下
1 | - (Class)class { |
打印结果如下
1 | id person = [[MJPerson alloc] init]; |
super面试题
这里有一道面试题,如下,相应的DemoInterview02-super
1 | @interface MJPerson : NSObject |
问[(__bridge id)obj print]
执行结果?
从viewDidLoad
函数内开始分析:
1 | id cls = [MJPerson class]; // 栈中定义一个变量cls,cls指向MJPerson这个类 |
我们先分析一个正常的调用场景的过程,如下
1 | id person = [[MJPerson alloc] init]; |
person
指针指向MJPerson的一个实例对象,当调用print
方法时,结合我们前面所学的Class的底层结构知道,会先通过person的isa指针拿到MJPerson这个类,在类中查找print
方法并调用。
person指向的实例对象的底层结构是一个结构体,这个结构体的第一个成员是isa指针,也就意味着person指针指向的实例对象地址,同时也是实例对象的isa的地址,而实例对象isa的指向的地址是MJPerson这个类的地址,内部就会有类似如下的指针指向关系。person ----> isa ----> MJPerson
我们再回过去分析上面的问题,它的指针指向关系如下:obj ----> cls ----> MJPerson
两者虽然内容不一样,但指向关系相同。所以我们可以分析[(__bridge id)obj print];
的流程,首先取obj指针指向的地址,为cls
,等价于正常方法调用的流程的取isa指针;然后取isa指向的对象地址,正常流程是MJPerson,而cls
指针指向的地址也是MJPerson,所以能正常调用print
方法。
那么打印的self.name
结果是什么呢
1 | - (void)print { |
上面我们可以分析出,[(__bridge id)obj print]
这段代码可以正确的执行,我们知道,_name
是MJPerson实例对象里面的一个属性,而实例对象结构体第一个成员是isa指针,接下来是_name
,也就是print
函数的self.name
是isa地址后面8个字节的空间的内容。
而obj
是存储在栈空间的一个变量,栈空间是从高地址->低地址依次分配内存的,我们先从简单的分析入手,把代码改成如下这样:
1 | - (void)viewDidLoad { |
此时打印的的结果是my name is 123
,我们分析下此时栈空间的内存地址结构
低地址->高地址 | 指针指向的内存区域 |
---|---|
obj | 指针指向cls |
cls | 指针指向MJPerson |
str | 指针指向”123” |
我们回到函数[(__bridge id)obj print]
的调用流程进行分析,调用过程中会取obj的isa
,并且把isa的地址增加_name
的大小作为偏移量,也就是8字节。也就是会取obj
指向的cls
,并把cls
的地址增加8个字节的偏移量作为_name
的内存地址,此时便宜后刚好是str
的内存区域。就解释了打印的结果是123
。
那么我们回到原本的问题,如下
1 | - (void)viewDidLoad { |
此时cls
的地址增加8字节是谁呢,这里有一个[super viewDidLoad];
调用,我们之前的文章分析过,[super message];
用clang重写会生成如下代码
1 | struct objc_super arg = {self, [UIViewController class]}; |
这个过程会生成一个objc_super
类型的结构体,结构体的第一个成员是self
,也就是ViewController
的实例对象。此时的函数内栈内存地址图如下
低地址->高地址 | 指针指向的内存区域 |
---|---|
obj | 指针指向cls |
cls | 指针指向MJPerson |
self | 指针指向ViewController实例对象 |
类指针 | 指针指向[UIViewController class] |
所以根据偏移量计算,_name
的位置刚好是self
对象,所以程序运行的结果如下
1 | [(__bridge id)obj print]; // my name is <ViewController: 0x600002eade10> |
super本质
对于[super message]
这里有一些知识要补充。
[super message]
的底层本质,我们上面也总结过,我们是根据源码导出的编译环境下的C++代码总结的,实际上并不能完全代表运行时的样子。
时机上运行时,真正调用的和编译时是不一样的,我们通过断点调试[super viewDidLoad]
的执行过程,选择Xcode->Debug->Debug Workflow->Always Show Disassembly
,来查看真实调用时转化的汇编代码,关键部分如下
1 | 0x10b457383 <+35>: movq 0x2b9e(%rip), %rsi ; "viewDidLoad" |
我们看到[super message]
真实调用的底层函数是objc_msgSendSuper2
,我们在苹果开源的objc4没有找到objc_msgSendSuper2
的C实现,不过有汇编实现,如下
1 | OBJC_EXPORT id _Nullable |
我们可以分析出objc_msgSendSuper2函数的第1个参数是receiver,第2个参数是class,但是它会通过class->superclass
获取父类。我们编译阶段导出的源码第2个参数直接是superclass。两者还是有一些细微差别的。
验证_objc_msgSendSuper2的实现
上面关于_objc_msgSendSuper2
的实现是我们根据汇编代码推出来的,实际上我们还可以通过运行时进行调试来验证我们推到的结论。
我们在[(__bridge id)obj print];
添加断点,进行Debug,如下
1 | (lldb) p obj |
我们发现obj紧挨着的下一个内存指向的是MJPerson,接下来指向的是ViewController的实例对象,再接下来指向的是ViewController的类。
如下表所示,验证了我们的猜想
低地址->高地址 | 指针指向的内存区域 |
---|---|
obj 0x00007ffee94da4e8 | 指针指向cls |
cls 0x0000000106725010 | 指针指向MJPerson |
self 0x00007ff795704800 | 指针指向ViewController实例对象 |
类指针 0x0000000106724f48 | 指针指向ViewController |
总结
关于super面试题的分析,我们要知道这结构要点
- 熟悉OC的消息发送;拿到receiver,取receiver的isa,根据isa找class
- 熟悉实例对象的内存结构;isa后面跟着成员变量
- 栈空间是从高地址->低地址依次分配内存
- [super message]会先生生成一个
objc_super
的结构体,然后转化为objc_msgSendSuper(结构体, SEL)
进行调用
super调用,底层会转换为objc_msgSendSuper2函数的调用,接收2个参数
struct objc_super2
SEL
1 | struct objc_super2 { |