Objective-C关于非ARC模式下的对象引用计数

Objective-C是一门简洁、强大、灵活的既具有面向对象特性也具有函数式编程特性的编程语言。由于它是C语言的马甲,也就是说,Objective-C可以将其源代码先转为纯C语言然后再编译为最终的目标代码,所以我们也可以用它来写纯C语言代码,它与C是完全兼容的!(这点与C++在语法特性上跟C语言兼容的特性不同)

由于有不少Objective-C爱好者对于ARC模式下的Objective-C感到十分困惑,所以希望能深入了解一下传统非ARC模式下的编程法则。通过对非ARC模式Objective-C工作模式的认知,我们甚至可以对整个Cocoa Framework的运行核心做更深层的认知。为何我不推荐使用ARC模式呢?

你用了ARC就得去记__strong、__weak、__unsafe_unretained、__autoreleasing、__bridge等等杂七杂八的关键字~这些乱七八糟的概念本身会把你搞晕,而且当你半懂不懂的时候一旦乱用反而会产生各种奇怪的bug~这些玩意儿倘若充斥在你的代码中,一来很丑,二来对于一些新手很容易被弄晕……所以说,ARC这货自其出生就带来了许多灾难!

而反观传统的非ARC模式,property就一个assign,一个retain,NSObject里就调用retain/release和autorelease方法~而且Apple对此的规则也非常简单——“不是你创建的就不需要你释放;是你创建的你才去释放它。”这一句话就能解决所有问题~

除此之外,无论你用ARC还是非ARC,你都需要搞懂Apple Cocoa Framework的消息循环机制,即autorelease是如何工作的。否则你的assign或weak属性的Objective-C对象啥时候被释放也都不会知晓~

综上所述,如果为了编程方便、可维护、可扩展,我们完全可以把ARC编译选项给关掉!另外,在Objective-C中往往把“方法调用”阐述为“消息发送”。比如[obj msg]一般大家描述为obj对象调用其msg成员方法。而正式用语上应该描述为向obj对象发送msg消息。在哪个对象的方法里执行这条语句的,那么称该对象为消息发送者;msg称为消息(即方法);obj则称为消息接收者。讲了那么多,下面开始切入正题!

在基于Foundation/Cocoa Framework的Objective-C中,我们定义一个类往往需要继承NSObject这一Foundation的基类。当我们调用NSObject的alloc类方法时,就会给要创建的对象分配存储空间;然后紧接着调用NSObject的init成员方法对创建的对象做初始化。这里就会对此对象做引用计数设置为1的操作。在基于Foundation/Cocoa Framework的Objective-C与传统的C++不同,它全面通过为每一对象指定引用计数来确定其生命周期。当某个对象的引用计数被减到0时,会触发调用该对象的dealloc成员方法。而上述的init方法就已经把对象的引用计数设置为1了。当我们调用NSObject的release成员方法时,该对象的引用计数减1。当我们调用NSObject的retain成员方法时,该对象的引用计数加1。通常,我们不要自己去重写NSObject的release与retain成员方法。下面我们先对此举一个简单的例子:

//
//  ViewController.m
//  iOSTest
//
//  Created by Zenny Chen on 15/11/8.
//  Copyright © 2015年 GreenGames Studio. All rights reserved.
//

@interface MyObj : NSObject

@end

@implementation MyObj

- (void)dealloc
{
    NSLog(@"MyObj deallocated!");
    [super dealloc];
}

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.

    MyObj *obj = [[MyObj alloc] init];
    // 这里obj的引用计数为1,我们可以将它打印出来,直接访问retainCount属性即可。
    NSLog(@"retain count: %tu", obj.retainCount);

    // 调用一次retain,让其引用计数加1
    [obj retain];
    NSLog(@"Now, retain count: %tu", obj.retainCount);

    // 调用一次release,让其引用计数减1
    [obj release];
    NSLog(@"After release, retain count: %tu\n", obj.retainCount);

    // 再次调用release,其引用计数为0,然后dealloc方法被立即触发
    [obj release];
}

通过上述代码,我们对retain/release引用计数的机制已经有了非常清晰的理解。不过,如果我们在Objective-C中会时常用到一些Cocoa库中的对象,包括一些如NSString、NSNumber之类的对象,倘若每分配一个对象都要写这种release方法进行释放就会变得非常啰嗦!因而,在基于Foundation/Cocoa Framework的Objective-C中引入了autorelease机制。

