简介:
在OC中我们说到对象通常指的是堆对象,因为对象都是alloc出来,而alloc都是在堆中分配对象的内存。但是我看到一片文章《Stack and Heap Objects in Objective-C》及文章的标题,我看到后大吃一斤,还可以有栈对象啊。
堆和栈:
简要说明一下堆和栈:
- 栈又叫堆栈,是一个存储局部变量、内部临时值和管理的内存区域。在现代系统中,每个执行线程都有一个栈。当调用一个函数时,一个栈帧被推送到栈上,并且函数本地数据存储在那里。当函数返回时,它的栈帧被销毁。所有这一切都是自动发生的,除了调用函数之外,程序员无需采取任何显式操作。
- 堆本质上就是内存中的所有东西。内存可以在任何时候分配到堆上,也可以在任何时候销毁。您必须显式地请求从堆中分配内存,如果不使用垃圾收集,也必须显式地释放它。这是存储需要比当前函数调用存活时间长一些的内容的地方。当调用
malloc
和free
函数时,您将访问是堆上的内存空间。
栈对象和堆对象:
那么什么是堆对象,什么是栈对象呢?
在Objective-C(和许多其他语言)中,一个对象只是一个具有特定布局的连续内存块;且对象通常是在堆中创建的:
NSObject *obj = [[NSObject alloc] init];
上面的代码表示,创建一个类NSObject
的对象赋值给指针变量 obj
,其中obj
是指针变量,地址存储在栈中,而[NSObject alloc]
分配的内存是在堆中的。
栈对象就是在栈上为该对象分配内存的对象;同样的堆对象即是在存储在堆内存上的对象。Objective-C没有任何直接的支持,但你可以手动构造一个这样的函数:
struct {
Class isa;
} fakeNSObject;
fakeNSObject.isa = [NSObject class];
NSObject *obj = (NSObject *)&fakeNSObject;
NSLog(@"%@", [obj description]);
栈对象的优点:
栈对象显然是可行的。除了上面的例子,真正的语言,比如c++,都有对栈对象的语言支持。在c++中,你可以在栈或堆上创建对象:
std::string stackString;
std::string *heapString = new std::string;
为什么c++中,会支持两种对象呢?这里不得不说一下堆对象的优点:
- 速度快:在栈上分配和释放内存非常快。当您构建程序时,所有的分配内存和计算偏移量等工作都由编译器完成。在运行时,函数
prolog
只是为所有局部变量划分出它所需的空间,并且代码知道这些空间的位置,因为它们都是提前计算的。栈分配基本上是免费的,而堆分配可能非常昂贵。 - 简单性:栈对象有一个已定义的生命周期,分配和释放都由编译器决定,不需要程序员手动管理;且因为它总是在声明它的作用域的末尾被销毁,不会造成内存泄漏。
栈对象的缺点:
-
严格定义的栈对象的生存期也是一个缺点,也是一个主要缺点:
在Objective-C(和c++,以及许多其他语言)中,对象创建后不可能移动对象。这样做的原因是,可能有很多指向该对象的指针,而这些指针没有被跟踪。它们都需要更新以跟踪移动,但没有办法实现这一点。
(注意:一般来说,这并不是不可能的,许多语言将对象移动作为一件理所当然的事情,通常作为垃圾收集方案的一部分。)
在Cocoa中使用的一样,Objective-C使用一个引用计数机制来进行内存管理。这个系统的优点是,任何一个对象都可以有多个“ 所有者”(及多个指针指向对象),并且在所有所有者放弃所有权之前,系统不会允许该对象被销毁。
栈分配的对象天生就有一个所有者,即创建它们的函数。如果Objective-C有栈对象,如果你把它传递给其他代码会发生什么?它们试图保留它?当创建对象的函数返回时,没有办法防止对象被销毁,所以retain
不能工作。试图保留对象的代码将会失败,最终会产生空引用,并崩溃。 -
另一个问题是栈对象不是很灵活:
在Objective-C中,实现初始化器销毁原始对象并返回一个新对象的情况并不少见。如何使用栈对象来实现呢?你真的不能这样做。Objective-C在运行时的灵活性很大程度上依赖于拥有堆对象,如果这样做就会舍弃这些灵活性。
OC中的栈对象:
在Objective-C中存在栈对象吗?答案是肯定的,就是Block代码块,在swift中叫做闭包。当你使用^{}
语法在函数中写块时,表达式的结果是一个栈对象。
但是我上面讨论的那些问题呢?
运行时动态的问题并不存在于块中。块有一个被语言固定的布局,如果不破坏二进制兼容性就不能改变它。块对象的大小可以在编译时计算,事实上,整个对象是由编译器生成的代码构建的,因此编写一个复杂的初始化器的可能性是不存在的。
对象生存期的问题确实存在于块中,但没有那么严重。这样做的原因是,因为block是一种新的对象,从未存在于语言和任何代码中,处理一个block就需要将它复制一份到堆上(及是在堆上创建一个副本,如block外部的指针会指向堆上的副本),而不是保留它,如果它想被外部指针引用。
Block栈对象的其它缺点:
Block的栈特性确实存在一些缺陷。例如下面的代码:
void (^block)();
if(x)
{
block = ^{ printf("x\n"); };
}
else
{
block = ^{ printf("not x\n"); };
}
block();
Block栈对象只在其封闭作用域的生命周期内有效,在这里,它们的封闭作用域在结束调用Block()之前停止存在。当你将Block传递给不知道它们是Block的代码时,还会发生其他错误:
[dictionary setObject: ^{ printf("hey hey\n"); } forKey: key];
字典将保留该块对象而不是复制它,导致空指针错误。
总结:
栈对象的速度和简单性是块的一大福音,但它也为粗心的程序员创造了一种全新的错误。
原文链接
网友评论