Objective-C内存管理:对象

程序的内存结构

一个程序内存结构能够大体分为2部分:只读部分和读写部分。只读部分包括.text.rodata段,读写部分又根据任务的不一样划分红了如下几个段:html

.text

代码段也叫文本段或者文本,存储了目标文件的一部分,或者包含虚拟地址空间中的可执行指定。其实就是存放了编译程序代码后机器指令。只读。git

.data

存储了全部能够修改的全局变量或者静态变量,而且这些变量是已经赋了初始值的。程序员

.bss

未初始化全局变量和静态变量。github

heap

使用 malloc, realloc, 和 free 函数控制的变量,堆在全部的线程,共享库,和动态加载的模块中被共享使用。释放工做由程序员控制,容易产生内存泄漏。objective-c

链表结构,从低地址向高地址生长的不连续内存区域,遍历方向页是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存,因此在大量对象建立但没有及时释放时会撑爆内存,因而可知,堆得到的空间比较灵活,也比较大。同时由于是链表结构,确定也会形成空间的不连续,从而形成大量的碎片,使程序效率下降。安全

stack

内部临时变量以及有关上下文的内存区域存放在栈中。程序在调用函数时,操做系统会自动经过压栈和弹栈完成保存函数现场等操做,不须要程序员手动干预。bash

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;
复制代码

数据段

错误的内存管理会带来的问题

  1. 使用已经释放或重写过的内存数据。 会形成数据混乱,一般的结果是crash,设置损坏用户的数据。
  2. 没有释放再也不使用的数据,形成的内存泄漏。 应用程序在使用过程当中不断增长的内存量,这可能致使系统性能不佳或内存占用过多而被终止。

引用计数管理内存

iOS中使用了引用计数来管理内存,引用计数中包含两种方式:MRC 和 ARC。这里假设读者使用过MRC而且有必定了解。

内存管理

内存管理原则

在MRC中有四个法则知道程序员手动管理内存:

  • 本身生成的对象,本身持有。 使用以allocnewcopy或者mutableCopy开头的方法建立的对象。(好比 alloc,newObject,mutableCopy
  • 非本身生成的对象,本身也能持有。
  • 不在须要本身持有对象的时候,释放。 经过release或者autorelease消息释放本身持有的对象。releaseautorelease在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。

不要在initdelloc中使用setter或者getter方法

若是存在继承和子类重写父类setter或者getter方法的前提下,可能会存在崩溃或异常状态。

在dealloc里不要调用属性的存取方法,由于有人可能会覆写这些方法,并于其中作一些没法再回收阶段安全执行的操做(上面已经提到)。此外,属性可能正处于“键值观察”(Key-Value Observation,KVO)机制的监控之下,该属性的观察者(Observer)可能会在属性值改变时“保留”或使用这个即将回首的对象。这种作法会令运行期系统的状态彻底失调,从而致使一些莫名其妙的错误。

不要在delloc中直接管理稀缺资源

对象delloc方法的调用可能存在的问题:

  1. 对象release存在两种可能,一是被强引用依赖其余对象释放后才会被释放顺序处理,二是若是是autorelease对象,释放的时机则是无序的。
  2. 对象内存泄漏时比较常见可是不会致命的错误,可是资源管理对于应该释放而没有释放的资源,引发的问题是可能要更加严重。
  3. 若是一个对象在乎外时间自动释放,它将在它碰巧所在的任何线程的自动释放池块中被释放。对于只能从一个线程触及的资源,这很容易致命。 因此对于稀缺资源(文件管理,网络链接,缓冲和缓冲)就不能按照预想的逻辑在delloc中及时处理。

修饰符

属性修饰符

@property (assign/retain/strong/weak/unsafe_unretained/copy) NSArray *array;
复制代码

assign: 代表setter 仅仅是一个简单的赋值操做,一般用于基本的数值类型,例如CGFloat和NSInteger。 retain: 引用计数加1。 strong: 和retain相似,引用计数加1。对象类型时默认就是strongweak: 和assign相似,当对象释放时,会自动设置为nilunsafe_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;使他们的类型一致,采用何种方式视具体上下文逻辑而定。

循环引用问题

使用引用计数管理内存时,不可避免的会遇到循环引用问题。 产生缘由是多个对象间存在相互引用,其中某个对象的释放都依赖于另外一个对象的释放,造成了一个独立的环状结构。

为了打破这个循环引用关系,有如下两种办法:

  1. 手动将其中的一条强引用置为nil。
  2. 使用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个比较特别的状况:

  1. 若是autoreleasepool无效时,autorelease对象是没法收到release消息,从而形成内存泄漏。在ARC状况下不多会出现autoreleasepool无效的状况下。

  2. 对于须要产生大量临时autorelease对象的逻辑,须要使用**@autoreleasepool{}**来当即释放来下降内存的峰值。

  3. 关于autoreleasepool在线程中的线程的布局,官方文档说每个线程都会在栈中维护建立的NSAutoreleasePool 对象,而且会把这个对象放到栈的顶部,从而确保在线程结束时autoreleasepool能在最后drain而且dealloc后从栈中移除。

  4. autoreleasepool与线程的关系,除了main thread外其余线程都没有自动生成的autoreleasepool。若是你的线程须要长时间存活或者会有autorelease对象生成,就必需要在线程一开始就建立autoreleasepool,不然就会有对象泄漏。尤为是长时间存活的线程,你还须要像主线程在runloop末尾定时的去drain。

内存泄漏检测

  1. 观察内存增加减小状况,在退出界面时,观察内存增加状况。
  2. 查看对象delloc方法调用状况。
  3. Xcode提供的Debug Memory Graph。
  4. Xcode提供的instruments。
  5. MLeaksFinder

参考资料

《Apple About Memory Management》
《Clang中ARC的实现》
《黑幕背后的 Autorelease》
《内存管理》

相关文章
相关标签/搜索