要使用autorelease首先需要一个autorelease pool。在Apple LLVM 2.0之前,我们通常需要使用这样的代码开辟一个autorelease pool——

    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

    // 在这里可以对任一继承NSObject的Objective-C对象使用autorelease
    // ...

    [pool drain];   // 将所有之前的autorelease对象的引用计数减1

而现在,我们可以直接使用@autoreleasepool:

    @autoreleasepool {
        // 在这里可以对任一继承NSObject的Objective-C对象使用autorelease
        // ...

    }   // 将所有之前的autorelease对象的引用计数减1

当对一个对象调用了init方法之后,然后立即再对它调用autorelease成员方法,那么此对象就变为autorelease对象了。此时,其引用计数仍然为1,但是它被其所在的autorelease pool给引用了。此时我们就必须注意,不能通过直接调用release方法使其引用计数减到0,否则当autorelease pool再对它做release操作时就会引发程序崩溃,因为该对象已经是一个无效对象了!比如以下代码:

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.

    @autoreleasepool {
        MyObj *obj = [[[MyObj alloc] init] autorelease];
        // 这里obj的引用计数为1,并且它是一个autorelease对象
        NSLog(@"retain count: %tu", obj.retainCount);

        // 调用一次retain,让其引用计数加1
        [obj retain];
        NSLog(@"Now, retain count: %tu", obj.retainCount);

        // 调用一次release,让其引用计数减1
        [obj release];
        NSLog(@"After release, retain count: %tu\n", obj.retainCount);

        // 再次调用release,其引用计数为0。这里仍然会触发dealloc方法的调用
        [obj release];

        NSLog(@"Over");
    }// 出了@autoreleasepool语句块(即调用了drain方法之后),之前对obj的引用仍然会对它使用release操作
}

以上代码,Over仍然会被打印,但是一旦出了@autoreleasepool语句块程序即会崩溃~所以,我们把最后一次的 release调用给屏蔽掉,这段代码就能完好地运行,且不会有任何内存泄漏情况。

autorelease pool可以嵌套,即在一个@autoreleasepool语句块中可再写一个@autoreleasepool语句块。那么内部@autoreleasepool语句块中定义的autorelease对象就归内部的autorelease pool管理。

那么我们现在理解了autorelease的机制,那么在Foundation/Cocoa Framework中我们如何判定这些类库中创建的方法是否为autorelease方法呢?很简单!Apple有一个很基本的命名规则——

1、如果是以alloc类方法分配的,或者是以new打头的方法获得的对象说明就是非autorelease对象。Apple称这些对象为“我们自己创建的对象”,所以最后必须自己负责调用release方法去释放它们。在Core Foundation中,则以CFCreate或含有开头Create前缀的函数名,那么在使用完这些对象后,我们仍然要以CFRelease相关的函数接口来释放这些对象。比如以下代码:

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.

    // 使用alloc分配的一个NSString对象
    NSString *str = [[NSString alloc] initWithUTF8String:"Hello"];

    // 使用CFStringCreateWithCString分配的一个CFStringRef对象
    CFStringRef cfStr = CFStringCreateWithCString(kCFAllocatorDefault, ", world", 
                                                                                 kCFStringEncodingUTF8);

    NSLog(@"String is: %@%@", str, cfStr);

    // 用完之后释放这些对象
    [str release];
    CFRelease(cfStr);
}

2、如果没有通过alloc方法获得的对象,或者是以类名打头进行初始化后所获得的对象,那么它们一般都是autorelease对象。比如以下代码:

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.

    // 使用stringWithUTF8String获得的一个NSString对象,属于autorelease对象
    NSString *str = [NSString stringWithUTF8String:"Hello, world!"];

    // 这里,str用完后无需自己去release,除非你对它调用了retain方法
    NSLog(@"string is: %@", str);
}

3、所有字面量都是autorelease对象。比如以下代码:

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.

    // 以下对象均为autorelease对象
    NSString *str = @"Hello";
    NSString *str2 = @(", world!");
    NSNumber *num = @100;
    NSArray *array = @[str, str2];
    NSDictionary *dict = @{str:num};

    // 上述对象用完之后均无需自己调用release来释放,除非你对它们调用了retain方法
    NSLog(@"String is: %@%@", array[0], array[1]);
    NSLog(@"Value is: %@", dict[str]);
}

