美文网首页iOS/MacOS开发IT/互联网iOS面试
关于孙源那道经典iOS面试题目的疑问

关于孙源那道经典iOS面试题目的疑问

作者: 林大鹏 | 来源:发表于2019-10-12 11:48 被阅读0次

今天我们来说一下关于孙源之前提出的那道经典面试题.
题目如下:

@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>
打印结果.png

对于这个打印结果,我们先来说一下,之前比较官方的解释,然后我们再来说一下对于这个解释的疑问。

关于这个解释这篇文章也解释很清楚:

一道值得思考的iOS面试题

不同的是,我是通过汇编来解释堆栈关系,而我这边的重点是在第二部分,如果对于这个解释已经了解,可以直接看第二部分

一.结果的官方解释

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类。

如下图所示:

对照图.png
  • [(__bridge id)obj print];所以这里调用就相当于FJFPerson实例对象的调用[person print],是能够正常调用

B.为什么打印出ViewController对象

    1. 我们在FJFPersonprint添加self&_name的地址信息
@implementation FJFPerson
- (void)print {
    NSLog(@"self: %p", self);
    NSLog(@"self.name: %p", &_name);
    
    NSLog(@"my name is %@", self.name);
}
@end

打印结果如下:

地址信息.png

从这个地址信息,我们可以看出selfname之间的地址差8个字节,即1个指针的距离.

  • 而在id cls = [Spark class];前面添加代码NSString *str = @"11111"; NSLog(@"cls address:%p str address:%p",&cls,&str);, 打印出如下信息:
地址对照图.png

可以看出:

  • cls的地址比str的地址值大8个字节。
  • self.namestr的地址值一样,指向字符串11111

因为函数调用采用栈的形式,栈的地址是从高地址到低地址,所以先入栈的strcls8个字节,而print函数里面的self地址和cls地址一致,是因为[obj print]的是通过clsisa来进行方法调用,所以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函数调用 
当前栈.png

经过汇编的分析,我们很容易看出cls的之前入栈的是selfviewController本身。

以上就是官方给出的分析,我们总结一下:

所有NSObject对象的首地址都是指向这个对象的所属类,反过来说如果一个地址指向某个类,我们可以把这个地址当做对象去用。所以编译可以通过,进行方法调用也不会报错。

打印结果是ViewController对象的原因是因为cls在栈上的数据结构符合它作为真实类的数据结构,self.name的地址正好是self对象的地址.

二. 存在的疑问

大家都知道:

arm64架构之前,isa就是一个普通的指针,存储着classMeta-Class对象的内存地址。
arm64架构开始,对isa进行了优化,变成了一个共用体(union)结构,还使用位域来存储更多的信息。

isa.jpg

也就是说在arm64架构中,一个实例对象比如personisa指针并没有直接指向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_msgSendarm64架构上的源码:

/********************************************************************
 *
 * 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,发现tmpPersonisaISA_MASK相与也是真正的类FJFPerson地址,即cls的地址。

地址.png
(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,同时我们将其他几个类地址打印输出看一下:

其他类地址.png

这里我们可以推导出,类地址实例对象的isa指针ISA_MASK掩码相与后得到的地址,所以当类地址再次和ISA_MASK掩码相与之后得到的肯定是类地址本身

因此,这道题目之所以能够正常的调用,就在于类地址的取值的设计,而上面说的如果某个地址指向类对象的地址,我们可以把这个地址当做对象去用的说法,是有点牵强的,至少在arm64架构上是这样。

测试demo

最后

如果大家有什么疑问或者意见向左的地方,欢迎大家留言讨论。

相关文章

网友评论

    本文标题:关于孙源那道经典iOS面试题目的疑问

    本文链接:https://www.haomeiwen.com/subject/wqvepctx.html