【异步绘制】UIView刷新原理 与 异步绘制

快捷目录

  • 壹、 iOS界面刷新机制
  • 贰、浅谈UIView的刷新与绘制
    • 概述
    • 一.UIView 与 CALayer
      • 1. 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有UIKitUIView,但是Mac OS有AppKitNSView的原因。他们功能上很相似,但是在实现上有着显著的区别。把这种功能的逻辑分开并封装成独立的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 contentsScale

contentsScale属性定义了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的运算显示在屏幕上的。这个过程大体分为六个阶段:

img

绘制

  • 布局 :首先一个视图由CPU进行Frame布局,准备视图(view)和图层(layer)的层级关系,以及设置图层属性(位置,背景色,边框)等等。
  • 显示:view的显示图层(layer),它的寄宿图片被绘制的阶段。所谓的寄宿图,就是上面我们提到过的layer所显示的内容。它有两种设置形式:一种是直接设置layer.contents,赋值一个CGImageRef;第二种是重写UIView的drawRect:CALayerDelegatedrawLayer: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有三个比较重要的布局属性:frameboundscenter.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的绘制流程

img

绘制

  • UIView调用setNeedsDisplay,这个方法我们已经介绍过了,它并不会立即开始绘制。
  • UIView 调用setNeedsDisplay,实际会调用其layer属性的同名方法,此时相当于给layer打上绘制标记。
  • 在当前run loop 将要结束的时候,才会调用CALayer的display方法进入到真正的绘制当中
  • 在CALayer的display方法中,会判断layer的代理方法displayLayer:是否被实现,如果代理没有实现这个方法,则进入系统绘制流程,否则进入异步绘制入口。

系统绘制

img

xitong

  • 在系统绘制开始时,在CALayer内部会创建一个绘制上下文,这个上下文可以理解为CGContextRef,我们在drawRect:方法中获取到的currentRef就是它。

  • 然后layer会判断是否有delegate,没有delegate就调用CALayerdrawInContext方法,如果有代理,并且你实现了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流畅度。

异步绘制流程

img

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 AnimationCAShapeLayer代替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)异步绘制时机及减少重复绘制

img

这里简单描述下绘制原理:当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、设计图

img

img

2、代码原理剖析(写在注释)

1)设置runloop监听及回调
/**runloop回调,并发执行异步绘制任务*/
static NSMutableSet<ADTask *> *_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 id<ADLayerDelegate> delegate = (id<ADLayerDelegate>)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%,内存占用也不多,但非常耗电):

img

img

img

2)开启异步后:
稳定60fps后开始快速滑动至列表底部的前后对比(帧率稳定在60fps,滑动过程非常流畅,cpu最高超过90%,内存占用到达200MB,耗电小)

img

img

img

通过以上对比得出的结论是:未开启“异步绘制和异步下载渲染”,虽然cpu、内存未见异常,但列表滑动卡顿,非常耗电;开启后,虽然内存占用翻倍、cpu也达到过90%,但相对于4G内存和6核CPU的iPhone11来说影响不大,流畅性和耗电得到保障。由此得出结论,UITableView性能优化的关键在于“系统资源充分满足调配的前提下,能异步的尽量异步”,否则主线程压力大引起卡顿,丢帧和耗电在所难免。

补充说明:当打开kOnlyShowText开关,仅显示文本内容进行测试时,在未打开kAsyncDraw开关前快速滑动列表,帧率出现40~50fps,可感知快速滑动下并不流畅。虽然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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/225878.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【UE】在蓝图中修改材质实例的参数的两种方式

目录 方式一、通过“在材质上设置标量/向量参数值”节点实现 方式二、通过“设置标量/向量参数值”节点实现 方式一、通过“在材质上设置标量/向量参数值”节点实现 1. 在材质中设置了两个参数 2. 创建材质实例 3. 创建一个蓝图&#xff0c;对静态网格体赋予材质实例 在事件…

【C++初阶】类与对象(上)

类与对象&#xff08;上&#xff09; 1.面向过程和面向对象初步认识2.类的引入3.类的定义4.类的访问限定符及封装4.1 访问限定符4.2 封装 5.类的作用域6.类的实例化7.类对象模型7.1 如何计算类对象的大小7.2 结构体内存对齐规则 8.this指针8.1 this指针的引出8.2 this指针的特性…

【算法与数据结构】53、LeetCode最大子数组和

文章目录 一、题目二、解法三、完整代码 所有的LeetCode题解索引&#xff0c;可以看这篇文章——【算法和数据结构】LeetCode题解。 一、题目 二、解法 思路分析&#xff1a;程序一共两个变量&#xff0c;一个result一个count。result用来记录最终的结果&#xff0c;count记录当…

LAMP平台——构建PHP运行环境

在构建LAMP平台时&#xff0c;各组件的安装顺序依次为Linux、Apache、MySQL、PHP。其中Apache和 MySQL的安装并没有严格的顺序&#xff1b;而PHP环境的安装一般放到最后&#xff0c;负责沟通Web服务器和数据库 系统以协同工作。 PHP 即 Hypertext Preprocessor&#xff08;超级…

数据结构 之map/set练习

文章目录 1. 只出现一次的数字算法原理&#xff1a;代码&#xff1a; 2. 随机链表的复制算法原理&#xff1a;代码&#xff1a; 3. 宝石与石头算法原理&#xff1a;代码&#xff1a; 4. 坏键盘打字算法原理&#xff1a;代码&#xff1a; 5. 前K个高频单词算法原理&#xff1a;代…

图片的批量建码怎么做?一图一码的制作方法

