UI相关

272 阅读9分钟

UITableView相关

重用机制

iOS如何实现cell的重用机制?

img

  • A1-A7使用相同的identifer,当tableView向上滑动,A1划出页面后,就被放入了重用池。
  • A7即将展示时,首先会在重用池中查看时候有相同identifercell可以被重用,如果有则直接取出使用,若无则创建一个新的cell

如何手动实现重用机制?

  • ViewReusePool类的声明

img

  • ViewReusePool类的实现

img

  • dequeueReusableView函数实现

img

  • addUsingView:函数实现

img

  • reset函数实现

img

  • ViewReusePool类的使用

img

数据源同步问题

  • 当数据源在主线程中有删除操作,同时在子线程上又有加载更多数据的操作时,就会出现数据源同步问题

img

数据源同步解决方案

并发访问、数据拷贝

  • 子线程返回主线程的数据中,仍然包含删除的这一条数据。img
  • 主线程进行删除操作时,将操作记录下来。之后在子线程同步数据时,同步删除操作。

![img](data:image/svg+xml;utf8,)

串行访问

  • 子线程的数据同步和主线程的删除操作全部放入一个串行队列中执行。
  • 删除动作可能会有延时。img

事件传递&视图相应

UIView和CALayer

UIView和CALayer的关系和区别

关系

img

  • UIView对象中的layer指向一个CALayer变量
  • UIView对象中的backgroundColor属性,是对CALayer同名属性的封装。
  • UIView展示部分是由CALayer中的contents来决定。contents对应的backing store其实是一个bitmap的位图。

区别

  • UIView为其提供内容,以及负责处理触摸等事件,参与响应链。
  • CALayer负责显示内容contents

Q:为什么UIView负责触摸事件,CALayer负责显示?

  • 设计模式,单一职责原则。

layoutSubviews 和 drawRect

layoutSubviews方便数据计算,drawRect方便视图重绘。

layoutSubviews在以下情况下会被调用:

  • init初始化不会触发layoutSubviews。
  • addSubview会触发layoutSubviews。
  • 设置view的Frame会触发layoutSubviews,当然前提是frame的值设置前后发生了变化。
  • 滚动一个UIScrollView会触发layoutSubviews。
  • 旋转Screen会触发父UIView上的layoutSubviews事件。
  • 改变一个UIView大小的时候也会触发父UIView上的layoutSubviews事件。 
  • 直接调用setLayoutSubviews。

drawRect在以下情况下会被调用

  • 如果在UIView初始化时没有设置rect大小,将直接导致drawRect不被自动调用。drawRect 掉用是在Controller->loadView,?Controller->viewDidLoad?两方法之后掉用的.所以不用担心在 控制器中,这些View的drawRect就开始画了.这样可以在控制器中设置一些值给View(如果这些View?draw的时候需要用到某些变量 值).
  • 该方法在调用sizeToFit后被调用,所以可以先调用sizeToFit计算出size。然后系统自动调用drawRect:方法。
  • 通过设置contentMode属性值为UIViewContentModeRedraw。那么将在每次设置或更改frame的时候自动调用drawRect:。
  • 直接调用setNeedsDisplay,或者setNeedsDisplayInRect:触发drawRect:,但是有个前提条件是rect不能为0。

drawRect方法使用注意点:

  • 若使用UIView绘图,只能在drawRect:方法中获取相应的contextRef并绘图。如果在其他方法中获取将获取到一个invalidate 的ref并且不能用于画图。drawRect:方法不能手动显示调用,必须通过调用setNeedsDisplay?或 者?setNeedsDisplayInRect,让系统自动调该方法。
  • 若使用calayer绘图,只能在drawInContext:?中(类似于drawRect)绘制,或者在delegate中的相应方法绘制。同样也是调用setNeedDisplay等间接调用以上方法
  • 若要实时画图,不能使用gestureRecognizer,只能使用touchbegan等方法来调用setNeedsDisplay实时刷新屏幕。

事件传递与视图响应链

img

img

事件分发机制

img

  • 当用户点击屏幕,事件会被UIApplication接受,并传递给UIWindow
  • UIWindow调用hitTest函数,在hitTest内调用pointInside判断事件是否在该视图内。
  • 若为false,则返回该视图,事件传递流程结束。
  • 若为true,则可倒叙遍历该视图的子视图,并调用子视图hitTest函数。
  • 找到最终hitTesttrue子视图,并依次返回,事件传递流程结束。

hitTest系统内部实现

img

  • 在当前视图子视图调用hitTest函数前,需要将当前坐标转换为子视图中的坐标。

img

Q:如何只让方形图片的圆形区域接受事件响应?img

  • 重写视图的pointInside函数,使得点击区域在圆形范围内返回true,否则返回false

img

响应者链原理

事件的响应是通过响应链来传递的。

img

  • UIView通过继承UIResponder,拥有以下函数

img

事件传递之后由谁来响应?

img

  • 如果响应视图无法处理响应事件,则响应事件会通过响应链传递给父视图尝试处理。
  • 通过 [responder nextResponder] 找到当前 responder 的下一个 responder,持续这个过程到最后会找到 UIApplication 对象。
  • 如果传递给UIApplication依然没有处理响应事件,则事件将被忽略。

VC生命周期

假设现在有一个 AViewController(简称 Avc) 和 BViewController (简称 Bvc),通过 navigationController 的push 实现 Avc 到 Bvc 的跳转,调用顺序如下: 1、A viewDidLoad 2、A viewWillAppear 3、A viewDidAppear 4、B viewDidLoad 5、A viewWillDisappear 6、B viewWillAppear 7、A viewDidDisappear 8、B viewDidAppear 如果再从 Bvc 跳回 Avc,调用顺序如下: 1、B viewWillDisappear 2、A viewWillAppear 3、B viewDidDisappear 4、A viewDidAppear

