内存管理Part 2
这一章主要讨论ARC。前一章提到,由于ARC是透过静态分析,在Compile Time决定应该要在程序代码的那些地方加入retain、release,所以,要使用ARC基本上相当简单,就是先把原本要手动管理内存的地方,把retain、 release都拿掉,在dealloc的地方,也把[super dealloc]
拿掉。
但,有了ARC,也不代表在开发iOS 或Mac OS X App 的时候,就不需要了解内存管理机制。例如,我们虽然很多代码会使用Objetive-C 语言开发,但是还是会经常用到C 语言,我们还是得要了解C 语言里头的内存管理。
而且,有时候,ARC也会把retain、release加错地方。在使用ARC之前,我们建议先阅读,ARC Best Practices 一文,里头提到绝大多数的问题。我们会在这边简单提一些「即使用了ARC,还是必须要注意的内存管理问题」。
ARC 可能会弄错释放内存的时机
在大概iOS 5 到iOS 6 的时代,写出这样程式,你会收到Bad Access 的错误而造成crash:
#import <QuartzCore/QuartzCore.h>
@implementation ViewController
- (CGColorRef)redColor
{
UIColor *red = [UIColor redColor];
CGColorRef colorRef = red.CGColor;
return colorRef;
}
- (void)viewDidLoad
{
[super viewDidLoad];
CGColorRef red = [self redColor];
self.view.layer.backgroundColor = red;
}
@end
之所以会发生这种错误,就在于Compiler所认定的「已经不需用使用某个内存,因此可以释放」的时机点有问题。以上面的代码来说,释放内存的时机应该是在self.view.layer.backgroundColor = red;
这一行之后,但是有一段时间,Compiler却认为是在return colorRef
这一行之前,red这个UIColor物件就已经没有被使用而该被释放,但释放了red,就会造成red实例里头所包含的CGColor也被释放,因此回传了已经被释放了colorRef变数而造成Bad Access。
#import <QuartzCore/QuartzCore.h>
@implementation ViewController
- (CGColorRef)redColor
{
UIColor *red = [UIColor redColor];
CGColorRef colorRef = red.CGColor;
// Compiler 可能會在這邊自動產生 [red release]
return colorRef;
}
- (void)viewDidLoad
{
[super viewDidLoad];
CGColorRef red = [self redColor];
self.view.layer.backgroundColor = red;
// 正确释放内存的时机应该是这里
}
@end
要了解哪些地方是weak reference
另外,ARC 有时候会在一些地方没做retain,结果却又自动多做了一次release 最后导致Bad Access 的错误。我们在讲Selector 的时候提到,我们可以将target/action 与必要的参数合起来变成另外一种实例,叫做NSInvocation,在ARC 环境下从NSInvocation 拿出参数时,就必须要额外注意内存管理问题。
比方说,我们现在要把对UIApplication 要求开启指定URL 这件事情,变成一个Invocation。
NSURL *URL = [NSURL URLWithString:@"http://kkbox.com"];
NSMethodSignature *sig = [UIApplication instanceMethodSignatureForSelector:
@selector(openURL:)];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig];
[invocation setTarget:[UIApplication sharedApplication]];
[invocation setSelector:@selector(openURL:)];
[invocation setArgument:&URL atIndex:2];
但假如我们用以下这段code 的方式,从invocation 取参数的时候,就会遇到Bad Access 错误:
NSURL *arg = nil;
[invocation getArgument:&arg atIndex:2];
NSLog(@"arg:%@", arg);
// 在這邊會 crash
之所以会crash的原因是,我们在透过getArgument:atIndex:
拿出参数的时候,getArgument:atIndex:
并不会帮我们把arg多retain一次,而到了用NSLog印出arg之后,ARC认为我们已经不会用到arg了,所以就对arg多做了一次release,于是retain与release就变得不成对。
我们要解决这个问题的方法是要把arg设为Weak Reference或Unsafe Unretained,让arg这个Objetive-C物件的指标不被ARC管理,要求ARC不要帮这个物件做任何自动的retain与release,在这边要使用__weak
或__unsafe_unretained
关键字。代码会像这样:
__weak NSURL *arg = nil;
[invocation getArgument:&arg atIndex:2];
NSLog(@"arg:%@", arg);
循环引用(循环Retain)
ARC 也不会排除循环Retain(Retain Cycle)的状况,遇到了循环Retain,还是会造成内存泄漏。循环Retain 就是,A 物件本身retain 了B 物件,但是B 物件又retain 了A 物件,结果我们要在释放A 的时候才有办法释放B,但是B 又得要在B 被释放的时候才会释放A ,最后导致A 与B 都没有办法被释放。这种状况通常最可能出现在:
- 把delegate 设为strong reference,我们会在讨论delegate 的时候继续讨论这个状况。
- 某个物件的某个property 是一个block,但是在这个block 里头把物件自己给retain 了一份。 我们会在讨论block 的时候讨论这个状况。
- 使用timer 的时候,到了dealloc 的时候才停止timer。
假如我们现在有一个view controller,我们希望这个view controller可以定时更新,那么,我们可能会使用+scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:
这个method建立timer实例,定时执行某个selector。我们要特别注意,在建立这个timer的时候,我们指定给timer的target,也会被timer retain一份,因此,我们想要在view controller在dealloc的时候,才停止timer就会有问题:因为view controller已经被timer retain起来了,所以只要timer还在执行,view controller就不可能走到dealloc的地方。
@import UIKit;
@interface ViewController : UIViewController
@property (strong, nonatomic) NSTimer *timer;
@end
@implementation ViewController
- (void)dealloc
{
[self.timer invalidate];
}
- (void)timer:(NSTimer *)timer
{
// Update views..
}
- (void)viewDidLoad
{
[super viewDidLoad];
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0
target:self
selector:@selector(timer:)
userInfo:nil
repeats:YES];
}
@end
要修正这个问题,我们应该改成,在viewDidDisappear: 的时候,就要停止timer。
Toll-Free Bridged
前面在讲Category 的时候提到,Foundation Framework 里头的每个实例,都有对应的C实现,这一层C 的实作叫做Core Foundation,当我们在使用Core Foundation 里头的C 型态时,像是CFString、CFArray 等,我们可以让这些型态变成可以接受ARC 的管理。这种让C 型态也可以被当做Objetive-C 实例,接受ARC 的内存管理的方式,叫做Toll-Free Bridged。
Toll-Free Bridged有三个语言关键字:__bridge
、__bridge_retained
、以及__bridge_transfer
。我们直接翻译苹果官网的定义:
__bridge
会把Core Foundation 的C 资料型态转换成Objetive-C 实例,但是不会多做retain 与release。__bridge_retained
会把Core Foundation 的C 资料型态转换成Objetive-C 实例,并且会做一次retain,但是之后必须由我们自己手动呼叫CFRelease,释放内存。__bridge_transfer
会把Core Foundation 实例转换成Objective-C 实例,并且会让ARC 主动添加retain 与release。
不见得每个Core Foundation型态都有办法转换成Objective-C物件。请参阅苹果官方的详细说明。
其他
Objetive-C 语言有了ARC 之后,除了禁止使用retain、release 这些关键字之外,也禁止我们手动建立NSAutoreleasePool,同时禁止了一些我们在ARC 之前的程序代码的写作方式(或是—奇技淫巧),包括我们不可以把Objective-C 实例放进C Structure 里头,Compiler 会告诉我们语法错误。
在有ARC 之前,我们之所以会把Objective-C 实例放进C Structure 里,大概会有几个目的,其一是,假如我们有某个Class 有很多成员变量,那我们可能会想以下这种写法将成员变量分成群组:
@interface MyClass : NSObject
{
struct {
NSString *memberA;
NSString *memberB;
} groupA;
struct {
NSString *memberA;
NSString *memberB;
} groupB;
}
@end
这样,如果我们想要使用groupA里头的memberA,可以用self.groupA.memberA
另外一种目的,则是有时候,我们可能会想要刻意隐藏某个Objective-C Class 里头有哪些成员变量。像下面这段code 里头,我们原本有一个Class 叫做MyClass,里头有privateMemberA 与privateMemberB 两个成员变量,原本应该直接写在MyClass 的声明里头,但是我们却刻意把这两个成员变量包进_Privates 这个C Structure 里头,而原本放在MyClass 成员变数宣告的地方,指剩下了一个叫做privates 的指标,光看到这个指标,让人难以理解这个Class 里头到底有什么东西。
@interface MyClass : NSObject
{
void *privates;
}
@end
typedef struct {
NSString *privateMemberA;
NSString *privateMemberB;
} _Privates;
@implementation MyClass
- (void)dealloc
{
_Privates *privateMembers = (_Privates *)privates;
[privateMembers->privateMemberA release];
[privateMembers->privateMemberB release];
free(privates);
privates = NULL;
[super dealloc];
}
- (instancetype)init
{
self = [super init];
if (self) {
privates = calloc(1, sizeof(_Privates));
_Privates *privateMembers = (_Privates *)privates;
privateMembers->privateMemberA = @"A";
privateMembers->privateMemberB = @"B";
}
return self;
}
@end
这种写法其实是种程式码保护的技巧,主要在防范class-dump,或是从class-dump衍生出的class-dump-z这些工具。class-dump可以从编译好的Binary中还原出每个class的header,当我们从class-dump抽出别人的App的header,看出有哪些Class,每个Class有哪些成员变量、有哪些method,也就可以看出整个App的架构大致如何。这种写法就是让别人用class-dump倒出我们App的header时,不会太容易可以了解我们一些重要的Class是如何运作,不过,对于反编译破解的人来说,其实只要花上时间,所有软件都有办法破解就是了。
怎样做逆向工程不是这份文件的重点。总之,有了ARC 之后,我们都无法继续使用以上两种的程式写作方式。