iOS 异步渲染

前言

异步绘制,就是可以在子线程把需要绘制的图形,提前在子线程处理好。将准备好的图像数据直接返给主线程使用,这样可以降低主线程的压力。

一 UIView绘制渲染原理和流程

UIView绘制流程.png

1. UIView调用setNeedsDisplay(setNeedsDisplay会调用自动调用drawRect方法);
2. 系统会立刻调用view的layer的同名方法[view.layer setNeedsDisplay],之后相当于在layer上面打上了一个脏标记;
3. 然后再当前runloop将要结束的时候,才会调用CALayer的display函数方法,然后才进入到当前视图的真正绘制工作的流程当中;
4. runloop即将结束, 开始视图的绘制流程;

1.系统默认绘制流程
系统绘制流程.png

1. CALayer内部创建一个backing store(CGContextRef)();
2. 判断layer是否有代理(1.有代理:调用delegete的drawLayer:inContext, 然后在合适的 实际回调代理, 在[UIView drawRect]中做一些绘制工作;2. 没有代理:调用layer的drawInContext方法。)
3. layer上传backingStore到GPU, 结束系统的绘制流程;

2.异步绘制流程
异步绘制流程.png

1. 某个时机调用setNeedsDisplay;
2. runloop将要结束的时候调用[CALayer display]
3. 如果代理实现了dispalyLayer将会调用此方法, 在子线程中去做异步绘制的工作;
4. 子线程中做的工作:创建上下文, 控件的绘制, 生成图片;
5. 转到主线程, 设置layer.contents, 将生成的视图展示在layer上面;

主要思想:

        //异步绘制:切换至子线程
        DispatchQueue.global().async {
            ///获取当前上下文
            UIGraphicsBeginImageContextWithOptions(size, false, scale)
            //1.获取上下文
            let context = UIGraphicsGetCurrentContext()
            //TODO
             ...............
            //生成图片
            let img = UIGraphicsGetImageFromCurrentImageContext()
            UIGraphicsEndImageContext()
            ///子线程完成工作, 切换到主线程展示
            DispatchQueue.main.async {
                self.layer.contents = img
            }
        }

二 异步绘制源码解析(参考YYKit)

以一个异步绘制的Label为主体,主要包括XWAsyncLayerDelegate,XWAsyncLayerDisplayTask,XWLabel,XWTransaction,XWAsyncLayer,XWsentinel;关系类图如下:


类图.png
异步绘制开始到结束流程:

1. 当XWLabel有新的更新提交时,通过XWTransaction将一个或者多个绘制的任务(layer.setNeedsDisplay)添加到transactionSet,并在Runloop注册了一个Observer
2. 当 RunLoop 进入休眠前、CA 处理完事件后,就会逐一执行transactionSet里的任务
3. 执行任务 layer.setNeedsDisplay会自动调用layer的display方法,判断是否需要异步绘制
4. 需要异步绘制,layer会向 delegate( UIView ),请求一个异步绘制的任务并将任务添加到异步队列中。在异步绘制时,Layer 会传递一个 BOOL(^isCancelled)() 这样的 block,绘制代码可以随时调用该 block 判断绘制任务是否已经被取消
5. 不需要异步绘制则直接同步绘制

2.1 XWTransaction之源码分析

XWTransaction存储了target和selector,通过仿照CoreAnimation的绘制机制,监听主线程RunLoop,在空闲阶段插入绘制任务,并将任务优先级设置在CoreAnimation绘制完成之后,然后遍历绘制任务集合进行绘制工作并且清空集合。

在runloop中注册observer:
private let onceToken = UUID().uuidString
private var transactionSet: Set<XWTransaction>?

private func XWTransactionSetup() {
    DispatchQueue.once(token: onceToken) {
        transactionSet = Set()
        /// 获取main RunLoop
        let runloop = CFRunLoopGetCurrent()
        var observer: CFRunLoopObserver?
        //RunLoop循环的回调
        let XWRunLoopObserverCallBack: CFRunLoopObserverCallBack = {_,_,_ in
            guard (transactionSet?.count) ?? 0 > 0 else { return }
            let currentSet = transactionSet
            //取完上一次需要调用的XWTransaction事务对象后后进行清空
            transactionSet = Set()
            //遍历set,执行里面的selector
            for transaction in currentSet! {
                _ = (transaction.target as AnyObject).perform(transaction.selector)
            }
        }
        observer = CFRunLoopObserverCreate(
            kCFAllocatorDefault,
            CFRunLoopActivity.beforeWaiting.rawValue | CFRunLoopActivity.exit.rawValue,
            true,
            0xFFFFFF,
            XWRunLoopObserverCallBack,
            nil
        )
        //将观察者添加到主线程runloop的common模式下的观察中
        CFRunLoopAddObserver(runloop, observer, .commonModes)
        observer = nil
    }
}

