Contents

最近在准备秋招复习一些 iOS 知识,继续看 Objective-C 高级编程:iOS 与 OS X 多线程和内存管理.

所谓

书读百遍其义自见

尽管还是有的地方是看得很晦涩无法领略真谛但是对 block 的循环引用问题又认识深了些。立马想到项目中遇到的情况进而验证发现确实存在循环引用导致内存泄漏的问题。

##封装

这是一个常见的 UI,整体是一个 tableView 顶部是 tableView 的 tableViewHeader。tableViewHeader 里面封装了 UIImageView 以及一个 UIButton 作取消用。tableViewHeader 同时对其本身 Tap 事件和 UIButton 的点击事件进行监听,通过 block 暴露给图中这个 Controller 调用方称为 rootController,采取 block + delegate 的方式联动。

Controller 是 ATAControllerViewController
tableViewHeader 是 ATATableViewHeader

rootController present 出 ATAControllerViewController;ATATableViewHeader 随之生成作为 ATAControllerViewController 的成员变量同时在 init 的时候统一配置了 block 操作。

@interface ATAControllerViewController () 
@end

@implementation ATAControllerViewController {
    ATATableViewHeader *_header;
}

生成 header

_header = [[ATATableViewHeader alloc] initWithAvatarUrl:_user.icon
                                                  onTap:^{
                                                      [self p_viewDidTap];//消去键盘
                                                  }
                                               onCancel:^{
                                                   [self dismissViewControllerAnimated:YES completion:nil];
                                               }];

而在 ATATableViewHeader 内部是这样

typedef void(^ATAHeaderTapHandler)();
typedef void(^ATAHeaderCancelHandler)();

@implementation ATATableViewHeader {
    UIImageView *_avatar;
    NSURL *_url;
    UIButton *_cancelButton;
    ATAHeaderTapHandler _tapBlock;
    ATAHeaderCancelHandler _cancelBlock;
}

- (instancetype)initWithAvatarUrl:(NSURL *)url
                            onTap:(ATAHeaderTapHandler)block
                         onCancel:(ATAHeaderCancelHandler)cancel {
    self = [self init];
    _url = url;
    [_avatar sd_setImageWithURL:url placeholderImage:[UIImage imageNamed:@"default"]];

    _tapBlock = block;//保存
    _cancelBlock = cancel;//保存

    UITapGestureRecognizer *res = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(didTap)];
    [self addGestureRecognizer:res];

    [_cancelButton addTarget:self
                      action:@selector(cancelButtonDidTap)
            forControlEvents:UIControlEventTouchUpInside];
    return self;
}


- (void)didTap {
    if (_tapBlock)
        _tapBlock();
}

- (void)cancelButtonDidTap {
    if (_cancelBlock)
        _cancelBlock();

}

所以这样就完成了封装,UIButton 的点击事件发生,调动 cancelButtonDidTap 调用 _cancelBlock, _tapBlock 同理。
前面说了 ATAControllerViewController 持有 _header 实例,而 _tapBlock = block; 在赋值的时候把栈上 block 复制到了堆上,因为 _tapBlock 有一个默认的内存修饰符 __strong_header 持有 block。

##问题分析

问题会发生在生成 _header 的时候

_header = [[ATATableViewHeader alloc] initWithAvatarUrl:_user.icon
                                                  onTap:^{
                                                      [self p_viewDidTap];//消去键盘
                                                  }
                                               onCancel:^{
                                                   [self dismissViewControllerAnimated:YES completion:nil];
                                               }];

在这里两个 block 都会截获 self 也就是 ATAControllerViewController 但是并不增加对 ATAControllerViewController 的引用计数。只有到 _tapBlock = block; 栈上 block 复制到了堆上,block 才持有了 ATAControllerViewController,后者的引用计数加一。

这样正是循环引用,那么内存泄漏时怎么发生的呢?

当系统不再引用 ATAControllerViewController 时,由于循环引用的存在每个对象的引用计数都不为零故而不会被 ARC 回收销毁。

##解决

__weak typeof(self) weakSelf = self;
_header = [[ATATableViewHeader alloc] initWithAvatarUrl:_user.icon
                                                      onTap:^{
                                                          [weakSelf viewDidTap];
                                                      }
                                                   onCancel:^{
                                                       [weakSelf dismissViewControllerAnimated:YES completion:nil];
                                                   }];

大家都知道加 weakSelf 能消除 block 的循环引用,那么具体是怎样呢?

在系统不再引用 ATAControllerViewController 时,后者被销毁(__weak 不增加引用计数),同时对应的 weakSelf 在销毁的同时被置为 nil _header 和 block 依次被销毁。
通过 dealloc 日志能看到

大功告成。

##其他

我想没有人想这么做,除非想在执行 block 的时候遭遇 EXC-BAD-ACCESS :P

@implementation ATATableViewHeader {
        UIImageView *_avatar;
        NSURL *_url;
        UIButton *_cancelButton;
        __weak ATAHeaderTapHandler _tapBlock;
        __weak ATAHeaderCancelHandler _cancelBlock;
}
Contents