从VVeboTableViewDemo到YYAsyncLayer

作者: xiAo__Ju | 来源:发表于2017-04-17 16:40 被阅读583次

    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

    相关文章

      网友评论

      • 09c6496446c9:你好,你在第一篇的末尾中,有这么一段话“技术淘汰原因 : 由于retina屏幕的出现,原来单位面积的像素增加,而CPU做的事情也变得多了起来,导致效率反而不及subViews方法。AsyncDisplayKit YYKit等新技术出现” YYKit的核心绘制就是YYAsyncLayer,你在这篇也说了,YYAsyncLayer与VVeboTableViewDemo的核心内容是一样的,都是异步生成图片。那么不就是说,YYAsyncLayer的异步绘制效率也没有subViews方法的高了?那么说YYKit是新技术,新在哪里啊?
      • AppleIdGX:下一篇呢?

      本文标题:从VVeboTableViewDemo到YYAsyncLayer

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