猿故I love iOS

开发中容易忽略的循环引用问题

2018-07-01  本文已影响9人  Code_Ninja

在以前MRC时代,我们管理对象的时候必须小心谨慎,避免对象不能正常释放。后来到了ARC时代了,虽然大大简化了我们对对象生命周期的管理,但是稍不注意还是会导致对象不能释放的问题。非常常见的情况就是因为对象之间形成了循环引用,导致对象不能正常释放。这里列举几种比较容易被我们忽略的循环引用问题。

一、cell的block中使用了self

比如一个自定义UITableViewCell或者UICollectionViewCell中定义一个block,用于把cell中的事件往外传递:

@interface YLTableViewCell : UITableViewCell
@property (nonatomic, copy) void (^actionBlock)(NSInteger type);
@end

在cellForRow中使用actionBlock时稍不注意使用了self:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    YLTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
    cell.actionBlock = ^(NSInteger type) {
        [self doSomethingWithType:type];
    };
    return cell;
}

在我刚接触公司项目的时候,发现项目中有不少页面不能正常释放的问题,经过排查发现全都是在cell的block中直接使用了self导致的内存泄露。这种情况其实也比较好理解,因为self强引用了tableView,而tableView对cell也是强引用,cell又通过block强引用了self,因此造成了循环引用。

二、block中使用的宏定义中使用了self

也许在你的项目中也有类似DLog这样的一个宏定义方法,用于在DEBUG模式下正常输出日志,在release模式下不输出日志的控制。像我们的DLog的定义如下:

#ifdef DEBUG
#define DLog(s, ...) NSLog( @"<%p %@:(%d)> %@", self, [[NSString stringWithUTF8String:__FILE__] lastPathComponent], __LINE__, [NSString stringWithFormat:(s), ##__VA_ARGS__] )
#else
#define DLog(s, ...)
#endif

然后,你会不会不经意间在一个不该直接使用self的block中顺手写了个DLog("some log")呢?你的一个不经意,可能会导致排查内存泄露问题排查俩小时。这里为什么,因为DLog的宏定义中直接使用了self,所以在block中使用宏定义时一定要确保你的宏定义中没有直接使用self。

三、通知addObserverForName:object:queue:usingBlock:中使用了self

我们在使用通知的时候,如果我们是使用常见的addObserver和removeObserver的方式,只要记得移除通知的监听,一般不会造成内存泄露的问题,即使不移除,也是会造成向一个对象发送不能识别的消息的奔溃问题。

但是,系统通知也为我们提供了一种更为简洁的,使用block处理通知回调的方法,比如这样子:

NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
    NSOperationQueue *queue = [NSOperationQueue mainQueue];
    [notificationCenter addObserverForName:UIApplicationDidEnterBackgroundNotification
                                    object:nil
                                     queue:queue
                                usingBlock:^(NSNotification * _Nonnull note) {
        NSLog(@"%@", self.view);
    }];

乍一看,这代码没啥问题啊,self又没有持有通知什么的。但是通过官方文档我们会发现这个方法会将block添加到系统通知调度表中,block会被copy一份到通知中心,知道被登记的观察者被移除。问题就出在这里,系统通知中心会持有这个block,而一旦你在该block中持有了self,那么系统通知就间接的持有了self,导致self不能正常释放。

正确的使用方式是不要在block中直接使用self,把该方法返回的观察者记录下来,在不需要继续监听时,把观察者从系统通知中心中移除:

- (void)dealloc
{
    [[NSNotificationCenter defaultCenter]removeObserver:_backgroundObserver];
}

- (void)viewDidLoad {
    [super viewDidLoad];
    NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
    NSOperationQueue *queue = [NSOperationQueue mainQueue];
    __weak typeof(self) weakSelf = self;
    self.backgroundObserver = [notificationCenter addObserverForName:UIApplicationDidEnterBackgroundNotification
                                    object:nil
                                     queue:queue
                                usingBlock:^(NSNotification * _Nonnull note) {
        NSLog(@"%@", weakSelf.view);
    }];

或者对于一次性的通知,在收到通知后在block中直接把观察者从系统通知中心中移除就好了:

NSNotificationCenter * __weak center = [NSNotificationCenter defaultCenter];
id __block token = [center addObserverForName:@"OneTimeNotification"
                                       object:nil
                                        queue:[NSOperationQueue mainQueue]
                                   usingBlock:^(NSNotification *note) {
                                       NSLog(@"Received the notification!");
                                       [center removeObserver:token];
                                   }];

四、单例的数组中add了self

例如有这样的场景:你有一个单例,在很多地方需要使用这个单例,当这个单例的某属性值发生改变时,你需要通知使用到了该单例的对象们。于是你给这个单例创建了一个数组,用于记录需要监听某属性发生改变的“代理们”。这样就很容易造成循环引用了,因为数组对添加进来的对象是强引用。即使没有形成循环引用,也会导致添加进来的对象,在从这个数组中移除前不能正常释放,你需要兼顾很多场景下如何将这些“代理们“正常的从单例的数组中移除。在我们的项目中,当时做这块功能的小伙伴,统一在这些”代理们“被pop出栈的时候从数组中移除了,在正常的流程下没有任何问题。但是随着业务的发展,当遇到这些”代理们“不是被pop出栈的情况时,就会造成内存泄露了。

遇到这种该怎么办呢?在不改变这种给所有”代理们”循环发送消息的这种方式的情况下,我们可以考虑将数组换成NSPointerArray来记录这些“代理们”。NSPointerArray是一个仿照数组功能的一个类,它可以指定添加进来的对象是强引用还是弱引用,它还能添加nil。我们这里只需创建NSPointerArray对象时指定它对数组内的对象是弱引用就好了。

NSPointerArray的简单使用举例:

NSPointerArray *pointArray = [[NSPointerArray alloc]initWithOptions:NSPointerFunctionsWeakMemory];//弱引用
ViewController *vc = [ViewController new];
[_pointArray addPointer:nil];
[_pointArray addPointer:(__bridge void * _Nullable)(vc)];
NSLog(@"count=%li", _pointArray.count);//2
[_pointArray compact];//移除空对象
NSLog(@"count=%li", _pointArray.count);//1
for(id pointer in _pointArray){
    NSLog(@"%@", pointer);
}

对应NSDictionary和NSSet,系统也提供了NSMapTable和NSHashTable,在需要对集合中的对象指定弱引用的时候,大家可以考虑一下使用它们。

上一篇 下一篇

猜你喜欢

热点阅读