今天我们来说一下关于孙源之前提出的那道经典面试题.
题目如下:
@interface FJFPerson : NSObject
// name
@property (nonatomic, copy) NSString *name;
- (void)print;
@end
@implementation FJFPerson
- (void)print {
NSLog(@"my name is %@", self.name);
}
@end
- (void)viewDidLoad {
[super viewDidLoad];
id cls = [FJFPerson class];
void *obj = &cls;
[(__bridge id)obj print];
}
打印出来的结果为:
2019-10-02 19:13:52.387769+0800 FJFRuntimeInterviewQuestionDemo[16143:509566] my name is <ViewController: 0x7ff0aac05df0>
对于这个打印结果,我们先来说一下,之前比较官方的解释,然后我们再来说一下对于这个解释的疑问。
关于这个解释这篇文章也解释很清楚:
不同的是,我是通过汇编来解释堆栈关系,而我这边的重点是在第二部分
,如果对于这个解释已经了解,可以直接看第二部分
。
一.结果的官方解释
A.为什么不会崩溃
id cls = [FJFPerson class];
这句代码里面的cls
指向的是FJFPerson
这个类。void *obj = &cls;
然后在这里obj
是一个指向cls
的指针。而通过如下源码:
struct objc_object {
private:
isa_t isa;
}
struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits;
}
我们可以看出这里objc_object
这个对象的首字段是isa
指向一个Class
。
也就是说,这里的obj
就相当于一个FJFPerson
实例对象的指针,指向了cls
,而cls
就相当于isa
指针,指向了FJFPerson
类。
如下图所示:
-
[(__bridge id)obj print];
所以这里调用就相当于FJFPerson
实例对象的调用[person print]
,是能够正常调用
B.为什么打印出ViewController
对象
- 我们在
FJFPerson
的print
添加self
和&_name
的地址信息
- 我们在
@implementation FJFPerson
- (void)print {
NSLog(@"self: %p", self);
NSLog(@"self.name: %p", &_name);
NSLog(@"my name is %@", self.name);
}
@end
打印结果如下:
从这个地址信息,我们可以看出self
和name
之间的地址差8
个字节,即1个指针
的距离.
- 而在
id cls = [Spark class];
前面添加代码NSString *str = @"11111"; NSLog(@"cls address:%p str address:%p",&cls,&str);
, 打印出如下信息:
可以看出:
-
cls
的地址比str
的地址值大8
个字节。 -
self.name
和str
的地址值一样,指向字符串11111
因为函数调用采用栈的形式,栈的地址是从高地址到低地址,所以先入栈的str
比cls
大8
个字节,而print
函数里面的self
地址和cls
地址一致,是因为[obj print]
的是通过cls
即isa
来进行方法调用,所以self
就是obj
,而self.name
的地址由于大self
地址8
个字节,所以self.name
的地址刚好和str
地址一致。
也就是说这里栈参数的数据结构格式,对应了obj对象地址的数据结构。
2. 至于为什么打印的是ViewController
对象:
从上面分析我们可以看出self.name
的值是在cls
之前入栈的值,与cls
相差8
个字节,因此我们通过汇编分析下堆栈信息:
FJFRuntimeInterviewQuestionDemo`-[ViewController viewDidLoad]:
0x10f8b10d0 <+0>: pushq %rbp
0x10f8b10d1 <+1>: movq %rsp, %rbp
0x10f8b10d4 <+4>: subq $0x40, %rsp - rsp - 0x40 ->开辟64个字节栈空间
0x10f8b10d8 <+8>: movq %rdi, -0x8(%rbp) - <ViewController: 0x7fd291512f90> 将self的值 给(rdp - 0x8)
0x10f8b10dc <+12>: movq %rsi, -0x10(%rbp) - rsi的 “viewDidLoad”, 将self的值给rdp - 0x10
0x10f8b10e0 <+16>: movq -0x8(%rbp), %rsi - 将self的值给rsi
0x10f8b10e4 <+20>: movq %rsi, -0x20(%rbp) - 将rsi的值给self给 内存地址(rdp-0x20)
0x10f8b10e8 <+24>: movq 0x2e39(%rip), %rsi ; (void *)0x000000010f8b3f40: ViewController 将”ViewController”字符串地址给rsi
0x10f8b10ef <+31>: movq %rsi, -0x18(%rbp) - 将”ViewController”字符串的地址给(rbp - 0x18)
0x10f8b10f3 <+35>: movq 0x2d7e(%rip), %rsi ; “viewDidLoad" 将viewDidLoad的字符串地址给rsi
0x10f8b10fa <+42>: leaq -0x20(%rbp), %rdi - 将内存地址(rbp-0x20)的地址给rdi
0x10f8b10fe <+46>: callq 0x10f8b184e ; symbol stub for: objc_msgSendSuper2 跳转到objc_msgSendSuper2命令
0x10f8b1103 <+51>: movq 0x2de6(%rip), %rsi ; (void *)0x000000010f8b4008: FJFPerson 将文本地址给rsi
0x10f8b110a <+58>: movq 0x2d6f(%rip), %rdi ; “class” 将"class”给rdi
0x10f8b1111 <+65>: movq %rdi, -0x38(%rbp) - 将rdi(“class”)的值给(rbp-0x38)
0x10f8b1115 <+69>: movq %rsi, %rdi - 将rsi(“FJFPerson”)给rdi
0x10f8b1118 <+72>: movq -0x38(%rbp), %rsi - 将(rbp-0x38)的值给rsi
0x10f8b111c <+76>: callq *0x1ee6(%rip) ; (void *)0x00007fff503b1780: objc_msgSend 调用objc_msgSend方法
0x10f8b1122 <+82>: movq %rax, %rdi - 将返回值给rdi
0x10f8b1125 <+85>: callq 0x10f8b185a ; symbol stub for: objc_retainAutoreleasedReturnValue
0x10f8b112a <+90>: movq %rax, -0x28(%rbp) - 将cls返回值给(rbp-0x28)
-> 0x10f8b112e <+94>: leaq -0x28(%rbp), %rax 将rbp-0x28地址给rax
0x10f8b1132 <+98>: movq %rax, -0x30(%rbp) 将rax的值(obj)给(rbp-0x30)
0x10f8b1136 <+102>: movq -0x30(%rbp), %rdi 将(rbp-0x30)的值给rdi
0x10f8b113a <+106>: movq 0x2d47(%rip), %rsi ; “print” 将print给rsi
0x10f8b1141 <+113>: callq *0x1ec1(%rip) ; (void *)0x00007fff503b1780: objc_msgSend 进行print函数调用
经过汇编的分析,我们很容易看出cls
的之前入栈的是self
即viewController
本身。
以上就是官方给出的分析,我们总结一下:
所有NSObject
对象的首地址
都是指向这个对象的所属类,反过来说如果一个地址指向某个类,我们可以把这个地址
当做对象
去用。所以编译可以通过,进行方法调用也不会报错。
打印结果是ViewController
对象的原因是因为cls
在栈上的数据结构符合它作为真实类的数据结构,self.name
的地址正好是self
对象的地址.
二. 存在的疑问
大家都知道:
在
arm64
架构之前,isa
就是一个普通的指针,存储着class
、Meta-Class
对象的内存地址。
从arm64
架构开始,对isa
进行了优化,变成了一个共用体(union)
结构,还使用位域来存储更多的信息。
也就是说在arm64
架构中,一个实例对象比如person
的isa
指针并没有直接指向FJFPerson
类,而是需要isa
地址和相应架构的ISA_MASK
掩码进行相与,才能得到真正的指向FJFPerson
类的地址。
# if __arm64__
# define ISA_MASK 0x0000000ffffffff8ULL
# elif __x86_64__
# define ISA_MASK 0x00007ffffffffff8ULL
接下来我们再来分析下调用的方法:
- (void)viewDidLoad {
[super viewDidLoad];
id cls = [FJFPerson class];
void *obj = &cls;
[(__bridge id)obj print];
}
-
id cls = [FJFPerson class];
这句代码里cls
是指向FJFPerson
类的指针 -
void *obj = &cls;
这里的obj
是一个二级指针,是指向cls
的的指针,也就是说obj
里面存储的就是一个单纯的cls
地址值,并非是个共用体 -
[(__bridge id)obj print];
这里是进行一个消息的发送
我们将ViewController.m
转换为C++
代码看一下:
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc ViewController.m -o ViewController.cpp
我们可以看到如下C++
代码:
static void _I_ViewController_viewDidLoad(ViewController * self, SEL _cmd) {
((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("ViewController"))}, sel_registerName("viewDidLoad"));
id cls = ((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("FJFPerson"), sel_registerName("class"));
void *obj = &cls;
((void (*)(id, SEL))(void *)objc_msgSend)((id)obj, sel_registerName("print"));
}
我们可以看出这里是直接对obj
进行print
的消息发送。
接下来我们看下objc_msgSend
在arm64
架构上的源码:
/********************************************************************
*
* id objc_msgSend(id self, SEL _cmd, ...);
* IMP objc_msgLookup(id self, SEL _cmd, ...);
*
* objc_msgLookup ABI:
* IMP returned in x17
* x16 reserved for our use but not used
*
********************************************************************/
.data
.align 3
.globl _objc_debug_taggedpointer_classes
_objc_debug_taggedpointer_classes:
.fill 16, 8, 0
.globl _objc_debug_taggedpointer_ext_classes
_objc_debug_taggedpointer_ext_classes:
.fill 256, 8, 0
ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame
MESSENGER_START
cmp x0, #0 // nil check and tagged pointer check 检查x0即isa是否为nil或者tagged Pointer
b.le LNilOrTagged // (MSB tagged pointer looks negative) 如果为nil或者tagged Pointer 就跳转到 LNilOrTagged
ldr x13, [x0] // x13 = isa 将isa的值给x13
and x16, x13, #ISA_MASK // x16 = class 将isa与isa_mask进行与操作得到相关的类地址
LGetIsaDone:
CacheLookup NORMAL // calls imp or objc_msgSend_uncached 进入正常的缓存方法查找
LNilOrTagged:
b.eq LReturnZero // nil check nil的检测
// tagged
mov x10, #0xf000000000000000
cmp x0, x10
b.hs LExtTag
adrp x10, _objc_debug_taggedpointer_classes@PAGE
add x10, x10, _objc_debug_taggedpointer_classes@PAGEOFF
ubfx x11, x0, #60, #4
ldr x16, [x10, x11, LSL #3]
b LGetIsaDone
通过源码我们可以分析:
[(__bridge id)obj print];
这句代码走到源码里面,源码取obj
存储的地址,也就是cls
的地址跟ISA_MASK
进行相与,我们会发现,cls
的地址与ISA_MASK
相与后的结果是cls
地址,我们加入一个FJFPerson
的实例对象tmpPerson
,发现tmpPerson
的isa
和ISA_MASK
相与也是真正的类FJFPerson
地址,即cls
的地址。
(lldb) p/x cls
(id) $0 = 0x0000000104c98fe8
(lldb) p/x obj
(FJFPerson *) $1 = 0x000000016b16bd78
(lldb) p/x tmpPerson->isa
(Class) $2 = 0x000001a104c98fed FJFPerson
(lldb) p/x 0x000001a104c98fed & 0x0000000ffffffff8ULL
(unsigned long long) $3 = 0x0000000104c98fe8
(lldb) p/x 0x0000000104c98fe8 & 0x0000000ffffffff8ULL
(unsigned long long) $4 = 0x0000000104c98fe8
我们将FJFPerson
类地址与ISA_MASK
掩码进行位数的比对:
0x0000000 1 0 2 f 1 4 f e 8
0x0000000 f f f f f f f f 8
我们会发现ISA_MASK
掩码为1的位数是大于类地址的位数,而后面的8
即二进制的1000
是因为字节对齐而补上的000
,同时我们将其他几个类地址打印输出看一下:
这里我们可以推导出,类地址
是实例对象的isa指针
和ISA_MASK
掩码相与后得到的地址,所以当类地址
再次和ISA_MASK
掩码相与之后得到的肯定是类地址本身
。
因此,这道题目之所以能够正常的调用,就在于类地址的取值
的设计,而上面说的如果某个地址
指向类对象的地址
,我们可以把这个地址
当做对象
去用的说法,是有点牵强的,至少在arm64
架构上是这样。
三.补充
有简友说这道题目的打印是个巧合,并建议我看一下release
的相关汇编的堆栈。
这里我分别打印了 真机 iOS 13、iOS 12.2 、iOS9 release版本的汇编
,模拟器 iPhone8 iOS13 release版本的汇编
,模拟器 iPhone8 iOS11.2版本的汇编
.
分析如下:
1. 真机 iOS 13 、 iOS 13、iOS 12.2 、iOS9 release版本的汇编(正常输出)
汇编代码:
FJFRuntimeInterviewQuestionDemo`-[ViewController viewDidLoad]:
0x102b165e0 <+0>: sub sp, sp, #0x30 ; =0x30 将sp-0x30,开辟栈空间
0x102b165e4 <+4>: stp x29, x30, [sp, #0x20] 将x29,x30放在(x29栈帧地址、x30程序链接寄存器)(sp+0x20)的地址处
0x102b165e8 <+8>: add x29, sp, #0x20 ; =0x20 将sp+0x20栈地址赋值给x29
-> 0x102b165ec <+12>: nop 空指令
0x102b165f0 <+16>: ldr x8, #0x28e0 ; (void *)0x0000000102b18ee0: ViewController 将0x28e0的地址给x8
0x102b165f4 <+20>: stp x0, x8, [sp, #0x10] 将x0, x8放入(sp+0x10)的地址处,x0是self(当前viewController地址),x8是字符串(“ ViewController")
0x102b165f8 <+24>: nop 空指令
0x102b165fc <+28>: ldr x1, #0x2824 ; "viewDidLoad” 将0x2824的值赋值给x1
0x102b16600 <+32>: add x0, sp, #0x10 ; =0x10 将sp+0x10的值赋值给x0
0x102b16604 <+36>: bl 0x102b16990 ; symbol stub for: objc_msgSendSuper2 调用objc_msgSendSuper2
0x102b16608 <+40>: nop 空指令
0x102b1660c <+44>: ldr x0, #0x288c ; (void *)0x0000000102b18fa8: FJFPerson将0x288c的值赋值给ldr
0x102b16610 <+48>: nop 空指令
0x102b16614 <+52>: ldr x1, #0x2814 ; "class” 将0x2814的值赋值给x1
0x102b16618 <+56>: bl 0x102b16984 ; symbol stub for: objc_msgSend 调用objc_msgSend
0x102b1661c <+60>: mov x29, x29 将x29的值赋值给x29,恢复栈
0x102b16620 <+64>: bl 0x102b169a8 ; symbol stub for: objc_retainAutoreleasedReturnValue 调用autorelase
0x102b16624 <+68>: str x0, [sp, #0x8] 将x0的值存储到sp+0x8的位置 (void *)0x0000000102e80f80: FJFPerson即为cls
0x102b16628 <+72>: nop 空指令
0x102b1662c <+76>: ldr x1, #0x2804 ; "print” 调用print
0x102b16630 <+80>: add x0, sp, #0x8 ; =0x8
0x102b16634 <+84>: bl 0x102b16984 ; symbol stub for: objc_msgSend
0x102b16638 <+88>: ldr x0, [sp, #0x8]
0x102b1663c <+92>: bl 0x102b1699c ; symbol stub for: objc_release
0x102b16640 <+96>: ldp x29, x30, [sp, #0x20]
0x102b16644 <+100>: add sp, sp, #0x30 ; =0x30
0x102b16648 <+104>: ret
堆栈信息:
寄存器信息:
2. 模拟器 iPhone8 iOS13 release版本的汇编(正常输出)
FJFRuntimeInterviewQuestionDemo`-[ViewController viewDidLoad]:
0x10dfde4cc <+0>: pushq %rbp rdp入栈
0x10dfde4cd <+1>: movq %rsp, %rbp 将rsp的值赋值给rdp
0x10dfde4d0 <+4>: pushq %r14 r14入栈
0x10dfde4d2 <+6>: pushq %rbx rbx入栈 rbx为self的地址值
0x10dfde4d3 <+7>: subq $0x20, %rsp rsp - 0x20的值给rsp
0x10dfde4d7 <+11>: leaq -0x28(%rbp), %rax 将rdp-0x28的地址给rax
0x10dfde4db <+15>: movq %rdi, (%rax) 将rdi的值赋值给rax所在地址空间 rdi是self的地址值
0x10dfde4de <+18>: movq 0x29fb(%rip), %rcx ; (void *)0x000000010dfe0ef8: ViewController 将字符串(viewController)赋值给rcx
0x10dfde4e5 <+25>: movq %rcx, 0x8(%rax) 将rcx的值赋值给(rax+0x8),也就是字符串”viewController”赋值给rax+0x8所在地址
0x10dfde4e9 <+29>: movq 0x2940(%rip), %rsi ; “viewDidLoad”,将字符串”viewDidLoad”的值赋值给rsi
0x10dfde4f0 <+36>: movq %rax, %rdi 将rax的值赋值给rdi, rdi的值为rdp-0x28
0x10dfde4f3 <+39>: callq 0x10dfde85e ; symbol stub for: objc_msgSendSuper2 调用objc_msgSendSuper2
-> 0x10dfde4f8 <+44>: movq 0x29a9(%rip), %rdi ; (void *)0x000000010dfe0fc0: FJFPerson
0x10dfde4ff <+51>: movq 0x2932(%rip), %rsi ; "class"
0x10dfde506 <+58>: movq 0x1afb(%rip), %r14 ; (void *)0x00007fff503b1780: objc_msgSend
0x10dfde50d <+65>: callq *%r14 调用objc_msgSend
0x10dfde510 <+68>: movq %rax, %rdi 将rax的值赋值给rdi,rax即为cls
0x10dfde513 <+71>: callq 0x10dfde86a ; symbol stub for: objc_retainAutoreleasedReturnValue 调用autorelease
0x10dfde518 <+76>: leaq -0x18(%rbp), %rbx 将(rbp-0x18)的地址值给rbx
0x10dfde51c <+80>: movq %rax, (%rbx) 将rax的值放入rbx所在的地址值,cls放在(rbp-0x18)
0x10dfde51f <+83>: movq 0x291a(%rip), %rsi ; "print" 将”print“地址给rsi
0x10dfde526 <+90>: movq %rbx, %rdi 将rbx的地址给rdi,即rdi的地址为(rbp-0x18)
0x10dfde529 <+93>: callq *%r14
0x10dfde52c <+96>: movq (%rbx), %rdi
0x10dfde52f <+99>: callq *0x1adb(%rip) ; (void *)0x00007fff503cb040: objc_release
0x10dfde535 <+105>: addq $0x20, %rsp
0x10dfde539 <+109>: popq %rbx
0x10dfde53a <+110>: popq %r14
0x10dfde53c <+112>: popq %rbp
0x10dfde53d <+113>: retq
堆栈信息:
寄存器信息:
3. 模拟器 iPhone8 iOS11.2版本的汇编 (崩溃)
汇编代码
FJFRuntimeInterviewQuestionDemo`-[ViewController viewDidLoad]:
0x104ce74cc <+0>: pushq %rbp rdp入栈
0x104ce74cd <+1>: movq %rsp, %rbp 将rsp的值赋值给rdp
0x104ce74d0 <+4>: pushq %r14 r14入栈 _UIApplicationLinkedOnVersion
0x104ce74d2 <+6>: pushq %rbx rbx入栈 "count" 是char类型的字符串
0x104ce74d3 <+7>: subq $0x20, %rsp rsp - 0x20的值给rsp
0x104ce74d7 <+11>: leaq -0x28(%rbp), %rax 将rdp-0x28的地址给rax
0x104ce74db <+15>: movq %rdi, (%rax) 将rdi的值赋值给rax所在地址空间
0x104ce74de <+18>: movq 0x29fb(%rip), %rcx ; (void *)0x0000000104ce9ef8: ViewController 将字符串(viewController)赋值给rcx
0x104ce74e5 <+25>: movq %rcx, 0x8(%rax) 将rcx的值赋值给rax+0x8地址
0x104ce74e9 <+29>: movq 0x2940(%rip), %rsi ; “viewDidLoad" 将字符串”viewDidLoad”的值赋值给rsi
0x104ce74f0 <+36>: movq %rax, %rdi 将rax的值赋值给rdi
0x104ce74f3 <+39>: callq 0x104ce785e ; symbol stub for: objc_msgSendSuper2 调用objc_msgSendSuper2
-> 0x104ce74f8 <+44>: movq 0x29a9(%rip), %rdi ; (void *)0x0000000104ce9fc0: FJFPerson 将FJFPerson地址给rdi
0x104ce74ff <+51>: movq 0x2932(%rip), %rsi ; “class" 将字符串”class“的地址值给rsi
0x104ce7506 <+58>: movq 0x1afb(%rip), %r14 ; (void *)0x000000010560b940: objc_msgSend 将objc_msgSend给r14
0x104ce750d <+65>: callq *%r14 调用objc_msgSend
0x104ce7510 <+68>: movq %rax, %rdi 将rax给rdi,rax为cls
0x104ce7513 <+71>: callq 0x104ce786a ; symbol stub for: objc_retainAutoreleasedReturnValue 调用objc_retainAutoreleasedReturnValue
0x104ce7518 <+76>: leaq -0x18(%rbp), %rbx 将(rbp-0x18)的值给rbx,
0x104ce751c <+80>: movq %rax, (%rbx) 将rax的值放入rbx指向的地址空间
0x104ce751f <+83>: movq 0x291a(%rip), %rsi ; “print” 将”print“地址给rsi
0x104ce7526 <+90>: movq %rbx, %rdi 将rbx的值给rdi
0x104ce7529 <+93>: callq *%r14 调用objc_msgSend
0x104ce752c <+96>: movq (%rbx), %rdi
0x104ce752f <+99>: callq *0x1adb(%rip) ; (void *)0x0000000105608cc0: objc_release
0x104ce7535 <+105>: addq $0x20, %rsp
0x104ce7539 <+109>: popq %rbx
0x104ce753a <+110>: popq %r14
0x104ce753c <+112>: popq %rbp
0x104ce753d <+113>: retq
堆栈日志:
寄存器信息:
从以上测试结果,我们可以看出,是否崩溃取决于在cls
之前入栈的值是否是NSObject
类型,能否打印出my name is <ViewController: 0x7ff0aac05df0>
,根本在于cls
之前入栈的是否是self
的地址。
因此之前的打印结果是ViewController对象的原因:
因为cls
在栈上的数据结构符合它作为真实类的数据结构,self.name
的地址正好是self
对象的地址,这个说法并没有错误。
最后
如果大家有什么疑问或者意见向左的地方,欢迎大家留言讨论。