[编写高质量iOS代码的52个有效方法](八)内存管理(下)

[编写高质量iOS代码的52个有效方法](八)内存管理(下)

参考书籍:《Effective Objective-C 2.0》 【英】 Matt Galloway

先睹为快

33.以弱引用避免保留环

34.以自动释放池块降低内存峰值

35.用僵尸对象调试内存管理问题

36.不要使用retainCount

目录

第33条:以弱引用避免保留环

对象图里经常会出现一种情况,就是几个对象都以某种方式相互引用,从而形成环。这种情况通常会泄漏内存,因为最后没有别的东西会引用环中的对象。而环里的对象会因为相互间的引用而继续存活,不被系统回收。

#import <Foundation/Foundation.h>

@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 <Foundation/Foundation.h>

@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 <Foundation/Foundation.h>
#import <objc/runtime.h>

@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。

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值