iOS开发精进iOS开发实用技术iOS-架构优化

记住:instrument的leaks工具并不能检测出所有的内存

2017-09-23  本文已影响421人  Lol刀妹
iu

写在前面

网上关于用instrument的leaks工具检查内存泄漏的文章很多,但是几乎没人提到一个细节:不是所有的内存泄漏都是leaks工具能够检测出来的

我之前也一直以为,只要用leaks工具来回跑几次,没有显示内存泄漏就大功告成了,直到前几天我封装一个模块的时候:

封装的UIScrollView占位图.gif

发现view removeFromSuperView后内存并没有降低。我不断的展示又移除占位图,但是内存都是只增不减:

内存只增不减

这里先不管是不是内存泄漏,removeFromSuperView后内存没有按照我们所期望的降低,肯定就是有问题的。

这是封装占位图的代码,是UIScrollView的category:

完整demo在这里

@implementation UIScrollView (PlaceholderView)

/**
 展示UIScrollView及其子类的占位图
 
 @param type 占位图类型
 @param reloadBlock 重新加载回调的block
 */
- (void)showPlaceholderViewWithType:(CQPlaceholderViewType)type reloadBlock:(void (^)())reloadBlock {
    //------- 背景view -------//
    UIView *bgView = [[UIView alloc] initWithFrame:self.frame];
    [self.superview addSubview:bgView];
    bgView.backgroundColor = [UIColor whiteColor];
    
    //------- 图标 -------//
    UIImageView *imageView = [[UIImageView alloc] init];
    [bgView addSubview:imageView];
    [imageView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.centerX.mas_equalTo(imageView.superview);
        make.centerY.mas_equalTo(imageView.superview).mas_offset(-80);
        make.size.mas_equalTo(CGSizeMake(70, 70));
    }];
    
    //------- 描述 -------//
    UILabel *descLabel = [[UILabel alloc] init];
    [bgView addSubview:descLabel];
    [descLabel mas_makeConstraints:^(MASConstraintMaker *make) {
        make.centerX.mas_equalTo(descLabel.superview);
        make.top.mas_equalTo(imageView.mas_bottom).mas_offset(20);
        make.height.mas_equalTo(15);
    }];
    
    //------- 重新加载button -------//
    UIButton *reloadButton = [[UIButton alloc] init];
    [bgView addSubview:reloadButton];
    [reloadButton setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
    [reloadButton setTitle:@"重新加载" forState:UIControlStateNormal];
    reloadButton.layer.borderWidth = 1;
    reloadButton.layer.borderColor = [UIColor blackColor].CGColor;
    
    [[reloadButton rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(id x) {
        // 执行block回调
        if (reloadBlock) {
            reloadBlock();
        }
        // 从父视图移除
        [bgView removeFromSuperview];
    }];
    [reloadButton mas_makeConstraints:^(MASConstraintMaker *make) {
        make.centerX.mas_equalTo(reloadButton.superview);
        make.top.mas_equalTo(descLabel.mas_bottom).mas_offset(20);
        make.size.mas_equalTo(CGSizeMake(120, 30));
    }];
    
    //------- 根据type设置不同UI -------//
    switch (type) {
        case CQPlaceholderViewTypeNoNetwork: // 网络不好
        {
            NSString *path = [[NSBundle mainBundle] pathForResource:@"无网" ofType:@"png"];
            imageView.image = [UIImage imageWithContentsOfFile:path];
            descLabel.text = @"网络异常";
        }
            break;
            
        case CQPlaceholderViewTypeNoGoods: // 没商品
        {
            NSString *path = [[NSBundle mainBundle] pathForResource:@"无商品" ofType:@"png"];
            imageView.image = [UIImage imageWithContentsOfFile:path];
            descLabel.text = @"一个商品都没有";
        }
            break;
            
        case CQPlaceholderViewTypeNoComment: // 没评论
        {
            NSString *path = [[NSBundle mainBundle] pathForResource:@"沙发" ofType:@"png"];
            imageView.image = [UIImage imageWithContentsOfFile:path];
            descLabel.text = @"抢沙发!";
        }
            break;
            
        default:
            break;
    }
}

@end

问题就出在“重新加载”按钮点击的那段代码里:

    [[reloadButton rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(id x) {
        // 执行block回调
        if (reloadBlock) {
            reloadBlock();
        }
        // 从父视图移除
        [bgView removeFromSuperview];
    }];

针对这个问题,我去问了一下,得到的答案是:RAC中subscribeNext会copy强引用nextBlock, 所以会对reloadBlock和bgView强引用(如果说法欠妥希望直接指出)。

现在问题就很明确了:每次展示占位图都新建了一个bgView,但是这个bgView removeFromSuperView后却没有释放,因此导致内存只增不减。

那么如何解决这个问题?

bgViewweak即可:

@weakify(bgView);
[[reloadButton rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(id x) {
    @strongify(bgView)
    // 执行block回调
    if (reloadBlock) {
        reloadBlock();
    }
    // 从父视图移除
    [bgView removeFromSuperview];
}];

再看内存变化情况,终于和期望的一样了:

内存有增有减

现在问责instrument ' leaks

该释放的内存没有释放,这就是内存泄漏,为什么你不提醒我?

instrument ' leaks:

好吧,既然它不正经回答那我就自己找答案:
我Google了一下,找到微信读书团队的这篇文章:

从苹果的开发者文档里可以看到,一个 app 的内存分三类:

  • Leaked memory: Memory unreferenced by your application that cannot be used again or freed (also detectable by using the Leaks instrument).
  • Abandoned memory: Memory still referenced by your application that has no useful purpose.
  • Cached memory: Memory still referenced by your application that might be used again for better performance.

其中 Leaked memory 和 Abandoned memory 都属于应该释放而没释放的内存,都是内存泄露,而 Leaks 工具只负责检测 Leaked memory,而不管 Abandoned memory。在 MRC 时代 Leaked memory 很常见,因为很容易忘了调用 release,但在 ARC 时代更常见的内存泄露是循环引用导致的 Abandoned memory,Leaks 工具查不出这类内存泄露,应用有限。

重点就是这句:

在 ARC 时代更常见的内存泄露是循环引用导致的 Abandoned memory,Leaks 工具查不出这类内存泄露。

看来是我一直误会leaks工具了:别人根本就不具备检测所有内存泄漏的能力,你却要强行用别人来检测所有内存泄漏。

对于 Abandoned memory,可以用 Instrument 的 Allocations 检测出来。

网上搜一下Allocations,应该有很多使用教程,还在只用leaks工具检查内存泄漏的同学(包括我)可以去看看。

总结

用instrument的leaks工具检查内存泄漏的时候还应该注意内存的变化趋势。

是否真的释放还得看是否执行dealloc,不单单是视图控制器的dealloc,还有视图控制器里的view。

死磕遇到的问题,终会有收货,而且印象深刻。

文中内存泄漏的demo

上一篇下一篇

猜你喜欢

热点阅读