欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

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

程序员文章站 2022-10-31 14:59:02
先睹为快 33.以弱引用避免保留环 34.以自动释放池块降低内存峰值 35.用僵尸对象调试内存管理问题 36.不要使用retaincount   第33条:以弱引用避免保留环 对象图里经常...

先睹为快

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实例。那么两个对象就会相互引用,出现保留环。

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

避免保留环的最佳方式就是弱引用。这种引用经常用来表示非拥有关系。将属性声明为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选项

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

下面代码就演示普通对象转换为僵尸对象的过程
注意:采用的是手动计数,在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。