[编写高质量iOS代码的52个有效方法](八)内存管理(下)
先睹为快
33.以弱引用避免保留环
34.以自动释放池块降低内存峰值
35.用僵尸对象调试内存管理问题
36.不要使用retaincount
第33条:以弱引用避免保留环
对象图里经常会出现一种情况,就是几个对象都以某种方式相互引用,从而形成环。这种情况通常会泄漏内存,因为最后没有别的东西会引用环中的对象。而环里的对象会因为相互间的引用而继续存活,不被回收。
#import @class eocclassa; @class eocclassb; @interface eocclassa : nsobject @property (nonatomic, strong) eocclassb *other; @end @interface eocclassb : nsobject @property (nonatomic, strong) eocclassa *other; @end
本段代码中就可能出现了保留环,如果把eocclassa实例的other属性设置为了某个eocclassb实例,而又把eocclassb实例的other属性设置成了这个eocclassa实例。那么两个对象就会相互引用,出现保留环。
避免保留环的最佳方式就是弱引用。这种引用经常用来表示非拥有关系。将属性声明为unsafe_unretained或weak即可。
#import @class eocclassa; @class eocclassb; @interface eocclassa : nsobject @property (nonatomic, strong) eocclassb *other; @end @interface eocclassb : nsobject @property (nonatomic, weak) eocclassa *other; @end
修改之后,eocclassb实例就不能再通过other属性来拥有eocclassa实例了。weak与unsafe_unretained的区别在于,系统把属性回收后,weak属性会自动设置为nil,而unsafe_unretained属性仍然指向那个已经回收的实例,这样可能会不安全。不过无论如何,只要所在对象已经被系统回收后,都不应该继续使用弱引用。
第34条:以自动释放池块降低内存峰值
在执行循环体时,一般会持续有新对象创建出来,并加入自动释放池中。这种对象都要等到循环执行完才会释放。这样一来,在执行循环时,应用程序所占内存量会持续上涨,而等到所有临时对象都释放后,内存用量又会突然下降。
nsarray *databaserecords = /* ... */; nsmutablearray *people = [nsmutablearray new]; for(nsdictionary *record in databaserecords){ eocperson *person = [[eocperson alloc] initwithrecord:record]; [people addobject:person]; }
这种情况不甚理想,尤其是循环长度无法预知时,再创建出一些临时的eocperson对象,它们本该提早回收的。增加一个自动释放池即可解决问题,把循环内的代码包裹在自动释放池块中,那么循环体中自动释放的对象就会在这个池,而不是线程的主池里:
nsarray *databaserecords = /* ... */; nsmutablearray *people = [nsmutablearray new]; for(nsdictionary *record in databaserecords){ @autoreleasepool{ eocperson *person = [[eocperson alloc] initwithrecord:record]; [people addobject:person]; } }
加上自动循环池之后,就会降低应用程序在执行循环时的内存峰值。因为系统会在块的末尾将临时对象回收掉。如果循环的内存用量不高,则尽量不建立额外的自动循环池,因为自动释放池块还是存在开销(虽然不大)。
在arc出现之前一般使用nsautoreleasepool对象,这样可以不用每次循环都清空池,通常用来创建偶尔需要清空的池:
nsarray *databaserecords = /* ... */; nsmutablearray *people = [nsmutablearray new]; int i = 0; // 创建自动释放池会被推入栈中,在对象上执行autorelease等于将其放到栈顶的自动释放池中。 nsautoreleasepool *pool = [[nsautoreleasepool alloc] init]; for(nsdictionary *record in databaserecords){ eocperson *person = [[eocperson alloc] initwithrecord:record]; [people addobject:person]; // 每执行10次循环,清空一次自动释放池 if (++i == 10){ [pool drain]; i = 0;; } } // 结束循环后,再次清空自动释放池 [pool drain];
第35条:用僵尸对象调试内存管理问题
向已回收的对象发送消息是不安全的。这么做是否可行完全取决于对象所占内存有没有为其他内容所覆写。cocoa提供了僵尸对象这个方便的功能。启用这项调试功能后,运行期系统会把所有已经回收的实例转化为特殊的僵尸对象,而不会真正回收它们。僵尸对象收到消息后,会抛出异常,其中准确说明了发送过来的消息,并描述回收之前的那个对象。僵尸对象是调试内存管理的最佳方式。
在xcode中打启用僵尸对象:点击下图中左上角标注的位置选择 edit scheme,再选择run中的diagnostics分页,勾选enabled zombine objects选项
下面代码就演示普通对象转换为僵尸对象的过程
注意:采用的是手动计数,在build settings中将objective-c automatic reference counting设为no即可不用arc。
#import #import @interface eocclass : nsobject @end @implementation eocclass @end void printclassinfo(id obj){ class cls = object_getclass(obj); class supercls = class_getsuperclass(cls); nslog(@"=== %s : %s ===", class_getname(cls), class_getname(supercls)); } int main(int argc, const char * argv[]) { eocclass *obj = [[eocclass alloc] init]; nslog(@"before release:"); printclassinfo(obj); [obj release]; nslog(@"after release"); printclassinfo(obj); return 0; }
运行结果:
2016-07-27 14:47:31.096 mrr orders[89086:765092] before release: 2016-07-27 14:47:31.097 mrr orders[89086:765092] === eocclass : nsobject === 2016-07-27 14:47:31.097 mrr orders[89086:765092] after release 2016-07-27 14:47:31.097 mrr orders[89086:765092] === _nszombie_eocclass : nil ===
对象所属的类已经由eocclass变为nszombie_eocclass。这个类是代码中没有定义的,在运行期生成的。编译器首次遇到eocclass类对象要变成僵尸对象时,就会在类名前加上_nszombie前缀生成对应的僵尸类。
僵尸类只是充当一个标记,它的作用会在消息转发过程中体现出来。当执行到完整转发时,“forwarding”函数会检查对象所属的类名,若名称前缀为nszombie,表明消息接收者是僵尸对象,需要特殊处理,此时会打印一条消息,其中指明僵尸对象收到的消息及原来所属的类(僵尸类去掉前缀),然后应用程序终止。
在之前代码末尾加上一句代码向僵尸对象发送消息:
int main(int argc, const char * argv[]) { eocclass *obj = [[eocclass alloc] init]; nslog(@"before release:"); printclassinfo(obj); [obj release]; nslog(@"after release"); printclassinfo(obj); // 向僵尸对象发送消息 [obj description]; return 0; }
运行结果
2016-07-27 15:02:32.822 mrr orders[89855:774958] before release: 2016-07-27 15:02:32.823 mrr orders[89855:774958] === eocclass : nsobject === 2016-07-27 15:02:32.823 mrr orders[89855:774958] after release 2016-07-27 15:02:32.823 mrr orders[89855:774958] === _nszombie_eocclass : nil === 2016-07-27 15:02:32.823 mrr orders[89855:774958] *** -[eocclass description]: message sent to deallocated instance 0x1006002e0
可以看到僵尸对象原来所属的类,收到的选择器以及对应的指针值都打印出来了。
第36条:不要使用retaincount
objective-c通过引用计数来管理内存,每个对象都有一个计数器,其值表明还有多少个其他对象想令此对象继续存活。nsobject协议中定义了下列方法,用于查询对象当前的保留计数:
- (nsuinteger)retaincount
arc中已经废弃此方法了,非arc环境仍然可用,但是不应该用。
首要原因在于:它所返回的保留计数只是某个给定时间点上的值,并未考虑稍后清空自动释放池,因此未必能真是反应实际的保留计数。
while([object reatincount]){ [object release]; }
这种写法的错误在于,它没有考虑后续的自动释放操作,假如对象在自动释放池中,稍后系统清空池子还要再释放对象一次,引起程序崩溃。而且reatincount可能永远不返回0,因为有时系统会优化对象的释放行为,在保留计数还是1的时候就把它回收了。如果对象已经回收了,循环还在进行,也会导致程序崩溃。
reatincount返回的保留计数具体值也不一定有用
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]);
运行结果:
2016-07-27 15:16:59.776 mrr orders[90612:784462] string retaincount = 18446744073709551615 2016-07-27 15:16:59.777 mrr orders[90612:784462] numberi retaincount = 9223372036854775807 2016-07-27 15:16:59.777 mrr orders[90612:784462] numberf retaincount = 1
第一个对象的保留计数是2的64次方减1,第二个是2的63次方减一.由于二者都是单例对象,所以其保留计数都很大。系统会尽可能把nsstring实现成单例对象,nsnumber也类似,它使用了一种叫做标签指针的概念来标注特定类型的数值,将有关信息都存放在指针值里。由于浮点数没有此优化,所以保留计数为1。
对于单例对象来说,保留计数永远不会变,保留及释放都是空操作。
由于对象可能出在自动释放池中,其保留计数未必如想象般精确,而且其他程序库也可能自行保留或释放对象,者都会扰乱计数的具体取值。所以任何情况下都不要使用retaincount。
上一篇: jquery 每隔自动刷新
推荐阅读
-
[编写高质量iOS代码的52个有效方法](十)Grand Central Dispatch(GCD)
-
[编写高质量iOS代码的52个有效方法](十一)系统框架
-
[编写高质量iOS代码的52个有效方法](一)Objective-C基础
-
[编写高质量iOS代码的52个有效方法](五)接口与API设计(下)
-
[编写高质量iOS代码的52个有效方法](八)内存管理(下)
-
编写高质量iOS与OSX代码的52个有效方法-第四章:协议与封装
-
Effective Objective-C 2.0 编写高质量iOS与OS X代码的52个有效方法(Matt Galloway著)读书笔记(一)
-
编写高质量iOS与OS X代码的52个有效方法 读后感
-
[编写高质量iOS代码的52个有效方法](十)Grand Central Dispatch(GCD)
-
[编写高质量iOS代码的52个有效方法](十一)系统框架