[这是第8篇]
导语:使用NSTimer/CADisplayLink容易发生循环引用,网上很多博文都提到解决该问题的办法。但是有些问题还是没有说清楚,结合自己在项目中的使用,说说我的解决办法。
发生循环引用的原因:
初始化NSTimer/CADisplayLink对象时候,指定target时候,会保留其目标对象,而NSTimer/CADisplayLink的目标对象如果恰好保留了计时器本身,就会导致循环引用。解决的办法主要有两种
方法一:扩展方法,使用block打破保留环####
- 这是《Effective Object-C 2.0 编写高质量iOS与OS的代码的52个有效方法》书中的建议,使用block方法,解决循环引用的问题。编码实现中,为NSTimer和CADisplayLink分别创建分类,扩展出新方法。
1、NSTimer+QSTool分类实现#####
// NSTimer+QSTool.h
typedef void(^QSExecuteTimerBlock) (NSTimer *timer);
@interface NSTimer (QSTool)
+ (NSTimer *)qs_scheduledTimerWithTimeInterval:(NSTimeInterval)timeInterval executeBlock:(QSExecuteTimerBlock)block repeats:(BOOL)repeats;
@end
// NSTimer+QSTool.m
@implementation NSTimer (QSTool)
+ (NSTimer *)qs_scheduledTimerWithTimeInterval:(NSTimeInterval)timeInterval executeBlock:(QSExecuteTimerBlock)block repeats:(BOOL)repeats{
NSTimer *timer = [self scheduledTimerWithTimeInterval:timeInterval target:self selector:@selector(qs_executeTimer:) userInfo:[block copy] repeats:repeats];
return timer;
}
+ (void)qs_executeTimer:(NSTimer *)timer{
QSExecuteTimerBlock block = timer.userInfo;
if (block) {
block(timer);
}
}
@end
2、CADisplayLink+QSTool分类实现#####
// CADisplayLink+QSTool.h
@class CADisplayLink;
typedef void(^QSExecuteDisplayLinkBlock) (CADisplayLink *displayLink);
@interface CADisplayLink (QSTool)
@property (nonatomic,copy)QSExecuteDisplayLinkBlock executeBlock;
+ (CADisplayLink *)displayLinkWithExecuteBlock:(QSExecuteDisplayLinkBlock)block;
@end
// CADisplayLink+QSTool.m
@implementation CADisplayLink (QSTool)
- (void)setExecuteBlock:(QSExecuteDisplayLinkBlock)executeBlock{
objc_setAssociatedObject(self, @selector(executeBlock), [executeBlock copy], OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (QSExecuteDisplayLinkBlock)executeBlock{
return objc_getAssociatedObject(self, @selector(executeBlock));
}
+ (CADisplayLink *)displayLinkWithExecuteBlock:(QSExecuteDisplayLinkBlock)block{
CADisplayLink *displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(qs_executeDisplayLink:)];
displayLink.executeBlock = [block copy];
return displayLink;
}
+ (void)qs_executeDisplayLink:(CADisplayLink *)displayLink{
if (displayLink.executeBlock) {
displayLink.executeBlock(displayLink);
}
}
@end
为什么这么做:
- 在初始化NSTimer/CADisplayLink对象时候,指定target时候,会保留其目标对象。我们的目的是绕开这个定时器对象强引用目标对象这个问题。在分类中,定时器对象指定的target是NSTimer/CADisplayLink类对象,这是个单例,因此计时器是否会保留它都无所谓。这么做,循环引用依然存在,但是因为类对象无需回收,所以能解决问题。
3、NSTimer和CADisplayLink的使用#####
假设在Controller中使用NSTimer。分三步(CADisplayLink的使用类似)
第一,我们可以在viewDidLoad中先初始化对象,在block中指定定时执行的办法,这里需要使用成对的weakSelf和strongSelf保证使用block不出现循环引用;
第二,在executeTimer:中定义需要定时处理的方法;
第三,在dealloc中调用定时器invalidate的方法,使定期器失效。
- (void)viewDidLoad {
[super viewDidLoad];
// ...
__weak typeof(self) weakSelf = self;
self.timer = [NSTimer qs_scheduledTimerWithTimeInterval:timeInterval executeBlock:^(NSTimer *timer) {
__weak typeof(weakSelf) strongSelf = weakSelf;
[strongSelf executeTimer:timer];
} repeats:YES];
[self.timer fire];
//...
}
- (void)executeTimer:(NSTimer *)timer{
//do something
}
- (void)dealloc{
[self.timer invalidate];
}
方法二:target弱引用目标对象
1、常见的错误解决办法
【警告】下面是错误的解决办法,是无效的(这么简单的话,《Effective Object-C 2.0》不至于单独开一节来说)
_weak typeof(self) weakSelf = self;
_timer = [NSTimer scheduledTimerWithTimeInterval:3.0f
target:weakSelf
selector:@selector(timerFire:)
userInfo:nil
repeats:YES];
无效的原因:
这是对使用weakSelf和strongSelf来打破block循环引用的不正确演绎。下面说一下为了使用weakSelf和strongSelf对block有效
在block外使用弱引用(weakSelf),这个弱引用(weakSelf)指向的self对象,在block内捕获的是这个弱引用(weakSelf),而不是捕获self的强引用,也就是说,这就保证了self不会被block所持有。
那疑问就来了,为什么还要在block内使用强引用(strongSelf) ,因为,在执行block内方法的时候,如果self被释放了咋办,造成无法估计的后果(可能没事,也有可能出个诡异bug),为了避免问题发生,block内开始执行的时候,立即生成强引用(strongSelf),这个强引用(strongSelf) 指向了弱引用(weakSelf)所指向的对象(self对象),这样以来,在block内部实际是持有了self对象,人为地制造了暂时的循环引用。为什么说是暂时?是因为强引用(strongSelf) 的生命周期只在这个block执行的过程中,block执行前不会存在,执行完会立刻就被释放了。
关键点来了:强引用(strongSelf) 指向了弱引用(weakSelf)所指向的对象,等价于强引用了对象
我们为NSTimer/CADisplayLink对象指定target时候,虽然传入了弱引用,但是造成的结果是:强引用了弱引用所引用的对象,也就是最终还是强引用了对象,而刚好对象又强引用了NSTimer/CADisplayLink对象。这样以来,循环引用还是没有解决。
引入中间对象,在这个对象中弱引用self,然后将这个对象传递给timer的构建方法
2、正确的决办法
该方法来自YYKit项目,项目中定义了YYWeakProxy这样的工具类解决
该方法引入一个YYWeakProxy对象,在这个对象中弱引用真正的目标对象。通过YYWeakProxy对象,将NSTimer/CADisplayLink对象弱引用目标对象。YYWeakProxy的实现如下:
//YYWeakProxy.h
@interface YYWeakProxy : NSProxy
@property (nullable, nonatomic, weak, readonly) id target;
- (instancetype)initWithTarget:(id)target;
+ (instancetype)proxyWithTarget:(id)target;
@end
//YYWeakProxy.m
@implementation YYWeakProxy
- (instancetype)initWithTarget:(id)target {
_target = target;
return self;
}
+ (instancetype)proxyWithTarget:(id)target {
return [[YYWeakProxy alloc] initWithTarget:target];
}
- (id)forwardingTargetForSelector:(SEL)selector {
return _target;
}
- (void)forwardInvocation:(NSInvocation *)invocation {
void *null = NULL;
[invocation setReturnValue:&null];
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector {
return [NSObject instanceMethodSignatureForSelector:@selector(init)];
}
- (BOOL)respondsToSelector:(SEL)aSelector {
return [_target respondsToSelector:aSelector];
}
- (BOOL)isEqual:(id)object {
return [_target isEqual:object];
}
- (NSUInteger)hash {
return [_target hash];
}
- (Class)superclass {
return [_target superclass];
}
- (Class)class {
return [_target class];
}
- (BOOL)isKindOfClass:(Class)aClass {
return [_target isKindOfClass:aClass];
}
- (BOOL)isMemberOfClass:(Class)aClass {
return [_target isMemberOfClass:aClass];
}
- (BOOL)conformsToProtocol:(Protocol *)aProtocol {
return [_target conformsToProtocol:aProtocol];
}
- (BOOL)isProxy {
return YES;
}
- (NSString *)description {
return [_target description];
}
- (NSString *)debugDescription {
return [_target debugDescription];
}
@end
3、YYWeakProxy的使用#####
假设在Controller中使用CADisplayLink。分三步(NSTimer的使用类似)
第一,我们可以在viewDidLoad中先初始化NSTimer/CADisplayLink对象,指定target是YYWeakProxy对象,和指定定时执行的办法
第二,在executeDispalyLink:中定义需要定时处理的方法;
第三,在dealloc中调用定时器invalidate的方法,使定期器失效。
- (void)viewDidLoad {
[super viewDidLoad];
// ...
self.displayLink = [CADisplayLink displayLinkWithTarget:[YYWeakProxy proxyWithTarget:self] selector:@selector(executeDispalyLink:)];
[self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
//...
}
- (void)executeDispalyLink:(CADisplayLink *)displayLink{
//...
}
- (void)dealloc{
[self.displayLink invalidate];
}
问题的关键来了:为什么NSProxy的子类YYWeakProxy可以解决NSTimer/CADisplayLink的循环引用问题。原因如下:
NSProxy本身是一个抽象类,它遵循NSObject协议,提供了消息转发的通用接口,NSProxy通常用来实现消息转发机制和惰性初始化资源。不能直接使用NSProxy。需要创建NSProxy的子类,并实现init以及消息转发的相关方法,才可以用。
YYWeakProxy继承了NSProxy,定义了一个弱引用的target对象,通过重写消息转发等关键方法,让target对象去处理接收到的消息。在整个引用链中,Controller对象强引用NSTimer/CADisplayLink对象,NSTimer/CADisplayLink对象强引用YYWeakProxy对象,而YYWeakProxy对象弱引用Controller对象,所以在YYWeakProxy对象的作用下,Controller对象和NSTimer/CADisplayLink对象之间并没有相互持有,完美解决循环引用的问题。
Demo源码见QSUseTimerDemo