破解 NSTimer 的強(qiáng)引用

問(wèn)題

很多人都知道, Timer 會(huì)通過(guò) Runloop 對(duì) target 進(jìn)行強(qiáng)引用, 需要手動(dòng)對(duì) Timer 進(jìn)行 invalidate 以便釋放強(qiáng)引用.
所以很多人就寫(xiě)了下面的代碼

...
override func viewDidLoad() {
    super.viewDidLoad()
    self.timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(tick), userInfo: nil, repeats: true)
}
deinit {
    self.timer.invalidate()
}
@objc func tick() {
    print("tick")
}

這樣寫(xiě), 很快就會(huì)發(fā)現(xiàn)問(wèn)題, 因?yàn)槟愕?ViewController 在彈出之后, 依然在打印 tick
簡(jiǎn)單分析一下, 因?yàn)?Timer 對(duì) target 有一個(gè)強(qiáng)引用, 導(dǎo)致無(wú)法釋放, 我們代碼中對(duì) Timer 的釋放函數(shù)卻在析構(gòu)函數(shù)中, 而析構(gòu)函數(shù)根本就無(wú)法被調(diào)用, 也就造成了引用無(wú)法釋放, Timer 也無(wú)法結(jié)束.

錯(cuò)誤的解決方案:

看到引用無(wú)法釋放, 你可能會(huì)想到使用弱引用解決, 于是寫(xiě)出了下面的代碼

