美文网首页
Swift中Timer的循环引用解决方案(类似NSProxy)

Swift中Timer的循环引用解决方案(类似NSProxy)

作者: a_超 | 来源:发表于2018-05-24 18:10 被阅读224次

    目标环境:Swift 4.0

    有效的代码


    /// 处理timer强引用类
    public class HCWeakTimerProxy: NSObject {
        
        weak var target:NSObjectProtocol?
        var sel:Selector?
        /// required,实例化timer之后需要将timer赋值给proxy,否则就算target释放了,timer本身依然会继续运行
        public weak var timer:Timer?
        
        public required init(target:NSObjectProtocol?, sel:Selector?) {
            self.target = target
            self.sel = sel
            super.init()
            // 加强安全保护
            guard target?.responds(to: sel) == true else {
                return
            }
            // 将target的selector替换为redirectionMethod,该方法会重新处理事件
            let method = class_getInstanceMethod(self.classForCoder, #selector(HCWeakTimerProxy.redirectionMethod))!
            class_replaceMethod(self.classForCoder, sel!, method_getImplementation(method), method_getTypeEncoding(method))
        }
        
        @objc func redirectionMethod () {
            // 如果target未被释放,则调用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方法释放的话,就会造成循环引用导致target无法释放的问题。

    一开始的解决思路是继承NSProxy,但出现了2个问题只好放弃:

    1. init无法重载,这个倒不是很要紧。
    2. NSInvocation不可用,这就很关键了,直接导致了forwardInvocation方法无法重载;
    NSInvocation' is unavailable in Swift: NSInvocation and related APIs not available  
    

    根据NSProxy的实现原理制作了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)
            }
        }
    }
    
    // 具体调用
    let proxy = Proxy.init(self)
    let timer = Timer.scheduledTimer(timeInterval: 2, target: proxy, selector: #selector(autoScroll), userInfo: nil, repeats: true)
    

    运行后发现几个问题:

    1. responds方法不会触发,因此这个方法就不需要重写了;
    2. 在不调用timer.invalidate的情况下成功销毁了target,但是timer还在;
    3. 由于timer还存在因此会继续触发forwardingTarget方法,并且进入了else分支,由于Proxy类中本身并无selector所指定方法,崩溃了,返回nil也一样会崩溃。

    因此要先解决两个问题:

    1. target销毁后,timer必须跟着销毁
    2. 由于target销毁后,forwardingTarget依然会触发,因此需要在Proxy类中动态注入selector方法。

    第一个问题好解决,只要在Proxy中弱引用timer,并在else分支销毁timer就可以了。
    第二个问题本来想在resolveInstanceMethod中动态注入方法,但发现这个方法中无法调用到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
            }
        }
    }
    

    但是发现获得的method是nil,崩溃。
    最后思考,当timer调用selector的时候,是否可以重定向到Proxy中的另一个方法中,由那个方法来执行forwardingTarget的工作。这样就算target销毁了,timer调用selector的时候也不会崩溃。最后得到的类如下:

    /// 处理timer强引用类
    public class HCWeakTimerProxy: NSObject {
        
        weak var target:NSObjectProtocol?
        var sel:Selector?
        /// required,实例化timer之后需要将timer赋值给proxy,否则就算target释放了,timer本身依然会继续运行
        public weak var timer:Timer?
        
        public required init(target:NSObjectProtocol?, sel:Selector?) {
            self.target = target
            self.sel = sel
            super.init()
            // 加强安全保护
            guard target?.responds(to: sel) == true else {
                return
            }
            // 将target的selector替换为redirectionMethod,该方法会重新处理事件
            let method = class_getInstanceMethod(self.classForCoder, #selector(HCWeakTimerProxy.redirectionMethod))!
            class_replaceMethod(self.classForCoder, sel!, method_getImplementation(method), method_getTypeEncoding(method))
        }
        
        @objc func redirectionMethod () {
            // 如果target未被释放,则调用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,写了一个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
        }
    }
    
    // 外部调用target直接传self就行
    self.timer = Timer.hc_scheduledTimer(timeInterval: 3, target: self, selector: #selector(autoScroll), userInfo: nil, repeats: true)
    

    相关文章

      网友评论

          本文标题:Swift中Timer的循环引用解决方案(类似NSProxy)

          本文链接:https://www.haomeiwen.com/subject/hdisjftx.html