美文网首页
有关iOS内存的一道面试题

有关iOS内存的一道面试题

作者: 过气的程序员DZ | 来源:发表于2020-09-06 17:44 被阅读0次

    有关内存的一道面试题:

    1. 能不能运行起来?为什么?
    2. 运行起来打印结果是什么?为什么?
    @interface Person : NSObject
    @property (copy, nonatomic) NSString *name;
    - (void)sayHello;
    @end
    
    @implementation Person
    - (void)sayHello {
        NSLog(@"%s - %@", __func__, self.name);
    }
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        id obj = [Person class];
        void *p = &obj;
        [(__bridge id)p sayHello];
    }
    
    @end
    

    如果一起问了上面的几个问题,那么第一个问题“能不能运行?”就很好解答了。肯定是能运行起来的,要不然后面的问题就没有意义了。我们直接运行看结果!

    运行结果:

    -[Person sayHello] - <ViewController: 0x7fa04c80be50>
    

    1.解答问题1:为什么能运行?


    逐行代码分析:

    1.1 id obj = [Person class];

    [Person class];返回的是Class类型

    + (Class)class OBJC_SWIFT_UNAVAILABLE("use 'aClass.self' instead");
    

    Class类型是一个结构体指针struct objc_class *。结构体中,第一个成员变量是isa

    typedef struct objc_class *Class;
    
    struct objc_class {
        Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
    
    //省略后续无关代码
    ......
    
    } OBJC2_UNAVAILABLE;
    

    id修饰的实例变量类型,一个实例变量在底层也是一个结构体指针struct objc_object *。结构体中第一个成员变量也是isa

    /// Represents an instance of a class.
    struct objc_object {
        Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
    };
    
    /// A pointer to an instance of a class.
    typedef struct objc_object *id;
    

    因此这行代码id obj = [Person class];可以理解为一个Class对象被强制转换成实例对象类型。

    1.2 void *p = &obj;

    定义一个指针类型p,指向obj的地址。

    我们先看看正常创建一个对象,内存结构是什么情况

    Person *per = [Person alloc];
    NSLog(@"%@, %p", per, &per);
    

    运行打印结果:

    <Person: 0x60000376c850>, 0x7ffeebd450e8
    

    per指针指向通过Person创建出来的实例对象在内存中开辟空间的首地址。per->Person实例地址->实例的isa


    而这行代码void *p = &obj;其实就是完成的是per->Person实例地址的过程,而第一句代码id obj = [Person class];就是Person实例地址->实例的isa过程。

    1.3 [(__bridge id)p sayHello];

    此时p调用sayHello,就相当于Person的实例调用sayHello。因此sayHello方法可以正常调用的。

    2.解答问题2:为什么打印<ViewController: 0x7fa04c80be50>


    内存空间开辟是连续的,而且alloc出来的对象存储在堆区。先看看正常创建对象:

    Person *per = [Person alloc];
    per.name = @"DZ";
    NSLog(@"%@, %p", per, &per);
    

    我们用lldb命令查看内存情况


    0x6代表的是堆区,堆区存储是从低地址到高地址的方式存储。0x000000010b3bf530地址存放的是per实例对象的isa,0x000000010b3bd038地址存放的是属性name。

    我们再来看看这道题开辟的内存情况:


    • 临时变量都是存放在栈区,所以地址是0x7开头的。
    • obj变量与p指向的首地址相同,说明obj被当做了p对象的isa
    • 此时调用实例属性name,会找isa后面偏移的8个地址中的值,也就是图中的0x00007fa04c80be50地址
    • 这个地址就是self,这也就是会打印ViewController: 0x7fa04c80be50

    栈区存储是从高地址到底地址的方式存储。入栈顺序如图:


    2.1 为什么会打印self呢?

    上图中在变量obj之前的栈空间的内容我们还不清楚,但是我们知道一点,在obj之前入栈的肯定是self。现在就开始研究在obj之前入栈的是一些什么东西。

    通过xcrun命令查看c++层面做了什么处理,进入到ViewController.m所在的路径,执行命令:

    xcrun -sdk iphonesimulator clang -rewrite-objc ViewController.m
    

    底层实现

    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 obj = ((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("class"));
        void *p = &obj;
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("sayHello"));
    }
    
    • 编译期间super关键字是调用objc_msgSendSuper函数,而它的第一个参数是一个结构体。结构体中有两个属性,一个是self,一个是ViewControlsuperclass,也就是UIViewController
    • 而结构体入栈是后面的属性先入栈,也就是说反向入栈。先入栈UIViewController,再入栈self
    • 再前面的入栈数据,就是viewDidLoad的参数了,先入栈的是ViewController * self,再入栈SEL _cmd
    • 也就是说入栈的先后顺序是:self->_cmd->UIViewController->self->obj->p

    因此题目中当调用属性name的时候,找到的是self

    2.2 进阶-打印栈中的数据

    - (void)viewDidLoad {
        [super viewDidLoad];
        
        id obj = [Person class];
        void *p = &obj;
        [(__bridge id)p sayHello];
        
        NSLog(@"==打印栈中数据==");
        void *sp  = (void *)&self;//self的地址
        void *end = (void *)&p;//p的地址
        long count = (sp - end) / 0x8;//都是指针类型8字节,所以除以8
        
        for (long i = 0; i <= count; i++) {
            void *address = sp - 0x8 * i;
            if ( i == 1) {//_cmd特殊处理一下,因为是字符串类型
                NSLog(@"%p : %s",address, *(char **)address);
            }else{
                NSLog(@"%p : %@",address, *(void **)address);
            }
        }
    }
    

    通过这种方式,我们可以清楚的看到栈中的内存情况,也证明了结构体属性是“反向”入栈的规则。

    2.2.1 superclass问题

    此处有一个问题,superclass的位置,为什么打印的是ViewController,不是应该打印UIViewController么?

    • 因为我们查看c++代码的时候,这个阶段是编译期间。编译期间super调用的是objc_msgSendSuper
    • 而在运行时,调用的是objc_msgSendSuper2,结构体中第二个参数是当前类。所以此处是ViewController
    • 可以参看本人的另一篇文章有关[self class]和[super class]的面试题,文章中有说明。

    3.扩展一下-添加入栈变量


    添加一行代码:

    - (void)viewDidLoad {
        [super viewDidLoad];
        
        NSString *temp = @"123";//追加的代码
        
        id obj = [Person class];
        void *p = &obj;
        [(__bridge id)p sayHello];
    }
    

    打印结果:

    -[Person sayHello] - 123
    

    如果你真的理解了,就会知道打印这个结果的原因。如果还不明白,请回到篇头,再仔细阅读一遍此文章了。

    相关文章

      网友评论

          本文标题:有关iOS内存的一道面试题

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