美文网首页iOS学习专题
一道OC综合题的反思

一道OC综合题的反思

作者: 01_Jack | 来源:发表于2020-03-03 12:30 被阅读0次

    前言

    这是一道很有意思的题,题目来自群友,据说原题出自sunny。自以为是的解答这道题后,群友抛出一个新的问题,发现之前的解释行不通,遂有此文。

    0x00 Code

    你可能见过这题,但后面有变形,别急着离开。

    @interface A : NSObject
    @property (copy) NSString *name;
    @end
    
    @implementation A
    - (void)print {
        NSLog(@"%@", _name);
    }
    @end
    
    
    @interface ViewController ()
    @end
    
    @implementation ViewController
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        id cls = [A class];
        void *obj = &cls;
        [((__bridge id)obj) print];
    }
    @end
    

    这题的考察点很多,我当时的解释大概是这样的:
    OC对象的内存分布是 isa+ivars,对象的起始地址存储着isa,即对象对应类的地址,通过isa可以找到对象的instanceMethods,所以题中的obj可以调用到print方法。至于name属性的值,还是因为isa+ivars,在函数栈中,先申请的内存地址高于后申请的内存地址,由于A类中只有一个属性,所以紧挨着cls的上一个变量就是对应name的值,当obj之上没有显示变量时,会找到方法对应的隐式参数 self与SEL,由于参数是从右向左压栈的,所以SEL先于self入栈,所以self与cls相邻,此时name的值就是方法中对应的隐藏参数self,即vc实例。

    0x01 变形的由来

    在答完这题后,群友的一句话让我石化了:如果去掉[super viewDidLoad]调用会崩。

    如果之前的解释是正确的,这里不应该Crash,因为隐式参数还在,但确实崩了。

    0x02 探索

    如图,去掉了super调用,并在print处打上断点,运行后作如下操作:

    先通过po查看_name,发现是个地址并非对象,所以NSLog中通过%@按照对象来解析显然是无法打印的,查看这个地址后发现里面存储着viewDidLoad对应的ASCII码。

    既然是ASCII码,如果用%s应该就可以正常显示了,再次做如下尝试:

    这里有几个注意点:

    1. 不能使用_name.UTF8String来转成char *,因为_name并不是NSString *类型
    2. 不能使用(__bridge char *)_name来转成char *,编译器会告诉你不兼容
    3. 通过(__bridge void *)_name_name转成通用类型指针,再通过%s控制符解析成字符串,此时程序是可以正常运行的并输出viewDidLoad

    回到问题本身,去掉super调用后,_name中的数据究竟是什么?由于内部存储着viewDidLoad 对应的ASCII码,猜测 _name 中的数据应该是viewDidLoad 对应的 SEL,再次运行程序后做如下操作验证:

    显然这个猜测是正确的。

    0x03 Xcode中参数压栈顺序

    做个简单的测试:

    void test(int a, int b) {
        int c = 1;
    }
    
    test(2, 3);
    

    控制台作如下操作:

    可以看到地址大小关系为a>b>c,并且这3个变量的地址是连续的。可见Xcode中的参数是从左往右压栈的。

    上文已经验证去掉super时,得到的其实是调用者的SEL,这就是隐藏参数中的SEL(因为参数从左往右压栈,self先入栈SEL后入栈,索引SEL与cls相邻)

    0x04 回归

    既然如此,那么为什么调用super的时候会打印出 vc的实例,按照隐藏参数来说,不应该是SEL吗?

    将代码改成原貌,并在[((__bridge id)obj) print]处打上断点,运行程序后做如下操作:


    获取obj地址,查看偏移8字节后的值(跳过isa)。发现图中两处1、2对应标记的地址相同,而且就是self与ViewController的类。此处偏移8字节后的值其实就是super,具体可见我的另一篇文章 《对super关键字的小验证》

    0x05 小结

    本题调用super时,对应找到的是super关键字生成的结构体的第一个成员变量,即消息接收者self。不调用super时找到的是对应调用函数的SEL(参数从左向右压栈,这里的SEL就是隐藏参数中的SEL),由于print中使用的是%@控制符,但此时对应找到的是字符串,由此导致崩溃,如果改成NSLog(@"%s", (__bridge void *)_name),仍然可以正常调用,此时输出viewDidLoad


    Have fun!

    相关文章

      网友评论

        本文标题:一道OC综合题的反思

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