Swift中Timer的循環(huán)引用解決方案(類似NSProxy)

目標(biāo)環(huán)境:Swift 4.0

有效的代碼


/// 處理timer強(qiáng)引用類
public class HCWeakTimerProxy: NSObject {
    
    weak var target:NSObjectProtocol?
    var sel:Selector?
    /// required,實(shí)例化timer之后需要將timer賦值給proxy答捕,否則就算target釋放了猾封,timer本身依然會(huì)繼續(xù)運(yùn)行
    public weak var timer:Timer?
    
    public required init(target:NSObjectProtocol?, sel:Selector?) {
        self.target = target
        self.sel = sel
        super.init()
        // 加強(qiáng)安全保護(hù)
        guard target?.responds(to: sel) == true else {
            return
        }
        // 將target的selector替換為redirectionMethod,該方法會(huì)重新處理事件
        let method = class_getInstanceMethod(self.classForCoder, #selector(HCWeakTimerProxy.redirectionMethod))!
        class_replaceMethod(self.classForCoder, sel!, method_getImplementation(method), method_getTypeEncoding(method))
    }
    
    @objc func redirectionMethod () {
        // 如果target未被釋放噪珊,則調(diào)用target方法晌缘,否則釋放timer
        if self.target != nil {
            self.target!.perform(self.sel)
        } else {
            self.timer?.invalidate()
            print("HCWeakProxy: invalidate timer.")
        }
    }
}

使用方式

let proxy = HCWeakTimerProxy.init(target: self, sel: #selector(autoScroll))
self.timer = Timer.scheduledTimer(timeInterval: self.scrollTimerInterval, target: proxy, selector: #selector(autoScroll), userInfo: nil, repeats: true)
proxy.timer = self.timer

如果有需要看解決思路的就往下看齐莲。

解決思路


Timer如果不使用invalidate方法釋放的話,就會(huì)造成循環(huán)引用導(dǎo)致target無法釋放的問題磷箕。

一開始的解決思路是繼承NSProxy选酗,但出現(xiàn)了2個(gè)問題只好放棄:

  1. init無法重載,這個(gè)倒不是很要緊岳枷。
  2. NSInvocation不可用芒填,這就很關(guān)鍵了,直接導(dǎo)致了forwardInvocation方法無法重載空繁;
NSInvocation' is unavailable in Swift: NSInvocation and related APIs not available  

根據(jù)NSProxy的實(shí)現(xiàn)原理制作了Proxy類

/// 此類無法解決殿衰,僅做思路參考
class Proxy: NSObject {

    weak var target:NSObjectProtocol?
    
    public init(_ target:NSObjectProtocol?) {
        self.target = target
        super.init()
    }
    
    override func responds(to aSelector: Selector!) -> Bool {
        return self.target?.responds(to:aSelector) == true
    }
    
    override func forwardingTarget(for aSelector: Selector!) -> Any? {
        if self.target?.responds(to: aSelector) == true {
            return self.target
        } else {
            return super.forwardingTarget(for: aSelector)
        }
    }
}

// 具體調(diào)用
let proxy = Proxy.init(self)
let timer = Timer.scheduledTimer(timeInterval: 2, target: proxy, selector: #selector(autoScroll), userInfo: nil, repeats: true)

運(yùn)行后發(fā)現(xiàn)幾個(gè)問題:

  1. responds方法不會(huì)觸發(fā),因此這個(gè)方法就不需要重寫了盛泡;
  2. 在不調(diào)用timer.invalidate的情況下成功銷毀了target闷祥,但是timer還在;
  3. 由于timer還存在因此會(huì)繼續(xù)觸發(fā)forwardingTarget方法傲诵,并且進(jìn)入了else分支凯砍,由于Proxy類中本身并無selector所指定方法,崩潰了拴竹,返回nil也一樣會(huì)崩潰悟衩。

因此要先解決兩個(gè)問題:

  1. target銷毀后,timer必須跟著銷毀
  2. 由于target銷毀后栓拜,forwardingTarget依然會(huì)觸發(fā)座泳,因此需要在Proxy類中動(dòng)態(tài)注入selector方法。

第一個(gè)問題好解決幕与,只要在Proxy中弱引用timer挑势,并在else分支銷毀timer就可以了。
第二個(gè)問題本來想在resolveInstanceMethod中動(dòng)態(tài)注入方法纽门,但發(fā)現(xiàn)這個(gè)方法中無法調(diào)用到target薛耻,即便使用self.forwardingTarget方法返回也是nil值

override class func resolveInstanceMethod(_ sel: Selector!) -> Bool {}

因此考慮在init方法中注入方法,Proxy類改為:

class Proxy: NSObject {

    weak var target:NSObject?
    public weak var timer:Timer?
    var sel:Selector?
    
    public init(target:NSObject?, sel:Selector?) {
        self.target = target
        super.init()
        let method = class_getInstanceMethod(target as? AnyClass, sel!)
        class_addMethod(self.classForCoder, sel!, method_getImplementation(method!), method_getTypeEncoding(method!))
    }
    
    override func forwardingTarget(for aSelector: Selector!) -> Any? {
        if self.target?.responds(to: aSelector) == true {
            return self.target
        } else {
            self.timer?.invalidate()
            return self
        }
    }
}

但是發(fā)現(xiàn)獲得的method是nil赏陵,崩潰饼齿。
最后思考,當(dāng)timer調(diào)用selector的時(shí)候蝙搔,是否可以重定向到Proxy中的另一個(gè)方法中缕溉,由那個(gè)方法來執(zhí)行forwardingTarget的工作。這樣就算target銷毀了吃型,timer調(diào)用selector的時(shí)候也不會(huì)崩潰证鸥。最后得到的類如下:

/// 處理timer強(qiáng)引用類
public class HCWeakTimerProxy: NSObject {
    
    weak var target:NSObjectProtocol?
    var sel:Selector?
    /// required,實(shí)例化timer之后需要將timer賦值給proxy,否則就算target釋放了枉层,timer本身依然會(huì)繼續(xù)運(yùn)行
    public weak var timer:Timer?
    
    public required init(target:NSObjectProtocol?, sel:Selector?) {
        self.target = target
        self.sel = sel
        super.init()
        // 加強(qiáng)安全保護(hù)
        guard target?.responds(to: sel) == true else {
            return
        }
        // 將target的selector替換為redirectionMethod泉褐,該方法會(huì)重新處理事件
        let method = class_getInstanceMethod(self.classForCoder, #selector(HCWeakTimerProxy.redirectionMethod))!
        class_replaceMethod(self.classForCoder, sel!, method_getImplementation(method), method_getTypeEncoding(method))
    }
    
    @objc func redirectionMethod () {
        // 如果target未被釋放,則調(diào)用target方法鸟蜡,否則釋放timer
        if self.target != nil {
            self.target!.perform(self.sel)
        } else {
            self.timer?.invalidate()
            print("HCWeakProxy: invalidate timer.")
        }
    }
}

使用方式:

let proxy = HCWeakTimerProxy.init(target: self, sel: #selector(autoScroll))
self.timer = Timer.scheduledTimer(timeInterval: 3, target: proxy, selector: #selector(autoScroll), userInfo: nil, repeats: true)
proxy.timer = self.timer

最后為了方便使用膜赃,不用每次都考慮proxy,寫了一個(gè)extension:

public extension Timer {
    
    public class func hc_scheduledTimer(timeInterval ti: TimeInterval, target aTarget: NSObjectProtocol, selector aSelector: Selector, userInfo aInfo: Any?, repeats yesOrNo: Bool) -> Timer {
        let proxy = HCWeakTimerProxy.init(target: aTarget, sel: aSelector)
        let timer = Timer.scheduledTimer(timeInterval: ti, target: proxy, selector: aSelector, userInfo:aInfo, repeats: yesOrNo)
        proxy.timer = timer
        return timer
    }
}

// 外部調(diào)用target直接傳self就行
self.timer = Timer.hc_scheduledTimer(timeInterval: 3, target: self, selector: #selector(autoScroll), userInfo: nil, repeats: true)
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末揉忘,一起剝皮案震驚了整個(gè)濱河市跳座,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌泣矛,老刑警劉巖疲眷,帶你破解...
    沈念sama閱讀 221,695評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異您朽,居然都是意外死亡狂丝,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,569評論 3 399
  • 文/潘曉璐 我一進(jìn)店門虚倒,熙熙樓的掌柜王于貴愁眉苦臉地迎上來美侦,“玉大人产舞,你說我怎么就攤上這事魂奥。” “怎么了易猫?”我有些...
    開封第一講書人閱讀 168,130評論 0 360
  • 文/不壞的土叔 我叫張陵耻煤,是天一觀的道長匆浙。 經(jīng)常有香客問我褪迟,道長,這世上最難降的妖魔是什么薪寓? 我笑而不...
    開封第一講書人閱讀 59,648評論 1 297
  • 正文 為了忘掉前任攘已,我火速辦了婚禮炮赦,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘样勃。我一直安慰自己吠勘,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,655評論 6 397
  • 文/花漫 我一把揭開白布峡眶。 她就那樣靜靜地躺著剧防,像睡著了一般。 火紅的嫁衣襯著肌膚如雪辫樱。 梳的紋絲不亂的頭發(fā)上峭拘,一...
    開封第一講書人閱讀 52,268評論 1 309
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼鸡挠。 笑死辉饱,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的拣展。 我是一名探鬼主播鞋囊,決...
    沈念sama閱讀 40,835評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼瞎惫!你這毒婦竟也來了溜腐?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,740評論 0 276
  • 序言:老撾萬榮一對情侶失蹤瓜喇,失蹤者是張志新(化名)和其女友劉穎挺益,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體乘寒,經(jīng)...
    沈念sama閱讀 46,286評論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡望众,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,375評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了伞辛。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片烂翰。...
    茶點(diǎn)故事閱讀 40,505評論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖蚤氏,靈堂內(nèi)的尸體忽然破棺而出甘耿,到底是詐尸還是另有隱情,我是刑警寧澤竿滨,帶...
    沈念sama閱讀 36,185評論 5 350
  • 正文 年R本政府宣布佳恬,位于F島的核電站,受9級(jí)特大地震影響于游,放射性物質(zhì)發(fā)生泄漏毁葱。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,873評論 3 333
  • 文/蒙蒙 一贰剥、第九天 我趴在偏房一處隱蔽的房頂上張望倾剿。 院中可真熱鬧,春花似錦蚌成、人聲如沸前痘。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,357評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽际度。三九已至,卻和暖如春涵妥,著一層夾襖步出監(jiān)牢的瞬間乖菱,已是汗流浹背坡锡。 一陣腳步聲響...
    開封第一講書人閱讀 33,466評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留窒所,地道東北人鹉勒。 一個(gè)月前我還...
    沈念sama閱讀 48,921評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像吵取,于是被迫代替她去往敵國和親禽额。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,515評論 2 359

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

  • 1.ios高性能編程 (1).內(nèi)層 最小的內(nèi)層平均值和峰值(2).耗電量 高效的算法和數(shù)據(jù)結(jié)構(gòu)(3).初始化時(shí)...
    歐辰_OSR閱讀 29,417評論 8 265
  • 場景:一個(gè)VC想引用一個(gè)帶著NStimer的View.但是一般的情況下我們要在VC的dealloc中還要銷毀Vie...
    圖長伴閱讀 644評論 0 1
  • 轉(zhuǎn)至元數(shù)據(jù)結(jié)尾創(chuàng)建: 董瀟偉皮官,最新修改于: 十二月 23, 2016 轉(zhuǎn)至元數(shù)據(jù)起始第一章:isa和Class一....
    40c0490e5268閱讀 1,726評論 0 9
  • 小時(shí)候脯倒,我喜歡坐在陽臺(tái)上,仰望星空捺氢。一片大黑幕布般的夜空藻丢,點(diǎn)綴著點(diǎn)點(diǎn)繁星,總是讓我癡迷不已摄乒。我曾伸著小手悠反,...
    雨不溫柔閱讀 561評論 0 1
  • 戰(zhàn)術(shù)和戰(zhàn)略的區(qū)別斋否。一般說來戰(zhàn)略是戰(zhàn)術(shù)的對立面,在象棋中拭荤,一心只想將茵臭,將死他,你就贏了穷劈。 圍棋講的是點(diǎn)數(shù)取勝笼恰。誰的字...
    設(shè)計(jì)師阿瑞閱讀 379評論 0 2