从VVeboTableViewDemo到YYAsyncLayer(二)

YYAsyncLayer源码分析

本节关键字

  • 异步绘制
  • RunLoop
YYAsyncLayer目录结构

这是YYAsyncLayer的结构

  • YYAsyncLayer:异步绘制的CALayer子类,这个类做的核心和VVeboTableViewDemoVVeboLabel 的核心是一模一样的。你可以到回去看看从VVeboTableViewDemo到YYAsyncLayer(一)

  • YYSentinel:线程安全的计数器。

  • YYTransaction:注册RunLoop在系统空闲时调用。(如果你不了解或者没有听说过RunLoop,不用担心,下面我同样会推荐相关的文章,让你了解和实践RunLoop)

  • YYAsyncLayerDisplayTask: 用于回调画布

  • YYAsyncLayerDelegate: 给接收YYAsyncLayer的UIView开的接口

本文依然是用Swift版YYAsyncLayer进行分析

YYAsyncLayer

屏幕快照 2017-04-14 下午5.10.56.png

上图中高亮的方法为整个类的核心方法

private func _displayAsync(_ async: Bool) {
        /// 如果需要使用异步绘制的地方没有实现该代理,直接返回
        guard let mydelegate = delegate as? YYAsyncLayerDelegate else { return }
        /// 接收来自需要异步绘制类的任务对象
        let task = mydelegate.newAsyncDisplayTask

        /// 如果display闭包为空,直接返回
        if task.display == nil {
            task.willDisplay?(self)
            contents = nil
            task.didDisplay?(self, true)
            return
        }

        // 是否需要异步绘制,默认是开启异步绘制的
        if async {
            /// 绘制将要开始
            task.willDisplay?(self)
            /// https://github.com/ibireme/YYAsyncLayer/issues/6
            /*
                一个Operation/Task对应唯一一个isCancelled,在NSOperation中是函数调用,在这里是这个isCancelled block。所以每次提交到queue的task的isCancelled block是不同的block对象,其中捕获的value的值都是这个task创建时sentinel.value的值,而捕获的sentinel的引用都是这个layer的sentinel的引用,最后在block执行的时候,value的值就是捕获的value,而sentinel.value则可能已经发生了变化。
             */
            let sentinel = _sentinel
            let value = sentinel!.value
            let isCancelled: (() -> Bool) = {
                return value != sentinel!.value
            }
            let size = bounds.size
            let opaque = isOpaque
            let scale = contentsScale
            let backgroundColor = (opaque && self.backgroundColor != nil) ? self.backgroundColor : nil
            /// 太小不绘制
            if size.width < 1 || size.height < 1 {
                var image = contents
                contents = nil
                if image != nil {
                    YYAsyncLayerGetReleaseQueue.async {
                        image = nil
                    }
                }
                task.didDisplay?(self, true)
                return
            }

            /// 将绘制操作放入自定义队列中
            YYAsyncLayerGetDisplayQueue.async {
                if isCancelled() {
                    return
                }
                /// 第一个参数表示所要创建的图片的尺寸;
                /// 第二个参数用来指定所生成图片的背景是否为不透明,如上我们使用true而不是false,则我们得到的图片背景将会是黑色,显然这不是我想要的;
                /// 第三个参数指定生成图片的缩放因子,这个缩放因子与UIImage的scale属性所指的含义是一致的。传入0则表示让图片的缩放因子根据屏幕的分辨率而变化,所以我们得到的图片不管是在单分辨率还是视网膜屏上看起来都会很好。
                
                /// 注意这个与UIGraphicsEndImageContext()成对出现
                /// iOS10 中新增了UIGraphicsImageRenderer(bounds: _)
                UIGraphicsBeginImageContextWithOptions(size, opaque, scale)

                /// 获取绘制画布
                /// 每一个UIView都有一个layer,每一个layer都有个content,这个content指向的是一块缓存,叫做backing store。
                /// UIView的绘制和渲染是两个过程,当UIView被绘制时,CPU执行drawRect,通过context将数据写入backing store
                /// http://vizlabxt.github.io/blog/2012/10/22/UIView-Rendering/
                guard let context = UIGraphicsGetCurrentContext() else { return }
                if opaque {
                    
                    /*
                     成对出现
                     CGContextSaveGState与CGContextRestoreGState的作用
                     
                     使用Quartz时涉及到一个图形上下文,其中图形上下文中包含一个保存过的图形状态堆栈。在Quartz创建图形上下文时,该堆栈是空的。CGContextSaveGState函数的作用是将当前图形状态推入堆栈。之后,您对图形状态所做的修改会影响随后的描画操作,但不影响存储在堆栈中的拷贝。在修改完成后。

                     您可以通过CGContextRestoreGState函数把堆栈顶部的状态弹出,返回到之前的图形状态。这种推入和弹出的方式是回到之前图形状态的快速方法,避免逐个撤消所有的状态修改;这也是将某些状态(比如裁剪路径)恢复到原有设置的唯一方式。
                     */
                    context.saveGState()
                    if backgroundColor == nil || backgroundColor!.alpha < 1 {
                        context.setFillColor(UIColor.white.cgColor) // 设置填充颜色,setStrokeColor为边框颜色

                        context.addRect(CGRect(x: 0, y: 0, width: size.width * scale, height: size.height * scale))
                        context.fillPath() // 填充路径

                        // 上面两句与这句等效
//                        context.fill(CGRect(x: 0, y: 0, width: size.width * scale, height: size.height * scale))
                    }
                    if let backgroundColor = backgroundColor {
                        context.setFillColor(backgroundColor)
                        context.addRect(CGRect(x: 0, y: 0, width: size.width * scale, height: size.height * scale))
                        context.fillPath()
                    }
                    context.restoreGState()
                }

                // 回调绘制
                task.display?(context, size, isCancelled)

                // 如果取消,提前结束绘制
                if isCancelled() {
                    UIGraphicsEndImageContext()
                    DispatchQueue.main.async {
                        task.didDisplay?(self, false)
                    }
                    return
                }

                // 从画布中获取图片,与UIGraphicsEndImageContext()成对出现
                let image = UIGraphicsGetImageFromCurrentImageContext()
                UIGraphicsEndImageContext()

                // 如果取消,提前结束绘制
                if isCancelled() {
                    DispatchQueue.main.async {
                        task.didDisplay?(self, false)
                    }
                    return
                }

                DispatchQueue.main.async {
                    if isCancelled() {
                        task.didDisplay?(self, false)
                    } else {
                        // 绘制成功
                        self.contents = image?.cgImage
                        task.didDisplay?(self, true)
                    }
                }
            }
        } else {
            // 同步绘制
            _sentinel.increase()
            task.willDisplay?(self)
            UIGraphicsBeginImageContextWithOptions(bounds.size, isOpaque, contentsScale)
            guard let context = UIGraphicsGetCurrentContext() else { return }
            if isOpaque {
                var size = bounds.size
                size.width *= contentsScale
                size.height *= contentsScale
                context.saveGState()
                if backgroundColor == nil || backgroundColor!.alpha < 1 {
                    context.setFillColor(UIColor.white.cgColor)
                    context.addRect(CGRect(origin: .zero, size: size))
                    context.fillPath()
                }
                if let backgroundColor = backgroundColor {
                    context.setFillColor(backgroundColor)
                    context.addRect(CGRect(origin: .zero, size: size))
                    context.fillPath()
                }
                context.restoreGState()
            }
            task.display?(context, bounds.size, {return false })
            let image = UIGraphicsGetImageFromCurrentImageContext()
            UIGraphicsEndImageContext()
            contents = image?.cgImage
            task.didDisplay?(self, true)
        }
    }

