通过YYFPSLabel了解NSTimer,CADisplayLink内存泄漏问题及解决方案

YYFPSLabel是ibireme的YYKit库中一个查看屏幕帧数工具,下面我们来看看这个库吧YYFPSLabel,我用Swift重写了FPSLabel,这个工这篇文章我们通过Swift的代码来分析

什么是CADisplayLink

CADisplayLink是CoreAnimation提供的另一个类似于NSTimer的类,它总是在屏幕完成一次更新之前启动,它的接口设计的和NSTimer很类似,所以它实际上就是一个内置实现的替代,但是和timeInterval以秒为单位不同,CADisplayLink有一个整型的frameInterval属性,指定了间隔多少帧之后才执行。默认值是1,意味着每次屏幕更新之前都会执行一次。但是如果动画的代码执行起来超过了六十分之一秒,你可以指定frameInterval为2,就是说动画每隔一帧执行一次(一秒钟30帧)或者3,也就是一秒钟20次,等等。

YYFPSLabel实现原理

CADisplayLink可以以屏幕刷新的频率调用指定selector,而且iOS系统中正常的屏幕刷新率为60Hz(60次每秒),所以使用 CADisplayLink 的 timestamp 属性,配合 timer 的执行次数计算得出FPS数。
刷新频率 = 次数/时间

源码分析

整个库主要包含两部分FPSLabel和WeakProxy,先看看FPSLabel吧,整体代码比较简单我就直接上代码,相关解释注释写的比较清楚

import UIKit

class FPSLabel: UILabel {
    var _link:CADisplayLink!
    //记录方法执行次数
    var _count: Int = 0
    //记录上次方法执行的时间,通过link.timestamp - _lastTime计算时间间隔
    var _lastTime: TimeInterval = 0
    var _font: UIFont!
    var _subFont: UIFont!
    
    fileprivate let defaultSize = CGSize(width: 55,height: 20)

