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

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末苦蒿,一起剝皮案震驚了整個濱河市殴胧,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌刽肠,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,482評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件免胃,死亡現(xiàn)場離奇詭異音五,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)羔沙,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,377評論 2 382
  • 文/潘曉璐 我一進(jìn)店門躺涝,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人扼雏,你說我怎么就攤上這事坚嗜。” “怎么了诗充?”我有些...
    開封第一講書人閱讀 152,762評論 0 342
  • 文/不壞的土叔 我叫張陵苍蔬,是天一觀的道長。 經(jīng)常有香客問我蝴蜓,道長碟绑,這世上最難降的妖魔是什么俺猿? 我笑而不...
    開封第一講書人閱讀 55,273評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮格仲,結(jié)果婚禮上押袍,老公的妹妹穿的比我還像新娘。我一直安慰自己凯肋,他們只是感情好谊惭,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,289評論 5 373
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著侮东,像睡著了一般圈盔。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上苗桂,一...
    開封第一講書人閱讀 49,046評論 1 285
  • 那天药磺,我揣著相機(jī)與錄音,去河邊找鬼煤伟。 笑死癌佩,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的便锨。 我是一名探鬼主播围辙,決...
    沈念sama閱讀 38,351評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼放案!你這毒婦竟也來了姚建?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,988評論 0 259
  • 序言:老撾萬榮一對情侶失蹤吱殉,失蹤者是張志新(化名)和其女友劉穎掸冤,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體友雳,經(jīng)...
    沈念sama閱讀 43,476評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡稿湿,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,948評論 2 324
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了押赊。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片饺藤。...
    茶點(diǎn)故事閱讀 38,064評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖流礁,靈堂內(nèi)的尸體忽然破棺而出涕俗,到底是詐尸還是另有隱情,我是刑警寧澤神帅,帶...
    沈念sama閱讀 33,712評論 4 323
  • 正文 年R本政府宣布再姑,位于F島的核電站,受9級特大地震影響找御,放射性物質(zhì)發(fā)生泄漏询刹。R本人自食惡果不足惜谜嫉,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,261評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望凹联。 院中可真熱鬧沐兰,春花似錦、人聲如沸蔽挠。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,264評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽澳淑。三九已至比原,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間杠巡,已是汗流浹背量窘。 一陣腳步聲響...
    開封第一講書人閱讀 31,486評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留氢拥,地道東北人蚌铜。 一個月前我還...
    沈念sama閱讀 45,511評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像嫩海,于是被迫代替她去往敵國和親冬殃。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,802評論 2 345

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