UItableView
重用机制
一般在iOS中,tableview的重用机制,我们在- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath方法中,首先通过dequeueReusableCellWithIdentifier获取重用池中的cell,如果获取的cell是nil那么就重新创建一个新的cell,那么重用机制的原理是什么呢?
- 首先我们刷新数据源的时候,整个重用池里面的cell跟屏幕的高度以及cell的个数有关,屏幕总共能够放下多少个cell那么重用池中就会创建多少个cell比如说:
- 当前屏幕中共放下了6个cell,当向上滑动的时候,cell1被隐藏了,相对的cell7从最下面露了出来,这时候就用到了重用机制,Cell1被隐藏时,Cell1的真的对象,就会放入到重用池当中,并且是待重用的状态,而cell7出来以后需要一个新的对象,重用池中有一个待使用的对象,正好被cell7使用,一次类推,随着界面滑动,cell的重用都是按照这个过程来进行的。已经创建的且不展示的cell会放在重用池中,但,当前页面展示的cell,超出了已经创建的cell时,就会再重新创建cell,滑出屏幕的cell会放到重用池中,等待使用
数据源同步
新闻、咨询类的App当中,数据源往往有一个删除操作、还同时需要LoadMore
- 删除操作主线程、LoadMore在子线程
- 这就需要考虑数据源的同步问题。
解决方案 - 并发拷贝数据:
① 需要记录操作,在子线程返回的数据源之前进行数据源的同步,之后再返回主线程loadUI
② 在并发中,假如数据量较大时,涉及到内存数据的拷贝,涉及到内存的消耗问题 - 串行访问
① 子线程和主线程任务同步,把子线程的操作放到串行队列中
② 假如子线层耗时较长的话,我们等待的时间可能也是比较长
事件传递&视图响应
UIView和CALayer关系和区别
UIView中的一个属性layer是CALayer类型变量,UIView中的属性backgroundColor实际上是layer中同名属性的一个包装,UIView的现实部分是由CALayer的contents来决定,contents所对应的是backking store,实际上是bitmap类型的一个位图。
- UIView为其提供内容,以及负责处理触摸等事件,参与响应链
- CALayer负责显示内容contents
为什么UIView为其提供内容,以及负责处理触摸等事件,参与响应链,CALayer负责显示内容contents?
设计原则,符合单一原则 ,职责分工。单一职责原则
事件传递机制
事件传递最主要的两个API:
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;
- hitTest最终是哪个视图响应这个事件就返回这个UIView
- pointInside判断某一个点击的位置是否在当前视图范围内,如果在就返回YES,不在返回NO
事件传递流程
此处涉及到一个递归查询,即对命中视图的子视图循环调用hittest方法,直到找出响应视图为止
hitTest系统内部的调用过程
1、 首先判断view的hidden(是否隐藏)、userinteractionEnabled(是否可以交互)和Alpha(透明度)
- 如果不可以直接返回nil
- 如果可以交互那么就调用走2、
2、 调用pointInside来判断这个点是否在当前视图范围内
- 如果不在当前视图,也会返回nil
- 如果在当前视图,就走3、
3、 倒序的形式便利当前view的子视图,便利过程中会调用所有子视图的hitTest的方法。
- 如果便利结束都没有找到对应的子视图去响应这个事件,如果当前位置在当前的视图范围内,就会把当前视图做为最终事件响应视图返回给调用方
- 假如子视图中的某个hitTest返回了最终的UIView,那么这个视图就做为最终事件响应视图,返回给调用方,继续对它的子视图进行遍历,直到它为最底层视图,或者没有再响应的视图
视图响应流程
uibutton->UIView->可能还存在view->uiwindow->UIApplication->UIApplicationDelegate
uibutton->UIView->可能还存在view->UIViewcontroller->uiwindow->UIApplication->UIApplicationDelegate
视图响应API
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
上述方法都是UIResponder的方法,UIView和UIViewController是继承于UIResponder,所以都有这些方法。
流程:1. 如果说最终视图无法响应
2.会传递给父视图进行响应,下一个响应者
3.如果父视图不响应那么继续传给父视图的父视图。
4.按照上述流程一直向上传递,直到传递给UIApplicationDelegate
5.假如一直到UIApplicationDelegate还没有找到?
忽略这个响应
图像显示原理
CPU和GPU两个硬件都是通过总线连接起来的。CPU输出的结果一般都是位图,在经过总线在合适的时机上传给GPU, GPU拿到位图以后,会做图层的一些渲染,包括纹理的合成之后会把结果放到Frame Buffer(缓冲区)由视频控制器根据信号在指定时间之前,去提取在帧缓冲区中的屏幕显示内容然后最终显示到手机屏幕上。
UIView显示部分是由CALayer来负责的,而CALayer中有个属性contents就是最终要绘制到手机屏幕上的一个位图,比如说是一个UILabel,那么contents中最终放的结果就是关于helloworld一个文字的位图,然后系统会在合适的时机回调给UIView的一个drawRect
方法,让程序员在此基础之上去绘制自己一些想要的自定义绘制的内容。
绘制好的位图最终会由Core Animation
这个框架提交给GPU中的OpenGL(ES)渲染管线,进行最终的位图的渲染包括纹理的合成然后显示在手机屏幕上。
总体流程
卡顿和掉帧
一般来说页面滑动的流畅性是60FPS,指的就是每一秒钟会有60帧的画面更新,我们在人眼上面所看到的就是流畅的效果,那基于此每隔16.7ms(1/60)就要产生一帧的画面,那么在16.7ms之内需要CPU和GPU共同协同完成产生最终的一帧的数据。
比如说CPU花费一定的时间(文本的布局,UI的计算包括一些视图的绘制,以及图片解码然后最终得到的位图提交给GPU),再由GPU进行相应的图层的合成、管理、渲染,准备好下一帧的画面,然后在下一帧的VSync信号到来的时候,就可以显示这个画面。
如果说CPU花费的时间特别长的话,那么留给GPU的时间就会减少,如果GPU进行相应的图层的合成、管理、渲染全部准备完毕可能就要总时间超过16.7ms,那么在下一帧VSync信号到来的时候
没有准备好这一帧的画面,那就由此产生掉帧,由此产生一个卡顿。
那么上述就可以总结UI卡顿掉帧的原因。
在规定的16.7ms之内在下一帧VSync到来之前并没有CPU和GPU共同完成下一帧画面的合成,于是就会造成卡顿掉帧。
滑动优化方案
基于TableView和ScrollView都有哪些优化方案,你又是怎么样做的?
- CPU:减轻CPU所做的工作时长、包括它的压力来达到优化的效果。
① 对象创建、调整、销毁放入子线程
② 预排版(布局计算、文本计算)放入到子线程去做
③ 上述2个方式主线程就可以有更多的时间去响应用户的交互
④ 预悬案(文本等异步绘制,图片编解码等) - GPU
① 纹理渲染(masksToBounds、圆角、阴影、蒙层都会涉及到GPU的离屏渲染)这样工作量就会加大基于这种情况我们就可以对GPU进行优化,可以依靠CPU的异步绘制来减轻GPU的离屏渲染压力。
② 视图混合(视图层级非常复杂,有多个视图层层叠加,那么GPU就要对这些视图进行合成,每一个像素点对应的像素值他需要做大量的计算,那么在一定程度上减轻视图层级的复杂性,那也可以减轻GPU的压力,也包括CPU的异步绘制来达到提交的位图本身就是层级少的视图,这样也可以减轻GPU的压力)。
绘制原理和异步绘制
UIView的绘制原理
当我们调用[UIView setNeedsDisplay]
的时候,实际并没有立刻发生当前视图的绘制工作,而是在之后的某个时机才会进行当前UIView的绘制工作,基于这个问题,这是为什么?
- 1、当调用这个方法
[UIView setNeedsDisplay]
这个方法 - 2、系统会自动调用
[view.layer setNeedsDisplay]
这个方法
相当于在view.layer
上面打上了一个标记,然后会在当前Runloop将要结束的时候调用view.layer
的[view.layer display]
然后才能进入到当前视图的真正的绘制工作。
[view.layer display]
中的方法会判断view.layer.delegate
是否会响应layer.delegate respondsTo @selector(displayLayer:)
方法,假如不响应这个方法,就会进入到系统的绘制流程,
如果说view.layer.delegate
响应displaylayer
方法,那么就会进入到异步绘制入口。
然后会在当前runloop即将结束的时候调用CALayer的display方法
系统绘制流程
在CAlayer内部会创建创建一个backingstore(CGContextRef),我们可以通过drawRect方法中可以通过上下文堆栈当中取出栈顶的(context)CGContextRef,也可以说是backingstore,然后判断是否有代理:
- 如果没有代理会调用[view.layer drawInContext:]方法。
- 如果有代理会调用view.layer.delegate drawLayer: inContext:方法,做当前视图的绘制工作,这是系统内部做的事情,然后在合适的时间回调给我们方法[view drawRect:]。
[view drawRect:]
默认是什么都不做,在系统的绘制的基础上再做其他的工作
无论是那种哪种方式最终都是CALayer来上传对应的backingstore到GPU,最后结束系统绘制的流程
实现异步绘制
异步绘制基于系统给开发者开放一个API[view.layer.delegate displayLayer:],如果我们实现了displayLayer:的方法,就可以进入到异步绘制的一个流程中。
- 代理负责生成对应的Bitmap位图
- 设置该bitmap作为layer.contents属性的值
原理流程
- 首先调用了[view setNeedsDisplay]的方法之后
- 在当前RunLoop将要结束的时候会由系统调用视图所对应layer的[view.layer display]
- 如果我们实现了这个代理方法displayLayer:,会通过子线程的切换在子线程中去做一个位图的绘制,此时主线程可以做其他的操作
子线程的主要工作
① CGBitmapContextCreate (core graphic的一个函数来创建一个位图的上下文)
② CoreGraphic API 通过这个API可以做当前UI控件的一些绘制工作
③ CGBitmapContextCreateImage 通过这个函数来根据上下文生成一张cgimage的图片 -
最后回到主队列,提交这个位图,设置当前视图的layer设置其setContents这样就完成了一个UI控件的异步绘制过程
离屏渲染
什么是离屏渲染?你是怎么理解的?
- On-Screen Rendering:
① 意为当前屏幕渲染,指的是GPU的渲染操作是在当前用于显示的屏幕缓冲区中进行的
② 在屏渲染是GPU层面中的一个概念 - Off-Screen Rendering:
① 意为离屏渲染,指的是GPU在当前屏幕缓冲区以外新开辟的一个缓冲区进行渲染操作
② 离屏渲染起源于GPU上面
当我们指定了UI视图的某些属性,标记为在未预合成之前不能用于当前屏幕上直接显示的时候,就会触发离屏渲染。
离屏渲染,何时会触发?
① 圆角(同时要设置maskToBounds一起使用的时候才能触发)
② 图层蒙版
③ 阴影
④ 光栅化
为何要避免离屏渲染?
触发离屏渲染的时候,会增加GPU的工作量,而增加了GPU的工作量很有可能导致了CPU和GPU工作耗时加起来的总耗时超出了16.7ms,那么就可能导致UI的卡顿和掉帧,所以我们需要避免离屏渲染。
总结
系统的UI事件传递机制是怎么样的?
hittest和PointInside的流程
使UITableView滚动更流程的方案或思路都有哪些?
- CPU:对于CPU在于子线程中对象的创建、调整和销毁、包括进行预排版以及图片的解码,采用异步绘制方案
- GPU:尽量避免离屏渲染,圆角之类的东西可以通过贝塞尔曲线或者直接贴图来做。尽量减轻视图层的复杂度,因为视图层的复杂程度,会影响到GPU的合成图片的耗时
什么是离屏渲染?
离屏渲染起源于GPU,在当前屏幕缓冲区之外新开辟一个缓存区进行渲染操作就是离屏渲染,
UIView和CALayer之间的关系是怎么样的?
- UIView是专门负责事件传递和视图响应的
- 而CALyaer全权负责视图的显示工作。
为什么要区分UIView和CALayer的职责划分
用到了六大设计原则的单一原则。