iOS精英班iOS开发攻城狮的集散地面试题

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

2017-04-28  本文已影响1046人  南华coder

[这是第8篇]

导语:使用NSTimer/CADisplayLink容易发生循环引用,网上很多博文都提到解决该问题的办法。但是有些问题还是没有说清楚,结合自己在项目中的使用,说说我的解决办法。

发生循环引用的原因:

初始化NSTimer/CADisplayLink对象时候,指定target时候,会保留其目标对象,而NSTimer/CADisplayLink的目标对象如果恰好保留了计时器本身,就会导致循环引用。解决的办法主要有两种

方法一:扩展方法,使用block打破保留环####

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

为什么这么做

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];

无效的原因:

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的循环引用问题。原因如下:

Demo源码见QSUseTimerDemo

上一篇 下一篇

猜你喜欢

热点阅读