解决NSTimer、CADisplaylink的内存泄漏
我们在NSTimer
或者CADisplayLink
的时候,如果使用不当,可能导致内存泄漏问题,至于内存泄漏的原因,很多人都会觉得是循环引用,什么ViewController强持有NSTimer(以一代二),NSTimer强持有ViewController。真的是够了!!!我们先用一段实例代码测试以上的结论:
@interface WeakTargetViewController ()
@property (nonatomic, strong) NSTimer *strongTimer1;
@property (nonatomic, strong) NSTimer *strongTimer2;
@property (nonatomic, weak) NSTimer *weakTimer1;
@property (nonatomic, weak) NSTimer *weakTimer2;
@end
@implementation WeakTargetViewController
#pragma mark - Lifecycle
- (void)viewDidLoad {
[super viewDidLoad];
[self _test1];
[self _test2];
[self _test3];
[self _test4];
}
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
}
#pragma mark - Private
- (void)_test1 {
self.strongTimer1 = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timerMethod:) userInfo:nil repeats:YES];
}
- (void)_test2 {
WEAK_SELF;
self.strongTimer2 = [NSTimer scheduledTimerWithTimeInterval:1 target:weakSelf selector:@selector(timerMethod:) userInfo:nil repeats:YES];
}
- (void)_test3 {
self.weakTimer1 = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timerMethod:) userInfo:nil repeats:YES];
}
- (void)_test4 {
WEAK_SELF;
self.weakTimer2 = [NSTimer scheduledTimerWithTimeInterval:1 target:weakSelf selector:@selector(timerMethod:) userInfo:nil repeats:YES];
}
#pragma mark - Actions
- (void)timerMethod:(NSTimer *)timer {
NSLog(@"%s", __func__);
}
运行上面的代码我们会发现,只要不调用invalidate
方法,ViewController永远不会被释放,而不是什么循环引用。其实真正导致内存泄漏的原因是NSTimer或者CADisplayLink对target的单向强引用。由于单向强持有,ViewController的dealloc
才不会被调用,如果我们在dealloc
方式中写了[self.strongTimer1 invalidate];
是不会被调用的,所以看上去好像是循环引用。但是如果我们在viewWillDisappear:
调用[self.strongTimer1 invalidate];
这样是有效的。
解决方法
以前在项目中的时候,我是分别针对NSTimer
和CADisplayLink
提供Target-Action
类型的分类方法。现在我将之前的代码整合一下,并提供Block的支持,代码如下:
NSObject+WeakTarget.h
#import <Foundation/Foundation.h>
#import <QuartzCore/QuartzCore.h>
typedef void(^WeakTargetHandlerBlock)(id objc);
@interface NSObject (WeakTarget)
- (NSTimer *)addTimerWithTimeInterval:(NSTimeInterval)timeInterval
selector:(SEL)aSelector
userInfo:(id)userInfo
repeats:(BOOL)repeat;
- (NSTimer *)addTimerWithWithTimeInterval:(NSTimeInterval)timeInterval
userInfo:(id)userInfo
repeats:(BOOL)repeat
block:(WeakTargetHandlerBlock)block;
- (CADisplayLink *)addDisplayLinkWithSelector:(SEL)sel;
- (CADisplayLink *)addDisplayLinkWithBlock:(WeakTargetHandlerBlock)block;
@end
NSObject+WeakTarget.m
#import "NSObject+WeakTarget.h"
#import <objc/message.h>
#define BLOCK_EXEC(block, ...) if (block) { block(__VA_ARGS__); };
typedef NS_ENUM(NSInteger, UsedType) {
UsedTypeNSTimerAction,
UsedTypeCADisplayLinkAction,
UsedTypeBlock,
};
@interface WeakTarget: NSObject
@property (nonatomic, weak) id objc;
@property (nonatomic, weak) id target;
@property (nonatomic, assign) SEL selector;
@property (nonatomic, copy) WeakTargetHandlerBlock block;
@property (nonatomic, assign) UsedType usedType;
@end
@implementation WeakTarget
- (void)dealloc {
NSLog(@"%@ dealloc", self.class);
}
- (void)weakTargetAction {
if (!self.target) {
void (*objcInvalidate_msgSend)(id, SEL) = (typeof(objcInvalidate_msgSend))objc_msgSend;
objcInvalidate_msgSend(self.objc, @selector(invalidate));
return;
}
switch (self.usedType) {
case UsedTypeNSTimerAction: {
void (*timerAction_msgSend)(id, SEL, NSTimer *) = (typeof(timerAction_msgSend))objc_msgSend;
timerAction_msgSend(self.target, self.selector, (NSTimer *)self.objc);
}
break;
case UsedTypeCADisplayLinkAction: {
void (*linkAction_msgSend)(id, SEL, CADisplayLink *) = (typeof(linkAction_msgSend))objc_msgSend;
linkAction_msgSend(self.target, self.selector, (CADisplayLink *)self.objc);
}
break;
case UsedTypeBlock: {
BLOCK_EXEC(self.block, self.objc);
}
break;
default:
break;
}
}
@end
@implementation NSObject (WeakTarget)
#pragma mark - Public
- (NSTimer *)addTimerWithTimeInterval:(NSTimeInterval)timeInterval selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)repeat {
if (![self respondsToSelector:aSelector]) {
return nil;
}
WeakTarget *timerTarget = [self _addWeakTargetWithUsedType:UsedTypeNSTimerAction];
timerTarget.selector = aSelector;
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:timeInterval
target:timerTarget
selector:@selector(weakTargetAction)
userInfo:userInfo
repeats:repeat];
timerTarget.objc = timer;
return timer;
}
- (NSTimer *)addTimerWithWithTimeInterval:(NSTimeInterval)timeInterval
userInfo:(id)userInfo
repeats:(BOOL)repeat
block:(WeakTargetHandlerBlock)block {
if (!block) {
return nil;
}
WeakTarget *timerTarget = [self _addWeakTargetWithUsedType:UsedTypeBlock];
timerTarget.block = block;
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:timeInterval
target:timerTarget
selector:@selector(weakTargetAction)
userInfo:userInfo
repeats:repeat];
timerTarget.objc = timer;
return timer;
}
- (CADisplayLink *)addDisplayLinkWithSelector:(SEL)aSelector {
if (![self respondsToSelector:aSelector]) {
return nil;
}
WeakTarget *linkTarget = [self _addWeakTargetWithUsedType:UsedTypeCADisplayLinkAction];
linkTarget.selector = aSelector;
CADisplayLink *link = [CADisplayLink displayLinkWithTarget:linkTarget selector:@selector(weakTargetAction)];
linkTarget.objc = link;
return link;
}
- (CADisplayLink *)addDisplayLinkWithBlock:(WeakTargetHandlerBlock)block {
if (!block) {
return nil;
}
WeakTarget *linkTarget = [self _addWeakTargetWithUsedType:UsedTypeBlock];
linkTarget.block = block;
CADisplayLink *link = [CADisplayLink displayLinkWithTarget:linkTarget selector:@selector(weakTargetAction)];
linkTarget.objc = link;
return link;
}
#pragma mark - Private
- (WeakTarget *)_addWeakTargetWithUsedType:(UsedType)usedType {
WeakTarget *target = [WeakTarget new];
target.usedType = usedType;
target.target = self;
return target;
}
@end
调用如下:
- (void)viewDidLoad {
[super viewDidLoad];
self.label = [[UILabel alloc] init];
[self.label addTimerWithWithTimeInterval:1 userInfo:nil repeats:YES block:^(id objc) {
NSLog(@"%s", __func__);
}];
[self addTimerWithTimeInterval:1 selector:@selector(timerMethod:) userInfo:nil repeats:YES];
//如果要使用selector形式的方法,self必须实现对应的selector方法
CADisplayLink *link = [self addDisplayLinkWithSelector:@selector(timerMethod:)];
[link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
CADisplayLink *link2 = [self addDisplayLinkWithBlock:^(id objc) {
NSLog(@"%s", __func__);
}];
[link2 addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
}
#pragma mark - Actions
- (void)timerMethod:(NSTimer *)timer {
NSLog(@"%s", __func__);
}
打印结果:

从上面的代码中可以看到,我们只需要在ViewController
中创建对应的NSTimer
即可。ViewController
和NSTimer
之间通过一个内部类WeakTarget
进行关联。在ViewController
被释放的时候,WeakTarget
的target
便不存在,当下一次再调用weakTargetAction
函数的时候,便会调用objcInvalidate_msgSend(self.objc, @selector(invalidate));
通知NSTimer
释放掉持有的对象WeakTarget
。这里既然知道了对象和对应方法,那就直接走消息转发就好了。