写在前面
网上关于用instrument的leaks工具检查内存泄漏的文章很多,但是几乎没人提到一个细节:不是所有的内存泄漏都是leaks工具能够检测出来的。
我之前也一直以为,只要用leaks工具来回跑几次,没有显示内存泄漏就大功告成了,直到前几天我封装一个模块的时候:
封装的UIScrollView占位图.gif发现view removeFromSuperView
后内存并没有降低。我不断的展示又移除占位图,但是内存都是只增不减:
这里先不管是不是内存泄漏,removeFromSuperView
后内存没有按照我们所期望的降低,肯定就是有问题的。
这是封装占位图的代码,是UIScrollView的category:
@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
后却没有释放,因此导致内存只增不减。
那么如何解决这个问题?
将bgView
weak即可:
@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。
死磕遇到的问题,终会有收货,而且印象深刻。
网友评论