如果你去对比从VVeboTableViewDemo到YYAsyncLayer(一)中的VVeboLabel,他们的核心思想其实是一模一样的

YYTransaction

这个类和另外两个类是独立的。那么他是干嘛用的了?作者构建他的理由是什么呢?
我们看看这张图:(在任意项目的func viewDidLoad()中打个断点,你的堆栈信息大概就是这样的)

屏幕快照 2017-04-17 下午2.28.54.png

图中有个CATransaction 的东西,似乎和YYTransaction很相似。其实他们不仅命名很相似,就是内部结构也很相似。
再看YYTransaction

屏幕快照 2017-04-17 下午2.35.41.png

其中YYTransactionSetup

func YYTransactionSetup() {
    DispatchQueue.once(token: onceToken) {
        transactionSet = Set()
        /// 获取main RunLoop
        let runloop = CFRunLoopGetMain()
        var observer: CFRunLoopObserver?
        
        /// http://www.jianshu.com/p/6757e964b956
        ///  创建一个RunLoop的观察者
        /// allocator:该参数为对象内存分配器,一般使用默认的分配器kCFAllocatorDefault。或者nil
        /// activities:该参数配置观察者监听Run Loop的哪种运行状态。在示例中,我们让观察者监听Run Loop的所有运行状态。
        /// repeats:该参数标识观察者只监听一次还是每次Run Loop运行时都监听。
        /// order: 观察者优先级,当Run Loop中有多个观察者监听同一个运行状态时,那么就根据该优先级判断,0为最高优先级别。
        /// callout:观察者的回调函数,在Core Foundation框架中用CFRunLoopObserverCallBack重定义了回调函数的闭包。
        /// context:观察者的上下文。 (类似与KVO传递的context,可以传递信息,)因为这个函数创建ovserver的时候需要传递进一个函数指针,而这个函数指针可能用在n多个oberver 可以当做区分是哪个observer的状机态。(下面的通过block创建的observer一般是一对一的,一般也不需要Context,),还有一个例子类似与NSNOtificationCenter的 SEL和 Block方式。
        observer = CFRunLoopObserverCreate(
            kCFAllocatorDefault,
            CFRunLoopActivity.beforeWaiting.rawValue | CFRunLoopActivity.exit.rawValue,
            true, 0xFFFFFF,
            YYRunLoopObserverCallBack,
            nil
        )
        //将观察者添加到主线程runloop的common模式下的观察中
        CFRunLoopAddObserver(runloop, observer, .commonModes)
        observer = nil
    }
}