1. 通过GCD实现注册一次runLoop监听kCFRunLoopBeforeWaiting与kCFRunLoopExit(仅会注册一次)
2 . 通过transactionSet: Set<XWTransaction>添加事件任务集
2. 在runLoop处于beforeWaiting和exit时在回调里逐一执行transactionSet的任务

注意指定了观察者的优先级:0xFFFFFF,这个优先级比CATransaction优先级为2000000的优先级更低。这是为了确保系统的动画优先执行,之后再执行异步渲染。

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

2.2 XWSentine之源码分析

XWSentine对OSAtomicIncrement32()函数的封装, 改函数为一个线程安全的计数器,用于判断异步绘制任务是否被取消
OSAtomicIncrement32是线程安全的,多线程下保障了数据的同步操作和安全

class XWSentinel: NSObject {
    private var _value: Int32 = 0
    public var value: Int32 {
        return _value
    }
    @discardableResult
    public func increase() -> Int32 {
        // OSAtomic原子操作更趋于数据的底层,从更深层次来对单例进行保护。同时,它没有阻断其它线程对函数的访问。
        return OSAtomicIncrement32(&_value)
    }
}

因为在iOS10中,方法OSAtomicAdd32,OSAtomicDecrement32已经被废弃('OSAtomicIncrement32' is deprecated:first deprecated in iOS 10.0)
需要使用对应的方法替换,具体如下:
1.#import <stdatomic.h>
2.将对应的计数器,由int32_t类型设置为atomic_int类型
3.OSAtomicAdd32 替换-> atomic_fetch_add(&atomicCount,1);
OSAtomicDecrement32 替换-> atomic_fetch_sub(&atomicCount, 1);

注:在开发过程中有多线程需要共享和同时记录时可使用OSAtomicIncrement32,或者OSAtomicAdd32保障线程安全

2.3 XWAsyncLayerDelegate之源码分析

XWAsyncLayerDelegate 的 newAsyncDisplayTask 是提供了 XWAsyncLayer 需要在后台队列绘制的内容。异步绘制的UIView必须实现该协议且返回异步绘制task

/**
 XWAsyncLayer's的delegate协议,一般是uiview。必须实现这个方法
 */
protocol XWAsyncLayerDelegate {
    
    //当layer的contents需要更新的时候,返回一个新的展示任务
    var newAsyncDisplayTask:  XWAsyncLayerDisplayTask { get }
}
2.4 XWAsyncLayerDisplayTask之源码分析

display在mainthread或者background thread调用,这要求display应该是线程安全的,这里是通过XWSentinel保证线程安全。willdisplay和didDisplay在mainthread调用。

/**
 XWAsyncLayer在后台渲染contents的显示任务类
 */
open class XWAsyncLayerDisplayTask: NSObject {
    
    /**
     这个block会在异步渲染开始的前调用,只在主线程调用。
     */
    public var willDisplay: ((CALayer) -> Void)?
    
    /**
     这个block会调用去显示layer的内容
     */
    public var display: ((_ context: CGContext, _ size: CGSize, _ isCancelled: (() -> Bool)?) -> Void)?
    
    /**
     这个block会在异步渲染结束后调用,只在主线程调用。
     */
    public var didDisplay: ((_ layer: CALayer, _ finished: Bool) -> Void)?
}

2.4 XWAsyncLayer之源码分析

XWAsyncLayer为了异步绘制而继承CALayer的子类。通过使用CoreGraphic相关方法,在子线程中绘制内容Context,绘制完成后,回到主线程对layer.contents进行直接显示。 通过开辟线程进行异步绘制,但是不能无限开辟线程

我们都知道,把阻塞主线程执行的代码放入另外的线程里保证APP可以及时的响应用户的操作。但是线程的切换也是需要额外的开销的。也就是说,线程不能无限度的开辟下去。
那么,dispatch_queue_t的实例也不能一直增加下去。有人会说可以用dispatch_get_global_queue()来获取系统的队列。没错,但是这个情况只适用于少量的任务分配。因为,系统本身也会往这个queue里添加任务的。
所以,我们需要用自己的queue,但是是有限个的。参考YY这个数量指定的值是16。

