在以前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,在需要对集合中的对象指定弱引用的时候,大家可以考虑一下使用它们。
五、NSTimer导致的循环引用
我们常用NSTimer作为延迟执行代码或者定时执行代码的一种实现方式,但是很遗憾的是NSTimer会对它的target进行强引用。这也就是为什么我们往往看到很多人使用NSTimer的时候都会找个“合适的时机”手动销毁掉timer。
比如我们常常像下面这样使用timer:
_timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
但是系统内部会强引用这个target也就是self,即使我们传入weakSelf依然不奏效,可见系统内部对target进行了一次__strong
操作。
其实我们没必要在使用NSTimer的地方寻找“手动释放timer的时机”,这样是不太安全的,因为随着业务的发展,难免会导致这个释放时机不正常的问题。事实上,系统有提供NSProxy代理类来专门处理这种问题(当然不限于此),我们可以写一个继承自NSProxy类的子类,在其内部弱持有timer的target对象,然后简单实现消息转发的方法就可以了,这里就不详细说明了,网上已经有很多这个问题的介绍了,大家看看YYWeaKProxy的实现就好了。
看到这里,你一定想吐槽系统NSTimer设计的太不人性化了吧?我们想使用个定时器都这么麻烦,还要弄个代理类来解决循环引用的问题,如果不仔细看官方文档还很容易忽略这个问题。
可能苹果也意识到了这一点,从iOS10开始,系统为我们提供了新的不会造成循环引用的使用NSTimer的方式:
__weak typeof(self) weakSelf = self;
_timer = [NSTimer timerWithTimeInterval:1 repeats:YES block:^(NSTimer *timer) {
[weakSelf timerAction];
}];
六、performSelector类的延迟执行代码导致的循环引用
官方文档中已明确说明,performSelector:withObject:afterDelay:
和performSelector:withObject:afterDelay:inModes:
方法内部就是用NSTimer来实现的延迟操作,所以,大家在使用这类方法的时候也需注意一下是否造成循环引用问题。