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

目标环境: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))