异步绘制主要代码如下:
 func displayAsync(async: Bool) {
        //获取delegate对象,这边默认是CALayer的delegate,持有它的UIView
        guard let delegate = self.delegate as? XWAsyncLayerDelegate else { return }
        //delegate的初始化方法
        let task = delegate.newAsyncDisplayTask
        if async {
            task.willDisplay?(self)
            let sentinel = _sentinel
            let value = sentinel!.value
            //判断是否要取消的block,在displayblock调用绘制前,可以通过判断isCancelled布尔值的值来停止绘制,减少性能上的消耗,以及避免出现线程阻塞的情况,比如TableView快速滑动的时候,就可以通过这样的判断,来避免不必要的绘制,提升滑动的流畅性.
            let isCancelled = {
                return value != sentinel!.value
            }
            // 异步绘制
            XWAsyncLayerGetDisplayQueue.async {
                guard !isCancelled() else { return }
               //获取上下文和size
                ..............
               //异步绘制
                task.display?(context, size, isCancelled)
                
                //若取消 则释放资源,取消绘制
                if isCancelled() {
                    //调用UIGraphicsEndImageContext函数关闭图形上下文
                    UIGraphicsEndImageContext()
                    DispatchQueue.main.async {
                        task.didDisplay?(self, false)
                    }
                    return
                }
                //主线程异步将绘制结果的图片赋值给contents
                DispatchQueue.main.async {
                    if isCancelled() {
                        task.didDisplay?(self, false)
                    }else{
                        self.contents = image?.cgImage
                        task.didDisplay?(self, true)
                    }
                }

            }

        }else{ 
             同步绘制
            _sentinel.increase()
            task.willDisplay?(self)
            task.display?(context, bounds.size, {return false })
            let image = UIGraphicsGetImageFromCurrentImageContext()
            UIGraphicsEndImageContext()
            contents = image?.cgImage
            task.didDisplay?(self, true)
        }
    }
display任务解析:

1. isCancelled捕获_sentinel计数器
2. 将异步绘制内容添加到异步队列中
3. 绘制任务开始时先通过isCancelled判断是否取消绘制,为false时通过获取de legate的task开启绘制
4. 绘制完成后关闭上下文,切回到主线程,将绘制的image赋值给layer的contens

注:isCancelled的block对_sentinel的value的捕获以和当前值比较以达到判断是否需要取消绘制

2.4 XWLabel之源码分析

XWLabel通过实现XWAsyncLayerDelegate协议返回异步绘制的XWAsyncLayerDisplayTask任务,重写layerClass返回自定义的XWAsyncLayer以实现异步绘制

class XWLabel: UIView, XWAsyncLayerDelegate {
   
   var attributedText: NSAttributedString? {
       didSet {
           if self.attributedText?.length ?? 0 > 0 {
               self.commitUpdate()
           }
       }
   }
   
   var displaysAsynchronously: Bool = false {
       didSet{
           if let asyncLayer = self.layer as? XWAsyncLayer {
               asyncLayer.displaysAsynchronously = self.displaysAsynchronously
           }
       }
   }
   
   ///MARK:XWAsyncLayerDelegate 返回绘制任务
   var newAsyncDisplayTask: XWAsyncLayerDisplayTask {
       
       let task = XWAsyncLayerDisplayTask()
       task.willDisplay = { layer in
           
       }
       task.display = { (context, size, isCancel) in
           
       }
       task.didDisplay = { (layer, finished) in
           
       }
       return task
   }
   
   ///MARK: 重写layerClass,返回异步的XWAsyncLayer
   override class var layerClass: AnyClass {
       return XWAsyncLayer.self
   }
   
   ///MARK: 提交更新,添加到runLoop队列中
   func commitUpdate() {
         //XWTransaction.transaction(with: self, selector: #selector(layoutNeedRedraw))?.commit()
       self.layoutNeedRedraw()
   }
   
   @objc func layoutNeedRedraw() {
       self.layer.setNeedsDisplay()
   }

}

总结

最后,我们把整个异步渲染的过程来串联起来。
1. UIView触发layoutSubviews,或者主动调用layer的setNeedsDisplay
2. layer调用display方法
3. 判断是否需要异步,需要异步将绘制任务添加到队列中
4. 绘制完成切回主线程,设置layer的contents

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