iOS 下关于指针指向及指针偏移的理解

1,745 阅读5分钟

废话开篇:iOS 下寻找对象及对象方法的调用本身就是寻址的过程,栈区是从高地址向低地址开辟,堆区是由低地址向高地址开辟。栈区是连续的,为什么说是连续的?因为,一个栈区指针需要开辟 8 个字节区域,里面保存着 “值” 的地址,可以连续的排布。而堆区是不连续的,因为堆区对象初始化遵循着 “内存” 对齐的原则。关于 对象,一般认为 对象 本质是结构体并且由 new 出来的一块内存区域,其实,从程序的角度来看, 也是结构体,对象在初始化之后也确实开辟了内存区域,但是 对象 可以理解为是作用来保存数据的,如果想要它去调用某个方法,其实是去寻找它对应的类地址,类地址保存在了 对象isa 指针下,通过访问类结构体下面的方法集合进行寻找及调用相匹配的方法。而对于 对象 的属性值的获取,则需要根据编译完后确定的内存排序进行内存偏移操作,找到对应的指针进行取值操作。

思考一、对象的 isa 到底指向了哪里?

先上一段代码:

    Person * p = [[Person alloc] init];

    NSLog(@"p 对象 栈地址=%p",&p);

    NSLog(@"p 对象 堆地址=%p",p);

    NSLog(@"p 对象 =%@",p);
    
    Class cls = [Person class];

    NSLog(@"class 栈区地址%@",cls);

打印如下:

image.png

这里可以清晰的看到,对于 p 对象的指针信息、内存信息、类信息打印结果很清晰。

但是, cls 只打印了类名而没打印具体的地址。

那么,用控制台输出一下:

image.png

分别解释输出的具体意义:

1、 cls 指针保存的内存地址,这里可以理解为 Person 类的首地址。

2、 p 对象 isa 指针指向的地址。

3、 这里直接打印 cls 存储对象,这里并没有输出对象地址,这里的对象其实指的就是

4、 打印 cls 指针栈区地址。

从上面可以看出,对象的 isa 指针指向的地址与的首地址其实是一致的。

image.png

再次打印 p 对象 isa 指针的地址,可以看到,p 对象首地址其实就是 isa 指针地址,所以,如果说 Person 下有一个属性的话,那么,这个属性的地址会在这个 isa 地址的基础上进行偏移 8 个字节。

Person 下先声明一个 name 属性

@property (nonatomic,strong) NSString * name;

打印如下:

image.png

可以看到,name 属性确实偏移了 8 个字节,这个 8 个字节的只是用来存储具体 name 对象的内地址。

到这里就比较清晰了,声明的变量 p栈区,而它指向的仅仅是一个 堆区 对象的首地址,而这个对象的其他属性的指针会在当前对象的首地址下依次 8 字节 递增排布,所以,程序的本质就是不断寻址的过程,到这里,也没有必要去纠结,指针是不是就在 栈区

思考二、指针的偏移

先上一段代码

    Person * p = [[Person alloc] init];
    NSLog(@"p 对象 栈地址=%p",&p);
    NSLog(@"p 对象 堆地址=%p",p);
    NSLog(@"p 对象 =%@",p);

    Class cls = [Person class];
    NSLog(@"class 栈区地址%@",cls);
    void  *kc = &cls;
    [( __bridge id)kc addObject:@"111"];

这里的 Person 类对象用一个 void* 修饰的指针接收,void* 可以理解为 id 类型,这里看到代码的最后程序调用了数组添加对象的方法,注意:这个方法在 Person 类对象里并没有声明及实现。

这里报错了,错误内容是 Person 类对象没有找到 addObject 方法。编译的时候程序不知道对象的类型,那么,调用已存在的方法是可以的,当运行时候就报错,其实也很好理解,通过思考一的理解,运行时候就去选择 isa 地址了,在类的结构体了找不到方法也就异常了。

image.png

下面修改一下代码,让这 kc 执行一个 Person 类声明并存在的方法。

image.png

p 对象跟 kc 均调用一下 showNameAddress 方法

image.png

打印如下:

image.png 分析一下:

p 对象紧接打印的 name 指针地址(3)是 p 对象 isa 指针 8 字节偏移,这里很好理解。

kc 紧接打印的 name 堆地址(5)是 p 对象 isa 指针(2),而且 name 的栈地址(4)是 p 对象的栈地址(1)。这是为什么?

来看看程序在执行过程中为 p 、cls、 kc 三个变量分配怎样的栈区地址。

image.png

可以看到,这三个临时变量的栈区地址是连续的,而且先声明的地址高,后声明的地址低,那么现在解释一下上面的问题:

首先 kc 执行 showNameAddress 方法时,其实是 cls 这个类首地址在执行实例方法,由于 cls 这个类的首地址跟 p 实例对象的 isa 地址是一致的,所以在寻找、运行方法的时候不会崩溃。

再次, showNameAddress 内部的代码可以这样理解,即,当前打印进入的指针偏移 8 个字节的堆栈信息,所以, 就指向了 cls 这个栈区指针被强制的偏移 8 个字节的指针,又因为栈区地址是高向低的开辟规则,那么,就自然而然的找到了 p 这个临时变量的指针。

到这里是不是以上的问题就清晰了?

当然,如果属性不只是一个,那么,对象调用某个属性的时候就会偏移与之声明属性顺序一致的 8 个字节为单位倍数的数值。所以,对象属性的获取、方法的调用时寻址的过程,OC 代码时给程序员看的,而指针的偏移值是程序编译完就确定,运行时它才不会管在堆栈信息里当前调用的是什么对象,就是这么任性。

代码拙劣,大神勿笑。