coding with objc & swift

Effective Obj-C ARC and Pitfalls in It

| Comments

Objective-C是一个非常酷的编程语言。TIOBE已经公布了十一月的编程语言排行榜,Objective-C很可能会再次成为“年度编程语言”。Objective-C很流行不仅仅只是因为iOS和Mac OS X平台的原因,还有得益于它在移动设备上的高性能。在Objective-C中,手动内存管理代替了垃圾回收机制。当然,现在Objective-C也不是那么的需要纯手动管理内存了。Apple引进了自动引用计数(Automatic Reference Counting, 简称ARC)机制,在编译时自动帮你加入内存管理代码。在大部分情况下,这很管用。然而,当你将ARC代码和Core Foundation对象混和使用的时候,总是会感觉很混乱。今天,我来谈谈ARC的要点和陷阱,尤其是在用toll-free bridging将Objc对象和CF对象相互转换的时候的一些注意事项。

简介

在这篇文章中,我先简单的介绍一下ARC的两个主要的使用场景:Objective-C的property 和 ARC修饰符。然后再着重讲解一下ARC与Core Foundation框架的内存管理问题。

ARC 和 Objective-C的property

首先,要感谢一下。没有@amattn写的《ARC最佳实践》,就很难有这篇文章。你可以点这个链接查看ARC Best Practices的原文。下面是我从他的文章里面总结的一些重点:

  • 需要retain的实例变量对象,用 strong
1
@property (nonautomic, strong) id childObject;
  • 为了防止引用循环(reference cycle),用 weak
1
@property (nonautomic, weak) id delegate;
  • 基本数据类型,用 assign
1
2
@property (nonatomic, assign) CGFloat width;
@property (nonatomic, assign) CGFloat height;
  • 不可变的容器类型以及字符串和block,用 copy。要避免使用可变容器类型(比如:NSMutableArray这些…)作为property,如果要用可变的容器类型,就用 strong
1
2
3
4
@property (nonatomic, copy) NSString* name;
@property (nonatomic, copy) NSArray* components;
@property (nonatomic, copy) (void (^)(void)) job;
@property (nonatomic, strong) NSMutableArray* badPatterns;
  • dealloc中
    • 移除观察者。
    • 注销通知。
    • ~~停掉timer。~~(译注:因为timer会retain你当前的这个对象,所以实际上在timer停掉前,这个对象是无法被dealloc的。 所以这里的这点是错误的,别把停掉timer的操作放到dealloc中。在确定不使用当前这对象的时候,要主动停掉timer。)
  • IBOutlets一般用 weak,除非它是文件的所有者(File’s Owner)在nib文件中的顶层对象。如果你将它设置为strong了,你就需要在-(void)viewDidUnload方法中将它设置为nil。

    (译注:viewDidUnload在iOS6.0以后没有了,要移到didReciveMemoryWorning中。详细情况请阅读苹果的《View Controller Programming Guide for iOS》)

ARC类型修饰符

关于类型修饰符的相关使用规则,你可以看看我先前的一篇文章。ARC相关的类型修饰符,有4个:

  • __strong表示到一个对象的强引用。只要有任意一个强引用指向对象,对象就不会被销毁。 这个是ARC中所有对象的默认属性。
  • __weak指定到一个对象的弱引用。弱引用不会使它指向的对象保持一直存在,当这个对象上没有任何强引用后,对象会被释放,弱引用也会被指向nil。
  • __unsafe_unretained指定到一个对象的弱引用,并且当它指向的对象被销毁后,也不会被指向nil,弱引用就变成一个野指针了。
  • __autoreleasing用来表示参数按引用传递,在函数返回后,这个参数就被自动释放了。

要注意,ARC类型修饰符只能用在指针上面。也就是说,你必须把修饰符放到星号的右边。

1
2
3
4
5
6
7
8
9
MyClass * __weak w_self = self; // 正确
MyClass __weak * w_self = self; // 错误! 可能导致很严重的bug!
__weak MyClass * w_self = self; // 错误!

__weak typeof(self) w_self = self;
// 正确,将会被展开为 __weak (MyClass *) w_self = self; 
// (参见gcc的typeof说明)

typeof(self) __weak w_self = self; // 正确, 这个安全些

难道,网上这么多写法都是错误的?那是因为苹果官方文档里面提到:“你应该正确修饰变量。当修饰符用在对象变量上的时候,正确的格式是 ClassName * qualifier variableName;” > You should decorate variables correctly. When using qualifiers in an object variable declaration, the correct format is: ClassName * qualifier variableName;

ARC 和 Toll-free绑定

ARC和Core Foundation混合使用时,要注意的问题最多。下面是一些重要的原则:

  • 当你传递Objc对象给一个CF引用时,你要retain它。
  • 当你传递一个CF引用给一个Objc对象时,你要release它。
  • 在传递时,如果你不明确的指定对象的所有权,会很危险。有时Clang编译器会帮你修正,但不是总是会帮你修正,这样就会造成很严重的问题。
  • Core Foundation里面没有autorelease。你必须遵守它的内存管理规则:
    • 名字中带有CreateCopy的函数返回的对象,你都会持有它,因此你要负责释放它。
    • 如果函数名字中是带的get,你不会持有它返回的对象,不需要你释放它。

