通过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中移除

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
    }
    
}
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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,826评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,968评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,234评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,562评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,611评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,482评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,271评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,166评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,608评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,814评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,926评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,644评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,249评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,866评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,991评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,063评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,871评论 2 354

推荐阅读更多精彩内容