iOS-UI部分知识点整理


UI视图相关.png

  • UITableView相关

  • 事件传递&视图响应

  • 系统的UI事件传递机制是怎么样的 ?

  • 使UITableView滚动更流畅的方案或思路都有哪些 ?

  • UIView和CALayer之间的关系是怎样的 ?

  • 图像显示原理

  • 卡顿&掉帧

  • 绘制原理&异步绘制

  • 离屏渲染 ?


UITableView相关

  • 重用机制

  • 数据源同步

重用机制

904629-0a1f0742409426a8.png

这里A1到A7视为同一个标识符,虚线是可视区域,当A1滑出可视区域的时候会放入重用池A,A7根据标识符从备用池取出一个可重用的cell,这样就达到了重用的一个目的

数据源同步

如何解决tableView在多线程的情况下修改或者访问数据源的一个同步问题? -- 俩个解决方案

  • 并发访问 & 数据拷贝

  • 串行访问

并发访问 & 数据拷贝

904629-14ad3dc448c9ec1c.png

上图中,一般做数据拷贝在主线程当中,拷贝之后会把拷贝的结果给子线程使用,同时在子线程中做新数据的网络请求,数据解析。 主线程在子线程请求数据的时候删除了一条数据,然后reloadUI 子线程在完成一系列操作之后,返回请求的结果,然后reloadUI。 这个时候问题就出现了,子线程的拷贝发生在主线程删除一条数据之前,所以子线程在返回给主线程的数据源列表中还包括了主线程删除的那条数据,导致数据异常.

怎么解决这种数据源的同步问题呢? 往下看

904629-5f3d17abda67c175.png

我们可以在主线程进行删除操作的时候把它记录下来,在子线程中将要返回数据更新主线程UI的时候同步删除操作。这就是并发访问的解决方案

串行队列

串行访问,这个时候要用到GCD中的串行队列

904629-df50b61232b6d8dd.png

子线程做网络请求,数据解析后数据放到串行队列中做预排版(都是在子线程当中完成).在这个过程中如果在主线程中删除了一条数据,需要以同步的方式在串行队列中处理,在上个任务数据排版完成之后再同步数据删除,然后再回到主线程更新UI,这样可以保证无论在主线程或者子线程都是在串行队列上进行操作的,避免数据源的错乱问题。 俩种方案各有利弊,具体视项目实际业务而定。

这里多提一下,队列和线程的关系,在iOS开发中除了一个特殊情况(主队列和主线程的关系),它二者之间是相互独立的,其实只要记住一个要点就可以了,主队列是一个特殊的队列,首先,它是一个串行队列,其次,它自始至终占用一个特殊的线程--主线程,而主线程,自始至终仅被主队列占用,其他任何队列,不管并行还是串行,都不能占用主线程,除去这个奇葩之外,任意队列不管是系统的还是自建的,不管是串行还是并行的,所有跑在这些队列上的线程都是子线程,都不能用来刷新UI


事件传递&视图响应

CALayer 与 UIView 关系

UIView 为CALayer提供内容,专门负责处理触摸等时间,参与响应链 CALayer 全权负责显示内容 contents

这里用到了六大设计原则之中的单一职责原则,这就是为什么要区分UIView和 CALayer 之间的工作分工.

事件传递机制

基本上是一个必考点,事件传递与俩个方法有关

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
//返回最终响应的事件
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
//判断点击位置是否在当前范围内

事件传递机制的总流程如下如所示


image.png

在看事件传递机制之前,先看一下 hitTest 的内部方法实现,看下图

904629-0a1f0742409426a8.png
  1. 首先判断当前视图 !hidden &$ userInteractionEnable && alpha > 0.01 条件通过的时候,到下一步. 否则返回nil,找不到当前视图

  2. 通过 pointInside 判断点击的点是否在当前范围内,为YES直接下一步. 不在则直接返回nil。

  3. 倒序遍历所有子视图,同时调用 hitTest 方法,如果某一个子视图返回了对应的响应视图,这个子视图会直接作为最终的响应视图给响应方,如果为 nil 则继续遍历下一个子视图。如果全部遍历结束都返回nil,那会返回当前点击位置在当前的视图范围内的视图作为最终响应视图.......

当我们点击屏幕时候的事件传递

UIApplication -> UIWindow -> hitTest:withEvent:

深入了解,做一个Demo,一个方形按钮,控制只在点击圆内的时候才响应点击事件,源码看这里(PPEventDemo)

视图响应链(注意和事件传递是倆概念)

看一下官方的一张图,一目了然

904629-a08dab22a59a5415.png

UIResponder 主要有三个方法

-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event

提问,下图中点击空白圆点,视图响应顺序是什么?如果最后传递到UIApplicationDelegate 依然没有响应,会发生什么?

image

正确顺序是 C2 -> B2 -> A 到 UIApplicationDelegate 依然没有任何视图处理该事件的时候,直接忽略此事件, 并不会崩溃。

图像显示原理

904629-43b92665ba5210e5.png

具体的看一下CPU和GPU做了哪些事,看下图

904629-b31c3b430f61acdf.png

CPU工作

  • UI布局

  • 文本计算

  • 绘制,比如 drawRect 方法

  • 图片编解码

  • 提交位图

GPU 渲染管线

  • 顶点着色

  • 图元装配

  • 光栅化

  • 片段着色

  • 片段处理

image.png
  • 纹理渲染

  • 视图混合


卡顿&掉帧

UI卡顿,掉帧的原因

904629-b199ad34aed68b88.png

一般页面滑动的流畅性是60fps,每秒会有60帧的画面更新,也就是每16.7ms需要产生一帧画面,这过程需要GPU,CPU协同产生一帧数据。