有了这些简单的认知之后,我们基本对引用计数有了比较深刻的认识了。由于在Cocoa Framework的消息机制中,它已经包含了一个autorelease pool,因此,当Cocoa Framework每处理完一个消息之后就会自动调用一次该消息中所有autorelease对象的release方法。我们还是以刚才定义的MyObj类来举一个例子:

@implementation ViewController

- (void)test
{
    MyObj *obj = [[[MyObj alloc] init] autorelease];
    NSLog(@"obj = %@", [obj description]);
}

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.

    [self test];

    // 在调用完我们自己定义的test方法之后,obj对象仍然没有被释放
    NSLog(@"Over");

    // 直到viewDidLoad这一消息彻底执行完成,
    // 我们会看到"MyObj deallocated!"被打印出来
}

所以,以上综合起来看,就是Apple之前提出的的那句名言——“是你分配的就由你负责释放,不是你分配就无须你自己释放!”

最后,我们来谈谈property以及相关需要注意的事项。在非ARC模式的Objective-C中,property的引用计数属性也就两种,1种是assign,另一种就是retain。由于Objective-C编译器会自动对property生成一个getter方法与一个setter方法(倘若该属性没有用readonly修饰)。那么对于assign限定符来说,setter方法不会对外部参数做retain处理;而对于retain限定符来说,其setter方法就会对外部参数做一次retain处理,使其引用计数加1。我们来看以下代码:

@interface ViewController ()

@property (nonatomic, assign) MyObj *objAssign;
@property (nonatomic, retain) MyObj *objRetain;

@end

@implementation ViewController

@synthesize objAssign, objRetain;

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.

    MyObj *obj = [[MyObj alloc] init];

    self.objAssign = obj;

    // 这里,obj的引用计数仍然为1
    NSLog(@"obj retain count: %tu", obj.retainCount);

    self.objRetain = obj;

    // 这里,obj的引用计数变为2
    NSLog(@"obj retain count: %tu", obj.retainCount);
}

通过上述代码例子,我们可以很清晰地看到retain与assign不同限定符对property的影响。所以,我们通常用的编程范式是:如果你定义的property是retain的,那么在该类的dealloc方法中对其调用release方法;而对于所有NSObject子类而言都推荐优先使用retain限定符;而对于像int、float等非NSObject子类或C语言基本类型而言,对它们用assign修饰。

此外,像Foundation/Cocoa Framework中还有许多方法都自带retain操作,比如NSMuatbleArray的addObject、UIView的addSubview等等,这些方法都可以通过文档看到其介绍。我们可以通过用option键加鼠标左键来看到方法的介绍。比如,像addSubview就有这样的字眼:“This method establishes a strong reference to view and sets its next responder to the receiver, which is its new superview”。这里,strong reference就是对参数的强引用,这意味着会对外部参数做引用计数加1操作。一般来说,我们看到以add、insert等词眼的方法名是就会反应出这些方法会对参数做一次引用计数加1操作;而像remove打头的方法,我们会反应出这些方法会对消息接收者做一次引用计数减1的操作。所以,我们一般在写UI界面代码时,要把某些子视图添加到父视图上,往往可以采用以下编程范式:

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.

    self.view.backgroundColor = [UIColor grayColor];

    // 这里通过alloc方法分配了label对象,它不是一个autorelease对象
    UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(120.0, 50.0, 100.0, 30.0)];
    label.textColor = [UIColor whiteColor];
    label.text = @"Hello";
    // 这里对label做了一次引用计数加1操作
    [self.view addSubview:label];
    NSLog(@"label retain count: %tu\n", label.retainCount);
    // 由于label是我分配的,所以这里添加完之后需要对它调用一次release方法
    [label release];

    // 添加一个按钮
    UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem];
    button.frame = CGRectMake(20.0, 50.0, 90.0, 35.0);
    [button setTitle:@"Tap" forState:UIControlStateNormal];
    [button addTarget:self action:@selector(tapTouched:) forControlEvents:UIControlEventTouchUpInside];
    // 由于button用的是buttonWithType获得的,它是一个autorelease对象而它不是我分配的,
    // 所以在添加完之后无需对它使用release方法。
    [self.view addSubview:button];
}
  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 8
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值