前言
做 APP 开发与我们联系最紧密的就是 UI 的开发了,你做 APP 开发,不做 UI 恐怕就是你很努力地学习,但是你从来不参加考试,所以在面试中 UI 视图一定会涉及,而具体面试都面啥,主要有以下几个方面,如下图 :本文主要就是从 UITableView、事件传递、视图响应、图像显示原理、卡顿掉帧、异步绘制和绘制原理以及离屏渲染 6 大块进行相关的讲解。
1. UITableView
重用机制
上图灰色部分就是tableView 中已经出现过得 A1 cell,当滑动不在屏幕显示时,将其放到重用池,当再次划出时从重用池里面取出,这样避免频繁创建,消耗内存和CPU,从而引起卡顿,如果继续向上滑动 A7 就将会从重用池里面取出,这个就是 tableView 的重用机制
数据源同步
数据源同步的问题多出现在新闻、资讯类 App中,对当前显示的内容进行删除操作的同时,进行 loadmore 操作,loadmore 操作时,会将当前的 data 进行 copy,loadmore 是在子线程进行处理,当服务器返回结果时,那么 loadmore 将原来的 data 和现在新的 data 合并一起,返回主线程data,这个时候,删除操作已经完成,而我们再次 reload 的时候,发现删除的数据,这个时候又显示了,这个就是数据源同步的问题。这种问题解决方案有两种
-
并发访问解决方案
在主线程执行删除的操作的时候,记录下来要删除的数据,然后等到子线程 loadmore 解析完成,回到主线程,将返回的新 data,再次删除即可,然后 reloadUI 即可
-
串行访问解决方案
串行就是,删除操作放到串行队列中,当删除完成以后将数据传到子线程中进行 loadmore 等操作,子线程完成数据的解析将数据传到主线程从而 reloadUI,这个也可以解决,但是会更加耗时,但是却比第一种更加节约内存,具体情况具体分析,牺牲时间换取空间
2. 事件传递与事件响应
UIView和CALayer的关系
- UIView 为其提供内容,以及负责处理触摸等事件,参与响应链
- CALayer 负责显示内容的 contents
深入问答:为什么会这么设计?
遵循程序设计的原则:
事件响应流程
点击屏幕,事件响应的核心其实方法就是
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event; // recursively calls -pointInside:withEvent:. point is in the receiver's coordinate system
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event; // default returns YES if point is in bounds
第一个方法返回的是一个UIView,是用来寻找最终哪一个视图来响应这个事件;第二个方法是用来判断某一个点击的位置是否在视图范围内,如果在就返回YES
流程描述
- 我们点击屏幕产生触摸事件,系统将这个事件加入到一个由UIApplication管理的事件队列中,UIApplication会从消息队列里取事件分发下去,首先传给UIWindow
- 在UIWindow中就会调用hitTest:withEvent:方法去返回一个最终响应的视图
- 在hitTest:withEvent:方法中就回去调用pointInside: withEvent:去判断当前点击的point是否在UIWindow范围内,如果是的话,就会去遍历它的子视图来查找最终响应的子视图
- 遍历的方式是使用倒序的方式来遍历子视图,也就是说最后添加的子视图会最先遍历,在每一个视图中都回去调用它的hitTest:withEvent:方法,可以理解为是一个递归调用
- 最终会返回一个响应视图,如果返回视图有值,那么这个视图就作为最终响应视图,结束整个事件传递;如果没有值,那么就会将UIWindow作为响应者
hitTest:withEvent: 方法调用
调用流程
- 首先会判断当前视图的hiden属性、是否可以交互以及透明度是否大于0.01,如果满足条件则进入下一步,否则返回nil
- 调用pointInside: withEvent:方法来判断这个点是否在当前视图范围内,如果满足条件则进入下一步,否则返回nil
- 然后以倒序的方式遍历它的子视图,在每个子视图中去调用hitTest:withEvent:方法,如果有一个子视图返回了一个最终的响应视图,那么就将这个视图返回给调用方;如果全部遍历完成都没有找到一个最终的响应视图,因为点击位置在当前视图范围内,就将当前视图作为最终响应视图返回
3. 图像显示原理
图像显示原理则总线连接 CPU 和 GPU 协同工作,CPU 提交位图,GPU进行位图图层渲染和纹理合成,最终将结果,提交到帧缓冲区,然后由帧缓冲区提交到视频控制器,最后显示在屏幕上
在 iOS 开发上, 显示的流程则是创建一个UIView,然后调用CALayer,生成要显示的内容,最后经过 drawRect绘制,提交到 CoreAnimation 生成位图,以上部分则是 CPU 的工作;然后经过 GPU 的 OpenGL管线渲染生成结果,最后显示在屏幕上
CPU 工作
cpu 的主要工作就是 UI 布局,frame 计算、文本计算、绘制、图片编码器、最后提交位图
GPU
GPU 主要是对 CPU 提供过来的位图顶点着色、图元装配、光栅化、片段着色、片段处理,最后将结果提交到帧缓存区
4. 卡顿&掉帧
为什么会掉帧和卡顿?
页面滑动一般就是每一秒钟会有 60 帧的画面更新,基于此就是每隔 16.7ms 也就是 1/60就有一帧画面刷新,在这 16.7ms 内就需要 CPU 和 GPU协同工作,产生一帧的数据。如果当CPU消耗的时间过长,或者 GPU 渲染的时间过长,产生一帧的数据耗时超过 16.7ms,那么在下一个 Vsyc 信号到来之前,当前画面没有准备好,那么就会产生掉帧,而在肉眼看来就是卡顿。
滑动优化方案
- CPU优化
- 对象创建、调整、销毁(子线程)
- 预排版(布局计算、文本计算)放到子线程处理
- 预渲染(文本的异步绘制、图片解码等)
- GPU优化
其实就是避免离屏渲染- 纹理渲染
- 视图混合
5. 绘制原理&异步绘制
UIView 的绘制原理
当 UIView的控件在调用[UIView setNeedsDisplay]其实并没有立刻执行绘制工作,而是在当前的 layer 上面打上一个脏标记,接着会调用 [view.layer setNeedsDisplay], 在当前 runloop 将要结束以后,则会调用 [CALayer dispaly]这个方法进行绘制
系统绘制流程
流程描述
- 在layer内部会创建一个backing store,我们可以理解为CGContextRef上下文
- 判断layer是否有delegate:
- 如果有delegate,则会执行[layer.delegate drawLayer:inContext](这个方法的执行是在系统内部执行的),然后在这个方法中会调用view的drawRect:方法,也就是我们重写view的drawRect:方法才会被调用到
- 如果没有delegate,会调用layer的drawInContext方法,也就是我们可以重写的layer的该方法,此刻会被调用到
- 最后把绘制完的backing store(可以理解为位图)提交给GPU
异步绘制
怎么进行异步绘制呢,其实就是基于系统给我们开的口子layer.delegate,如果遵从或者实现了displayLayer方法,我们就可以进入到异步绘制流程当中,在异步绘制的过程当中通过代理负责生成 bitmap,最后将绘制的bitmap 作为 layer.contents 属性的值
6. 离屏渲染
什么是在屏渲染(On-Screen Rendering)?
就是说当前的屏幕渲染,指的是 GPU 操作发生在当前用于显示屏幕缓冲区进行
什么是离屏渲染?(Off-Screen Rendering)
指的是 GPU 在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作
比如说我们设置的圆角属性,蒙层遮罩都会触发离屏渲染
何时会触发?
- 设置图层的圆角且和 maskToBounds=YES 一起使用时
- 图层蒙版
- 阴影
- 光栅化
为何要避免?
- 创建新的渲染缓冲区,消耗内存
- 上下文切换(GPU额外的开销)
触发离屏渲染时,会增加 GPU 的工作量,增加 GPU 工作量可能导致 GPU+CPU 工作总耗时(60fps 为例)超过 16.7ms,从而造成UI卡顿 和掉帧,因为我们要避免离屏渲染
总结
以上就是我们 ui 视图部分 iOS 面试中常问到的。高频的题目有
- 系统事件的 UI 的传递机制是怎样的?
- 使 UITableView滑动更加流畅的方案或者思路有哪些?
- 什么是离屏渲染?
- UIView 和 CALayer之间的关系是怎么样的?
参考文章
https://www.jianshu.com/p/2c16077b50f8
https://www.jianshu.com/p/254ef1640f68