ARC中有两种方法来retain一个CF对象:用类型转换符(__bridge_retained) 或者 C函数CFBridgingRetain。我更喜欢用后面那种,因为它让代码更简洁更清晰。

1
2
3
4
CFArrayRef arr = CFBridgingRetain( @[@"abc", @"def", @3.14] );
// or CFArrayRef arr = (__bridge_retained CFArrayRef)@[...];
// do stuffs..
CFRelease(arr);

当你用Core Foundation中名字中带有Create或者Copy的方法获得一个对象的时候,用(__bridge_transfor)或者CFBridgingRealease来将对象的所有权转移给ARC,这样就能让ARC负责对象的释放。

1
2
3
4
- (void)logFirstNameOfPerson:(ABRecordRef)person {
NSString *name = (NSString *)CFBridgingRelease(ABRecordCopyValue(person, kABPersonFirstNameProperty));
NSLog(@"Person's first name: %@", name);
}

Toll-free绑定的注意事项

当心下面这种代码:

1
2
3
4
- (CGColorRef)foo {
	UIColor* color = [UIColor redColor];
	return [color CGColor];
}

当心!它任何时候都可能崩溃。由于我们没有保持对UIColor的引用,它可能在创建后就被立即释放掉了!它带的CGColor也跟着释放掉了,就可能引起崩溃。有3个方法来修正这个问题:

  • 使用__autorelease修饰符。这样UIColor就被延迟到当前run loop结束时才被释放,就不会崩溃了。我觉得也许是@amattn第一个发现了这种解决方法。
1
2
3
4
- (CGColorRef)getFooColor {
	UIColor* __autoreleasing color = [UIColor redColor];
	return [color CGColor];
}
  • 使用Core Foundation的命名规则来改变返回值的所有权。

    “` objc - (CGColorRef)fooColorCopy { UIColor* color = [UIColor redColor]; CGColorRef c = CFRetain([color CGColor]); return c; }

CGColorRef c = [obj fooColorCopy]; // do stuffs CFRelease(c); “`

  • 用self来持有CF对象。如果self被dealloc后,仍然会引起崩溃。
1
2
3
4
- (CGColorRef)getFooColor {
	CGColorRef c = self.myColor.CGColor;
	return c;
}

ARC中使用Block的注意事项

如果你用block做实例变量,block会隐式的retain你的self,这样就造成了引用循环。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@interface MyClass {
	id child;
}
@property (nonautomic, strong) (void(^)(void)) job;
@end


@implementation MyClass

- (void)foo {
	self.job = ^{
	[child work];
	// will expand to [self->child work]
	};
}

解决这个问题的唯一方法是使用self的弱引用,然后在block内部临时将弱引用转换为强引用。在block内部使用强引用的原因,是因为我们想确保在使用self的时候它不会被其它人释放掉。

1
2
3
4
5
6
7
8
9
10
- (void)foo {
	MyClass* __weak w_self = self;
	self.block = ^{
		MyClass* s_self = w_self; // self retained, but only in this scope!
		if (s_self) {
			[s_self->child work];
			// do other stuffs
		}
	};
}

NSError的注意事项

如果你要实现一个带NSError参数的方法,要注意正确使用类型修饰符!

1
2
- (void)doStuffWithError:(NSError* __autoreleasing *)error; // correct
- (void)doStuffWithError:(__autoreleasing NSError **)error; // wrong!

当你创建一个NSError对象的时候,通常最好把它定义成一个自动释放的对象:

1
2
3
NSError* __autoreleasing error = nil; // 正确
__autoreleasing NSError* error = nil; // 错误
NSError* error = nil; // clang编译器下正确。(clang编译器会帮我们做优化)

总结

ARC很方便,但是刚开始使用时可能不容易搞清楚它的用法。当你面对复杂的对象所有权关系时,可以写一些测试测试代码,看看对象的retain count是怎么样的。下面是我用来测试retain count的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- (void)testCGColorRetainCount1
{
	CGColorRef s_ref;
	@autoreleasepool {
		UIColor * __autoreleasing shadowColor = [UIColor colorWithRed:0.12 green:0.12 blue:0.12 alpha:1.0];
		s_ref = shadowColor.CGColor;
		CFRetain(s_ref);
	}
	STAssertEquals(CFGetRetainCount(s_ref), 1L, @"retain count owned by us");

	CGColorRef(^strangeBlock)(void) = ^{
		return CGColorCreateCopy(s_ref);
	};

	CGColorRef myCopy = strangeBlock();
	STAssertEquals(CFGetRetainCount(s_ref), 2L, @"retain count owned by block and us");
	CFRelease(s_ref);
	STAssertEquals(CFGetRetainCount(s_ref), 1L, @"retain count owned by block");
	STAssertEquals(CFGetRetainCount(myCopy), 1L, @"retain count owned by us");
	CFRelease(myCopy);
}

希望这篇文章对你有帮助!欢迎讨论和分享!

译注:如果你还想了解ARC更详细的知识,可以看看官方文档《Transitioning to ARC Release Notes》或这篇《iOS5 ARC完全指南》

原文:Effective Obj-C ARC and Pitfalls in It

Comments