美文网首页Mac·iOS开发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