29. 理解引用计数
retain 递增保留计数。
release 递减保留计数。
autorelease 待稍后清理"自动释放池"(autorelease pool)时,再递减保留计数。
调用者已通过alloc方法表达了想令该对象继续存活下去的意愿。不过请注意,这并不是说对象此时的保留计数必定是1。在alloc或"initWithInt:"方法的实现代码中,也许还有其他对象也保留了此对象,所以,其保留计数可能会大于1。能够肯定的是: 保留计数至少为1。保留计数这个概念就应该这样来理解才对。绝不应该说保留计数一定是某个值,只能说你所执行的操作是递增了该计数还是递减了该计数。
为避免不经意间使用了无效对象,一般调用完release之后都会清空指针。这就能保证不会出现可能指向无效对象的指针,这种指针通常称为"悬挂指针"(dangling pointer)。比方说,可以这样编写代码来防止此情况发生:
NSNumber *number = [[NSNumber alloc] initWithInt:1337];
[array addObject:number];
[number release];
number = nil;
**※ 自动释放池 **
在Objective-C的引用计数架构中,自动释放池是一项重要特性。调用release会立刻递减对象的保留计数(而且还有可能令系统回收此对象),然而有时候可以不调用它,改为调用autorelease,此方法会在稍后递减计数(相当于执行一次release),通常是在下一次"事件循环"(event loop)时递减,不过也可能执行得更早些(参见第34条)。
这个具体之后再讨论吧~
30. 以ARC简化引用计数
由于ARC会自动执行retain、release、autorelease等操作,所以直接在ARC下调用这些内存管理方法是非法的。具体来说,不能调用下列方法:
- retain
- release
- autorelease
- dealloc
例如:
if ([self shouldLogMessage]) {
NSString *message = [[NSString alloc] initWithFormat:@"I am object, %p", self];
NSLog(@"message = %@", message);
//Added by ARC
[message release];
}
实际上,ARC在调用这些方法时,并不通过普通的Objective-C消息派发机制,而是直接调用其底层的C语言版本。这样做性能更好,因为保留及释放操作需要频繁执行,所以直接调用底层函数能节省很多CPU周期。比方说,ARC会调用与retain等价的底层函数objec_retain。这也是不能覆写retain、release或autorelease的缘由,因为这些方法从来不会被直接调用。笔者在本节后面的文字中将用等价的Objective-C方法来指代与之相关的底层C语言版本,这对于那些手动管理过引用计数的开发者来说更易理解。
※ 使用ARC时必须遵循的方法命名规则
将内存管理语义在方法名中表示出来早已成为Objective-C的惯例,而ARC则将之确立为硬性规定。这些规则简单地体现在方法名上。若方法名以下列词语开头,则其返回的对象归调用者所有:
- alloc
- new
- copy
- mutableCopy
归调用者所有的意思是: 调用上述四种方法的那段代码要负责释放方法所返回的对象。也就是说,这些对象的保留计数是正值,而调用了这四种方法的那段代码要将其中一次保留操作抵消掉。如果还有其他对象保留此对象,并对其调用了autorelease,那么保留计数的值可能比1大,这也是retainCount方法不太有用的原因之一(参见第36条)。
若方法名不以上述四个词语开头,则表示其所返回的对象并不归调用者所有。在这种情况下,返回的对象会自动释放,所以其值在跨越方法调用边界后依然有效。要想使对象多存活一段时间,必须令调用者保留它才行。
这里其实再一次解释了autorelease~ 举个例子:
- (EOCPerson*)newPerson {
EOCPerson *person = [[EOCPerson alloc] init];
return person;
/**
* The method name begins with `new’, and since `person’
* already has an unbalanced +1 reference count from the
* `alloc’, no retains, releases or autoreleases are
* required when returning.
*/
}
- (EOCPerson*)somePerson {
EOCPerson *person = [[EOCPerson alloc] init];
return person;
/**
* The method name does not begin with one of the "owning"
* prefixes, therefore ARC will add an autorelease when
* returning `person’.
* The equivalent manual reference counting statement is:
* return [person autorelease];
*/
}
- (void)doSomething {
EOCPerson *personOne = [self newPerson];
// …
EOCPerson *personTwo = [self somePerson];
// …
/**
* At this point, `personOne’ and `personTwo’ go out of
* scope, therefore ARC needs to clean them up as required.
* - `personOne’ was returned as owned by this block of
code, so it needs to be released.
* - `personTwo’ was returned not owned by this block of
code, so it does not need to be released.
* The equivalent manual reference counting cleanup code
* is:
* [personOne release];
*/
}
也就是说ARC会自己根据我们的方法名是不是以alloc/new/copy/mutablecopy来开头,在末尾为我们返回实例判断要不要加autorelease,如果加了,那么在使用方法的时候,结尾就不用再次release;如果没有加autorelease,在使用时出了作用域以后会加一次release。
除了会自动调用"保留"与"释放"方法外,使用ARC还有其他好处,它可以执行一些手动操作很难甚至无法完成的优化。例如,在编译期,ARC会把能够互相抵消的retain、release、autorelease操作约简。如果发现在同一个对象上执行了多次"保留"与"释放"操作,那么ARC有时可以成对地移除这两个操作。
举个优化例子:
// From a class where _myPerson is a strong instance variable
_myPerson = [EOCPerson personWithName:@"Bob Smith"];
//调用"personWithName:"方法会返回新的EOCPerson对象,而此方法在返回对象之前,为其调用了autorelease方法。由于实例变量是个强引用,所以编译器在设置其值的时候还需要执行一次保留操作。因此,前面那段代码与下面这段手工管理引用计数的代码等效:
EOCPerson *tmp = [EOCPerson personWithName:@"Bob Smith"];
_myPerson = [tmp retain];
然而刚刚autorelease之后就retain,其实是相抵消的。编译时期为了向后兼容并没有对这点优化,而运行时其实是做了检查避免了先release再retain的,这样会更快。
在方法中返回自动释放的对象时,不再直接调用对象的autorelease方法,而是改为调用objc_autoreleaseReturnValue,设置全局数据结构中的一个标志位,此函数会检视当前方法返回之后即将要执行的那段代码,是否要retain返回对象,若要retain,那么不执行autorelease;若不retain,那么需要再执行autorelease。
执行到retain那一句的时候只要检查那个标志位,如果已经设置了,说明之前有autorelease并且没有执行,于是这里也不用真的去retain了,相互抵消;如果没有设置标志位,就执行retain。
@interface EOCClass : NSObject {
id _object;
}
@implementation EOCClass
- (void)setup {
_object = [EOCOtherClass new];
}
@end
ARC会自动改写为:
- (void)setup {
id tmp = [EOCOtherClass new];
_object = [tmp retain];
[tmp release];
}
由于retain后立刻release,所以其实抵消了,优化后其实仍旧是:
_object = [EOCOtherClass new];
所以会有下面的现象:
- (void)testRetain {
NSObject *ret = [self someProperty];
NSLog(@"ret test retain:%lu", (unsigned long)[ret arcDebugRetainCount]);
}
- (NSObject *)someProperty {
NSObject *A = [[NSObject alloc] init];
return A;
}
输出:
2020-06-18 15:13:59.315172+0800 Example1[35885:622032] ret test retain:1
A虽然是autorelease也持有着对象,但是因为ret赋值的时候的retain没有真的retain,而是抵消了autorelease,所以它的retainCount是1而不是2~
一些内存修饰符:
- __strong: 默认语义,保留此值
- __unsafe_unretained: 不保留此值,这么做可能不安全,因为等到再次使用变量时,其对象可能已经回收了。
- __weak: 不保留此值,但是变量可以安全使用,因为如果系统把这个对象回收了,那么变量也会自动清空。(常用与解决retain cycle)
- __autoreleasing: 把对象"按引用传递"(pass by reference)给方法时,使用这个特殊的修饰符。此值在方法返回时自动释放。
__autoreleasing是神马呢?其实使用__autoreleasing 修饰符对应MRC调用 autorelease方法,因为ARC是不能调用autorelease的,所以可以通过__autoreleasing修饰把对象放入autorelease pool,可参考:https://www.jianshu.com/p/0258ed2133ff
※ ARC如何清理实例变量
ARC之后一般不需再编写dealloc方法,ARC会借用Objective-C++的一项特性来生成清理例程:编译器如果发现某个对象里含有C++对象,就会生成名为.cxx_destruct析构的方法,方法中有自动调用超类dealloc的方法。回收Objective-C++对象时,待回收的对象会调用所有C++对象的析构方法,利用该方法中生成清理内存所需的代码。
非Objective-C的对象,如CoreFoundation中的对象或是由malloc()分配在堆中的内存,仍需要清理
// ARC下:
- (void)dealloc
{
CFRelease(_coreFoundationObject);
free(_heapAllocatedMemoryBlob)
}
ARC只负责管理Objective-C对象的内存。尤其要注意: CoreFoundation对象不归ARC管理,开发者必须适时调用CFRetain/CFRelease。
31. 在dealloc方法中只释放引用并解除监听
那么,应该在dealloc方法中做些什么呢?主要就是释放对象所拥有的引用,也就是把所有Objective-C对象都释放掉,ARC会通过自动生成的.cxx_destruct方法(参见第30条),在dealloc中为你自动添加这些释放代码。对象所拥有的其他非Objective-C对象也要释放。比如CoreFoundation对象就必须手动释放,因为它们是由纯C的API所生成的。
在dealloc方法中,通常还要做一件事,就是把原来配置过的观测行为都清理掉(KVO以及Observer)。如果用NSNotificationCenter给此对象订阅过某种通知,那么一般应该在这里注销。这样的话,通知系统就不再把通知发给回收后的对象了,若是还向其发送通知,则必然会令应用程序崩溃。
但是最新的ios系统已经不需要在dealloc里面取消Observer了哈,监听也会由系统自动取消啦。
如果不是ARC,在dealloc里面还要调用super dealloc哈。
但不是所有资源都应该统一在dealloc里面释放的,有些大块资源不能等它自动释放,如果用完了就应该释放掉,例如各种Connection。
因为如果内存泄漏,根本就不会走dealloc,那么这些大块内存就不能被释放掉了。
系统并不保证每个创建出来的对象的dealloc都会执行。极个别情况下,当应用程序终止时,仍有对象处于存活状态,这些对象没有收到dealloc消息。在Mac OS X及iOS应用程序所对应的application delegate中,都含有一个会于程序终止时调用的方法。如果一定要清理某些对象,那么可在此方法中调用那些对象的“清理方法”。
- (void)applicationWillTerminate:(UIApplication *)application
※ dealloc里面不能做什么
不要在里面随便调用其他方法。
如果在这里调用的方法又要异步执行某些任务,或是又要继续调用他们自己的某些方法,那么等到那些任务执行完毕时,系统已经把当前这个待回收的对象彻底摧毁了。调用dealloc方法的那个线程会执行“最终的释放操作”(final release),令对象的保留计数降为0,而某些方法必须在特性的线程里(比如主线程)调用才行。若在dealloc里调用了那些方法,则无法保证当前这个线程就是那些方法所需要的线程。
在dealloc里也不要调用属性的存取方法。
因为有人可能会覆写这些方法,并于其中做一些无法在回首阶段安全之行的操作。此外,属性可能正处于“键值观测”(Key-Value Observation, KVO)机制的监控之下,该属性的观察者(observer)可能会在属性值改变时“保留”或使用这个即将回收的对象。
32. 编写“异常安全代码”时留意内存管理问题
OC和C++都是有异常的(纯C是木有的),他们可以互相进行异常处理,也就是OC抛出的,会被C++ catch。
使用MRC处理异常的时候应该注意内存释放,因为如果抛出了异常,可能释放的代码不会跑到,于是最好将release放到finally:
@try{
EOCSomeClass *object = [[EOCSomeClass alloc] init];
[object doSomethingThatMayThrow];
}
@catch(...){
Nslog(@"There was an error!");
}
// 无论是否发生异常,@finally块中的代码都会执行
@finally{
[object release];
}
而ARC里面是没有release方法的,并且其实ARC也没有默认会处理异常的内存问题,原因是ios只有在非常严重的问题的时候才会抛出异常,并且这个时候一般应用即将被终止,内存泄漏也就没有意义了,于是不需要处理。
如果打开编译器的-fobjc-arc-exceptions标志,可以开启ARC生成安全处理异常所用的附加代码。(尽量不要开启)只是这段代码会严重影响运行期的性能,即使不抛出异常。这种场景主要是Objective-C++会用到,因为c++经常会抛出异常,如果有大量异常捕获操作时,应考虑重构代码,用21条的NSError式错误信息传递法来取代异常。
33. 以弱引用避免retain cycle
一般来说,如果不拥有某对象,那就不要保留它。这条规则对collection例外,collection虽然并不直接拥有其内容,但是它要代表自己所属的那个对象来保留这些元素。
34. 以“自动释放池”降低内存峰值
释放对象有两种方式:
- 调用release方法,使其保留计数立即递减
- 调用autorelease方法,将其加入”自动释放池“中。自动释放池用于存放那些需要在稍后某个时刻释放的对象。清空自动释放池时,系统会向其中的对象发送release消息。
下面这段代码中的花括号定义了自动释放池的范围。自动释放池于左花括号处创建,并于对应的右花括号处自动清空。位于自动释放池范围内的对象,将在此范围末尾处收到 release 消息。自动释放池可以嵌套。系统在自动释放对象时,会把它放到最内层的池里。比方说:
@autoreleasepool {
NSString *string = [NSString stringWithFormat:@"1= %i", 1];
@autoreleasepool {
NSNumber *number = [NSNumber numberWithInt:1];
}
}
将自动释放池嵌套用的好处是,可以借此控制应用程序的内存峰值,使其不致过高。
是否应该用池来优化效率,完全取决于具体的应用程序。首先得监控内存用量,判断其中有没有需要解决的问题,如果没完成这一步,那就别急着优化。尽管自动释放池块的开销不太大,但毕竟还是有的,所以尽量不要建立额外的自动释放池。
35. 使用僵尸对象调试内存管理问题
调试内存管理问题很令人头疼。大家都知道,向已回收的对象发送消息是不安全的。这么做有时可以,有时不行。具体可行与否,完全取决于对象所占内存有没有为其他内容所复写。而这块内存有没有移作他用,又无法确定,因此,应用程序只是偶尔崩溃。在没有崩溃的情况下,那块内存可能只复用了其中一部分,所以部分对象中的某些二进制数据依然有效。还有一种可能,就是那块内存恰好为另外一个有效且存货的对象所占据。在这种情况下,运行期系统会把消息转发到新对象那里,而此对象也许能应答,也许不能。如果能,那程序就不崩溃,可你会觉得奇怪:为什么收到消息的对象不是预想的那个呢?若新对象无法响应选择子,则程序依然会崩溃。
所幸Cocoa提供了“僵尸对象“(Zombie Object)这个非常方便的功能。启用这项调试功能之后,运行期系统会把所有已经回收的实例转化为特殊的”僵尸对象“,而不是真正回收他们。这种对象所在的核心内存无法重用,因此不可能遭到复写。僵尸对象收到消息之后,会抛出异常,其中准确说明了发送过来的消息,并描述了回收之前的那个对象。僵尸对象是调试内存管理问题的最佳方式。将NSZombieEnabled环境变量设为YES,即可开启此功能。
给僵尸对象发消息后,控制台会打印消息,而应用程序会终止。打印出来的消息就像这样:-[CFString respondsToSelector:] message sent to deallocated instance 0x7ff9e9c080e0
在Xcode里面可以这么设置(Edit Schema):
僵尸调试具体原理
举个例子,如果有个类是Zombie,当他变为僵尸对象后,也就是dealloc以后,对象所属的类会由Zombie变为_NSZombie_Zombie。但是,这个新类是从哪里来的呢?代码中没有定义过这样一个类。而且,在启用僵尸对象后,如果编译器每看到一种可能变成僵尸的对象,就创建一个与之对应的类,那也低效了。
_NSZombie_Zombie实际上是在运行期生成的,当首次碰到Zombie类的对象要变成僵尸对象时,就会创建那么一个类。在创建的过程中用到了运行期程序库里得函数,它们的功能很强大,可以操作类列表(class list)。僵尸类(zombie class)是从名为NSZombie的模板类里复制出来的。这些僵尸类没有多少事情可做,只是充当一个标记。
这个过程其实就是NSObject的dealloc方法所做的事。运行期系统如果发现NSZombieEnabled环境变量已设置,那么就把”dealloc“方法的“调配”(swizzle)成一个会执行生成_NSZombie_XXX类代码的版本。执行到程序末尾时,对象所属的类已经变为_NSZombie_OriginalClass了,其中OriginalClass指的是原类名。
代码中的关键之处在于:对象所占内存没有(通过调用free()方法)释放,因此,这块内存不可复用。虽说内存泄漏了,但这只是个调试手段,发布正式应用程序时不会把这项功能打开,所以这种泄漏问题无关紧要。
但是,系统为何要给每个变为僵尸的类都创建一个对应的模型呢?这是因为,给僵尸对象发消息之后,系统可由此知道该对象原来所属的类。假如把所有僵尸对象都归到NSZombie类里,那原来的类名就丢了。
僵尸类的作用会在消息转发过程中体现出来。NSZombie类(以及所有从该类拷贝出来的类)并未实现任何方法。此类没有超类,因此和NSObject一样,也是个“根类”,该类只有一个实例变量,叫做isa,所有Objective-C的根类都必须有此变量。
由于这个轻量级的类没有实现任何方法,所以发给它的全部消息都要经过“完整的消息转发机制”。在完整的消息转发机制中,_ _ forwarding _ _是核心,调试程序时,大家可能在栈回溯消息里看见过这个函数。它首先要做的事情就包括检查接收消息的对象所属的类名。若名称前缀为NSZombie,则表明消息接收者是僵尸对象,需要特殊处理。此时会打印一条消息,其中指明了僵尸对象所收到的消息及原来所属的类,然后应用程序就终止了。
- 系统会修改对象的isa指针,令其指向特殊的僵尸类,从而使改对象变为僵尸对象。僵尸类能够响应所有的选择子,响应方式为:打印一条包含消息内容及其接收者的消息,然后终止应用程序。
总结一下这个过程:
- object dealloc被调配到了新的方法,会在运行时生成一个_NSZombie_XXX的类,并且不会释放object的内存
- 将object的isa指针指向新的_NSZombie_XXX类
- 当object接收到消息以后,由于_NSZombie_XXX类并没有处理这个消息,消息就会进行转发,转发时会通过类名前缀看是不是僵尸类,并打印消息
36. 不要使用retainCount
它所返回的保留计数只是某个给定时间点上的值。并未考虑到系统会稍后把自动释放池清空,因而不会将后续的释放操作从返回值里减去,所以此值就未必能真实反应实际的保留计数了。
reatinCount可能永远不返回0,因为有时系统会优化对象的释放行为,在保留计数还是1的时候就把它回收了。
而且对于NSString和NSNumber而言其实retainCount没意义,他们都是单例,也解决了之前的为什么autorelease pool都不能释放掉NSString字面量的问题。
※ NSString与NSNumber
NSString *string = @"Some string";
NSLog(@"string retainCount = %lu",[string retainCount]);
NSNumber *numberI = @1;
NSLog(@"numberI retainCount = %lu",[numberI retainCount]);
NSNumber *numberF = @3.14f;
NSLog(@"numberF retainCount = %lu",[numberF retainCount]);
运行结果:
string retainCount = 18446744073709551615
numberI retainCount = 9223372036854775807
numberF retainCount = 1
第一个对象的保留计数是2的64次方减1,第二个是2的63次方减一。由于二者都是单例对象,所以其保留计数都很大。系统会尽可能把NSString实现成单例对象,NSNumber也类似。
如果字符串像本例所举的这样,是个编译常量(compile-time constant),那么就可以这样来实现了。在这用情况下,编译器会把NSString对象所表示的数据放到应用程序的二进制文件里,这样的话,运行程序时就可以直接用了,无须再创建NSString对象。
NSNumber也类似,它使用了一种叫做“标签指针”(tagged pointer)的概念来标注特定类型的数值。这种做法不使用NSNumber对象,而是把与数值有关的全部消息都放在指针值里面。运行期系统会在消息派发(参见第11条)期间检测到这种标签指针,并对它执行相应操作,使其行为看上去和真正的NSNumber对象一样。这种优化只在某些场合使用,比如范例中的浮点数对象就没有优化,所以其保留计数就是1。
对于单例对象来说,保留计数永远不会变,保留及释放都是空操作。而且两个单例之间的计数其实也不会一致。