iOS内存管理

26 4月

内存管理不是iOS特有,几乎所有程序员都会遇到内存管理的问题。内存分栈区和堆区,栈区靠操作系统申请释放无需程序员关心,需要关心的是堆区内存的申请和释放。

原理说起来很容易就是申请和释放要配对出现,常见有三种释放内存的方式:

  • 显式释放内存:C的free,C++的delete
  • 基于引用计数释放内存:C++的Smart Pointer,Objective-C
  • 垃圾回收:Java,C#

iOS一直不支持垃圾回收(OS X曾短暂支持过现已废弃),只支持引用计数方式管理内存,从早期的MRC演进到了现在的ARC。

MRC

MRC(Manual Retain Count)手动引用计数,顾名思义就是程序员需要自己确保对象的retain与release的成对出现。对象创建好之后引用计数为1,哪天引用计数为0了,对象就会被销毁内存将被回收。

allocallocWithZonecopycopyWithZonemutableCopymutableCopyWithZone方法创建的对象,会retain持有该对象。

也可以用retain方法持有别人创建的对象。

当不需要这些对象时,可以用releaseautorelease来释放内存。

引用计数也会出现两个对象直接或间接地互相持有对方,甚至自己持有自己的引用,导致Retain Cycle。解决方式有两种:

对于类属性property:声明时设为assign,要释放时手动置为nil

@property (nonatomic, assign) NSObject *parent;

对于block:用__block修饰符来修饰使用到的变量

AutoRelease

AutoRelease这个名字貌似有点歧义,感觉是将手动释放内存升级到自动释放内存。其实AutoRelease是为了解决延迟销毁对象的问题。

例如下面代码中,既不能在return前也不能在return后释放对象。

-(NSObject*)object {
    NSObject *o = [[NSObject alloc] init];
    // [o release];  // 这行代码究竟应该在return前执行还是return后执行呢?
    return o;
}

AutoRelease就是为了解决上述问题,可以延迟释放对象。

程序员可以手动调用autorelease方法,也可以用工厂方法的返回值(工厂方法默认使用了autorelease)。

// 和普通用[alloc init]方法创建的对象不同,工厂方法返回的对象默认使用了autorelease
+ (id)typeRemainderOfMethodName
+ (id)dataWithContentsOfURL:(NSURL *)url;
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];  // 需要在AutoreleasePool里使用autorelease方法
...
NSObject *obj = [[NSObject alloc] init] autorelease];  // 方式一:手动调用autorelease方法
...
NSString *string;
char *cString = "Hello World";
string = [NSString stringWithUTF8String:cstring];  // 方式二:工厂方法的返回值会自动调用autorelease方法
...
[pool release];

其实程序员很少自己去手动创建AutoreleasePool,因为每个线程(包括主线程)都会拥有一个专属的NSRunLoop对象,并且会在有需要的时候自动创建。NSRunLoop对象的每个eventloop开始前,系统会自动创建一个AutoreleasePool,并在eventloop结束时drain。每一个线程都会维护自己的AutoreleasePool堆栈,即AutoreleasePool是与线程紧密相关的,每一个AutoreleasePool只对应一个线程。

善用AutoRelease可以解决一些内存峰值问题,例如下面循环结束前是不会自动触发autorelease的,导致循环次数很多时,内存占用率高:

// 会有内存峰值问题
for(int i=0; i<100; i++) {
    NSError*error;
    NSString*fileContents = [NSString stringWithContentsOfURL:urlArray[i] 
                                      encoding:NSUTF8StringEncoding error:&error];
}

// 解决方式:
for(int i=0; i<100; i++) {
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];  // 每次循环都开启AutoreleasePool
    NSError*error;
    NSString*fileContents = [NSString stringWithContentsOfURL:urlArray[i] 
                                      encoding:NSUTF8StringEncoding error:&error];
    [poolrelease]; // 每次循环结束就关闭AutoreleasePool
}

ARC

MRC需要程序员手动调用retain,release,autorelease,导致内存管理一直是开发的噩梦。

iOS 4.3版引入了ARC,它是Objective-C编译器的特性,而不是运行时特性或者垃圾回收机制。

ARC的内存管理机制与MRC手动机制差不多,只是不再需要手动调用retain,release,autorelease了,编译器会在在适当位置插入retain,release,autorelease。

但ARC仍旧有野指针问题,所以iOS 5版支持了week关键字(用week修饰的对象,在没有引用时会自动置为nil)。

所以现在的内存管理是:编译时ARC+运行时week搭配使用。

ARC里得到对象的方式:alloc,new,copy,mutableCopy,init

ARC里对象修饰符有四种:__strong,__weak,__unsafe_unretained,__autoreleasing

__strong:ARC里用strong强引来取代retain,持有对象,将对象引用计数+1,同时编译器会自动在不需要它的时候插入release代码。而且__strong是默认修饰符,所以写代码时不用显式地加上__strong修饰符:

// 声明变量时,ARC和MRC代码一样都是(ARC里 __strong 是默认修饰符,不用显式写)
id obj = [[NSObject alloc] init];

// ARC编译器会默认加上 __strong 的修饰符:
id __strong obj = [[NSObject alloc] init];

// 作用域里的ARC和MRC的代码不一样,ARC的代码:
{
  id __strong obj = [[NSObject alloc] init];
  // [obj release];  // ARC里不需要这行了,编译器会在作用域结束时自动插入这行代码
}

// 作用域里的MRC的代码
{
  idobj = [[NSObject alloc] init];
  [obj release];
}

__weak:不持有对象,在没有引用时会自动置为nil。所以下面的写法是错误的:

// 会报错,不能将可以retain的对象赋值给weak修饰的变量
id __weak obj = [[xxx alloc] init];

__unsafe_unretained:不持有对象,在没有引用时不会自动置为nil。平时不常用,和__weak相似,__weak出现前是代替__weak的,即在iOS 4.3~5之间用于代替__weak去修饰对象的属性的。但有一种情况,在在C结构体里使用到Objective-C对象时,只能用它来修饰。

__autoreleasing:表明传引用的参数(id *)在返回时是autorelease的,效果同MRC下调用autorelease方法,即被修饰的对象会被加入autorelease pool。

@autoreleasepool {
    NSError *error;
    NSString *fileContents = [NSString stringWithContentsOfURL:url 
                                       encoding:NSUTF8StringEncoding error:&error];  // 参数是autorelease的
    ...
}

ARC下同样会有Retain Cycle的问题,解决方式有:对property用weak修饰。对block用__weak, @weakify & @strongify来修饰

用weak修饰property来解决Retain Cycle的问题:

// 错误代码,会有Retain Cycle的问题
@property (nonatomic, copy) void(^myBlock)(void);

- (void)test
{
    self.myBlock = ^{
        [self doSomething];  // self持有block的指针,block里又使用了self,形成了Retain Cycle
    };
}

// 解决方式1
@property (nonatomic, copy) void(^myBlock)(void);

- (void)test
{
    @weakify(self);
    self.myBlock = ^{
        [@strongify(self);
        [self doSomething];
    };
}

// 解决方式2
@property (nonatomic, copy) void(^myBlock)(void);

- (void)test
{
    __weak typeof(self) weakSelf = self;
    self.myBlock = ^{
        [weakSelf doSomething];
    };
}

QA

nonatomic修饰的属性在多线程设置属性时非常容易产生crash,因为nonatomic的属性被赋值后会将oldValue释放,如果两个线程同时设置nonatomic的属性后,会释放两次oldValue导致crash。所以多线程时属性要用atomic修饰。

发表评论

电子邮件地址不会被公开。 必填项已用*标注