图像显示原理

图像显示流程

img

  • CPUGPU是通过事件总线链接在一起的。
  • CPU输出的位图,在适当时机由事件总线上传给GPU
  • GPU会对位图进行渲染,然后将结果放入帧缓冲区。
  • 视频控制器通过Vsync信号,在指定时间(16.7ms)之前,从帧缓冲区中提取屏幕显示内容,然后显示在显示器上。

UI视图显示过程

img

  • 当创建一个UIView对象,它的显示部分由CALayer来控制的。
  • CALayer有一个contents属性,就是最终绘制到屏幕上的位图
  • 在绘制contents内容时,系统会回调drawRect:函数,我们可以在函数中增加绘制内容。
  • 绘制好的位图会通过Core Animation框架,最终经由GPU当中的OpenGL渲染管线,渲染在屏幕上。

CPU工作过程

img

1、Layout

  • UI布局(frame设置)
  • 文本计算(size计算)

2、Display

  • 绘制(drawRect:

3、Prepare

  • 图片编解码

4、Commit

  • 提交位图

GPU渲染管线过程

img

卡顿&掉帧的原因

img

  • 按照每秒60FPS刷新率,每隔16.7ms就会有一次Vsync信号。
  • 16.7ms内,需要CPUGPU协同产生这一帧的画面,并在下一次Vsync信号来临时,显示这一帧的画面。
  • 如果CPUGPU的工作时长超过16.7ms,那么当Vsync信号来临时,无法提供这一帧的画面,就会出现掉帧现象。
  • 上一帧没有显示的画面,会在下一次Vsync信号来临时显示。

滑动优化方案

CPU

  • 对象创建、调整销毁可以放在子线程。
  • 预排版(布局计算、文本计算)操作,可以放在子线程操作。
  • 预渲染(文本等异步绘制图片编解码等)操作,降低CPU的耗时。

GPU

  • 避免离屏渲染,降低纹理渲染的耗时。
  • 如果视图层级复杂,GPU在视图合成时会做大量的计算。可以通过异步绘制等机制,减少视图层级,减轻GPU的压力。

绘制原理&异步绘制

UIView的绘制原理

img

  • 当调用setNeedsDispaly函数,实际是调用view.layersetNeedsDispaly函数。
  • 该函数会将layer标记,在runloop即将结束时,调用CALayer display函数,进入当前视图的真正绘制。
  • CALayer display函数中,会判断它的代理是否响应displayLayer:函数,如果YES,则可进行异步绘制,否则进入系统绘制流程

系统的绘制流程

img

  • layer会创建一个backing store,在drawRect:函数中可以拿到这个上下文。
  • layer会判断是否有代理,如果有代理,在系统内部绘制完成后,会调用UIView drawRect:,允许在系统绘制基础上,进行增添修改。
  • 最终由CALayer上传backing storeGPU

异步绘制

  • 如果layer存在代理,则由代理执行display:函数生成位图,并设置该bitmap作为layer.contents属性。

img

离屏渲染

  • 在屏渲染(On-Screen Rendering)意为当前屏幕渲染,指的是GPU的渲染操作是在当前用于显示屏幕缓冲区中进行。

  • 离屏渲染(Off-Screen Rendering)意为离屏渲染,指的是GPU的再当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作。

    img

什么场景会触发离屏渲染?

  • 圆角(需要和maskToBounds一起使用)
  • 图层蒙版
  • 阴影 (没有设置ShadowPath)
  • 光栅化
  • 抗锯齿
  • 不透明

如何解决离屏渲染?

  • clearColor可以通过直接设置颜色来解决。
  • alpha为0时候用hidden替换。
  • 圆角、边框解决方案:
    • UIBezierPath
    • 使用Core Graphics为UIView加圆角
    • 直接处理图片为圆角
    • 后台处理圆角
  • 阴影解决方案:shadowPath替换。
  • 尝试开启CALayer.shouldRasterize。
  • 对于不透明的View,设置opaque为YES,这样在绘制该View时,就不需要考虑被View覆盖的其他内容(尽量设置Cell的view为opaque,避免GPU对Cell下面的内容也进行绘制)

为什么要避免离屏渲染?

  • 创建新的渲染缓冲区,会有内存上的开销。
  • 多通道渲染管线,最终需要合成,会涉及上下文切换,增加GPU的开销。
  • 总结:离屏渲染会增加GPU的处理时间,这样可能导致CPU + GPU的总处理时间超过16.7ms,从而出现掉帧卡顿的现象。

CPU 渲染和离屏渲染的区别

由于GPU的浮点运算能力比CPU强,CPU渲染的效率可能不如离屏渲染。但如果仅仅是实现一个简单的效果,直接使用 CPU 渲染的效率又可能比离屏渲染好,毕竟普通的离屏渲染要涉及到缓冲区创建和上下文切换等耗时操作。对一些简单的绘制过程来说,这个过程有可能用CoreGraphics,全部用CPU来完成反而会比GPU做得更好。一个常见的 CPU 渲染的例子是:重写 drawRect 方法,并且使用任何 Core Graphics 的技术进行了绘制操作,就涉及到了 CPU 渲染。整个渲染过程由 CPU 在 App 内同步地完成,渲染得到的bitmap最后再交由GPU用于显示。总之,具体使用 CPU 渲染还是使用 GPU 离屏渲染更多的时候需要进行性能上的具体比较才可以。