外贸网站 模板,建设vip网站相关视频,微信公众号的模板网站,.net做的网站代码快捷目录 壹、 iOS界面刷新机制贰、浅谈UIView的刷新与绘制概述一.UIView 与 CALayer1. UIView 与 CALayer的关系2. CALayer的一些常用属性contents属性contentGravity属性contentsScale属性maskToBounds属性contentsRect属性 二.View的布局与显示1.图像显示原理2.布局layoutSu… 快捷目录 壹、 iOS界面刷新机制贰、浅谈UIView的刷新与绘制概述一.UIView 与 CALayer1. UIView 与 CALayer的关系2. CALayer的一些常用属性contents属性contentGravity属性contentsScale属性maskToBounds属性contentsRect属性 二.View的布局与显示1.图像显示原理2.布局layoutSubviews()方法setNeedsLayout()方法layoutIfNeeded()方法 3.显示drawRect:方法setNeedsDisplay()方法 三.UIView的系统绘制与异步绘制流程UIView的绘制流程系统绘制异步绘制什么是异步绘制?异步绘制流程 四.总结 叁、 iOS列表性能优化之异步绘制一、需求背景1、现状2、需求 二、解决方案及亮点1、方案概述2、问题点3、分析过程1异步绘制时机及减少重复绘制2队列的并发和择优 三、详细设计1、设计图2、代码原理剖析写在注释1设置runloop监听及回调2创建、获取文本异步绘制队列并择优选取3异步绘制4异步下载缓存图片 四、使用示例1文本异步绘制2图片异步下载渲染 五、成效举证六、核心代码范围 壹、 iOS界面刷新机制
当在操作 UI 时比如改变了 Frame、更新了 UIView/CALayer 的层次时或者手动调用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后这个 UIView/CALayer 就被标记为待处理并被提交到一个全局的容器去。
苹果注册了一个 Observer 监听 BeforeWaiting(即将进入休眠) 和 Exit (即将退出Loop) 事件回调去执行一个很长的函数 _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()。这个函数里会遍历所有待处理的 UIView/CAlayer 以执行实际的绘制和调整并更新 UI 界面。
这个函数内部的调用栈大概是这样的
_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()QuartzCore:CA::Transaction::observer_callback:CA::Transaction::commit();CA::Context::commit_transaction();CA::Layer::layout_and_display_if_needed();CA::Layer::layout_if_needed();[CALayer layoutSublayers];[UIView layoutSubviews];CA::Layer::display_if_needed();[CALayer display];[UIView drawRect]; //只有初始化frame的时候才会触发更新界面并不会再次触发。如果想触发可手动调setNeedsDisplay方法。关于setNeedsLayout、setNeedsDisplay以及layoutIfNeeded方法的说明 setNeedsLayout会触发上面的界面刷新流程runloop休眠或退出后会触发layoutSubviews方法
setNeedsDisplay会触发上面的界面刷新流程runloop休眠或退出后会触发drawRect方法
layoutIfNeeded如果有需要刷新的标记frame变化或者约束变化会触发上面的界面刷新流程runloop休眠或退出后会触发layoutSubviews方法如果没有标记不会调用layoutSubviews。该方法一般用于Autolayout布局时及时获取各视图的frame。
贰、浅谈UIView的刷新与绘制
概述
UIView是我们在做iOS开发时每天都会接触到的类几乎所有跟页面显示相关的控件也都继承自它。但是关于UIView的布局、显示、以及绘制原理等方面笔者一直一知半解只有真正了解了它的原理才能更好的服务我们的开发。并且在市场对iOS开发者要求越来越高的大环境下对App页面流畅度的优化也是对高级及以上开发者必问的面试题这就需要我们要对UIView有更深的认知。
一.UIView 与 CALayer
UIView一个视图UIView就是在屏幕上显示的一个矩形块比如图片文字或者视频它能够拦截类似于鼠标点击或者触摸手势等用户输入。视图在层级关系中可以互相嵌套一个视图可以管理它的所有子视图的位置,在iOS当中所有的视图都从一个叫做UIView的基类派生而来UIView可以处理触摸事件可以支持基于Core Graphics绘图可以做仿射变换例如旋转或者缩放或者简单的类似于滑动或者渐变的动画。
CALayer:CALayer类在概念上和UIView类似同样也是一些被层级关系树管理的矩形块同样也可以包含一些内容像图片文本或者背景色管理子图层的位置。它们有一些方法和属性用来做动画和变换。和UIView最大的不同是CALayer不处理用户的交互。
CALayer并不清楚具体的响应链iOS通过视图层级关系用来传送触摸事件的机制于是它并不能够响应事件即使它提供了一些方法来判断一个触点是否在图层的范围之内。
1. UIView 与 CALayer的关系
每一个UIView都有一个CALayer实例的图层属性也就是所谓的backing layer视图的职责就是创建并管理这个图层以确保当子视图在层级关系中添加或者被移除的时候他们关联的图层也同样对应在层级关系树当中有相同的操作. 两者的关系实际上这些背后关联的图层(Layer)才是真正用来在屏幕上显示和做动画UIView仅仅是对它的一个封装提供了一些iOS类似于处理触摸的具体功能以及Core Animation底层方法的高级接口。 这里引申出面试常问的一个问题为什么iOS要基于UIView和CALayer提供两个平行的层级关系呢为什么不用一个简单的层级来处理所有事情呢
原因在于要做职责分离单一职责原则这样也能避免很多重复代码。在iOS和Mac OS两个平台上事件和用户交互有很多地方的不同基于多点触控的用户界面和基于鼠标键盘有着本质的区别这就是为什么iOS有UIKit和UIView但是Mac OS有AppKit和NSView的原因。他们功能上很相似但是在实现上有着显著的区别。把这种功能的逻辑分开并封装成独立的Core Animation框架苹果就能够在iOS和Mac OS之间共享代码使得对苹果自己的OS开发团队和第三方开发者去开发两个平台的应用更加便捷。
2. CALayer的一些常用属性
contents属性
CALayer的contents属性可以让我们为layer图层设置一张图片我们看下它的定义
/* An object providing the contents of the layer, typically a CGImageRef,* but may be something else. (For example, NSImage objects are* supported on Mac OS X 10.6 and later.) Default value is nil.* Animatable. */property(nullable, strong) id contents;这个属性的类型被定义为id意味着它可以是任何类型的对象。在这种情况下你可以给contents属性赋任何值你的app都能够编译通过。但是,如果你给contents赋的不是CGImage那么你得到的图层将是空白的。事实上你真正要赋值的类型应该是CGImageRef它是一个指向CGImage结构的指针UIImage有一个CGImage属性它返回一个CGImageRef但是要使用它还需要进行强转:
layer.contents (__bridge id _Nullable)(image.CGImage);contentGravity属性
/* A string defining how the contents of the layer is mapped into its* bounds rect. Options are center, top, bottom, left,* right, topLeft, topRight, bottomLeft, bottomRight,* resize, resizeAspect, resizeAspectFill. The default value is* resize. Note that bottom always means Minimum Y and top* always means Maximum Y. */property(copy) CALayerContentsGravity contentsGravity;如果我们为图层layer设置contents为一张图片那么可以使用这个属性来让图片自适应layer的大小它类似于UIView的contentMode属性但是它是一个NSString类型而不是像对应的UIKit部分那里面的值是枚举。contentsGravity可选的常量值有以下一些
kCAGravityCenter
kCAGravityTop
kCAGravityBottom
kCAGravityLeft
kCAGravityRight
kCAGravityTopLeft
kCAGravityTopRight
kCAGravityBottomLeft
kCAGravityBottomRight
kCAGravityResize
kCAGravityResizeAspect
kCAGravityResizeAspectFill例如如果要让图片等比例拉伸去自适应layer的大小可以直接这样设置
layer.contentsGravity kCAGravityResizeAspect;contentsScale属性
/* Defines the scale factor applied to the contents of the layer. If* the physical size of the contents is (w, h) then the logical size* (i.e. for contentsGravity calculations) is defined as (w /* contentsScale, h / contentsScale). Applies to both images provided* explicitly and content provided via -drawInContext: (i.e. if* contentsScale is two -drawInContext: will draw into a buffer twice* as large as the layer bounds). Defaults to one. Animatable. */property CGFloat contentsScalecontentsScale属性定义了contents设置图片的像素尺寸和视图大小的比例默认情况下它是一个值为1.0的浮点数。这个属性其实属于支持Retina屏幕机制的一部分它的值等于当前设备的物理尺寸与逻辑尺寸的比值。如果contentsScale设置为1.0将会以每个点1个像素绘制图片如果设置为2.0则会以每个点2个像素绘制图片。当用代码的方式来处理contents设置图片的时候一定要手动的设置图层的contentsScale属性否则图片在Retina设备上就显示得不正确啦。代码如下
layer.contentsScale [UIScreen mainScreen].scale;maskToBounds属性
maskToBounds属性的功能类似于UIView的clipsToBounds属性如果设置为YES则会将超出layer范围的图片进行裁剪.
contentsRect属性
contentsRect属性在我们的日常开发中用的不多它的主要作用是可以让我们显示contents所设置图片的一个子区域。它是单位坐标取值在0到1之间。默认值是{0, 0, 1, 1}这意味着整个图片默认都是可见的如果我们指定一个小一点的矩形比如{0,0,0.5,0.5},那么layer显示的只有图片的左上角也就是1/4的区域。 实际上给layer的contents赋CGImage的值不是唯一的设置其寄宿图的方法。我们也可以直接用Core Graphics直接绘制。通过继承UIView并实现-drawRect:方法来自定义绘制如果单独使用CALayer那么可以实现其代理(CALayerDelegate)方法- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx;在这里面进行自主绘制。实际的方法绘制流程我们在下面进行探讨。 二.View的布局与显示
1.图像显示原理
在开始介绍图像的布局与显示之前我们有必要先了解下图像的显示原理也就是我们创建一个显示控件是怎么通过CPU与GPU的运算显示在屏幕上的。这个过程大体分为六个阶段 绘制
布局 首先一个视图由CPU进行Frame布局准备视图(view)和图层(layer)的层级关系,以及设置图层属性位置背景色边框等等。显示view的显示图层(layer)它的寄宿图片被绘制的阶段。所谓的寄宿图就是上面我们提到过的layer所显示的内容。它有两种设置形式一种是直接设置layer.contents赋值一个CGImageRef;第二种是重写UIView的drawRect:或CALayerDelegate的drawLayer:inContext:方法实现自定义绘制。注意如果实现了这两个方法会额外的消耗CPU的性能。准备这是Core Animation准备发送数据到渲染服务的阶段。这个阶段主要对视图所用的图片进行解码以及图片的格式转换。PNG或者JPEG压缩之后的图片文件会比同质量的位图小得多。但是在图片绘制到屏幕上之前必须把它扩展成完整的未解压的尺寸通常等同于图片宽 x 长 x 4个字节。为了节省内存iOS通常直到真正绘制的时候才去解码图片。提交CPU会将处理视图和图层的层级关系打包通过IPC内部处理通信通道提交给渲染服务渲染服务由OpenGL ES和GPU组成。生成帧缓存渲染服务首先将图层数据交给OpenGL ES进行纹理生成和着色生成前后帧缓存。再根据显示硬件的刷新频率一般以设备的VSync信号和CADisplayLink为标准进行前后帧缓存的切换。渲染 将最终要显示在画面上的后帧缓存交给GPU进行采集图片和形状运行变换应用纹理和混合最终显示在屏幕上。 注意当图层被成功打包发送到渲染服务器之后CPU仍然要做如下工作为了显示屏幕上的图层Core Animation必须对渲染树种的每个可见图层通过OpenGL循环转换成纹理三角板。由于GPU并不知晓Core Animation图层的任何结构所以必须要由CPU做这些事情。 前四个阶段都在软件层面处理通过CPU第五阶段也有CPU参与只有最后一个完全由GPU执行。而且你真正能控制只有前两个阶段布局和显示Core Animation框架在内部处理剩下的事务你也控制不了它。所以接下来我们来重点分析布局与显示阶段。
2.布局
布局布局就是一个视图在屏幕上的位置与大小。UIView有三个比较重要的布局属性framebounds和center.UIView提供了用来通知系统某个view布局发生变化的方法也提供了在view布局重新计算后调用的可重写的方法。
layoutSubviews()方法
layoutSubviews():当一个视图“认为”应该重新布局自己的子控件时它便会自动调用自己的layoutSubviews方法在该方法中“刷新”子控件的布局.这个方法并没有系统实现需要我们重新这个方法在里面实现子控件的重新布局。这个方法很开销很大因为它会在每个子视图上起作用并且调用它们相应的layoutSubviews方法.系统会根据当前run loop的不同状态来触发layoutSubviews调用的机制,并不需要我们手动调用。以下是他的触发时机
直接修改 view 的大小时会触发调用addSubview会触发子视图的layoutSubviews用户在 UIScrollView 上滚动layoutSubviews 会在UIScrollView和它的父view上被调用用户旋转设备更新视图的 constraints 这些方式都会告知系统view的位置需要被重新计算继而会调用layoutSubviews.当然也可以直接触发layoutSubviews的方法。
setNeedsLayout()方法
setNeedsLayout()方法的调用可以触发layoutSubviews,调用这个方法代表向系统表示视图的布局需要重新计算。不过调用这个方法只是为当前的视图打了一个脏标记告知系统需要在下一次run loop中重新布局这个视图。也就是调用setNeedsLayout()后会有一段时间间隔然后触发layoutSubviews.当然这个间隔不会对用户造成影响因为永远不会长到对界面造成卡顿。
layoutIfNeeded()方法
layoutIfNeeded()方法的作用是告知系统当前打了脏标记的视图需要立即更新不要等到下一次run loop到来时在更新,此时该方法会立即触发layoutSubviews方法。当然但如果你调用了layoutIfNeeded之后并且没有任何操作向系统表明需要刷新视图那么就不会调用layoutsubview.这个方法在你需要依赖新布局无法等到下一次 run loop的时候会比setNeedsLayout有用。
3.显示
和布局的方法类似显示也有触发更新的方法它们由系统在检测到更新时被自动调用或者我们可以手动调用直接刷新。
drawRect:方法
在上面我们提到过如果要设置视图的寄宿图除了直接设置view.layer.contents属性还可以自主进行绘制。绘制的方法就是实现view的drawRect:方法。这个方法类似于布局的layoutSubviews方法它会对当前View的显示进行刷新不同的是它不会触发后续对视图的子视图方法的调用。跟layoutSubviews一样我们不能直接手动调用drawRect:方法应该调用间接的触发方法让系统在 run loop 中的不同结点自动调用。具体的绘制流程我们在本文第三节进行介绍。
setNeedsDisplay()方法
这个方法类似于布局中的setNeedsLayout。它会给有内容更新的视图设置一个内部的标记但在视图重绘之前就会返回。然后在下一个run loop中系统会遍历所有已标记的视图并调用它们的drawRect:方法。大部分时候在视图中更新任何 UI 组件都会把相应的视图标记为“dirty”通过设置视图“内部更新标记”在下一次run loop中就会重绘而不需要显式的调用setNeedsDisplay.
三.UIView的系统绘制与异步绘制流程
UIView的绘制流程
接下来我们看下UIView的绘制流程 绘制
UIView调用setNeedsDisplay,这个方法我们已经介绍过了它并不会立即开始绘制。UIView 调用setNeedsDisplay实际会调用其layer属性的同名方法此时相当于给layer打上绘制标记。在当前run loop 将要结束的时候才会调用CALayer的display方法进入到真正的绘制当中在CALayer的display方法中,会判断layer的代理方法displayLayer:是否被实现如果代理没有实现这个方法则进入系统绘制流程否则进入异步绘制入口。
系统绘制 xitong 在系统绘制开始时在CALayer内部会创建一个绘制上下文这个上下文可以理解为CGContextRef,我们在drawRect:方法中获取到的currentRef就是它。 然后layer会判断是否有delegate没有delegate就调用CALayer的drawInContext方法如果有代理并且你实现了CALayerDelegate协议中的-drawLayer:inContext:方法或者UIView中的-drawRect:方法其实就是前者的包装方法那么系统就会调用你实现的这两个方法中的一个。 关于这里的代理我的理解是如果你直接使用的UIView那么layer的代理就是当前view你直接实现-drawRect:然后在这个方法里面进行自主绘制; 如果你用的是单独创建的CALayer那么你需要设置layer.delegate self; 当然这里的self就是持有layer的视图或是控制器了这时你需要实现-drawLayer:inContext:方法然后在这个方法里面进行绘制。 最后CALayer把位图传给GPU去渲染也就是将生成的 bitmap 位图赋值给 layer.content 属性。 注意使用CPU进行绘图的代价昂贵除非绝对必要否则你应该避免重绘你的视图。提高绘制性能的秘诀就在于尽量避免去绘制。 异步绘制
什么是异步绘制?
通过上面的介绍我们熟悉了系统绘制流程系统绘制就是在主线程中进行上下文的创建控件的自主绘制等这就导致了主线程频繁的处理UI绘制的工作如果要绘制的元素过多过于频繁就会造成卡顿。而异步绘制就是把复杂的绘制过程放到后台线程中执行从而减轻主线程负担来提升UI流畅度。
异步绘制流程 pic
上面很明显的展示了异步绘制过程
从上图看异步绘制的入口在layer的代理方法displayLayer:如果要进行异步绘制我们必须在自定义view中实现这个方法在displayLayer:方法中我们开辟子线程在子线程中我们创建绘制上下文并借助Core Graphics 相关API完成自主绘制完成绘制后生成Image图片最后回到主线程把Image图片赋值给layer的contents属性。
当然我们在日常开发中还要考虑线程的管理与绘制时机等问题使用第三方库YYAsyncLayer可以让我们把注意力放在具体的绘制上,具体的使用流程可以点这里去查看.
四.总结
我们知道当我们实现了CALayerDelegate协议中的-drawLayer:inContext:方法或者UIView中的-drawRect:方法图层就创建了一个绘制上下文这个上下文需要的大小的内存可从这个算式得出图层宽X图层高X4字节宽高的单位均为像素。对于一个在Retina iPad上的全屏图层来说这个内存量就是 2048X15264字节相当于12MB内存图层每次重绘的时候都需要重新抹掉内存然后重新分配。可见使用Core Graphics利用CPU进行绘制代价是很高的那么如何进行高效的绘图呢iOS-Core-Animation-Advanced-Techniques给出了答案我们在日常开发中完全可以使用Core Animation的CAShapeLayer代替Core Graphics进行图形的绘制具体的方法这里就不介绍了感兴趣的可以自行去查看。
参考引用 iOS-Core-Animation-Advanced-Techniques YYAsyncLayer https://juejin.cn/post/6844903567610871816
叁、 iOS列表性能优化之异步绘制
https://juejin.cn/post/6901957495548608525#heading-20
一、需求背景
1、现状
iOS所提供的UIKit框架其工作基本是在主线程上进行界面绘制、用户输入响应交互等等。当大量且频繁的绘制任务以及各种业务逻辑同时放在主线程上完成时便有可能造成界面卡顿丢帧现象即在16.7ms内未能完成1帧的绘制帧率低于60fps黄金标准。目前常用的UITableView或UICollectionView在大量复杂文本及图片内容填充后如果没有优化处理快速滑动的情况下易出现卡顿流畅性差问题。
2、需求
不依赖任何第三方pod框架主要从异步线程绘制、图片异步下载渲染等方面尽可能优化UITableView的使用提高滑动流畅性让帧率稳定在60fps。
(网上有很多优秀的性能优化博客和开源代码本方案也是基于前人的经验结合自身的理解和梳理写成demo关键代码有做注释很多细节值得推敲和持续优化不足之处望指正。)
二、解决方案及亮点
1、方案概述
• 异步绘制任务收集与去重;
• 通过单例监听main runloop回调执行异步绘制任务
• 支持异步绘制动态文本内容减轻主线程压力并缓存高度减少CPU计算
• 支持异步下载和渲染图片并缓存仅在可视区域渲染
• 异步队列并发管理择优选取执行任务
• 发现UITableView首次reload会触发3次的系统问题初始开销增大待优化
2、问题点
• 异步绘制时机及减少重复绘制
• 队列的并发和择优
3、分析过程
1异步绘制时机及减少重复绘制 这里简单描述下绘制原理当UI被添加到界面后我们改变Frame或更新 UIView/CALayer层次或调用setNeedsLayout/setNeedsDisplay方法均会添加重新绘制任务。这个时候系统会注册一个Observer监听BeforeWaiting(即将进入休眠)和Exit(即将退出Loop)事件并回调执行当前绘制任务setNeedsDisplay-display-displayLayer最终更新界面。
由上可知我们可以模拟系统绘制任务的收集在runloop回调中去执行并重写layer的dispaly方法开辟子线程进行异步绘制再返回主线程刷新。
当同个UI多次触发绘制请求时怎样减少重复绘制以便减轻并发压力比较重要。本案通过维护一个全局线程安全的原子性状态在绘制过程中的关键步骤处理前均校验是否要放弃当前多余的绘制任务。
2队列的并发和择优
一次runloop回调经常会执行多个绘制任务这里考虑开辟多个线程去异步执行。首选并行队列可以满足但为了满足性能效率的同时确保不过多的占用资源和避免线程间竞争等待更好的方案应该是开辟多个串行队列单线程处理并发任务。
接下来的问题是异步绘制创建几个串行队列合适
我们知道一个n核设备并发执行n个任务最多创建n个线程时线程之间将不会互相竞争资源。因此不建议数量设置超过当前激活的处理器数并可根据项目界面复杂度以及设备性能适配适当限制并发开销文本异步绘制最大队列数设置如下
#define kMAX_QUEUE_COUNT 6- (NSUInteger)limitQueueCount {if (_limitQueueCount 0) {// 获取当前系统处于激活状态的处理器数量NSUInteger processorCount [NSProcessInfo processInfo].activeProcessorCount;// 根据处理器的数量和设置的最大队列数来设定当前队列数组的大小_limitQueueCount processorCount 0 ? (processorCount kMAX_QUEUE_COUNT ? kMAX_QUEUE_COUNT : processorCount) : 1;}return _limitQueueCount;
}文本的异步绘制串行队列用GCD实现图片异步下载通过NSOperationQueue实现两者最大并发数参考SDWebImage图片下载并发数的限制数6。
如何择优选取执行任务文本异步队列的选取可以自定义队列的任务数标记在队列执行任务前计算1当任务执行结束计算-1。这里忽略每次绘制难易度的略微差异我们便可以判定任务数最少接近于最优队列。图片异步下载任务交由NSOperationQueue处理并发我们要处理的是让同个图片在多次并发下载请求下仅生成1个NSOperation添加到queue即去重只下载一次并缓存且在下载完成后返回主线程同步渲染多个触发该下载请求的控件本案demo仅用一张图片所以这种情况必须考虑到。
三、详细设计
1、设计图 2、代码原理剖析写在注释
1设置runloop监听及回调
/**runloop回调并发执行异步绘制任务*/
static NSMutableSetADTask * *_taskSet nil;
static void ADRunLoopCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {if (_taskSet.count 0) return;NSSet *currentSet _taskSet;_taskSet [NSMutableSet set];[currentSet enumerateObjectsUsingBlock:^(ADTask *task, BOOL *stop) {[task excute];}];
}/** task调用函数
- (void)excute {((void (*)(id, SEL))[self.target methodForSelector:self.selector])(self.target, self.selector);
}
*/- (void)setupRunLoopObserver {// 创建任务集合_taskSet [NSMutableSet set];// 获取主线程的runloopCFRunLoopRef runloop CFRunLoopGetMain();// 创建观察者监听即将休眠和退出CFRunLoopObserverRef observer CFRunLoopObserverCreate(CFAllocatorGetDefault(),kCFRunLoopBeforeWaiting | kCFRunLoopExit,true, // 重复0xFFFFFF, // 设置优先级低于CATransaction(2000000)ADRunLoopCallBack, NULL);CFRunLoopAddObserver(runloop, observer, kCFRunLoopCommonModes);CFRelease(observer);2创建、获取文本异步绘制队列并择优选取
- (ADQueue *)ad_getExecuteTaskQueue {// 1、创建对应数量串行队列处理并发任务并行队列线程数无法控制if (self.queueArr.count self.limitQueueCount) {ADQueue *q [[ADQueue alloc] init];q.index self.queueArr.count;[self.queueArr addObject:q]; q.asyncCount 1;NSLog(queue[%ld]-asyncCount:%ld, (long)q.index, (long)q.asyncCount);return q;}// 2、当队列数已达上限择优获取异步任务数最少的队列NSUInteger minAsync [[self.queueArr valueForKeyPath:min.asyncCount] integerValue];__block ADQueue *q nil;[self.queueArr enumerateObjectsUsingBlock:^(ADQueue * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {if (obj.asyncCount minAsync) {*stop YES;q obj;}}];q.asyncCount 1;NSLog(queue[%ld]-excute-count:%ld, (long)q.index, (long)q.asyncCount);return q;
}- (void)ad_finishTask:(ADQueue *)q {q.asyncCount - 1;if (q.asyncCount 0) {q.asyncCount 0;}NSLog(queue[%ld]-done-count:%ld, (long)q.index, (long)q.asyncCount);
}3异步绘制
/**维护线程安全的绘制状态*/
property (atomic, assign) ADLayerStatus status;- (void)setNeedsDisplay {// 收到新的绘制请求时同步正在绘制的线程本次取消self.status ADLayerStatusCancel;[super setNeedsDisplay];
}- (void)display {// 标记正在绘制self.status ADLayerStatusDrawing;if ([self.delegate respondsToSelector:selector(asyncDrawLayer:inContext:canceled:)]) {[self asyncDraw];} else {[super display];}
}- (void)asyncDraw {__block ADQueue *q [[ADManager shareInstance] ad_getExecuteTaskQueue];__block idADLayerDelegate delegate (idADLayerDelegate)self.delegate;dispatch_async(q.queue, ^{// 重绘取消if ([self canceled]) {[[ADManager shareInstance] ad_finishTask:q];return;}// 生成上下文contextCGSize size self.bounds.size;BOOL opaque self.opaque;CGFloat scale [UIScreen mainScreen].scale;CGColorRef backgroundColor (opaque self.backgroundColor) ? CGColorRetain(self.backgroundColor) : NULL;UIGraphicsBeginImageContextWithOptions(size, opaque, scale);CGContextRef context UIGraphicsGetCurrentContext();if (opaque context) {CGContextSaveGState(context); {if (!backgroundColor || CGColorGetAlpha(backgroundColor) 1) {CGContextSetFillColorWithColor(context, [UIColor whiteColor].CGColor);CGContextAddRect(context, CGRectMake(0, 0, size.width * scale, size.height * scale));CGContextFillPath(context);}if (backgroundColor) {CGContextSetFillColorWithColor(context, backgroundColor);CGContextAddRect(context, CGRectMake(0, 0, size.width * scale, size.height * scale));CGContextFillPath(context);}} CGContextRestoreGState(context);CGColorRelease(backgroundColor);} else { CGColorRelease(backgroundColor);} // 使用context绘制[delegate asyncDrawLayer:self inContext:context canceled:[self canceled]];// 重绘取消if ([self canceled]) {[[ADManager shareInstance] ad_finishTask:q];UIGraphicsEndImageContext();return;}// 获取imageUIImage *image UIGraphicsGetImageFromCurrentImageContext();UIGraphicsEndImageContext();// 结束任务[[ADManager shareInstance] ad_finishTask:q];// 重绘取消if ([self canceled]) {return;}// 主线程刷新dispatch_async(dispatch_get_main_queue(), ^{self.contents (__bridge id)(image.CGImage);});});
}4异步下载缓存图片
#pragma mark - 处理图片
- (void)ad_setImageWithURL:(NSURL *)url target:(id)target completed:(void (^)(UIImage * _Nullable image, NSError * _Nullable error))completedBlock {if (!url) {if (completedBlock) {NSDictionary *userInfo {NSLocalizedFailureReasonErrorKey: NSLocalizedStringFromTable(Expected URL to be a image URL, AsyncDraw, nil)};NSError *error [[NSError alloc] initWithDomain:kERROR_DOMAIN code:NSURLErrorBadURL userInfo:userInfo];completedBlock(nil, error);}return;}// 1、缓存中读取NSString *imageKey url.absoluteString;NSData *imageData self.imageDataDict[imageKey];if (imageData) {UIImage *image [UIImage imageWithData:imageData];if (completedBlock) {completedBlock(image, nil);}} else {// 2、沙盒中读取NSString *imagePath [NSString stringWithFormat:%/Library/Caches/%, NSHomeDirectory(), url.lastPathComponent];imageData [NSData dataWithContentsOfFile:imagePath];if (imageData) {UIImage *image [UIImage imageWithData:imageData];if (completedBlock) {completedBlock(image, nil);}} else {// 3、下载并缓存写入沙盒ADOperation *operation [self ad_downloadImageWithURL:url toPath:imagePath completed:completedBlock];// 4、添加图片渲染对象[operation addTarget:target];}}
}- (ADOperation *)ad_downloadImageWithURL:(NSURL *)url toPath:(NSString *)imagePath completed:(void (^)(UIImage * _Nullable image, NSError * _Nullable error))completedBlock {NSString *imageKey url.absoluteString;ADOperation *operation self.operationDict[imageKey];if (!operation) {operation [ADOperation blockOperationWithBlock:^{NSLog(AsyncDraw image loading~);NSData *newImageData [NSData dataWithContentsOfURL:url];// 下载失败处理if (!newImageData) {[self.operationDict removeObjectForKey:imageKey];NSDictionary *userInfo {NSLocalizedFailureReasonErrorKey: NSLocalizedStringFromTable(Failed to load the image, AsyncDraw, nil)};NSError *error [[NSError alloc] initWithDomain:kERROR_DOMAIN code:NSURLErrorUnknown userInfo:userInfo];if (completedBlock) {completedBlock(nil, error);}return;}// 缓存图片数据[self.imageDataDict setValue:newImageData forKey:imageKey];}];// 设置完成回调__block ADOperation *blockOperation operation;[operation setCompletionBlock:^{NSLog(AsyncDraw image load completed~);// 取缓存NSData *newImageData self.imageDataDict[imageKey];if (!newImageData) {return;}// 返回主线程刷新[[NSOperationQueue mainQueue] addOperationWithBlock:^{UIImage *newImage [UIImage imageWithData:newImageData];// 遍历渲染同个图片地址的所有控件[blockOperation.targetSet enumerateObjectsUsingBlock:^(id _Nonnull obj, BOOL * _Nonnull stop) {if ([obj isKindOfClass:[UIImageView class]]) {UIImageView *imageView (UIImageView *)obj;// ADImageView内部判断“超出可视范围放弃渲染”imageView.image newImage;}}];[blockOperation removeAllTargets];}];// 写入沙盒[newImageData writeToFile:imagePath atomically:YES];// 移除任务[self.operationDict removeObjectForKey:imageKey];}];// 加入队列[self.operationQueue addOperation:operation];// 添加opertion[self.operationDict setValue:operation forKey:imageKey];}return operation;
}四、使用示例
1文本异步绘制
implementation ADLabel#pragma mark - Pub MD
- (void)setText:(NSString *)text {_text text;[[ADManager shareInstance] addTaskWith:self selector:selector(asyncDraw)];
}
// 绑定异步绘制layer(Class)layerClass {return ADLayer.class;
}#pragma mark - Pri MD
- (void)asyncDraw {[self.layer setNeedsDisplay];
}#pragma mark - ADLayerDelegate
- (void)layerWillDraw:(CALayer *)layer {
}- (void)asyncDrawLayer:(ADLayer *)layer inContext:(CGContextRef __nullable)ctx canceled:(BOOL)canceled {if (canceled) {NSLog(异步绘制取消~);return;}UIColor *backgroundColor _backgroundColor;NSString *text _text;UIFont *font _font;UIColor *textColor _textColor;CGSize size layer.bounds.size;CGContextSetTextMatrix(ctx, CGAffineTransformIdentity);CGContextTranslateCTM(ctx, 0, size.height);CGContextScaleCTM(ctx, 1, -1);// 绘制区域CGMutablePathRef path CGPathCreateMutable();CGPathAddRect(path, NULL, CGRectMake(0, 0, size.width, size.height));// 绘制的内容属性字符串NSDictionary *attributes {NSFontAttributeName : font,NSForegroundColorAttributeName: textColor,NSBackgroundColorAttributeName : backgroundColor,NSParagraphStyleAttributeName : self.paragraphStyle ?:[NSParagraphStyle new]};NSMutableAttributedString *attrStr [[NSMutableAttributedString alloc] initWithString:text attributes:attributes];// 使用NSMutableAttributedString创建CTFrameCTFramesetterRef framesetter CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attrStr);CTFrameRef frame CTFramesetterCreateFrame(framesetter, CFRangeMake(0, attrStr.length), path, NULL);CFRelease(framesetter);CGPathRelease(path);// 使用CTFrame在CGContextRef上下文上绘制CTFrameDraw(frame, ctx);CFRelease(frame);
}2图片异步下载渲染
implementation ADImageView#pragma mark - Public Methods
- (void)setUrl:(NSString *)url {_url url;[[ADManager shareInstance] ad_setImageWithURL:[NSURL URLWithString:self.url] target:self completed:^(UIImage * _Nullable image, NSError * _Nullable error) {if (image) {self.image image;}}];
}五、成效举证
针对本案制作了AsyncDrawDemo是一个图文排列布局的UITableView列表类似新闻列表TestTableViewCell.m中有异步绘制和图片异步下载渲染开关
#define kAsyncDraw true // 异步开关
//#define kOnlyShowText true // 仅显示文本进行测试kAsyncDraw开启前后测试对比清单
• 同样加载1000条数据的列表
• 动态文本缓存高度
• 同一设备真机iPhone11 iOS13.5.1
• 操作列表首次加载完成帧率显示60fps后快速向上滑动至底部
本案通过YYFPSLabel观察帧率大致均值变化以及内存/CPU变化截图如下
1未开启异步前
稳定60fps后开始快速滑动至列表底部的前后对比帧率最低到1fps滑动过程异常卡顿cpu未超过40%内存占用也不多但非常耗电 2开启异步后 稳定60fps后开始快速滑动至列表底部的前后对比帧率稳定在60fps滑动过程非常流畅cpu最高超过90%内存占用到达200MB耗电小 通过以上对比得出的结论是未开启“异步绘制和异步下载渲染”虽然cpu、内存未见异常但列表滑动卡顿非常耗电开启后虽然内存占用翻倍、cpu也达到过90%但相对于4G内存和6核CPU的iPhone11来说影响不大流畅性和耗电得到保障。由此得出结论UITableView性能优化的关键在于“系统资源充分满足调配的前提下能异步的尽量异步”否则主线程压力大引起卡顿丢帧和耗电在所难免。 补充说明当打开kOnlyShowText开关仅显示文本内容进行测试时在未打开kAsyncDraw开关前快速滑动列表帧率出现4050fps可感知快速滑动下并不流畅。虽然UITableView性能优化主要体现在大图异步下载渲染的优化文本高度的缓存对于多核CPU设备性能提升效果确实不明显但文本异步绘制则让性能更上一层。 六、核心代码范围
DEMO地址https://github.com/stkusegithub/AsyncDraw
代码位于目录 AsyncDrawDemo/AsyncDrawDemo/Core/下
\---AsyncDraw
---ADManager.h
---ADManager.m
---ADLayer.h
---ADLayer.m
---ADTask.h
---ADTask.m
---ADQueue.h
---ADQueue.m
---ADOperation.h
---ADOperation.m
\---AsyncUI
---ADLabel.h
---ADLabel.m
---ADImageView.h
---ADImageView.m-End-
参考链接 https://blog.csdn.net/chokshen/article/details/108714429 https://www.jianshu.com/p/bd7fdc6722ad https://jishuin.proginn.com/p/763bfbd80508