有关内存的一道面试题:
- 能不能运行起来?为什么?
- 运行起来打印结果是什么?为什么?
@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
,一个是ViewControl
的superclass
,也就是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
如果你真的理解了,就会知道打印这个结果的原因。如果还不明白,请回到篇头,再仔细阅读一遍此文章了。
网友评论