《Objective-C高级编程 iOS与OS X多线程和内存管理》笔记——ARC部分

第一部分:ARC

iOS 5引入了ARC(Automatic Reference Counting)自动引用计数,让编译器来进行内存管理(其实是让编译器在Objective-C里为我们插入retain / release / autorelease等代码,对于C/C++的部分还是要我们自己来管理内存)。

若要指定某个文件不使用ARC,需要为该文件加上-fno-objc-arc

内存管理的思考方式

  1. 自己生成的对象,自己持有
  2. 非自己生成的对象,自己也能持有
  3. 不再需要自己持有的对象时释放
  4. 非自己持有的对象无法释放

对象操作与Objective-C方法的对应

对象操作 Objective-C方法
生成并持有对象 alloc/new/copy/mutableCopy等方法
持有对象 retain方法
释放对象 release方法
废弃对象 dealloc方法

id obj = [[NSObject alloc] init];
这种情况下,obj生成并持有对象

id obj = [NSArray array];
这种情况下,取得的对象存在,但自己不持有对象

id obj = [NSArray array]; [obj retain];
这种情况下,先是不持有对象,retain之后便持有对象

id obj = [[NSObject alloc] init]; [obj autorelease];
这种情况下,取得对象的存在,但自己并不持有对象

几个错误情况如下:

id obj = [NSArray array]; [obj release];因为不能释放自己并不持有的对象,所以会报错

id obj = [[NSObject alloc] init]; [obj release]; [obj release];释放之后再次释放非自己持有的对象,会造成崩溃

引用计数

在OC里面可以调用retainCount来查看某个对象的引用计数。

NSObject * obj = [[NSObject alloc] init];
NSLog(@"referenceCount = %u", [obj retainCount]);
[obj release];
NSLog(@"referenceCount = %u", [obj retainCount]);

此处的打印,第一个retainCount是1。但是第二个retainCount的值是不确定的,(虽然很可能是1),因为obj对象已经被释放了,当最后一次执行release的时候,系统知道马上就要回收内存了,就没有必要将retainCount减一了,不管减不减已,该对象的内存可定会被回收,此时再去改变retainCount的值并没有意义。

向一个已经释放的对象发送消息的时候,非常有可能造成崩溃,EXC_BAD_ACCESS大多数情况就是内存访问错误。

苹果通过散列表管理引用计数

通过引用计数追溯对象

通过引用计数表管理引用计数的好处如下:

  1. 分配内存给对象的时候,无需考虑内存块头部(GNU将引用计数放在对象的内存块头部)
  2. 引用计数表各记录中存有内存块地址,可以从各个记录追溯到各对象的内存块

autorelease

autorelease是自动释放,它更类似于C语言中的自动变量(局部变量)的特性。autorelease会像C语言的自动变量那样来对待对象实例,当超出其作用域时,对象实例的release实例方法被调用。

autorelease的具体使用方法如下:

  1. 生成并持有NSAutoreleasePool对象
  2. 调用已分配对象的autorelease实例方法
  3. 废弃NSAutoreleasePool对象

对于所有调用过autorelease实例方法的对象,在废弃NSAutoreleasePool时(即[pool drain]时),都将调用release实例方法。

当大量产生autorelease对象时,只要不废弃NSAutoreleasePool对象,那么生成的对象就不能释放,因此有时会产生内存不足的现象。

很多类方法返回的对象是autorelease的对象。例如[NSMutableArray arrayWithCapacity:1]
其实只要不是alloc/new/copy/mutableCopy生成的对象,基本上都是autorelease的。

所有权修饰符

ARC有效时,有四种所有权修饰符:

  1. __strong修饰符
  2. __weak修饰符
  3. __unsafe_unretained修饰符
  4. __autoreleasing修饰符

附有 __strong__weak__autorelease修饰符的变量会被初始化为nil。

__strong修饰符是id类型和对象类型默认的所有权修饰符。id和对象类型在没有明确指定所有权修饰符时,默认为__strong修饰符。附有__storng修饰符的变量在超出其变量作用域的时候,会释放其被赋予的对象。

id obj = [[NSObject alloc] init];这里的id变量,实际上被附加了所有权修饰符。即该代码等同于 id __strong obj = [[NSObject alloc] init];(变量obj为强引用,持有该对象。当超出其作用域时,强引用失效,对象的所有者不存在,因此废弃该对象。)

id __strong obj = [NSArray array]; 变量obj为强引用,持有该对象。当变量超出其作用域时,强引用失效,对象的所有者不存在,因此废弃该对象。

1. id __strong obj0 = [[NSObject alloc] init]; /*对象A*/
//obj0持有对象A的强引用
2. id __strong obj1 = [[NSObject alloc] init]; /*对象B*/
//obj1持有对象B的强引用
3. id __strong obj2 = nil;
//obj2不持有任何对象
4. obj0 = obj1;
//obj0持有对象B的强引用,原先对对象A的强引用失效,对象A的持有者不存在,因此废弃对象A
5. obj2 = obj0;
//obj2持有由obj0赋值的对象B的强引用。此时,对象B的强引用变量为obj0,obj1,obj2
6. obj1 = nil;
//obj1对对象B的强引用失效。对象B的强引用变量为obj0,obj2
7. obj0 = nil;
//obj0对对象B的强引用失效。对象B的强引用变量为obj2
8. obj2 = nil;
//对象B的持有者不存在,因此废弃对象B

__weak修饰符可以解决“循环引用”的问题。带有__strong修饰符的变量在持有对象时,很容易发生循环引用,循环引用容易发生内存泄露。__weak修饰符提供弱引用,弱引用不能持有对象。

