内存管理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 都没有办法被释放。这种状况通常最可能出现在:

  1. 把delegate 设为strong reference,我们会在讨论delegate 的时候继续讨论这个状况。
  2. 某个物件的某个property 是一个block,但是在这个block 里头把物件自己给retain 了一份。 我们会在讨论block 的时候讨论这个状况。
  3. 使用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 之后,我们都无法继续使用以上两种的程式写作方式。

相关阅读

results matching ""

    No results matching ""