[精通Objective-C]内存管理
参考书籍:《精通Objective-C》【美】 Keith Lee
目录
程序的内存使用情况
Objective-C可执行程序是由(可执行)代码、初始化和未初始化的程序数据、链接信息、重定位信息、局部数据和动态数据构成的。
程序数据包括以静态方式声明的变量和程序常量(即在程序编译时在代码中设置的常数)。可执行代码、程序数据以及链接和重定位信息会以静态方式被分配内存,并在程序的生命周期一直存在。
局部(自动)数据在语句块中声明并且仅在该语句块中有效,当该语句块执行后局部数据不会继续存在。从语法方面讲,Objective-C的符合语句块就是由括号封装的语句集合。自动数据被存储在程序栈中,程序栈通常是在执行程序/线程前就被设定尺寸的内存段。栈用于存储局部变量和调用方法/函数的上下文数据,以及调用完方法后继续执行程序的代码地址,操作系统会自动管理这些内存,这些数据会获得栈中的内存,而分配给这些数据的内存会在它们失效后被释放。
在运行时,Objective-C会将创建的对象(通过NSObject类的alloc方法)存储在动态分配的内存即堆内存中。以动态方式创建对象就意味着需要进行内存管理,因为在堆内存中创建的对象永远不会超出其作用范围。
不进行内存管理和错误的内存管理通常会导致以下结果:
内存泄漏:如果程序没有释放不再使用的对象,就会导致出现该问题。如果程序没有使用为其分配的内存,就会浪费内存资源;如果系统继续为程序分配内存并且没有释放这些内存,程序最终会耗尽系统内存。
悬挂指针:如果程序释放了仍在使用的对象,就会导致该问题。如果将来程序尝试访问这些对象,就会出现程序错误。
Objective-C的内存管理是通过引用计数实现的,引用计数是一种通过对象的唯一引用,确定对象是否正在被使用的技术。如果对象的引用计数降到了0,对象就会被视为不再有用,而且运行时系统会释放它的内存。Objective-C开发环境提供了两种内存管理机制:手动管理(MRR)和自动引用技术(ARC)。
手动管理
Objective-C对象是通过指向Objective-C对象内存地址的变量(即指针),以间接方式访问的。对象指针实现了Objective-C对象的访问功能,但是它们本身不能管理所有权。
MRR内存管理基本原则
1.为创建的所有对象设置所有权
2.应使用retain方法获取对象(你尚未拥有)的所有权
3.当不再使用某个对象时,必须放弃其所有权
4.不能放弃不归你所有的对象的所有权
// 对象通过alloc消息创建后,变量atom就拥有了该对象的所有权(原则1)
Atom *atom = [[Atom alloc] init];
// 变量href获取了这个对象的所有权(原则2),不能写成 Atom *href = atom; 这样写的话href没有获取对象的所有权,一旦atom释放了,href就不再指向一个合法的位置,出现指针悬空。
Atom *href = [atom retain];
// 变量atom释放,但href依旧拥有该对象的所有权
[atom release];
// 变量href释放,对象引用计数变为0,运行时系统可以释放对象了
[href release];
也可以通过autorelease方法延迟释放操作
// 在自动释放池代码块的末尾,调用对象中的释放方法,无需编写调用对象中release方法的具体代码
@autoreleasepool {
// 对象被创建并初始化后,会立刻收到autoreleasepool消息,可以确保所有通过autorelease消息创建的对象都会在程序结束前、自动释放池代码快的末尾被释放
Atom *atom = [[[Atom alloc] init] autorelease];
}
使用MRR
下面开发一个简单的示例程序,程序共包含3个类:OrderEntry类,OrderItem类和Address类。其中每个OrderEntry对象都拥有一个相应的OrderItem对象和Address对象。
首先需要创建一个新工程,在设置工程存储位置时,取消勾选Source Control复选框,由于Xcode 6之后的版本,工程都是默认使用ARC,完成创建后,需要在下图所示位置进行修改,取消使用ARC。
首先是Address类,接口部分不用编辑
Address.m
#import "Address.h"
@implementation Address
-(id)init{
if ((self = [super init])) {
NSLog(@"Initializing Address object");
}
return self;
}
-(void)dealloc{
NSLog(@"Deallocating Address object");
// 对象的dealloc方法调用了对象父类的dealloc方法,从而确保回收对象占用的内存
[super dealloc];
}
@end
下面是OrderItem类
OrderItem.h
#import <Foundation/Foundation.h>
@interface OrderItem : NSObject
{
@public NSString *name;
}
-(id) initWithName:(NSString *)itemName;
@end
OrderItem.m
#import "OrderItem.h"
@implementation OrderItem
-(id) initWithName:(NSString *)itemName{
if ((self = [super init])) {
NSLog(@"Initializing OrderItem object");
name = itemName;
//自定义初始化方法将输入参数赋予实例变量
//OrderItem对象不再创建输入参数,对象就不再拥有该实例变量的所有权,所以发送retain消息(原则2)
[name retain];
}
return self;
}
-(void) dealloc{
NSLog(@"Deallocating OrderItem object");
//释放实例变量name
[name release];
[super dealloc];
}
@end
最后是OrderEntry类
OrderEntry.h
#import <Foundation/Foundation.h>
#import "OrderItem.h"
#import "Address.h"
@interface OrderEntry : NSObject
{
@public OrderItem *item;
NSString *orderId;
Address *shippingAddress;
}
-(id) initWithId:(NSString *)oid;
@end
OrderEntry.m
#import "OrderEntry.h"
@implementation OrderEntry
-(id) initWithId:(NSString *)oid{
if ((self = [super init])) {
NSLog(@"Initializing OrderEntry object");
orderId = oid;
//向变量orderId发送一条retain消息(原因同OrderItem中的name变量)
[orderId retain];
//创建并初始化OrderItem和Address对象,所以OrderEntry拥有它们的所有权,并负责释放
item = [[OrderItem alloc] initWithName:@"Doodle"];
shippingAddress = [[Address alloc] init];
}
return self;
}
-(void) dealloc{
NSLog(@"Deallocation OrderEntry object");
//释放对象拥有所有权的实例变量和对象
[item release];
[orderId release];
[shippingAddress release];
[super dealloc];
}
@end
在main.m中测试
#import <Foundation/Foundation.h>
#import "OrderEntry.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
//创建手动释放的OrderEntry对象
NSString *orderId = [[NSString alloc] initWithString:@"A1"];
OrderEntry *entry = [[OrderEntry alloc] initWithId:orderId];
//释放orderId变量,但OrderEntry对象仍旧引用着该变量,所以该变量不会被释放
[orderId release];
NSLog(@"New order, ID = %@, item = %@", entry->orderId, entry->item->name);
//必须手动释放OrderEntry对象!!!
[entry release];
//创建自动释放池代码块末尾释放的OrderEntry对象
OrderEntry *autoEntry = [[[OrderEntry alloc] initWithId:@"A2"] autorelease];
NSLog(@"New order, ID = %@, item = %@", autoEntry->orderId, autoEntry->item->name);
}
return 0;
}
运行结果:
2016-07-01 13:33:09.765 MRR Orders[60798:1376502] Initializing OrderEntry object
2016-07-01 13:33:09.766 MRR Orders[60798:1376502] Initializing OrderItem object
2016-07-01 13:33:09.766 MRR Orders[60798:1376502] Initializing Address object
2016-07-01 13:33:09.767 MRR Orders[60798:1376502] New order, ID = A1, item = Doodle
2016-07-01 13:33:09.767 MRR Orders[60798:1376502] Deallocation OrderEntry object
2016-07-01 13:33:09.767 MRR Orders[60798:1376502] Deallocating OrderItem object
2016-07-01 13:33:09.767 MRR Orders[60798:1376502] Deallocating Address object
2016-07-01 13:33:09.767 MRR Orders[60798:1376502] Initializing OrderEntry object
2016-07-01 13:33:09.767 MRR Orders[60798:1376502] Initializing OrderItem object
2016-07-01 13:33:09.767 MRR Orders[60798:1376502] Initializing Address object
2016-07-01 13:33:09.767 MRR Orders[60798:1376502] New order, ID = A2, item = Doodle
2016-07-01 13:33:09.767 MRR Orders[60798:1376502] Deallocation OrderEntry object
2016-07-01 13:33:09.767 MRR Orders[60798:1376502] Deallocating OrderItem object
2016-07-01 13:33:09.767 MRR Orders[60798:1376502] Deallocating Address object
如上述结果所示,所有对象的创建/保留和释放消息都达到了平衡,如果你要确保程序没有内存泄漏,可以使用Product菜单中的Analyze工具进行检测,如果把[orderId release];
注释掉就会检测到潜在的内存泄漏问题。
自动引用计数
ARC可以为Objective-C对象和块提供自动内存管理功能,是苹果公司推荐使用的内存管理方式。
ARC规则和约定
1.不能手动编写发送retain、retainCount、release、autorelease和dealloc消息的代码。ARC会在编译时根据需要自动插入这些消息。ARC会在你编写的类(没有编写dealloc方法的情况)中自动创建dealloc方法,释放起拥有的对象并在dealloc方法中调用父类的dealloc方法。
2.不能直接进行id和(void*)类型的互转,ARC只能管理Objective-C对象和块,不能使用C结构中的对象指针。
3.需要使用自动释放池代码块执行由ARC管理的自动释放操作
4.不能使用内存区(NSZone)以及相关的框架函数。
5.为了和非ARC代码合作,不能创建以copy开头的方法和自动声明属性
ARC的声明周期限定符
__strong //强引用,默认设置,表明任何使用alloc/init消息创建的对象都会在其作用范围内被保留
__weak //弱引用,表明对象随时可以被释放,只有当对象拥有其它强引用时才有用,对象被释放后,带__weak限定符的变量会被设置为nil
__unsafe_unretained //与__weak限定符类似,只是对象被释放后,指针不会被设置为空,而是会处于悬挂状态
__autoreleasing //用于通过引用传递对象
使用ARC
下面开发一个简单的示例程序,程序同样地共包含3个类:OrderEntry类,OrderItem类和Address类。其中每个OrderEntry对象都拥有一个相应的OrderItem对象和Address对象。
由于Xcode 6之后的版本,工程都是默认使用ARC,不再需要其它额外操作。
下面是3个类的实现文件,由于接口文件与使用MRR时完全一样,就不再写出
Address.m
#import "Address.h"
@implementation Address
-(id) init{
if ((self = [super init])) {
NSLog(@"Initializing Address object");
}
return self;
}
-(void) dealloc{
NSLog(@"Deallocating Address object");
}
@end
OrderItem.m
#import "OrderItem.h"
@implementation OrderItem
-(id) initWithName:(NSString *)itemName{
if ((self = [super init])) {
NSLog(@"Initializing OrderItem object");
name = itemName;
}
return self;
}
-(void) dealloc{
NSLog(@"Deallocating OrderItem object");
}
@end
OrderEntry.m
#import "OrderEntry.h"
@implementation OrderEntry
-(id) initWithId:(NSString *)oid{
if ((self = [super init])) {
NSLog(@"Initializing OrderEntry object");
orderId = oid;
item = [[OrderItem alloc] initWithName:@"Doodle"];
shippingAddress = [[Address alloc] init];
}
return self;
}
-(void) dealloc{
//这里输出加一个orderId,以便后面的验证
NSLog(@"Deallocation OrderEntry object with ID %@",orderId);
}
@end
可以看到3个类的实现部分与使用MRR时也都大同小异,只是去掉了所有的retain、release消息的发送和[super dealloc]调用父类的dealloc方法(因为ARC会自动调用)。
在main.m中测试
#import <Foundation/Foundation.h>
#import "OrderEntry.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
//创建手动释放的OrderEntry对象
NSString *a1 = @"A1";
NSString *orderId = [[NSString alloc] initWithString:a1];
OrderEntry *entry = [[OrderEntry alloc] initWithId:orderId];
//将ID设置为nil,以验证ARC保留了该值
a1 = nil;
NSLog(@"New order, ID = %@, item = %@", entry->orderId, entry->item->name);
//将OrderEntry对象设置为nil,ARC会自动释放它,如果注释掉本行代码,该对象会在自动释放池代码块末尾被释放
entry = nil;
//创建自动释放的OrderEntry对象
OrderEntry *autoEntry = [[OrderEntry alloc] initWithId:@"A2"];
NSLog(@"New order, ID = %@, item = %@", autoEntry->orderId, autoEntry->item->name);
}
return 0;
}
运行结果
2016-07-01 17:03:26.252 ARC Orders[71749:1493022] Initializing OrderEntry object
2016-07-01 17:03:26.253 ARC Orders[71749:1493022] Initializing OrderItem object
2016-07-01 17:03:26.253 ARC Orders[71749:1493022] Initializing Address object
2016-07-01 17:03:26.253 ARC Orders[71749:1493022] New order, ID = A1, item = Doodle
2016-07-01 17:03:26.253 ARC Orders[71749:1493022] Deallocation OrderEntry object with ID A1
2016-07-01 17:03:26.253 ARC Orders[71749:1493022] Deallocating Address object
2016-07-01 17:03:26.253 ARC Orders[71749:1493022] Deallocating OrderItem object
2016-07-01 17:03:26.253 ARC Orders[71749:1493022] Initializing OrderEntry object
2016-07-01 17:03:26.253 ARC Orders[71749:1493022] Initializing OrderItem object
2016-07-01 17:03:26.253 ARC Orders[71749:1493022] Initializing Address object
2016-07-01 17:03:26.253 ARC Orders[71749:1493022] New order, ID = A2, item = Doodle
2016-07-01 17:03:26.253 ARC Orders[71749:1493022] Deallocation OrderEntry object with ID A2
2016-07-01 17:03:26.253 ARC Orders[71749:1493022] Deallocating Address object
2016-07-01 17:03:26.253 ARC Orders[71749:1493022] Deallocating OrderItem object
相比MRR,ARC简化了大量代码
处理循环引用
ARC无法自动处理循环引用,假设OrderEntry对象拥有一个OrderItem实例变量且OrderItem对象拥有一个OrderEntry实例变量,默认情况都是强引用,就会在这两个对象之间造成循环引用。两个对象永远都不会被释放,因而导致内存泄漏。这种问题可以用弱引用解决没被弱引用的对象不属于引用它的对象,从而可以消除循环引用。苹果公司约定,父对象强引用其所有的子对象,子对象弱引用其父对象(如果有必要)。
#import <Foundation/Foundation.h>
@class OrderEntry;
@interface OrderItem : NSObject
{
@public NSString *name;
//使用弱引用
OrderEntry *__weak entry;
}
-(id) initWithName:(NSString *)itemName;
@end