今天我们来说一下关于孙源之前提出的那道经典面试题.
题目如下:
@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
架构上是这样。
最后
如果大家有什么疑问或者意见向左的地方,欢迎大家留言讨论。
网友评论