iOS實錄8:解決NSTimer/CADisplayLink的循環(huán)引用

[這是第8篇]

導語:使用NSTimer/CADisplayLink容易發(fā)生循環(huán)引用,網(wǎng)上很多博文都提到解決該問題的辦法。但是有些問題還是沒有說清楚,結(jié)合自己在項目中的使用,說說我的解決辦法。

發(fā)生循環(huán)引用的原因:

初始化NSTimer/CADisplayLink對象時候,指定target時候,會保留其目標對象,而NSTimer/CADisplayLink的目標對象如果恰好保留了計時器本身,就會導致循環(huán)引用。解決的辦法主要有兩種

方法一:擴展方法,使用block打破保留環(huán)####

  • 這是《Effective Object-C 2.0 編寫高質(zhì)量iOS與OS的代碼的52個有效方法》書中的建議,使用block方法,解決循環(huán)引用的問題。編碼實現(xiàn)中,為NSTimer和CADisplayLink分別創(chuàng)建分類,擴展出新方法。
1、NSTimer+QSTool分類實現(xiàn)#####
//  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分類實現(xiàn)#####
//  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類對象,這是個單例,因此計時器是否會保留它都無所謂。這么做,循環(huán)引用依然存在,但是因為類對象無需回收,所以能解決問題。
3、NSTimer和CADisplayLink的使用#####

假設(shè)在Controller中使用NSTimer。分三步(CADisplayLink的使用類似)

第一,我們可以在viewDidLoad中先初始化對象,在block中指定定時執(zhí)行的辦法,這里需要使用成對的weakSelf和strongSelf保證使用block不出現(xiàn)循環(huán)引用;
第二,在executeTimer:中定義需要定時處理的方法;
第三,在dealloc中調(diào)用定時器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》不至于單獨開一節(jié)來說)

_weak typeof(self) weakSelf = self;
_timer = [NSTimer scheduledTimerWithTimeInterval:3.0f
                                          target:weakSelf
                                        selector:@selector(timerFire:)
                                        userInfo:nil
                                         repeats:YES];

無效的原因:

  • 這是對使用weakSelf和strongSelf來打破block循環(huán)引用的不正確演繹。下面說一下為了使用weakSelf和strongSelf對block有效

  • 在block外使用弱引用(weakSelf),這個弱引用(weakSelf)指向的self對象,在block內(nèi)捕獲的是這個弱引用(weakSelf),而不是捕獲self的強引用,也就是說,這就保證了self不會被block所持有。

  • 那疑問就來了,為什么還要在block內(nèi)使用強引用(strongSelf) ,因為,在執(zhí)行block內(nèi)方法的時候,如果self被釋放了咋辦,造成無法估計的后果(可能沒事,也有可能出個詭異bug),為了避免問題發(fā)生,block內(nèi)開始執(zhí)行的時候,立即生成強引用(strongSelf),這個強引用(strongSelf) 指向了弱引用(weakSelf)所指向的對象(self對象),這樣以來,在block內(nèi)部實際是持有了self對象,人為地制造了暫時的循環(huán)引用。為什么說是暫時?是因為強引用(strongSelf) 的生命周期只在這個block執(zhí)行的過程中,block執(zhí)行前不會存在,執(zhí)行完會立刻就被釋放了。

  • 關(guān)鍵點來了強引用(strongSelf) 指向了弱引用(weakSelf)所指向的對象,等價于強引用了對象

  • 我們?yōu)镹STimer/CADisplayLink對象指定target時候,雖然傳入了弱引用,但是造成的結(jié)果是:強引用了弱引用所引用的對象,也就是最終還是強引用了對象,而剛好對象又強引用了NSTimer/CADisplayLink對象。這樣以來,循環(huán)引用還是沒有解決。
    引入中間對象,在這個對象中弱引用self,然后將這個對象傳遞給timer的構(gòu)建方法

2、正確的決辦法

該方法來自YYKit項目,項目中定義了YYWeakProxy這樣的工具類解決

該方法引入一個YYWeakProxy對象,在這個對象中弱引用真正的目標對象。通過YYWeakProxy對象,將NSTimer/CADisplayLink對象弱引用目標對象。YYWeakProxy的實現(xiàn)如下:

//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的使用#####

假設(shè)在Controller中使用CADisplayLink。分三步(NSTimer的使用類似)

第一,我們可以在viewDidLoad中先初始化NSTimer/CADisplayLink對象,指定target是YYWeakProxy對象,和指定定時執(zhí)行的辦法
第二,在executeDispalyLink:中定義需要定時處理的方法;
第三,在dealloc中調(diào)用定時器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];
}

問題的關(guān)鍵來了:為什么NSProxy的子類YYWeakProxy可以解決NSTimer/CADisplayLink的循環(huán)引用問題。原因如下:

  • NSProxy本身是一個抽象類,它遵循NSObject協(xié)議,提供了消息轉(zhuǎn)發(fā)的通用接口,NSProxy通常用來實現(xiàn)消息轉(zhuǎn)發(fā)機制和惰性初始化資源。不能直接使用NSProxy。需要創(chuàng)建NSProxy的子類,并實現(xiàn)init以及消息轉(zhuǎn)發(fā)的相關(guān)方法,才可以用。

  • YYWeakProxy繼承了NSProxy,定義了一個弱引用的target對象,通過重寫消息轉(zhuǎn)發(fā)等關(guān)鍵方法,讓target對象去處理接收到的消息。在整個引用鏈中,Controller對象強引用NSTimer/CADisplayLink對象,NSTimer/CADisplayLink對象強引用YYWeakProxy對象,而YYWeakProxy對象弱引用Controller對象,所以在YYWeakProxy對象的作用下,Controller對象和NSTimer/CADisplayLink對象之間并沒有相互持有,完美解決循環(huán)引用的問題。

Demo源碼見QSUseTimerDemo

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容