到这里,我们可以感觉到YYTransaction的用途和CATransaction的用途是有某种相似之处的。

再看

苹果对CATransaction的定义(你也可以看看这本书里的解释)

A mechanism for batching multiple layer-tree operations into atomic updates to the render tree.

**谷歌翻译: ** 用于将多个层树操作批量化为渲染树的原子更新的机制。

事务是通过CATransaction类来做管理,这个类的设计有些奇怪,不像你从它的命名预期的那样去管理一个简单的事务,而是管理了一叠你不能访问的事务。CATransaction没有属性或者实例方法,并且也不能用+alloc和-init方法创建它。但是可以用+begin和+commit分别来入栈或者出栈。
任何可以做动画的图层属性都会被添加到栈顶的事务,你可以通过+setAnimationDuration:方法设置当前事务的动画时间,或者通过+animationDuration方法来获取值(默认0.25秒)。
Core Animation在每个run loop周期中自动开始一次新的事务(run loop是iOS负责收集用户输入,处理定时器或者网络事件并且重新绘制屏幕的东西),即使你不显式的用[CATransaction begin]开始一次事务,任何在一次run loop循环中属性的改变都会被集中起来,然后做一次0.25秒的动画。

通过这段解释,我们可以获得关键信息是:CATransaction是用了对事物来做管理的。Core Animation在每个run loop周期中自动开始一次新的事务

在这里你不需要恐惧RunLoop,即使我们一点也不了解,下面的代码也是可以看懂的

let YYRunLoopObserverCallBack: CFRunLoopObserverCallBack = {_,_,_ in
     if (transactionSet?.count ?? 0) == 0 {
          return
      }
      let currentSet = transactionSet
      transactionSet = Set()
      for item in currentSet! {
           _ = (item.target as? NSObject)?.perform(item.selector)
       }
}


func YYTransactionSetup() {
    DispatchQueue.once(token: onceToken) {
        transactionSet = Set()
        /// 获取main RunLoop
        let runloop = CFRunLoopGetMain()
        var observer: CFRunLoopObserver?

        /// http://www.jianshu.com/p/6757e964b956
        ///  创建一个RunLoop的观察者
        /// allocator:该参数为对象内存分配器,一般使用默认的分配器kCFAllocatorDefault。或者nil
        /// activities:该参数配置观察者监听Run Loop的哪种运行状态。在示例中,我们让观察者监听Run Loop的所有运行状态。
        /// repeats:该参数标识观察者只监听一次还是每次Run Loop运行时都监听。
        /// order: 观察者优先级,当Run Loop中有多个观察者监听同一个运行状态时,那么就根据该优先级判断,0为最高优先级别。
        /// callout:观察者的回调函数,在Core Foundation框架中用CFRunLoopObserverCallBack重定义了回调函数的闭包。
        /// context:观察者的上下文。 (类似与KVO传递的context,可以传递信息,)因为这个函数创建ovserver的时候需要传递进一个函数指针,而这个函数指针可能用在n多个oberver 可以当做区分是哪个observer的状机态。(下面的通过block创建的observer一般是一对一的,一般也不需要Context,),还有一个例子类似与NSNOtificationCenter的 SEL和 Block方式。
        observer = CFRunLoopObserverCreate(
            kCFAllocatorDefault,
            CFRunLoopActivity.beforeWaiting.rawValue | CFRunLoopActivity.exit.rawValue,
            true, 0xFFFFFF,
            YYRunLoopObserverCallBack,
            nil
        )
        //将观察者添加到主线程runloop的common模式下的观察中
        CFRunLoopAddObserver(runloop, observer, .commonModes)
        observer = nil
    }
}

不难理解这段代码的主要作用:

  • 是观察RunLoop的状态为beforeWaitingexit时执行回调。
  • 其中selector其实就是CATransaction中的事物,target就是执行selector的对象
结论

那现在我们就可以基本明白了YYTransaction的作用就是,
把你需要执行的方法(事物),先存储起来,等到RunLoop的状态为beforeWaitingexit时统一执行。

通过这两篇源码的分析,异步绘制这个概念应该在大脑里已经有了一定的印象了,稍加练习,其实就可以熟练掌握。

尾巴

YYAsyncLayer的核心就是这些了,其实通篇看下来,你会发现基本没有什么费脑的地方。在佩服作者的同时,我们更多的是需要反思自己,虽然每个人的天赋不一样,但是我们的努力程度之低,往往没到拼天赋那一步。

在下一篇文章中我会逐一对这些进行回答,并且贴出它们的原理,与大家一同真正掌握iOS优化

  • 为什么需要60fps?
  • 为什么要减少混合?
  • 为什么要避免离屏渲染?
  • UIView和CALayer的关系?
  • 为什么在4之后Twitter的绘制方案不能提升性能了?
    ......

推荐文章

RunLoop:
深入理解RunLoop
iOS线下分享《RunLoop》
iOS RunLoop 编程手册 (译)
runloop原理

YYAsyncLayer使用:
http://www.itwendao.com/article/detail/62384.html

其他
iOS Core Animation: Advanced Techniques中文译本

YYAsyncLayer.swift

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

推荐阅读更多精彩内容