首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

iOS 异步渲染

iOS 异步渲染

前言

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

一 UIView绘制渲染原理和流程

1. UIView调用setNeedsDisplay(setNeedsDisplay会调用自动调用drawRect方法);

2. 系统会立刻调用view的layer的同名方法[view.layer setNeedsDisplay],之后相当于在layer上面打上了一个脏标记;

3. 然后再当前runloop将要结束的时候,才会调用CALayer的display函数方法,然后才进入到当前视图的真正绘制工作的流程当中;

4. runloop即将结束, 开始视图的绘制流程;

1.系统默认绘制流程

1. CALayer内部创建一个backing store(CGContextRef)();

2. 判断layer是否有代理(1.有代理:调用delegete的drawLayer:inContext, 然后在合适的 实际回调代理, 在[UIView drawRect]中做一些绘制工作;2. 没有代理:调用layer的drawInContext方法。)

3. layer上传backingStore到GPU, 结束系统的绘制流程;

2.异步绘制流程

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;关系类图如下:

异步绘制开始到结束流程:

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添加事件任务集

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

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

  • 发表于:
  • 原文链接https://kuaibao.qq.com/s/20201130A01Z7O00?refer=cp_1026
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券