程序的内存结构
一个程序内存结构可以大致分为2部分:只读部分和读写部分。只读部分包括.text和.rodata段,读写部分又根据任务的不同划分成了以下几个段:
.text
代码段也叫文本段或者文本,存储了目标文件的一部分,或者包含虚拟地址空间中的可执行指定。其实就是存放了编译程序代码后机器指令。只读。
.data
存储了所有可以修改的全局变量或者静态变量,并且这些变量是已经赋了初始值的。
.bss
未初始化全局变量和静态变量。
heap
使用 malloc, realloc, 和 free 函数控制的变量,堆在所有的线程,共享库,和动态加载的模块中被共享使用。释放工作由程序员控制,容易产生内存泄漏。
链表结构,从低地址向高地址生长的不连续内存区域,遍历方向页是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存,所以在大量对象创建但没有及时释放时会撑爆内存,由此可见,堆获得的空间比较灵活,也比较大。同时因为是链表结构,肯定也会造成空间的不连续,从而造成大量的碎片,使程序效率降低。
stack
内部临时变量以及有关上下文的内存区域存放在栈中。程序在调用函数时,操作系统会自动通过压栈和弹栈完成保存函数现场等操作,不需要程序员手动干预。
LIFO结构,从高地址向低地址生长。栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由alloca函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行释放,无需我们手工实现。
栈是一块连续的内存区域,栈顶的地址和栈的最大容量是系统预先规定好的。能从栈获得的空间较小。如果申请的空间超过栈的剩余空间时,例如递归深度过深,将提示stackoverflow。
栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。
注意:在Objective-C中,函数内的临时对象,对象指针是放在栈中,而对象数据其实存储在堆中。 在针对一些场景,比如将这个临时对象放到一个collection结构中时,在结束作用域时,在堆中更好处理。还有一部分则是历史原因可以追溯到NeXTSTEP version 2.0时候的设计。 why does objective-c store objects on the heap instead of on the stack
int val = 3;
char string[] = "Hello World";
这两个变量的值会一开始被存储在 .text 中(因为值是写在代码里面的),在程序启动时会拷贝到 .data 去区中。
而不初始化的话,像下面这样,这个变量就会被放在 bss 段中。
static int i;
错误的内存管理会带来的问题
- 使用已经释放或重写过的内存数据。 会造成数据混乱,通常的结果是crash,设置损坏用户的数据。
- 没有释放不再使用的数据,造成的内存泄漏。 应用程序在使用过程中不断增加的内存量,这可能导致系统性能不佳或内存占用过多而被终止。
引用计数管理内存
iOS中使用了引用计数来管理内存,引用计数中包含两种方式:MRC 和 ARC。这里假设读者使用过MRC并且有一定了解。
内存管理
内存管理原则
在MRC中有四个法则知道程序员手动管理内存:
- 自己生成的对象,自己持有。
使用以
alloc、new、copy或者mutableCopy开头的方法创建的对象。(比如alloc,newObject,mutableCopy) - 非自己生成的对象,自己也能持有。
- 不在需要自己持有对象的时候,释放。
通过
release或者autorelease消息释放自己持有的对象。release和autorelease在ARC中都不再需要手动调用。 - 非自己持有的对象无需释放。
四个法则对应的代码:
/*
* 自己生成并持有该对象
*/
id obj0 = [[NSObeject alloc] init];
id obj1 = [NSObeject new];
/*
* 持有非自己生成的对象
*/
id obj = [NSArray array]; // 非自己生成的对象,且该对象存在,但自己不持有
[obj retain]; // 自己持有对象
/*
* 不在需要自己持有的对象的时候,释放
*/
id obj = [[NSObeject alloc] init]; // 此时持有对象
[obj release]; // 释放对象
/*
* 指向对象的指针仍就被保留在obj这个变量中
* 但对象已经释放,不可访问
*/
/*
* 非自己持有的对象无法释放
*/
id obj = [NSArray array]; // 非自己生成的对象,且该对象存在,但自己不持有
[obj release]; // ~~~此时将运行时crash 或编译器报error~~~ 非 ARC 下,调用该方法会导致编译器报 issues。此操作的行为是未定义的,可能会导致运行时 crash 或者其它未知行为
非自己持有的对象无法释放,这些对象什么时候释放呢?这就要利用autorelease对象来实现的,autorelease对象不会在作用域结束时立即释放,而是会加入autoreleasepool释放池中,应用程序在事件循环的每个循环开始时在主线程上创建一个autoreleasepool,并在循环最后调用drain将其排出,这时会调用autoreleasepool中的每一个对象的release方法。
常量对象
内存中的常量对象(类对象,常量字符串对象等)是在.data或.bss字段,他们没有引用计数机制,永远不能释放这些对象。给这些对象发送消息retainCount后,返回的是NSUIntergerMax。
不要在init和delloc中使用setter或者getter方法
如果存在继承和子类重写父类setter或者getter方法的前提下,可能会存在崩溃或异常状态。
在dealloc里不要调用属性的存取方法,因为有人可能会覆写这些方法,并于其中做一些无法再回收阶段安全执行的操作(上面已经提到)。此外,属性可能正处于“键值观察”(Key-Value Observation,KVO)机制的监控之下,该属性的观察者(Observer)可能会在属性值改变时“保留”或使用这个即将回首的对象。这种做法会令运行期系统的状态完全失调,从而导致一些莫名其妙的错误。
不要在delloc中直接管理稀缺资源
对象delloc方法的调用可能存在的问题:
- 对象release存在两种可能,一是被强引用依赖其他对象释放后才会被释放顺序处理,二是如果是autorelease对象,释放的时机则是无序的。
- 对象内存泄漏时比较常见但是不会致命的错误,但是资源管理对于应该释放而没有释放的资源,引起的问题是可能要更加严重。
- 如果一个对象在意外时间自动释放,它将在它碰巧所在的任何线程的自动释放池块中被释放。对于只能从一个线程触及的资源,这很容易致命。
所以对于稀缺资源(文件管理,网络连接,缓冲和缓冲)就不能按照预想的逻辑在
delloc中及时处理。
修饰符
属性修饰符
@property (assign/retain/strong/weak/unsafe_unretained/copy) NSArray *array;
assign: 表明setter 仅仅是一个简单的赋值操作,通常用于基本的数值类型,例如CGFloat和NSInteger。
retain: 引用计数加1。
strong: 和retain类似,引用计数加1。对象类型时默认就是strong。
weak: 和assign类似,当对象释放时,会自动设置为nil。
unsafe_unretained: 的语义和assign类似,不过是用于对象类型的,表示一个非拥有(unretained)的,同时也不会在对象被销毁时置为nil的(unsafe)关系。性能优于weak参照weak实现原理。
copy: 类似storng,不过在赋值时进行copy操作而不是retain,在setter中比较明显的会发现一个copy行为。 常在model或者赋值时使用,防止外部修改或者多线程中修改。
变量修饰符
__strong: 对象默认使用标识符,retain+1。只要存在strong指针指向一个对象那他就会一直保存存活。
__weak: 弱引用对象,引用计数不会增加。对象被销毁时自己被置为 nil 。
__unsafe_unretained: 不会持有对象,引用计数不会增加,但是在对象被销毁时不会自动置为nil,该指针依旧指向原来的地址,这就变成一个悬垂指针了。
__autoreleasing: 用来表示id *修饰的参数,并且在返回时被自动释放掉。
// ClassName * qualifier variableName;
// for example:
MyClass * __weak myWeakReference;
MyClass * __unsafe_unretained myUnsafeReference;
qualifier只能放在 * 和 变量名 之间,但是放到其他位置也不会报错,编译器对此做过优化。
在使用引用地址传值时需要特别注意,比如以下代码能正常工作:
NSError *error;
BOOL OK = [myObject performOperationWithError:&error];
if (!OK) {
// Report the error.
// ...
}
但是,这里有一个错误的隐式声明:
NSError * __strong error;
而方法的声明是:
-(BOOL)performOperationWithError:(NSError * __autoreleasing *)error;
因此编译器会重写:
NSError * __strong error;
NSError * __autoreleasing tmp = error;
BOOL OK = [myObject performOperationWithError:&tmp];
error = tmp;
if (!OK) {
// Report the error.
// ...
}
当然你也可以创建-(BOOL)performOperationWithError:(NSError * __strong *)error;方法,也可以创建NSError * __autoreleasing error;使他们的类型一致,采用何种方式视具体上下文逻辑而定。
循环引用问题
使用引用计数管理内存时,不可避免的会遇到循环引用问题。 产生原因是多个对象间存在相互引用,其中某个对象的释放都依赖于另一个对象的释放,形成了一个独立的环状结构。
为了打破这个循环引用关系,有以下两种办法:
- 手动将其中的一条强引用置为nil。
- 使用
weak弱引用的方式,修饰对象。
AutoreleasePool
上面也有提到了AutoreleasePool,这在整个内存管理中扮演了非常重要的角色。 在NSAutoreleasePool文档中:
In a reference counted environment, Cocoa expects there to be an autorelease pool always available. If a pool is not available, autoreleased objects do not get released and you leak memory. In this situation, your program will typically log suitable warning messages.
The Application Kit creates an autorelease pool on the main thread at the beginning of every cycle of the event loop, and drains it at the end, thereby releasing any autoreleased objects generated while processing an event. If you use the Application Kit, you therefore typically don’t have to create your own pools. If your application creates a lot of temporary autoreleased objects within the event loop, however, it may be beneficial to create “local” autorelease pools to help to minimize the peak memory footprint.
autoreleasepool 和线程的关系 Each thread (including the main thread) maintains its own stack of NSAutoreleasePool objects (see Threads). As new pools are created, they get added to the top of the stack. When pools are deallocated, they are removed from the stack. Autoreleased objects are placed into the top autorelease pool for the current thread. When a thread terminates, it automatically drains all of the autorelease pools associated with itself. Threads If you are making Cocoa calls outside of the Application Kit’s main thread—for example if you create a Foundation-only application or if you detach a thread—you need to create your own autorelease pool.
If your application or thread is long-lived and potentially generates a lot of autoreleased objects, you should periodically drain and create autorelease pools (like the Application Kit does on the main thread); otherwise, autoreleased objects accumulate and your memory footprint grows. If, however, your detached thread does not make Cocoa calls, you do not need to create an autorelease pool.
文中提到了autoreleasepool4个比较特别的情况:
-
如果autoreleasepool无效时,autorelease对象是无法收到release消息,从而造成内存泄漏。在ARC情况下很少会出现autoreleasepool无效的情况下。
-
对于需要产生大量临时autorelease对象的逻辑,需要使用**@autoreleasepool{}**来立即释放来降低内存的峰值。
-
关于autoreleasepool在线程中的线程的布局,官方文档说每一个线程都会在栈中维护创建的NSAutoreleasePool 对象,并且会把这个对象放到栈的顶部,从而确保在线程结束时autoreleasepool能在最后drain并且dealloc后从栈中移除。
-
autoreleasepool与线程的关系,除了main thread外其他线程都没有自动生成的autoreleasepool。如果你的线程需要长时间存活或者会有autorelease对象生成,就必须要在线程一开始就创建autoreleasepool,否则就会有对象泄漏。尤其是长时间存活的线程,你还需要像主线程在runloop末尾定时的去drain。
内存泄漏检测
- 观察内存增长减少情况,在退出界面时,观察内存增长情况。
- 查看对象
delloc方法调用情况。 - Xcode提供的Debug Memory Graph。
- Xcode提供的instruments。
- MLeaksFinder
参考资料
《Apple About Memory Management》
《Clang中ARC的实现》
《黑幕背后的 Autorelease》
《内存管理》