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];
    [pool release]; // 每次循环结束就关闭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:不持有对象,所引用的对象计数不会加1。所引用的对象被销毁时会自动置为nil。常用于block, delegate, NSTimer,以解决循环引用带来的内存泄漏问题。

下面的写法是错误的:

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

weak与assign很像,区别是:weak不会产生野指针问题。因为weak修饰的对象释放后(引用计数器值为0),指针会自动被置nil,之后再向该对象发消息也不会崩溃,所以weak是安全的。assign通常用于修饰基础数据类型,此时与weak一样是安全的。assign也可以修饰对象,与weak一样不持有对象,所引用的对象计数不会加1。但会产生野指针问题,修饰的对象释放后,指针不会自动被置空,此时向对象发消息会崩溃,所以assign修饰对象是不安全的。

weak安全的原理,即当对象被销毁后,自动置为nil的原理:Runtime维护了一个weak表(其实就是个哈希表,Key是所指对象的地址,Value是Weak指针的地址的数组),用于存储指向某个对象的所有weak指针。当对象被回收的时候,会将所有Weak指针的值设为nil。(参照objc-weak.m)

__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的问题,解决方法见下面。

循环引用

多个对象相互之间有strong引用,不能释放让系统回收。避免循环引用的方式:

1.类属性:将strong改为weak引用

2.delegate循环引用:一般在声明delegate的时候都要使用弱引用weak,或者assign。(weak和assign的区别见上面,MRC的话只能用assign,在ARC的情况下最好使用weak。)

3.NSTimer循环引用:在控制器内,创建NSTimer作为其属性,由于定时器创建后也会强引用该控制器对象,这样该对象和定时器就相互循环引用了。需要我们手动断开循环引用:如果是不重复定时器,在回调方法里将定时器invalidate并置为nil。如果是重复定时器,在合适的位置将其invalidate并置为nil

4.block循环引用:block会持有block中的对象,如果此时block中的对象又持有了该block,则会造成循环引用。解决方式用__weak或@weakify配合@strongify来修饰:

// 错误代码,会有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];
    };
}

关键字总结

上面关于关键字的说明有点零散,总结一下:

strong:指向并持有该对象,引用计数会加1。引用计数为0销毁,可以通过将变量强制赋值nil来进行销毁。

weak:指向但是并不持有该对象,引用计数不会加1。在Runtime中对该属性进行了相关操作,无需处理,可以自动销毁

assign:assign主要用于修饰基本数据类型,例如NSInteger,CGFloat,存储在栈中,内存不用程序员管理

copy:和strong类似,多用于修饰有可变类型的不可变对象上NSString,NSArray,NSDictionary上。例如

@property(nonatomic, strong) NSString *strongStr;
@property(nonatomic, copy) NSString *copyyStr;

// 当字符串是NSString时,由于是不可变字符串,所以,不管使用strong还是copy修饰,都是指向原来的对象,copy操作只是做了一次浅拷贝。
// 当字符串是NSMutableString时,strong只是将源字符串的引用计数加1,而copy则是对原字符串做了次深拷贝,从而生成了一个新的对象,并且copy的对象指向这个新对象。
// 即:如果字符串是NSMutableString,使用strong只会增加引用计数。但是copy会执行一次深拷贝,会造成不必要的内存浪费。而如果字符串是NSString时,strong和copy效果一样,就不会有这个问题。
// 但通常我们声明NSString时,也不希望它改变,所以建议使用copy,这样可以避免NSMutableString带来的错误。

__unsafe_unretain:类似于weak,但是当对象被释放后,指针本身并不会自动销毁,这也就造成了野指针,访问被释放的地址就会Crash,所以说它是不安全的。但__weak在指向的内存销毁后,可以将指针变量置为nil,这样更加安全。

atomic:这个属性是为了保证在多线程的情况下,编译器会自动生成一些互斥加锁的代码,避免该变量的读写不同步的问题。注意:atomic可以保证setter和getter存取的线程安全,但并不保证整个对象是线程安全的。例如,声明一个NSMutableArray的原子属性array,此时self.array和self.array = otherArray都是线程安全的。但是,使用[self.array objectAtIndex:index]就不是线程安全的,需要用锁来保证线程安全性。

nonatomic:如果该对象无需考虑多线程的情况,这个属性会让编译器少生成一些互斥代码,可以提高效率。但在多线程设置属性时非常容易产生crash,因为nonatomic的属性被赋值后会将oldValue释放,如果两个线程同时设置nonatomic的属性后,会释放两次oldValue导致crash。所以多线程时属性要用atomic修饰。

发表评论

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