×

iOS实录8:解决NSTimer/CADisplayLink的循环引用

96
南华coder
2017.04.28 12:04* 字数 1503

[这是第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

iOS实践录
Web note ad 1