iOS经典面试题之分析“内存平移”的原理

一、抛砖引玉

  • 现在有一个YDWPerson类 ,其中有一个属性 name 和一个实例方法saySomething,如下:
	@interface YDWPerson : NSObject
	
	@property (nonatomic, copy) NSString *name;
	- (void)saySomething;
	
	@end
	
	@implementation YDWPerson
	
	- (void)saySomething {
    
    
	    NSLog(@"%s",__func__);
	}

	@end
  • 通过以下代码的方式,能否调用实例方法?为什么?
	Class cls = [YDWPerson class];
    void *boy = &cls;
    [(__bridge id)boy saySomething];

二、调试分析

① 不访问类变量
  • 在日常开发中,我们采取的如下的调用方式:
	YDWPerson *person = [YDWPerson alloc];
    [person saySomething];
  • 运行代码,可以看到,其中两种方式都可以调用成功:

在这里插入图片描述

  • 而 boy 指针指向 YDWPerson 类的地址,和对象 person 的 isa 指向 YDwPerson 类的地址一样,所以 [(__bridge id)boy saySomething];这里也能调用对象方法。
  • [person saySomething]的本质是对象发送消息,那么 person 的 isa 指向类YDWPerso,即 person 的首地址指向 YDWPerson 的首地址,我们可以通过YDWPerson 的内存平移找到 cache,在 cache 中查找方法:

在这里插入图片描述

  • [(__bridge id)boy saySomething] 中的 boy 是来自于 YDWPerson 这个类,然后有一个指针 boy,将其指向 YDWPerson 的首地址:

在这里插入图片描述

  • 因此,person 是指向 YDWPerson 类的结构,boy 也是指向 YDWPerson 类的结构,然后都是在 YDWPerson 中的 methodList 中查找方法:

在这里插入图片描述

② 访问打印类变量
  • 修改 saySomething 方法实现,如下所示:
	@implementation YDWPerson
	
	- (void)saySomething {
    
    
	    NSLog(@"%s %@",__func__, self.name);
	}
	
	@end
  • 再次运行,查看打印结果:

在这里插入图片描述

  • 可以看到:boy 调用打印的 name 是 <ViewController: 0x7fbee94051c0>,而 person 调用打印的 name 则是 (null),为什么打印不一致呢?
③ 原理分析
  • 首先 person 调用 name,是由于 self 指向 person 的内存结构,然后通过内存平移8字节,去取值 name,即 self 指针首地址平移 8 字节获得,如下:

