一, 常见内存泄露种类
- block 循环引用 及 delegate 的强引用.
- 单例对 block 进行了 copy 操作, 或对 delegate 进行了强引用操作.
- 给 NSTimer 添加了 runloop, runloop 一直持有 NSTimer
- 某些非OC对象的内存要及时释放
CGImageRef image = CGImageSourceCreateImageAtIndex(source, i, NULL);
CGImageRelease(image);
二, block 类型的循环引用常见形式
搜索了项目中所有的"^(", 共 6681 处, 排除参数为MASConstraintMaker类型的block, 名称为 enumerateObjectsUsingBlock: 类型的 block, 或者名称为animateWithDuration:animations:completion: 方法且最后一个参数为(BOOL finished)的, 发现了以下几种情况.
1. block 中含有下划线的属性, 如:
[_moreButton bk_addEventHandler:^(UIButton *sender) {
sender.selected = !sender.selected;
_moreTableView.hidden = !sender.selected;
} forControlEvents:UIControlEventTouchUpInside];
WeakSelf
item1.operationBlock = ^(){
_bAllow = !_bAllow;
[self updateForbidMsg:_bAllow];
if (_bAllow) {
[self postAllowMsgSettingLevel:_level];
} else {
[self postForbidMsgSetting];
}
};
2. cell 的block 中含有tableview
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
CarTeamInformationActCell *cell = [tableView dequeueReusableCellWithIdentifier:[CarTeamInformationActCell reuseID] forIndexPath:indexPath];
WeakSelf
__weak UITableView *weakTableView = tableView;
cell.didSelectedPickUp = ^{
[weakSelf selectPickUp:weakTableView indexPath:indexPath];
};
return cell;
}
3. 单例类中含有 copy 的对象
由于单例的内存 build 释放, copy 的 block 也会对对象进行强引用.
+ (instancetype)footerWithRefreshingBlock:(MJRefreshComponentRefreshingBlock)refreshingBlock
{
MJRefreshFooter *cmp = [[self alloc] init];
cmp.refreshingBlock = refreshingBlock;
return cmp;
}
三, runloop 对 target 的持有
NSTimer 会强引用传入的target,如果同时把 NSTimer 加入 NSRunLoop , 这个timer又会被NSRunLoop强引用, 铁索连环, 导致 target 无法释放.
NSTimer *timer = [NSTimer timerWithTimeInterval:5 target:self selector:@selector(onTimer) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
在快速进入并退出直播间的时候, MLeaksFinder 会弹框提示直播间的内存没有及时释放, 点击对应的 retain cycle, 发现是 NSTimer -> __ NSCFTimer 泄露, 可以使用 NStimer 的 userinfo 参数, 将定时器操作封装成 block 通过 userinfo 传递.
+ (id)safe_scheduledTimerWithTimeInterval:(NSTimeInterval)inTimeInterval block:(void (^)())inBlock repeats:(BOOL)inRepeats {
void (^block)() = [inBlock copy];
id ret = [self scheduledTimerWithTimeInterval:inTimeInterval target:self selector:@selector(jdExecuteSimpleBlock:) userInfo:block repeats:inRepeats];
return ret;
}
+ (void)jdExecuteSimpleBlock:(NSTimer *)inTimer {
if ([inTimer userInfo]) {
void (^block)() = (void (^)())[inTimer userInfo];
block();
}
}
由于 NSTimer 是基于runloop的,所以在GCD(延迟操作)里使用时会出现问题. 也可以使用YYWeakProxy中转
- (void)initTimer {
YYWeakProxy *proxy = [YYWeakProxy proxyWithTarget:self];
_timer = [NSTimer timerWithTimeInterval:0.1 target:proxy selector:@selector(onBtnClick:) userInfo:nil repeats:YES];
}
四, 强引用父类
曾经遇到过子类在初始化时, 用一个 strong 类型的属性指向了父类, 导致父类拥有了子类, 子类也强引用父类, 无法释放内存.查遍了相关类的所有属性, 才发现此问题.
@property (nonatomic,strong) UIViewController *parentVc;
...
- (instancetype)initWithParentVc:(UIViewController *)vc{
if (self = [super init]) {
self.parentVc = vc;
}
return self;
}
五, 总结
- 也是最近崩溃的问题稍微有点儿多, 反复进入直播间, 很容易出现崩溃
- 排除了有直接导致崩溃的原因后, 发现有可能是内存太高了
- 对比以往版本, 发现内存增高的原因只可能是内存泄露了, 有一些循环引用导致内存没有及时释放.
- 使用腾讯的 MLeaksFinder 初步定位没有释放内存的对象, 然后在有嫌疑的类中查找"^{" 和 "^(", 检查被 copy 的 block 中是否有对 self 的强引用, 或者下划线属性.
- 在这些类中加上 dealloc, 检查有没有被调用.