如下,比如 CPU 花费时间绘图解码之后将位图交给 GPU 合成渲染,准备下一个VSync信号的到来。 第二段,当 CPU 解码,计算时间过长的时候,留给GPU渲染的时间就很少,所以 GPU 需要合成渲染全部准备完毕需要的时间可能就要超过16.7ms,在下一帧VSync信号到来的时候没有准备这一帧画面,所以就会产生掉帧.

904629-b688c0c8d9d16e35.png

UITableView 的滑动优化方案

了解以上内容之后,问题来了,对于 UITableView 有哪些优化方案? 我们就可以基于 GPU 和 CPU 这俩方面来进行解答

CPU
  • 在子线程中进行对象创建,调整,销毁

  • 在子线程中进行预排版(布局计算,文本计算)

  • 在子线程中进行预渲染(文本异步绘制(下面会提到),图片编解码等) 像对象创建,布局计算等都可以放到子线程去做,主线程可以有更多的时间去响应用户的交互

GPU
  • 纹理渲染 比如一些圆角和阴影的设置,容易触发离屏渲染,导致GPU工作量非常大, 这是一个优化点.尽量避免离屏渲染,减轻GPU的压力。
  • 视图混合 当有多个视图层层叠加,视图合成,每一个像素的合成对应的像素值,需要进行大量的计算。可以在一定程度上减轻图层的复杂度,通过CPU层面的异步绘制机制,达到提交的位图本身是一个层级少的视图. 具体事例可以看一下这里

进一步,了解一下UI的绘制原理


绘制原理&异步绘制

绘制原理

通过一幅图看一下UIView的视图绘制原理

904629-5235355555296421.png

在调用UIView setNeesDisplay 并不会立刻发生对应视图的对应工作只是打上一个刷新的标记,实际上是到当前 Runloop 快要结束的时候([CALayerdisplay])才会开始介入到UI视图的绘制当中, 如果不响应 displayLayer代理方法的时候就会走系统绘制流程,如果响应这个方法,就会走异步绘制的入口,这样就给我们异步绘制留有了一个余地.

我们看一下系统绘制的流程图

904629-a3cc0a17bfa265b3.png

在 drawRect之前会调用 drawLayer, 举个例子,可以更方便看到系统的调用过程

- (void)drawRect:(CGRect)rect {
  CGContextRef con = UIGraphicsGetCurrentContext();
  CGContextAddEllipseInRect(con, CGRectMake(0,0,100,200));
  CGContextSetRGBFillColor(con, 0, 0, 1, 1);
  CGContextFillPath(con);
}

此时的堆栈

904629-d1f702959ef86f2e.png

那么怎么实现异步绘制呢?

异步绘制

看图说话

904629-1b201093b6bd076c.png

这里看看在全局队列子线程里做的工作, 首先通过CGBitmapContextCreate()创建一个位图的上下文,然后通过CoreGraphic API做一些UI控件的绘制工作,之后再通过CGBitmapContextCreateImage()生成CGImage图片,再回到主队列 提交位图。到这里就完成了一个UI控件的异步绘制。


离屏渲染

什么是离屏渲染?你有什么理解? 何时会触发?

有离屏,自然也有在屏渲染,看一下概念

On-Screen Rendering
当前屏幕渲染,指的是GPU的渲染操作是在当前用于显示的屏幕缓冲区中进行的
Off-Screen Rendering
离屏渲染,指的是GPU在当前屏幕缓冲区以为新开辟一个新的缓冲区进行渲染操作

离屏渲染的源头是在GPU上,也就是当我们设置的某一层UI的属性被指定为在为合成以前不能渲染在屏幕上直接显示的时间发生离屏渲染。离屏渲染表示渲染发生在屏幕之外,你可能认为这是一句废话。为了真正解释清楚什么是离屏渲染,我们先来看一下正常的渲染通道(Render-Pass)


5726964-e40f0d7e32ad0a82.png

首先,OpenGL提交一个命令到Command Buffer,随后GPU开始渲染,渲染结果放到Render Buffer中,这是正常的渲染流程。但是有一些复杂的效果无法直接渲染出结果,它需要分步渲染最后再组合起来,比如添加一个蒙版(mask):


5726964-ad682be0a90b76e1.png

在前两个渲染通道中,GPU分别得到了纹理(texture,也就是那个相机图标)和layer(蓝色的蒙版)的渲染结果。但这两个渲染结果没有直接放入Render Buffer中,也就表示这是离屏渲染。直到第三个渲染通道,才把两者组合起来放入Render Buffer中。离屏渲染意味着把渲染结果临时保存,等用到时再取出,因此相对于普通渲染更占用资源。

离屏渲染何时会触发?

  • 圆角(和 maskToBounds一起使用)

  • 图层蒙版

  • 阴影

  • 光栅化

为什么要避免离屏渲染? 第一个问题,在触发离屏渲染的时候,会触发OpenGL的多通道渲染管线从而增加GPU的工作量,导致掉帧卡顿的情况,同时离屏渲染也会让GPU创建新的缓冲区,在渲染的过程中GPU会频繁的进行上下文切换增大GPU的工作量。离屏渲染的UI优化参考。UIKit性能调优实战讲解


问题总结

  • 系统的UI事件传递机制是怎么样的 ?

  • 使UITableView滚动更流畅的方案或思路都有哪些 ?

  • 什么是离屏渲染 ? (位于GPU层面) ?

  • UIView和CALayer之间的关系是怎样的 ?

你可能感兴趣的:(iOS-UI部分知识点整理)