美文网首页
有关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