ObjC 是使用引用计数来管理对象内存的,关于引用计数更加详细的解释,可以看 理解 iOS 的内存管理,这篇文章还讲了 ARC 下可能存在的内存管理问题。

ARC 是编译器的特性,在编译的时候自动插入管理引用计数的代码,给 ObjC 对象提供自动内存管理,并生成相应的 dealloc 方法。ARC 可以让你更加专注于编写代码的逻辑而不是去管理内存,但是在 ARC 下遵循 ObjC 的命名规则就显得很重要了。

ARC 下内存管理变量关键字

在 ARC 下,虽然编译器帮我们做了大部分的内存管理的工作,但是我们还是要了解在 ARC 下的内存管理关键字。

__strong

对一个对象进行强引用,表示拥有该对象,就像在 MRC 下进行 retain 是一样的,当一个对象没有强引用指针引用时,这个对象就再不被拥有,这时这个对象就会被销毁。

NSObject * __strong objc = [[NSObject alloc] init]; == NSObject * objc = [[NSObject alloc] init];

在 ARC 下,默认的指针就是强引用指针。

__weak

对一个对象进行弱引用,不会拥有该对象,不会改变这个对象的内存周期,即不会改变该对象的引用计数。当被引用的对象被销毁时,weak 指针会自动置空,这样就可以避免野指针访问错误。常用来解决循环引用问题。

__typeof__(self) __weak weakSelf = self;

__weak 只在 iOS 5 以上版本可用,iOS 5 以下的版本用随后介绍的 __unsafe_unretained

__unsafe_unretained

__weak 相似,不会拥有指向的对象。但是指向的对象被销毁时不会置 nil,就会变成悬挂指针,即会发生野指针错误。

__typeof__(self) __unsafe_unretained weakSelf = self;

在现在的 iOS 版本下,这个内存管理关键字几乎没有用处,因为 __weak 总是更好的选择。但是如果你想在结构体中声明一个对象的成员变量,你就需要用到这个关键字来避免 Xcode 的编译错误,但是这样你就得花精力去管理这个成员变量的内存周期,所以使用类去实现是更好的选择。

__autoreleasing

使用这个关键字主要为了延长对象的存活周期,不要被过早的销毁。这个关键字常用来声明对象的指针。

NSError * __autoreleasing error = nil;

即使我们没有使用 __autoreleasing 声明,编译器还是会在编译器时期帮我们自动添加。

NSError *error; 
NSError *__autoreleasing tempError = error;
[data writeToFile:filename options:NSDataWritingAtomic error:&tempError];

在这里有个问题需要注意:

- (BOOL)doSomethingWithDictionary:(NSDictionary *)dictionary error:(NSError * __autoreleasing *)error {
    // NSError * __block temp = nil;
    [dictionary enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
        // do sth with key & obj
        if (error && [some error happened]) {
            *error = [NSError errorWithDomain:@"TestError" code:1 userInfo:nil];
            // tempError = [NSError errorWithDomain:@"TestError" code:1 userInfo:nil];
        }
    }];
    
    if (error) {
        // *error = tempError;
    }
}

上面的代码看着好像没有什么问题,但是在 enumerateKeysAndObjectsUsingBlock: 的 block 里,会自动创建一个 @autoreleasepool {}, 当离开此次遍历,error 就会被释放掉,最后就不能得到想要的 error 信息。

可按照上面注释掉的代码解决。

ARC 内存管理问题

虽然 ARC 的出现让我们不需要花太多的精力在内存管理上,但是有些内存管理问题 ARC 还是没有办法处理的。例如:循环引用和 Core Foundation。

循环引用

循环引用就是两个对象相互强引用对方,造成两个对象都不能被释放,从而引起内存泄漏。

block 的循环引用

在 block 的使用中是最容易发生内存泄漏的地方,一个对象拥有这个 block,但是在这个 block 里又用到了该对象,block 会持有内部引用的对象,这样就会发生循环引用。

在 block 中有两种方式来解决循环引用的问题:

  1. 主动断开循环引用:在 block 使用完,主动将 block 清空,这样就可以断开 block 对内部持有对象的强引用,也就断开了循环引用。

    self.completionBlock = nil;
    
  2. 使用弱引用:弱引用不会影响引用对象的内存管理周期,并且在引用对象销毁时置 nil。

    __typeof(self) __weak weakSelf = self;
    self.completionBlock = ^{
      __typeof(weakSelf) strongSelf = weakSelf;
     if (strongSelf) {
       // do something with self
      }
    };
    

NSTimer

NSTimer Class Reference 指出 NSTimer 会强引用 target。并且官方的 Timer Programming Topics 指出: 我们不应该在 dealloc 中 invalidate timer。

举一个例子,我们让 timer 在我们的 ViewController 中不断调用 handleTimer 方法.

.h
@property (nonatomic, strong) NSTimer *timer;

.m
- (void)viewDidLoad {
    [super viewDidload];
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(handleTimer:) userInfo:nil repeats:YES];
}

这个时候,timer 和我们的 ViewController 就是循环引用的。即使我们在 dealloc 方法中 invalidate timer 也是没用的。因为 timer 强引用着 VC。而 dealloc 是在对象销毁的时候才会被调用。

即使 VC 对 NSTimer 没有一个强引用,还是有可能会发生内存泄漏,如果一个 timer 添加到 runloop 中,runloop 会对 NSTimer 有一个强引用,如果我们不主动 invalidate timer 的话,runloop 持有 timer,timer 对 VC 也有一个强引用,就会导致内存泄漏。

Note in particular that run loops maintain strong references to their timers, so you don’t have to maintain your own strong reference to a timer after you have added it to a run loop.

因此在使用 NSTimer 时,特别是循环的 NSTimer 时。我们需要注意在什么地方 invalidate 计时器,在上面这个例子,我们可以在 viewWillDisappear 里面做这样的工作。

performSelector

iOS 内存管理机制 这篇文章中还提到了使用 performSelector 可能存在的内存泄漏。

编译器不知道即将调用的 selector 是什么,不了解方法签名和返回值,所以编译器无法用 ARC 的内存管理规则来判断返回值是否应该释放。因此,ARC 采用了比较谨慎的做法,不添加释放操作,即在方法返回对象时就可能将其持有,从而可能导致内存泄露。

Core Foundation

ARC 不会去管理 Core Foundation 的对象,但是我们在 ARC 下编写代码也不可避免会碰到 Core Foundation 的对象,所以知道一定的 Core Foundation 内存管理对开发这很有帮助的。

关于 Core Foundation 的内存管理:Memory Management Programming Guide for Core Foundation

ARC 下的 dealloc 方法

  • 移除通知中心 (NSNotificationCenter) 的观察者:从 iOS 9 开始,不再需要移除通知中心 (NSNotificationCenter) 的观察者。如果你的 App 依然支持 iOS 8,你还是需要移除观察者。 More details: NSNotificationCenter automatic reregistration
  • 移除 KVO 观察者
  • 释放一些 Core Foundation 的对象

不允许主动调用此方法,runtime 会在对象被销毁之前调用此方法。当应用直接结束的时候,对象可能不会接收到 dealloc 方法。

在 ARC 下不需要也不允许编写 [super dealloc];,因为 ARC 下 runtime 会处理好父类的 dealloc 链;但是在 MRC 下必须要在 dealloc 方法的最后调用 [super dealloc]; 来执行父类的清理操作。

扩展阅读

相关链接