在使用图片展示内容时&#xff0c;经常会有同一类型的图片信息是有区别的&#xff0c;如果需要将每张图片批量生成二维码图片&#xff0c;那么出了一张一张去制作之外&#xff0c;有没有能够一键批量建码的功能可以解决这个问题呢&#xff1f;下面来给大家分享一下图片批量建码…

SpringBoot 源码解析1:环境搭建

SpringBoot 源码解析1&#xff1a;环境搭建 1.项目结构2.pom.xml3.MyApplication 1.项目结构 最简单的spring-web项目 2.pom.xml <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0"xmlns…

记删除CK不彻底问题 及 新增表TTL机制

问题背景&#xff1a;对CK表进行删除时&#xff0c;发现无法彻底删除&#xff0c;并报错如下&#xff1a; 同时也会有下面的报错信息&#xff1a; 解决过程&#xff1a; 确认CK 节点是否健康存活&#xff0c;select * from system.clusters 可以查看&#xff1b;确认CK元数据是…

基于ssm日用品网站设计论文

摘 要 现代经济快节奏发展以及不断完善升级的信息化技术&#xff0c;让传统数据信息的管理升级为软件存储&#xff0c;归纳&#xff0c;集中处理数据信息的管理方式。本日用品网站就是在这样的大环境下诞生&#xff0c;其可以帮助管理者在短时间内处理完毕庞大的数据信息&…

linux系统的u盘/mmc/sd卡等的支持热插拔和自动挂载行为

1.了解mdev mdev是busybox自带的一个简化版的udev。udev是从Linux 2.6 内核系列开始的设备文件系统&#xff08;DevFS&#xff09;的替代品&#xff0c;是 Linux 内核的设备管理器。总的来说&#xff0c;它取代了 devfs 和 hotplug&#xff0c;负责管理 /dev 中的设备节点。同时…

openEuler商业化进展可观:累计装机量超610万套,市场持续扩容

12月15日至16日&#xff0c;以“崛起数字时代&#xff0c;引领数智未来”为主题的操作系统大会&#xff06;openEuler Summit 2023在北京国家会议中心举办。大会旨在汇聚全球产业界创新力量&#xff0c;构筑坚实的基础软件根基&#xff0c;推动基础软件技术持续创新&#xff0c…

win10 node-red安装及管理配置

win10 node-red安装及管理配置 一、安装node.js环境二、安装node-red环境2.1 node-red安装2.2 node-red安全登录方式 三、pm2管理node-red服务3.1 安装pm23.2 pm2管理node-red服务 四、常用命令4.1 npm命令4.2 pm2命令 更多 本文旨在详细介绍windows10系统下的node-red开发配置…

visio打出根号,下标,并调整符号的大小

插入公式对象 打出根号和带下标的字母 调整符号大小 把做好的公式符号弄到visio中的图中 ctrla 复制符号 进入visio中粘贴 并 调整大小 调整大小直接拖动边框上的圆点即可。

java SSM酒店客房管理系统myeclipse开发mysql数据库springMVC模式java编程计算机网页设计

一、源码特点 java SSM酒店客房管理系统是一套完善的web设计系统&#xff08;系统采用SSM框架进行设计开发&#xff0c;springspringMVCmybatis&#xff09;&#xff0c;对理解JSP java编程开发语言有帮助&#xff0c;系统具有完整的源代 码和数据库&#xff0c;系统主要采…

在开发微信小程序的时候,报错navigateBack:fail cannot navigate back at firstpage

这个错误的意思是&#xff1a;在这个页面已经是第一个页面了&#xff0c;没办法再返回了 报错原因 这个错误原因其实也简单&#xff0c;就是在跳转的时候使用了wx.redirectTo()&#xff0c;使用wx.redirectTo()相当于重定向&#xff0c;不算是从上一个页面跳转过来的&#xf…

com.sun.org.apache.xerces.internal.impl.dv.util.Base64

com.sun.org.apache.xerces.internal.impl.dv.util.Base64 Access restriction: The type Base64 is not API (restriction on required library D:\Java\jdk1.8.0_341\jre\lib\rt.jar) Maven Update Project 虽然没错误了&#xff0c;但是有警告&#xff0c;好奇&#xff1f;…

python进度条

分享一个进度条python库 瞬间觉得很酷 :)) 它的名字叫tqdm 效果图&#xff1a; 代码&#xff1a; import time from tqdm import tqdmfor i in tqdm(range(100), desc"Loading", unit"kb"):time.sleep(0.1)

中通单号查询,中通快递物流查询,对需要的单号进行备注

批量查询中通快递单号的物流信息&#xff0c;对需要的单号进行备注。 所需工具&#xff1a; 一个【快递批量查询高手】软件 中通快递单号若干 操作步骤&#xff1a; 步骤1&#xff1a;运行【快递批量查询高手】软件&#xff0c;并登录 步骤2&#xff1a;点击主界面左上角的“…

系列一、Linux中安装MySQL

一、Linux中安装MySQL 1.1、下载MySQL安装包 官网&#xff1a;https://dev.mysql.com/downloads/file/?id523327 我分享的&#xff1a; 链接&#xff1a;https://pan.baidu.com/s/188_9RnBYlWVzFb_UJH5aaQ?pwdyyds 提取码&#xff1a;yyds 1.2、上传至/opt目录 & 解压…

Linux Conda 安装 Jupyter

在Linux服务器Conda环境上安装Jupyter过程中遇到了无数的报错&#xff0c;特此记录。 目录 步骤一&#xff1a;安装Anaconda3 步骤二&#xff1a;配置Conda源 步骤三&#xff1a;安装Jupyter 安装报错&#xff1a;simplejson.errors.JSONDecodeError 安装报错&#xff1a;…