下面分别是循环引用和自引用的示例,这两种都容易发生内存泄露。

循环引用

自引用

编译器会对下方的代码发出警告:

id __weak obj = [[NSObject alloc] init];

插入waring图片

由于obj变量对刚生成的对象时弱引用,弱引用不持有对象,则刚生成的对象没有持有者,会被立即废弃。

使用__weak修饰符的好处是,若指向的对象被废弃,则此弱引用自动失效且被赋值为nil,如下方代码所示。
weak nil
打印出:obj is =(null)

__unsaf_unretained修饰符是不安全的所有权修饰符。附有该修饰符的变量不属于编译器的内存管理对象。

id __unsaf_unretained obj = [[NSObject alloc] init];

__unsaf_unretained修饰符也是不能持有对象的,所以上述这条代码的结果是对象生成了之后马上就被废弃了。

__autorelease修饰符。在ARC有效时,用@autorelease块替代NSAutoreleasePool类,使用__autorelease修饰符来替代调用autorelease方法。编译器会检查生成对象的时候是不是以alloc/new/copy/mutableCopy开始,如果不是则自动将生成的对象注册到autoreleasepool。

id的指针或者对象的指针会默认加上__autorelease修饰符。
以下源代码会发生编译器错误:

NSError * error = nil;
NSError ** pError = &error;

插入错误图片

这是因为error被默认为是附加了__strong类型的修饰符;而pError是指向对象的指针,默认是附加了__autorelease修饰符。赋值给对象指针时,所有权一定要一样。

我们在调用某些方法的时候,为了得到详细的错误信息,经常会在方法的参数中传递NSError对象的指针。
例如某方法的声明为:

-(void)performOperationWithError:(NSError **)error

其实等同于

-(void)performOperationWithError:(NSError * __autorelease *)error

但是我们调用这个函数的时候,传进去的是NSError * error = nil;它(默认)是__strong修饰符的。上文说到赋值给对象指针时,所有权一定要一样,为什么此处调用的时候确没有报错呢——这是因为编译器帮我们自动做了转化,自动转化为__autorelease修饰符。

将老项目改成ARC

很多老项目都是手动管理内存,哎~本身看老代码已经是折磨人的一件事情了,偏又不是ARC的,这不是要了老命了吗(┬_┬)。可以在项目里面找到如下图所示的标志位,将其设置为YES,编译运行会报很多错,你仔细照着改就行了,不过如果是C函数,那么一定要小心。

将项目改成arc

刚接触Objective-C时自己给自己挖的坑

刚接触oc的时候其实不是特别明白指针啊、对象啊,就自己给自己挖过坑。%>_<%

大体是这样的:将数组作为参数传递给方法,在方法里面对数组进行修改。

假如代码是这样的:

- (void)viewDidLoad {
    [super viewDidLoad];

    NSString * str = @"hello";
    NSMutableArray * array = [NSMutableArray arrayWithObjects:@"abc",@"efg",@"xyz", nil];
    NSLog(@"before,the str=%@",str);
    NSLog(@"before,the array=%@",array);
    [self changeValue:str array:array];
    NSLog(@"after,the str=%@",str);
    NSLog(@"after,the array=%@",array);
}

- (void)changeValue:(NSString *)strPara array:(NSMutableArray *)arrPara
{
    strPara = @"world";
    [arrPara removeLastObject];
    
    NSLog(@"end of function, the strPara=%@",strPara);
    NSLog(@"end of function, the arrPara=%@",arrPara);
}

当时自己以为传进changValue里面,在里面修改了参数的值,修改的是形参的值,所以最后打印出来应该两个参数都不会变化。结果打印出来是这样的:

在函数中改变参数

说明我一开始就没理解!!!

过程应该是这样的,

1.一开始的时候有字符串和数组两个内存块,分别有一个强引用指向它们。

一开始的内存情况

2.当刚进入到changValue函数的时候,两个内存块又分别多了一个强引用

一进入函数的时候的内存情况

3.当执行函数中的strPara = @"world";这句话之后,其实是开辟了一块新内存,新内存里存放的是是字符串world,strPara指向了这块新内存。

改变字符串的时候其实是将引用指向别处

4.当执行函数中的[arrPara removeLastObject];的时候,因为array与arrayPara指向的是同一块内存,所以这块内存中存储的数组去除了最后一个元素。

改变数组的元素

5.当出了changeValue函数,打印str与array指向的内容的时候,会发现字符串没变,但数组少了一个元素。

在dealloc里面将注册的通知取消

如果工程开启了ARC,那么你在dealloc里面调用[super dealloc]的时候是会报错的。但是还是可以在dealloc里面做一些事情的————比如说取消注册的通知。

 [[NSNotificationCenter defaultCenter] removeObserver:self];


2016-05-05 新增

ARC并不能解决项目中全部的内存管理问题,还有一些是需要开发者自己处理的。比如底层的Core Foundation对象,就不在ARC的管理之下,需要自己维护这些对象的引用计数。

另外,有时候需要将Core Foundation对象转换成Objective-C对象,这个时候就需要告诉编译器,转换过程中引用计数如何调整。关键字如下:

__bridge:只作类型转换,不修改相关对象的引用计数。原来的Core Foundation对象在不用时,需要调用CFRelease方法。

__bridge_retained:类型转换后,将相关对象的引用计数加1,原来的Core Foundation对象在不用时,需要调用CFRelease方法。

__bridge_transfer:类型转换后,将对象的引用计数交给ARC管理,员阿里的Core Foundation对象在不用时,不需要调用CFRelease方法。


array里面对其元素是强引用还是copy还是弱引用?


Show Comments