override func viewDidLoad() {
    super.viewDidLoad()
    weak var weakself = self
    self.timer = Timer.scheduledTimer(timeInterval: 1, target: weakself, selector: #selector(tick), userInfo: nil, repeats: true)
}
...

結(jié)果是令人失望的, 即使這樣, 控制器也無(wú)法被釋放. 強(qiáng)引用的是變量指向的對(duì)象, 而不是變量本身, 弱引用并不能解決這個(gè)問(wèn)題.

正確的姿勢(shì)

既然無(wú)法直接解決, 那我們可以通過(guò)一個(gè)局部變量作為 target, 然后在局部變量中存放一個(gè) weakself 來(lái)解決引用問(wèn)題.
先來(lái)定義一個(gè)類(lèi)型用來(lái)存放 weakself 和接收 Timer 事件

class TimerTarget: NSObject {
    weak var target: AnyObject?
    let timerSelctor: Selector
    init(target: AnyObject, selector: Selector) {
        self.timerSelctor = selector
        super.init()
        self.target = target
    }
    @objc func timerTick() {
        _ = self.target?.perform(self.timerSelctor)
    }
}

這里面通過(guò)一個(gè) weak 屬性指向了控制器, 這樣, 即使 Timer 對(duì) TimerTarget 增加一個(gè)強(qiáng)引用, 也不會(huì)對(duì)控制器的引用計(jì)數(shù)產(chǎn)生影響

override func viewDidLoad() {
    super.viewDidLoad()
    self.timer = Timer.scheduledTimer(timeInterval: 1, target: TimerTarget(target: self, selector: #selector(tick)), selector: #selector(TimerTarget.timerTick), userInfo: nil, repeats: true)
}

這樣操作后, 控制器就可以正常釋放了. 而且, 由于控制器會(huì)在釋放時(shí), 主動(dòng)釋放 Timer, 這樣, TimerTarget 也可以正常釋放

進(jìn)階, 更簡(jiǎn)潔的解決方案

上面的代碼看起來(lái)略顯累贅, 有沒(méi)有跟簡(jiǎn)單的辦法?
當(dāng)然有, 使用 NSProxy 類(lèi), 將 Timer 觸發(fā)的方法轉(zhuǎn)發(fā)到我們的控制器中.
不過(guò)遺憾的是, Swift 由于不支持 NSInvocation 以及構(gòu)造函數(shù)無(wú)法構(gòu)造(NSProxy 沒(méi)有 init 方法), 只能使用混編來(lái)解決.
我們來(lái)創(chuàng)建一個(gè) OC 的類(lèi)
WeakTarget.h 文件

#import <Foundation/Foundation.h>

@interface WeakTarget : NSProxy
@property(weak, nonatomic) id target;
- (instancetype)initWithTarget:(id)target;
@end

WeakTarget.m 文件

#import "WeakTarget.h"

@implementation WeakTarget
- (instancetype)initWithTarget:(id)target{
    self.target = target;
    return self;
}
-(void)forwardInvocation:(NSInvocation *)invocation {
    [invocation invokeWithTarget:self.target];
}
-(NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
    return [self.target methodSignatureForSelector:sel];
}
@end

另外, 沒(méi)有用過(guò)混編的同學(xué)注意, 在詢問(wèn)是否需要?jiǎng)?chuàng)建bridge header 的時(shí)候, 一定要選擇創(chuàng)建, 否則就需要自己創(chuàng)建了.
XXX-Bridging-Header.h 中添加對(duì)這個(gè)頭文件的引用.

#import "WeakTarget.h"

這樣準(zhǔn)備工作就好了.
使用方式如下

self.timer = Timer.scheduledTimer(timeInterval: 1, target: WeakTarget(target: self), selector: #selector(tick), userInfo: nil, repeats: true)

本質(zhì)上還是一樣的, 都是在WeakTarget 里面存放一個(gè)弱引用的對(duì)象, 避免對(duì)控制器進(jìn)行強(qiáng)引用.
使用NSProxy 可以將方法重定向到控制器中, 代碼更加整潔.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末憎亚,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子羹应,更是在濱河造成了極大的恐慌电抚,老刑警劉巖矫限,帶你破解...
    沈念sama閱讀 206,311評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件亡电,死亡現(xiàn)場(chǎng)離奇詭異碟刺,居然都是意外死亡跋核,警方通過(guò)查閱死者的電腦和手機(jī)粥庄,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén)丧失,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人惜互,你說(shuō)我怎么就攤上這事布讹。” “怎么了训堆?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,671評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵描验,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我坑鱼,道長(zhǎng)膘流,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,252評(píng)論 1 279
  • 正文 為了忘掉前任鲁沥,我火速辦了婚禮呼股,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘黍析。我一直安慰自己卖怜,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,253評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布阐枣。 她就那樣靜靜地躺著马靠,像睡著了一般。 火紅的嫁衣襯著肌膚如雪蔼两。 梳的紋絲不亂的頭發(fā)上甩鳄,一...
    開(kāi)封第一講書(shū)人閱讀 49,031評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音额划,去河邊找鬼妙啃。 笑死,一個(gè)胖子當(dāng)著我的面吹牛俊戳,可吹牛的內(nèi)容都是我干的揖赴。 我是一名探鬼主播,決...
    沈念sama閱讀 38,340評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼抑胎,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼燥滑!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起阿逃,我...
    開(kāi)封第一講書(shū)人閱讀 36,973評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤铭拧,失蹤者是張志新(化名)和其女友劉穎赃蛛,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體搀菩,經(jīng)...
    沈念sama閱讀 43,466評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡呕臂,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,937評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了肪跋。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片歧蒋。...
    茶點(diǎn)故事閱讀 38,039評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖澎嚣,靈堂內(nèi)的尸體忽然破棺而出疏尿,到底是詐尸還是另有隱情瘟芝,我是刑警寧澤易桃,帶...
    沈念sama閱讀 33,701評(píng)論 4 323
  • 正文 年R本政府宣布,位于F島的核電站锌俱,受9級(jí)特大地震影響晤郑,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜贸宏,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,254評(píng)論 3 307
  • 文/蒙蒙 一造寝、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧吭练,春花似錦诫龙、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,259評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至分尸,卻和暖如春锦聊,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背箩绍。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工孔庭, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人材蛛。 一個(gè)月前我還...
    沈念sama閱讀 45,497評(píng)論 2 354
  • 正文 我出身青樓圆到,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親卑吭。 傳聞我的和親對(duì)象是個(gè)殘疾皇子芽淡,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,786評(píng)論 2 345