前言
- 这篇文章主要通过一个例子,来分析内存平移,并加深对类的研究。
举例分析
@interface LGPerson : NSObject
- (void)sayHello;
@end
@implementation LGPerson
- (void) saySomething { NSLog(@"%s: 你好", __func__); }
@end
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
// 指针读取类地址,强转为对象,调用sayHello。
Class cls = [LGPerson class];
void * ht = &cls;
[(__bridge id)ht saySomething];
// 实例化对象,调用sayHello
LGPerson * person = [LGPerson new];
[person saySomething];
}
@end
发现打印如下: 相同的结果
-[LGPerson saySomething] : 你好
-[LGPerson saySomething] : 你好
我们在写saySomething的时候会发现这里有代码的自动提示
- 那我们就要提出疑问了,为什么会这样子呢?我们先猜想 ,指针读取类地址,强转为对象这个肯定就是person对象了;
分析如下:
- 我们知道对象方法的本质是:对象调用对象方法,是对象的isa指针去指向类本身,让类本身调用该对象方法。
-
其次方法本质其实就是objc_msgSend进行消息转发,其中经过一系列的方法查找的过程;在快速查找过程中,就是去cache中查找方法,在这里回顾一下类和对象的数据结构如下:
所以经过上面的分析,我们就可以知道,为什么通过获取类的首地址可以调用该对象方法,得出一个结论:就是类的首地址通过是可以通过强转为这个person的对象的。
那么我们给类加个属性然后再方法中打印试试如下:
- (void)saySomething
{
NSLog(@"%s - %@",__func__,self.kc_hobby); //kc_hobby是个字符串
}
{
Class cls = [LGPerson class];
void *kc = &cls; //
[(__bridge id)kc saySomething];
LGPerson *person = [LGPerson alloc];
[person saySomething]; // self.kc_name = nil - (null)
}
---------
-[LGPerson saySomething] - ViewController
-[LGPerson saySomething] - (null)
第二个打印为null,我们都知道,但是为什么第一个self.kc_hobby打印的为ViewController呢?
首先我们先打印下这个kc如下:
解释*(void **)的作用:
(void**) 代表的是指向指针的指针。 读取是地址值
加上 * 进行解引用:*(void **) ,可读取到地址中存放的内容。
分析:在给属性赋值的时候发现kc是没法调用属性的set方法,其次在进行内存平移读取属性的值的时候,很明显,发生的异常,在内存中属性会遵循内存对齐原则,字符串占8个字符。其实这里读到viewcontroller是发生了越界读取。这里也可以看出来,(__bridge id)kc 只是一个只有指针的对象,并不包含任何属性信息。
总结: &cls 只是骗编译器 他是个isa 其实他不是 他只是和isa 一样 指向了LGProson。
拓展: 结构体压栈
可以通过自定义一个结构体,判断结构体内部成员的压栈情况
低地址->高地址
,递增的,栈中结构体内部
的成员是反向压入栈,即低地址->高地址
,是递增的,
回到kc的案例,可以通过下面这段代码打印下栈的存储:
- (void)viewDidLoad {
[super viewDidLoad];
// ViewController 当前的类
// self cmd (id)class_getSuperclass(objc_getClass("LGTeacher")) self cls kc person
Class cls = [LGPerson class];
void *kc = &cls; //
LGPerson *person = [LGPerson alloc];
person.kc_hobby = @"KC";
NSLog(@"%p - %p",&person,kc);
// 隐藏参数 会压入栈帧
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);
}
}
// LGPerson - 0x7ffeea0c50f8
[(__bridge id)kc saySomething]; // 1 2 - <ViewController: 0x7f7f7ec09490>
[person saySomething]; // self.kc_name = nil - (null)
//
}
// 打印如下:
<ViewController: 0x100f076a0> [对应的 self ]
viewDidLoad [对应的 _cmd]
ViewController [对应的 superClass]
<ViewController: 0x100f076a0> [对应的self]
LGPerson [对应的cls]
<LGPerson: 0x16f44db48> [对应的kc]
- 打印顺序为:
self ->_cmd -> superclass -> self -> cls -> ht
补充:
函数内部定义的局部变量和数组,都存放在栈区; (比如每个函数都有的(id self, SEL _cmd))
Clang当前的ViewController.m文件。
clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk ViewController.m
所以内存地址从高位到低位的顺序为:
self
-> _cmd
->__rw_objc_super对象
->cls
->ht
看下__rw_objc_super结构体:
struct __rw_objc_super {
struct objc_object *object; //在当前控制器里 self
struct objc_object *superClass; //在当前控制器里 ViewController
__rw_objc_super(struct objc_object *o, struct objc_object *s) : object(o), superClass(s) {}
};
结构体内部元素,是后面元素先插入内存栈中。所以__rw_objc_super内部的内存顺序为: ViewController -> self
最终内存顺序为: self -> _cmd -> ViewController -> self -> cls -> ht
其中为什么
class_getSuperclass
是ViewController
,因为objc_msgSendSuper2
返回的是当前类
,两个self
,并不是同一个self,而是栈的指针不同,但是指向同一片内存空间
普通person流程:
person -> kc_hobby - 内存平移8字节
kc流程:经过内存平移+8,即为
self
,指向<ViewController:>
其中 person 与 LGPerson的关系是 person是以LGPerson为模板的实例化对象,即alloc有一个指针地址,指向isa,isa指向LGPerson,它们之间关联是有一个
isa指向;
而kc也是指向LGPerson的关系,编译器会认为 kc也是LGPerson的一个实例化对象,即kc相当于isa,即首地址,指向LGPerson,具有和person一样的效果,简单来说,我们已经完全将编译器骗过了,即kc也有`kc_hobby。由于person查找kc_hobby是通过内存平移8字节,所以kc也是通过内存平移8字节去查找kc_hobby。
哪些东西在栈里 哪些在堆里?
alloc
的对象 都在堆
中指针、对象
在栈
中,例如person指向的空间
在堆
中,person所在的空间在栈中临时变量
在栈
中属性值
在堆
,属性随对象是在栈
中
注意:
堆
是从小到大,即低地址->高地址
栈
是从大到小,即从高地址->低地址分配