[這是第8篇]
導(dǎo)語:使用NSTimer/CADisplayLink容易發(fā)生循環(huán)引用顿颅,網(wǎng)上很多博文都提到解決該問題的辦法。但是有些問題還是沒有說清楚千扔,結(jié)合自己在項目中的使用冗尤,說說我的解決辦法。
發(fā)生循環(huán)引用的原因:
初始化NSTimer/CADisplayLink對象時候绞吁,指定target時候幢痘,會保留其目標(biāo)對象,而NSTimer/CADisplayLink的目標(biāo)對象如果恰好保留了計時器本身家破,就會導(dǎo)致循環(huán)引用颜说。解決的辦法主要有兩種
方法一:擴(kuò)展方法,使用block打破保留環(huán)####
- 這是《Effective Object-C 2.0 編寫高質(zhì)量iOS與OS的代碼的52個有效方法》書中的建議汰聋,使用block方法门粪,解決循環(huán)引用的問題。編碼實(shí)現(xiàn)中烹困,為NSTimer和CADisplayLink分別創(chuàng)建分類玄妈,擴(kuò)展出新方法。
1、NSTimer+QSTool分類實(shí)現(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分類實(shí)現(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時候,會保留其目標(biāo)對象酝锅。我們的目的是繞開這個定時器對象強(qiáng)引用目標(biāo)對象這個問題诡必。在分類中,定時器對象指定的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弱引用目標(biāo)對象
1欢瞪、常見的錯誤解決辦法
【警告】下面是錯誤的解決辦法活烙,是無效的(這么簡單的話,《Effective Object-C 2.0》不至于單獨(dú)開一節(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的強(qiáng)引用回懦,也就是說,這就保證了self不會被block所持有次企。
那疑問就來了怯晕,為什么還要在block內(nèi)使用強(qiáng)引用(strongSelf) ,因為缸棵,在執(zhí)行block內(nèi)方法的時候舟茶,如果self被釋放了咋辦,造成無法估計的后果(可能沒事,也有可能出個詭異bug)吧凉,為了避免問題發(fā)生隧出,block內(nèi)開始執(zhí)行的時候,立即生成強(qiáng)引用(strongSelf)客燕,這個強(qiáng)引用(strongSelf) 指向了弱引用(weakSelf)所指向的對象(self對象)鸳劳,這樣以來,在block內(nèi)部實(shí)際是持有了self對象也搓,人為地制造了暫時的循環(huán)引用赏廓。為什么說是暫時?是因為強(qiáng)引用(strongSelf) 的生命周期只在這個block執(zhí)行的過程中傍妒,block執(zhí)行前不會存在幔摸,執(zhí)行完會立刻就被釋放了。
關(guān)鍵點(diǎn)來了:強(qiáng)引用(strongSelf) 指向了弱引用(weakSelf)所指向的對象颤练,等價于強(qiáng)引用了對象
我們?yōu)镹STimer/CADisplayLink對象指定target時候既忆,雖然傳入了弱引用,但是造成的結(jié)果是:強(qiáng)引用了弱引用所引用的對象嗦玖,也就是最終還是強(qiáng)引用了對象患雇,而剛好對象又強(qiáng)引用了NSTimer/CADisplayLink對象。這樣以來宇挫,循環(huán)引用還是沒有解決苛吱。
引入中間對象,在這個對象中弱引用self器瘪,然后將這個對象傳遞給timer的構(gòu)建方法
2翠储、正確的決辦法
該方法來自YYKit項目,項目中定義了YYWeakProxy這樣的工具類解決
該方法引入一個YYWeakProxy對象橡疼,在這個對象中弱引用真正的目標(biāo)對象援所。通過YYWeakProxy對象,將NSTimer/CADisplayLink對象弱引用目標(biāo)對象欣除。YYWeakProxy的實(shí)現(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通常用來實(shí)現(xiàn)消息轉(zhuǎn)發(fā)機(jī)制和惰性初始化資源。不能直接使用NSProxy疲憋。需要創(chuàng)建NSProxy的子類凿渊,并實(shí)現(xiàn)init以及消息轉(zhuǎn)發(fā)的相關(guān)方法,才可以用缚柳。
YYWeakProxy繼承了NSProxy埃脏,定義了一個弱引用的target對象,通過重寫消息轉(zhuǎn)發(fā)等關(guān)鍵方法秋忙,讓target對象去處理接收到的消息彩掐。在整個引用鏈中,Controller對象強(qiáng)引用NSTimer/CADisplayLink對象灰追,NSTimer/CADisplayLink對象強(qiáng)引用YYWeakProxy對象堵幽,而YYWeakProxy對象弱引用Controller對象,所以在YYWeakProxy對象的作用下弹澎,Controller對象和NSTimer/CADisplayLink對象之間并沒有相互持有朴下,完美解決循環(huán)引用的問題。
Demo源碼見QSUseTimerDemo