在这里插入图片描述

  • boy 是内存中一个 8 字节的指,指向 cls,相当于 person 指针指向 YDWPerson 的一个实例,所以 self.name 的获取,相当于 boy 首地址的指针也需要平移 8 字节寻找 name,那么此时的 boy 的指针地址是多少?平移 8 字节获得的是什么呢?
  • boy 是一个指针,是存在栈中的,栈是一个先进后出的结构,参数传入就是一个不断压栈的过程:
    • 其中隐藏参数会压入栈,且每个函数都会有两个隐藏参数(id self,sel _cmd),可以通过 clang 查看底层编译;
    • 隐藏参数压栈的过程,其地址是递减的,而栈是从高地址->低地址分配的,即在栈中,参数会从前往后一直压栈(栈是一个先进后出的队列,内存从高地址到低地址分配,所以先压入栈的地址高);
    • super 通过 clang 查看底层的编译,是objc_msgSendSuper,其第一个参数是一个结构体__rw_objc_super(self,class_getSuperclass);
	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"));
	
	    Class cls = ((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("YDWPerson"), sel_registerName("class"));
	    void *boy = &cls;
	    YDWPerson *person = ((YDWPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("YDWPerson"), sel_registerName("alloc"));
	    
	    ((void (*)(id, SEL))(void *)objc_msgSend)((id)(__bridgeid)boy, sel_registerName("saySomething"));
	    ((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("saySomething"));
	}
  • 因此入栈的变量如下:self–>_cmd–> cls–> kc–> person,但是[super viewDidLoad];调用时会产生一个结构体传入参数,因此这个结构体也会被压入到当前栈中:self–>_cmd–>(id)class_getSuperclass(objc_getClass(“YDWTeacher”))–>self–> cls–> boy–> person;
    • self和_cmd是viewDidLoad方法的两个隐藏参数,是高地址->低地址正向压栈的;
    • class_getSuperClass 和 self为objc_msgSendSuper2中的结构体成员,是从最后一个成员变量,即低地址->高地址反向压栈的;
  • 添加以下代码:
	Class cls = [YDWPerson class];
    void *boy = &cls;
    YDWPerson *person = [YDWPerson alloc];
    NSLog(@"%p - %p", &person, boy);
    
    void *sp  = (void *)&self;
    void *end = (void *)&person;
    long count = (sp - end) / 0x8;
        
    for (long i = 0; i<count; i++) {
    
    
        void *address = sp - 0x8 * i;
        if ( i == 1) {
    
    
            NSLog(@"%p : %s",address, *(char **)address);
        } else {
    
    
            NSLog(@"%p : %@",address, *(void **)address);
        }
    }
    [(__bridge id)boy saySomething];
    [person saySomething];
  • 运行结果如下所示:
	// 0x7ffee77cd058:person   0x7ffee77cd068:boy
	2021-03-20 20:19:16.190498+0800 内存平移[68463:5044422] 0x7ffee77cd058 - 0x7ffee77cd068
	// self
	2021-03-20 20:19:16.190649+0800 内存平移[68463:5044422] 0x7ffee77cd088 : <ViewController: 0x7f7fbe2045f0>
	// _cmd
	2021-03-20 20:19:16.190745+0800 内存平移[68463:5044422] 0x7ffee77cd080 : viewDidLoad
	// superclass
	2021-03-20 20:19:16.190853+0800 内存平移[68463:5044422] 0x7ffee77cd078 : ViewController
	// self
	2021-03-20 20:19:16.190934+0800 内存平移[68463:5044422] 0x7ffee77cd070 : <ViewController: 0x7f7fbe2045f0>
	// cls
	2021-03-20 20:19:16.191027+0800 内存平移[68463:5044422] 0x7ffee77cd068 : YDWPerson
	// boy 
	2021-03-20 20:19:16.191135+0800 内存平移[68463:5044422] 0x7ffee77cd060 : <YDWPerson: 0x7ffee77cd068>
	2021-03-20 20:19:16.191238+0800 内存平移[68463:5044422] -[YDWPerson saySomething] <ViewController: 0x7f7fbe2045f0>
	2021-03-20 20:19:16.191333+0800 内存平移[68463:5044422] -[YDWPerson saySomething] (null)
  • 其中,为什么 class_getSuperclass 是 ViewController,因为 objc_msgSendSuper2返回的是当前类,两个self,并不是同一个self,而是栈的指针不同,但是指向同一片内存空间;
    • [(__bridge id)kc saySomething]调用时,此时的 boy 是 YDWPerson: 0x7ffee77cd068,所以 saySomething 方法中传入 self 的还是 YDWPerson,但并不是我们通常认为的 YDWPerson,而是我们当前传入的消息接收者,即YDWPerson: 0x7ffee77cd068,是 YDWPerson 的实例对象,此时的操作与普通的 YDWPerson 是一致的,即 YDWPerson 的地址内存平移 8 字节;
    • 普通person流程:person -> name - 内存平移8字节;
    • boy 流程:0x7ffee77cd068 + 0x80 -> 0x7ffee77cd070,即为self,指向<ViewController: 0x7f7fbe2045f0>,如下图所示:

在这里插入图片描述

  • 其中 person 与 YDWPerson 的关系是 person 是以 YDWPerson 为模板的实例化对象,即 alloc 有一个指针地址,指向 isa,isa 指向 YDWPerson,它们之间关联是有一个 isa 指向。
  • 而 boy 也是指向 YDWPerson 的关系,编译器会认为 boy 也是 YDWPerson 的一个实例化对象,即 boy 相当于 isa,即首地址,指向 YDWPerson,具有和 person 一样的效果,简单来说,我们已经完全将编译器“骗”过了,即 boy 也有 name。由于person查找 name 是通过内存平移 8 字节,所以 boy 也是通过内存平移 8 字节去查找name;
④ 栈和堆分别存放了什么?
  • alloc的对象存放在堆中;
  • 指针、对象存放于栈中,例如 person 指向的空间在堆中,person 所在的空间在栈中;
  • 临时变量存放栈中;
  • 属性值存放在堆中,属性随对象是存放在栈中;

三、总结

  • 堆是从小到大,即低地址->高地址;
  • 栈是从大到小,即从高地址->低地址分配;
    • 函数隐藏参数会从前往后一直压栈,即 从高地址->低地址 开始入栈,
    • 结构体内部的成员是从低地址->高地址;
  • 一般情况下,内存地址有如下规则:
    • 0x60 开头表示在堆中;
    • 0x70 开头的地址表示在栈中;
    • 0x10 开头的地址表示在全局区域中。

猜你喜欢

转载自blog.csdn.net/Forever_wj/article/details/115032175