Effective objective-c-- 内存管理
- 前言
-
- 理解引用计数
- 以ARC简化引用计数
- 在dealloc方法中只释放引用并解除监听
- 编写"异常安全代码"时留意内存管理问题
- 以弱引用避免保留环
-
- [unsafe_unretained 和 weak](#unsafe_unretained 和 weak)
- 要点
- 以"自动释放池"释放内存峰值
前言
寒假比较忙没能能认真看,就拖到学校了来看了,进度落后很多了,只能尽量赶了;
理解引用计数
Objective-C 语言使用引用计数来管理内存,也就是说,每个对象都有个可以递增或递减的计数器。如果想使某个对象继续存活,那就递增其引用计数:用完了之后,就递减其计数。计数变为 0,就表示没人关注此对象了,于是,就可以把它销毁。
要注意开启ACR功能后,引用计数的方法无法使用 ;
引用计数工作原理
在引用计数架构下,对象有个计数器,用以表示当前有多少个事物想令此对象继续存活下去。这在 Objective-C 中叫做 "保留计数" (retain count),不过也可以叫 "引用计数"(reference count)。NSObject 协议声明了下面三个方法用于操作计数器,以递增或递减其值:
Retain 递增保留计数。
release 递减保留计数。
autorelease 待稍后清理 "自动释放池"(autorelease pool)时,再递减保留计数。
查看保留计数的方法叫做 retainCount,此方法不太有用
图演示了对象自创造出来之后历经一次 "保留" 及两次 "释放" 操作的过程。
使用引用计数的方法要关闭ACR,方法如下:在bulid settings->all->Combined->Apple Clang-language-oBjectiveC->Automatic Reference Counting设置为NO
看下面这段代码:
objectivec
NSMutableArray* array = [[NSMutableArray alloc] init] ;
NSNumber* number = [[NSNumber alloc] initWithInt:15] ;
// [array addObject:number] ;
[number release] ;
NSLog(@"%@",number) ;
// NSLog(@"%lu",(unsigned long)[number retainCount]) ;
// NSLog(@"%@",[array objectAtIndex:0]) ;
[array release] ;
上面这段代码我是想看看number在引用计数为0是调用该对象会不会崩溃,不过出乎意料的是是没有崩,这里的解释是:
在某些情况下,对已释放的对象进行访问可能不会立即导致崩溃。这是因为已释放的对象在内存中仍然存在一段时间,并且指针仍然指向该内存位置。这被称为"悬垂指针"(Dangling Pointer)。
当你尝试访问已释放的对象时,可能会发生以下情况之一:
你可能会幸运地访问到一个仍然有效的对象。在这种情况下,你可能不会立即遇到崩溃或错误。
你可能会访问到无效的对象或者已经被其他对象覆盖的内存。在这种情况下,你可能会遇到崩溃、内存访问错误或者其他不可预测的行为。
书上的例子:
objectivec
```objectivec
NSMutableArray *array = [[NSMutableArray alloc] init];
NSNumber *number = [[NSNumber alloc] initWithInt:2023];
[array addObject:number];
[number release];
[array release];
创建数组之后,把number加入其中的时候,系统会为number retain一次,也就是number的引用计数为2,接下来不需要number对象的时候我们释放了它,在这个例子里能知道number的引用计数现在还为1
为了避免在不经意间使用了无效对象,一般relase之后都会清空指针,这样能保证不出现悬空指针
objectivec
NSMutableArray *array = [[NSMutableArray alloc] init];
NSNumber *number = [[NSNumber alloc] initWithInt:2023];
[array addObject:number];
[number release];
[array release];
number == nil;
属性存取方法中的内存管理
不光是数组,其他对象也可以保留别的对象,这一般通过访问 "属性"来实现,而访问属性时,会用到相关实例变量的获取方法及设置方法。若属性为 "strong 关系"(strong relationship),则设置的属性值会保留。比方说,有个名叫 foo 的属性由名为 _foo 的实例变量所实现,那么,该属性的设置方法会是这样:
此方法将保留新值并释放旧值,然后更新实例变量,令其指向新值。顺序很重要。假如还未保留新值就先把旧值释放了,而且两个值又指向同一个对象,那么,先执行的 release 操作就可能导致系统将此对象永久回收。而后续的 retain 操作则无法令这个已经彻底回收的对象复生,于是实例变量就成了悬挂指针了。(这里上面也提到了) ;
自动释放池
在 Objective-C 的引用计数架构中,自动释放池是一项重要特性。调用 release 会立刻递减对象的保留计数(而且还有可能令系统回收此对象),然而有时候可以不调用它,改为调用 autorelease,此方法会在稍后递减计数,通常是在下一次 "事件循环"(event loop)时递减,不过也可能执行得更早些。
对于上面这个方法,我们需要延迟str对象的回收释放,也就是说,我们要用automatic release ;
用 autorelease,它会在稍后释放对象,从而给调用者留下了足够长的时间,使其可以在需要时先保留返回值。换句话说,此方法可以保证对象在跨越 "方法调用边界"(method callboundary)后一定存活。实际上,释放操作会在清空最外层的自动释放池(参见第 34 条)时执行,除非你有自己的自动释放池,否则这个时机指的就是当前线程的下一次事件循环
autorelease 能延长对象生命周期,使其在跨越方法调用边界后依然可以存活一段时间。
保留环
使用引用计数机制时,经常要注意的一个问题就是 "保留环"(retain cycle),也就是呈环状相互引用的多个对象。这将导致内存泄漏,因为循环中的对象其保留计数不会降为 0。对于循环中的每个对象来说,至少还有另外一个对象引用着它。图里的每个对象都引用了另外两个对象之中的一个。在这个循环里,所有对象的保留计数都是 1。
在垃圾收集环境中,通常将这种情况认定为 "孤岛"(island of isolation)。此时,垃圾收集器会把三个对象全都回收走。而在 Objective-C 的引用计数架构中,则享受不到这一便利。通常采用 "弱引用"(weak reference,参见第 33 条)来解决此问题,或是从外界命令循环中的某个对象不再保留另外一个对象。这两种办法都能打破保留环,从而避免内存泄漏。
要点
- 引用计数机制通过可以递增递减的计数器来管理内存。对象创建好之后,其保留计数至少为 1。若保留计数为正,则对象继续存活。当保留计数降为 0 时,对象就被销毁了。
- 在对象生命期中,其余对象通过引用来保留或释放对象。保留于释放操作分别会递增及递减保留计数。
以ARC简化引用计数
此代码有内存泄漏问题,因为 if 语句块末尾并未释放 message 对象。由于在 if 语句之外无法引用 message,所以此对象所占的内存泄漏了(这里"泄漏"的意思是:没有正确释放已经不再使用的内存)。
- 由于 ARC 会自动执行 retain、release 、autorelease 等操作,所以直接在 ARC 下调用这些内存管理方法是非法的。
ARC的优点除了方便外,还有:ARC 在调用这些方法时,并不通过普通的 Objective-C 消息派发机制,而是直接调用其底层 C 语言版本。这样做性能更好,因为保留及释放操作需要频繁执行,所以直接调用底层函数能节省很多 CPU 周期。
使用ARC时必须遵循的方法和命名规则
将内存管理语义在方法名中表示出来早已成为 Objective-C 的惯例,而 ARC 则将之确立为硬性规定。这些规则简单地体现在方法名上。若方法名以下列词语开头,则其返回的对象归调用者所有:
- alloc
- new
- copy
- mutableCopy
归调用者所有的意思是: 调用上述四种方法的那段代码要负责释放方法所返回的对象。也就是说,这些对象的保留计数是正值,而调用了这四种方法的那段代码要将其中一次保留操作抵消掉。
要注意:如果还有其他对象保留此对象,并对其调用了 autorelease,那么保留计数的值可能比 1 大,这也是 retainCount 方法不太有用的原因之一。
若方法名不以上述四个词语开头,则表示其所返回的对象并不归调用者所有。 在这种情况下,返回的对象会自动释放
维系这些规则所需的全部内存管理事宜均由 ARC 自动处理
变量的内存管理语义
ARC 也会处理局部变量与实例变量的内存管理。
默认情况下,每个变量都是指向对象的强引用。
对于以下的代码:
在非ARC下执行的setter方法的实现是这样的:
但这个方法很明显是不安全的,如果只有当前对象还在引用这个值,那么设置方法中的释放操作会使该值的保留计数降为0,从而导致系统将其回收。接下来再执行保留操作,就会令应用程序崩溃。
ARC 会用一种安全的方式来设置:先保留新值,再释放旧值,最后设置实例变量。
在应用程序中,可用下列修饰符来改变局部变量与实例变量的语义:
__strong: 默认语义,保留此值。
__unsafe_unretained: 不保留此值,这么做可能不安全,因为等到再次使用变量时,其对象可能已经回收了。
__weak: 不保留此值,但是变量可以安全使用,因为如果系统把这个对象回收了,那么变量也会自动清空。
__autoreleasing: 把对象 "按引用传递" (pass by reference)给方法时,使用这个特殊的修饰符。此值在方法返回时自动释放。
- 我们经常会给局部变量加上修饰符,用以打破由"块"(block),所引入的"保留环"(retain cycle)。块会自动保留其所捕获的全部对象,而如果这其中有某个对象又保留了块本身,那么就可能导致 "保留环"。可以用 __weak 局部变量来打破这种 "保留环":
ARC如何清理实例变量
要管理其内存,ARC 就必须在 "回收分配给对象的内存"(deallocate)
当手动管理引用计数时,你可能会像下面这样自己来编写 dealloc 方法:
如果有非 Objective-C 的对象,不需要像原来那样调用超类的 dealloc 方法。
ARC 环境下,dealloc 方法可以像这样写:
因为 ARC 会自动生成回收对象时所执行的代码,所以通常无须再编写 dealloc 方法。这能减少项目源代码的大小,而且可以省去其中一些样板代码(boilerplate code)。
覆写内存管理方法
不使用ARC时,可以覆写内存管理方法。比方说,在实现单例类的时候,因为单例不可释放,所以我们经常覆写release方法,将其替换为"空操作"
要点
- 有 ARC 之后,程序员就无须担心内存管理问题了。使用 ARC 来编程,可省去类中的许多 "样板代码"。
- ARC 管理对象生命期的办法基本上就是:在合适的地方插入 "保留" 及 "释放"操作。
- 在 ARC 环境下,变量的内存管理语义可以通过修饰符指明,而原来需要手工执行 "保留" 及 "释放"操作。
- 由方法所返回的对象,其内存管理语义总是通过方法名来体现。ARC 将此确定为开发者必须遵守的规则。
- ARC 只负责管理 Objective-C 对象的内存。尤其要注意: CoreFoundation 对象不归 ARC
管理,开发者必须适时调用 CFRetain/CFRelease。
在dealloc方法中只释放引用并解除监听
对象在经历其生命期后,最终会为系统所回收,这时就要执行 dealloc 方法了。在每个对象的生命期内,此方法仅执行一次,也就是当保留计数降为 0 的时候。
实际上,程序库会以开发者察觉不到的方式操作对象,从而使回收对象的真正时机和预期的不同。你决不应该自己调用 dealloc 方法,运行期系统会在适当的时候调用它。
那么,应该在 dealloc 方法中做什么呢?
主要就是释放对象所拥有的引用,也就是把所有 Objective-C 对象都释放掉,对象所拥有的其他非 Objective-C 对象也要释放。比如 CoreFoundation 对象就必须手工释放,因为它们是由纯C 的API 所生成的。
所以可以是:
最好还要加上[super dealloc] ;
编写 dealloc 方法时还需要注意,不要在里面随便调用其他方法。
- 如果在这里所调用的方法又要异步执行某些任务,或是又要继续调用它们自己的某些方法,那么等到那些任务执行完毕时,系统已经把当前这个待回收的对象彻底摧毁了。这会导致很多问题,且经常使应用程序崩溃,因为那些任务执行完毕后,要回调此对象,告诉该任务已完成,而此时如果对象已摧毁,那么回调操作就回出错。
- 在 dealloc 里也不要调用属性的存取方法,因为有人可能会覆写这些方法,并与其中做一些无法在回收阶段安全执行的操作。
要点
- 在 dealloc 方法里,应该做的事情就是释放指向其他对象的引用,并取消原来订阅的"键值观测"(KVO)或
NSNOtificationCenter 等通知,不要做其他事情。 - 如果对象持有文件描述符等系统资源,那么应该专门编写一个方法来释放此种资源。这样的类要和其使用者约定: 用完资源后必须调用 close
方法。 - 执行异步任务的方法不应该在 dealloc 里调用; 只能在正常状态下执行的那些方法也不应在 dealloc
里调用,因为此时对象已处于正在回收的状态了。
编写"异常安全代码"时留意内存管理问题
纯 C 中没有异常,而 C++ 与 Objective-C 都支持异常。在当前的运行期系统中,C++ 与 Objective-C 的异常相互兼容,也就是说,从其中一门语言里抛出的异常能用另外一门语言所编写的 "异常处理程序"(exception handler)来捕获。
比如使用 Objective-C++ 来编码时,或是编码中用到了第三方程序库而此程序库所抛出的异常又不受你控制时,就需要捕获及处理异常了。
C++ 的析构函数(destructor)由 Objective-C 的异常处理例程(exception-handle routine)来运行。这对于 C++ 对象很重要,由于抛出异常会缩短其生命周期,所以发生异常时必须析构,不然就会泄漏,
手动管理的方式如下:
但上面的方法中,如果dosomethingThatMayThrow方法判断抛出异常,会直接进入catch块,所以没能release导致内存泄漏,解决方法时在设一个@finalllay块,将release操作放到其中。
以弱引用避免保留环
首先我们要了解什么是保留环:
对象图里经常会出现一种情况,就是几个对象都以某种方式互相引用,从而形成"环"(cycle)。由于 Objective-C 内存管理模型使用引用计数架构,所以这种情况通常会泄漏内存,因为最后没有别的东西会引用环中的对象。
下面给一个书上的例子;
两个类之间的引用关系如图:
上面就是一个简单的保留环,即使外界不在引用这两个对象,这两个对象依赖于彼此的引用关系,其引用计数不会降到0,即他们不会被系统自动回收,造成内存泄漏 ;
下面用图表示一个更复杂的保留环,
如果只剩一个引用还指向保留环中的实例,而现在又把这个引用移除,那么整个保留环就泄漏了。也就是说,没办法再访问其中的对象了。图中所示的保留环更复杂一些,其中有四个对象,只有 ObjectB 还为外界所引用,把仅有的这个引用移除之后,四者所占内存就泄漏了。
避免保留环的最佳方式就是弱引用。这种引用经常用来表示 "非拥有关系"(nonowning relationship)。将属性声明为 unsafe_unretained
可以把上面的代码修改成下面这样:
修改之后,EOCClassB 实例就不再通过 other 属性来拥有 EOCClassA 实例了。属性特质 (attribute) 中的 unsafe_unretained 一词表明,属性值可能不安全,而且不归此实例所拥有。如果系统已经把属性所指的那个对象回收了,那么在其上调用方法可能会使应用程序崩溃。由于本对象并不保留属性对象,因此其有可能为系统所回收。
unsafe_unretained 和 weak
weak与 unsafe_unretained 的作用完全相同。然而,只要系统把属性回收,属性值就会自动设为 nil。
使用 weak 而非 unsafe_unretained 引用可以令代码更安全。应用程序也许会显示出错误的数据,但不会直接崩溃。这么做显然比令终端用户直接看到程序退出要好。
要点
- 将某些引用设为 weak,可避免出现 "保留环"。
- weak 引用可以自动清空,也可以不自动清空。自动清空(autonilling)是随着 ARC
而引入的新特性,由运行期系统来实现。在具备自动清空功能的弱引用上,可以随意读取其数据,因为这种引用不会指向已经回收过的对象。
以"自动释放池"释放内存峰值
创建自动释放池所用语法如下:
一般情况下无须担心自动释放池的创建问题。
通常只有一个地方需要创建自动释放池,那就是在 main 函数里,我们是自动释放池来包裹应用程序的主入口点 (main application entry point)。
自动释放池于左花括号处创建,并于对应的右花括号处自动清空。位于自动释放池范围内的对象,将在此范围末尾处收到 release 消息。自动释放池可以嵌套。系统在自动释放对象时,会把它放到最内层的池里。
将自动释放池嵌套用的好处是,可以借此控制应用程序的内存峰值,使其不致过高。
上面这段代码的for循环中会产生许多多余的变量占用内存,由于无法通过指针来直接调用release来释放内存,所以这里最好设置自动释放池来控制内存
自动释放池机制就像 "栈"(stack)一样。系统创建好自动释放池之后,就将其推入栈中,而清空自动释放池,则相当于将其从栈中弹出。在对象上执行自动释放操作,就等于将其放入栈顶的那个池里。
是否应该用池来优化效率,完全取决于具体的应用程序。首先得监控内存用量,判断其中有没有需要解决的问题,如果没完成这一步,那就别急着优化。尽管自动释放池块的开销不太大,但毕竟还是有的,所以尽量不要建立额外的自动释放池。
要点
- 自动释放池排布在栈中,对象收到 autorelease 消息后,系统将其放入最顶端的池里。
- 合理运用自动释放池,可降低应用程序的内存峰值。
- @autoreleasepool 这种新式写法能创建出更为轻便的自动释放池。
不要使用retainCount
如果在 ARC 中调用,编译器就会报错,这和在 ARC 中调用 retain、release、autorelease 方法时的情况一样。虽然此方法已经正式废弃了,但还是经常有人误解它,其实这个方法根本就不应该调用。若在不启用 ARC 的环境下编程(说真的,还是在 ARC 下编程比较好),那么仍可调用此方法,而编译器不会报错。
此方法之所以无用,其首要原因在于:它返回的保留计数只是某个给定时间点上的值。该方法并未考虑到系统会稍后把自动释放池清空,因而不会将后续的释放操作从返回值里减去,这样的话,此值就未必能真实反映实际的保留计数了。
那么,只为了调试而使用 retainCount 方法行不行呢?即便只为调试,此方法也不是很有用。由于对象可能处在自动释放池中,所以其保留计数未必如想象般精确。而且其他程序库也可能自行保留或释放对象,这都会扰乱保留计数的具体取值。看了具体的计数值之后,你可能还误以为是自己的代码修改了它,殊不知其实是由深埋在另外一个程序库中的某段代码所改的。
- 对象的保留计数看似有用,实则不然,因为任何给定时间点上的"绝对保留计数"(absolute retain
count)都无法反映对象生命期的全貌。 - 引入 ARC 之后,retainCount 方法就正式废止了,在 ARC 下调用该方法会导致编译器报错。