    override init(frame: CGRect) {
        super.init(frame: frame)
        if frame.size.width == 0 && frame.size.height == 0 {
            self.frame.size = defaultSize
        }
        self.layer.cornerRadius = 5
        self.clipsToBounds = true
        self.textAlignment = NSTextAlignment.center
        self.isUserInteractionEnabled = false
        self.backgroundColor = UIColor.white.withAlphaComponent(0.7)
        
        _font = UIFont(name: "Menlo", size: 14)
        if _font != nil {
            _subFont = UIFont(name: "Menlo", size: 4)
        }else{
            _font = UIFont(name: "Courier", size: 14)
            _subFont = UIFont(name: "Courier", size: 4)
        }
        
        _link = CADisplayLink(target: WeakProxy.init(target: self), selector: #selector(FPSLabel.tick(link:)))
        _link.add(to: RunLoop.main, forMode: .commonModes)
    }
    
    //CADisplayLink 刷新执行的方法
    @objc func tick(link: CADisplayLink) {
        
        guard _lastTime != 0 else {
            _lastTime = _link.timestamp
            return
        }
        
        _count += 1
        let timePassed = link.timestamp - _lastTime
        
        //时间大于等于1秒计算一次,也就是FPSLabel刷新的间隔,不希望太频繁刷新
        guard timePassed >= 1 else {
            return
        }
        _lastTime = link.timestamp
        let fps = Double(_count) / timePassed
        _count = 0
        
        let progress = fps / 60.0
        let color = UIColor(hue: CGFloat(0.27 * (progress - 0.2)), saturation: 1, brightness: 0.9, alpha: 1)
        
        let text = NSMutableAttributedString(string: "\(Int(round(fps))) FPS")
        text.addAttribute(NSAttributedStringKey.foregroundColor, value: color, range: NSRange(location: 0, length: text.length - 3))
        text.addAttribute(NSAttributedStringKey.foregroundColor, value: UIColor.white, range: NSRange(location: text.length - 3, length: 3))
        text.addAttribute(NSAttributedStringKey.font, value: _font, range: NSRange(location: 0, length: text.length))
        text.addAttribute(NSAttributedStringKey.font, value: _subFont, range: NSRange(location: text.length - 4, length: 1))
        self.attributedText = text
    }
    
    // 把displaylin从Runloop modes中移除
    deinit {
        _link.invalidate()
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
}

NSTimer、CADisplayLink内存泄漏

如果_link = CADisplayLink(target: self, selector: #selector(FPSLabel.tick(link:))) Target 直接设置成 self 会造成内存泄漏。CADisplayLink强引用Target。当 CADisplayLink 加入 NSRunLoop 中,NSRunLoop会强引用CADisplayLink。如果仅仅在deinit中调用CADisplayLink的invalidate方法是没用的,因为NSRunLoop 一直都在,CADisplayLink不释放,Target被强引用,Target 的 deinit 方法不会被调用,CADisplayLink的invalidate方法也不被调用,CADisplayLink不会从NSRunLoop中移除

通过YYFPSLabel了解NSTimer,CADisplayLink内存泄漏问题及解决方案_第1张图片
Screen Shot 2018-04-06 at 11.05.02 PM.png

解决方法

1.改变CADisplayLink的invalidate的方法调用时机

如果Target是UIViewController,在viewWillDisappear方法中调用CADisplayLink的invalidate的方法,但是如果Target是A那么A push或者present到B时CADisplayLink就被释放了,但实际上这个时候我们并不希望CADisplayLink被释放

2.通过Proxy避免避免CADisplayLink对Target的强引用
下面是WeakProxy的代码

import UIKit

class WeakProxy: NSObject {
    
    weak var target: NSObjectProtocol?
    
    init(target: NSObjectProtocol) {
        self.target = target
        super.init()
    }
    
    //Returns a Boolean value that indicates whether the receiver implements or inherits a method that can respond to a specified message.
    override func responds(to aSelector: Selector!) -> Bool {
        return (target?.responds(to: aSelector) ?? false) || super.responds(to: aSelector)
    }
    
    //系统会将aSelector转发给target执行
    override func forwardingTarget(for aSelector: Selector!) -> Any? {
        return target
    }
    
}
通过YYFPSLabel了解NSTimer,CADisplayLink内存泄漏问题及解决方案_第2张图片
Screen Shot 2018-04-06 at 11.36.52 PM.png
_link = CADisplayLink(target: WeakProxy.init(target: self), selector: #selector(FPSLabel.tick(link:)))
 _link.add(to: RunLoop.main, forMode: .commonModes)
weak var target: NSObjectProtocol?

WeakProxy对self进行了弱引用,这样self的 deinit就能被调用,然后 CADisplayLink的invalidate也会被调用,CADisplayLink成功被释放。

3.Block

import UIKit

class CADisplayLinkProxy {

    var displaylink: CADisplayLink?
    var handle: (() -> Void)?

    init(handle: (() -> Void)?) {
        self.handle = handle
        displaylink = CADisplayLink(target: self, selector: #selector(updateHandle))
        displaylink?.add(to: RunLoop.current, forMode: .commonModes)
    }

    @objc func updateHandle() {
        handle?()
    }

    func invalidate() {
        displaylink?.remove(from: RunLoop.current, forMode: .commonModes)
        displaylink?.invalidate()
        displaylink = nil
    }
}  

Usage

var displaylinkProxy = CADisplayLinkProxy(handle: { [weak self] in
                    self?.updateTime()
                })

iOS10新的API已经支持NSTimer用Block解决这个问题了

if #available(iOS 10.0, *) {
        self.timer = Timer(timeInterval: 6.0, repeats: true, block: { [weak self]  (timer) in
            
        })
    }

注意:使用weakSelf不能解决循环引用问题

那为什么这样可以解决循环引用呢?

weak var weakSelf = self  
request.responseString(encoding: NSUTF8StringEncoding) {(res) -> Void in  
    if let strongSelf = weakSelf {  
        //do something  
    }  
} 

因为在block外使用弱引用(weakSelf),这个弱引用(weakSelf)指向的self对象,在block内捕获的是这个弱引用(weakSelf),而不是捕获self的强引用,也就是说,这就保证了self不会被block所持有。

那疑问就来了,为什么还要在block内使用强引用(strongSelf) ,因为,在执行block内方法的时候,如果self被释放了咋办,造成无法估计的后果(可能没事,也有可能出个诡异bug),为了避免问题发生,block内开始执行的时候,立即生成强引用(strongSelf),这个强引用(strongSelf) 指向了弱引用(weakSelf)所指向的对象(self对象),这样以来,在block内部实际是持有了self对象,人为地制造了暂时的循环引用。为什么说是暂时?是因为强引用(strongSelf) 的生命周期只在这个block执行的过程中,block执行前不会存在,执行完会立刻就被释放了。

强引用(strongSelf) 指向了弱引用(weakSelf)所指向的对象,等价于强引用了对象

下面这中不能解决循环引用

weak var weakSelf = self
_link = CADisplayLink(target: weakSelf!, selector: #selector(FPSLabel.tick(link:)))
_link.add(to: RunLoop.main, forMode: .commonModes)

it unwraps weakSelf when the CADisplayLink is initialized and passes a strong reference to self as the target.

我们为NSTimer/CADisplayLink对象指定target时候,虽然传入了弱引用,但是造成的结果是:强引用了弱引用所引用的对象,也就是最终还是强引用了对象,这和你直接传self进来效果是一样的。这样的做唯一作用是如果在CADisplayLink运行期间self被释放了,CADisplayLink的target也就置为nil,仅此而已。

参开文章
NSTimer、CADisplayLink 内存泄漏
How to set CADisplayLink in Swift with weak reference between target and CADisplayLink instance

你可能感兴趣的:(通过YYFPSLabel了解NSTimer,CADisplayLink内存泄漏问题及解决方案)