本文首要内容
一.UITableView相关
二.事情传递&视图呼应
三.UI图画显现原理
四.UI卡顿&掉帧
五.UIView的制作原理&异步制作
六.离屏烘托
一.UITableView相关
1、重用机制
cell = [tableView dequeueReusableCellWithIdentifier:identifier];
如图,虚线部分为屏幕所显现内容,其间A3-A5都完全显现在屏幕傍边,A2和A6只要一部分显现在屏幕中,假设当时UITableView是将屏幕向上滑动的中间状态成果,此时A1会被参加到重用池中(由于A1已经被滚出到屏幕之外了),再持续向上滑动UITableView,A7就会从重用池中依据指定的identifier标识符取出一个可重用的cell。假设A1-A7都用同一个标识符,A7就能够复用A1所创立的cell的内存或许说是空间,然后达到cell的重用(复用)的目的。
实例
字母索引条
在工程中首要界说了类ViewReusePool
代表视图的重用池,用来完成重用机制,IndexedTableView
是UITableView的带索引条的子类,终究在ViewController.m
中运用带索引条的IndexedTableView
。
如下为ViewReusePool.h
,关于重用池的完成计划。首要创立承继于NSObject
的类V iewReusePool
,用这个类来完成重用机制。其间界说了3个办法dequeueReusableView
办法是从重用池傍边取出一个可重用的view作为此办法的回来值,addUsingView:
办法是向重用池傍边增加一个视图,reset
办法是重置办法,将当时运用中的视图悉数移动到可重用行列傍边。
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
// 完成重用机制的类
@interface ViewReusePool : NSObject
// 从重用池傍边取出一个可重用的view
- (UIView *)dequeueReusableView;
// 向重用池傍边增加一个视图
- (void)addUsingView:(UIView *)view;
// 重置办法,将当时运用中的视图移动到可重用行列傍边
- (void)reset;
@end
ViewReusePool.m
有对应办法的完成。在完成中创立了ViewReusePool
类的扩展,并在其间增加了两个成员变量:waitUseQueue
代表等候运用的行列,usingQueue
代表运用中的行列,都是用调集来完成。在初始化进程中创立等候运用的行列和运用中的行列。接着对声明的3个办法进行完成。
dequeueReusableView
是从重用池傍边取出一个可重用的view
。首要从调集_waitUseQueue
中取出一个目标,假设取出的目标为nil
,阐明当时没有可重用的view
,回来nil
;假设取出的目标存在,则先要把等候重用行列中的这个视图移除,再将该视图增加到正在运用的行列傍边,回来这个视图作为可重用的视图。
addUsingView:
是向重用池傍边增加一个视图。首要进行反常判别,假设增加的view
为空,什么也不做,直接回来;假设有值,就把view
增加到运用中的行列傍边。
reset
是重置办法,将当时运用中的视图移动到可重用行列傍边。首要声明局部变量view
,然后遍历运用行列中的视图,假设存在,将其从运用中行列移除,参加到等候运用的行列傍边,即将运用中行列的视图悉数移动到等候运用的行列傍边
。
#import "ViewReusePool.h"
@interface ViewReusePool()
// 等候运用的行列
@property(nonatomic, strong) NSMutableSet *waitUseQueue;
// 运用中的行列
@property(nonatomic, strong) NSMutableSet *usingQueue;
@end
@implementation ViewReusePool
// 初始化
- (instancetype)init {
self = [self init];
if (self) {
// 1.创立等候中的行列和运用中的行列
_waitUseQueue = [NSMutableSet set];
_usingQueue = [NSMutableSet set];
}
return self;
}
#pragma mark - 从重用池傍边取出一个可重用的view
- (UIView *)dequeueReusableView {
// 2.1 从调集_waitUseQueue中取出一个目标
UIView *view = [_waitUseQueue anyObject];
// 2.2-1 假设取出的目标为nil,阐明当时没有可重用的view
if (view == nil) {
return nil;
}else {
// 2.2-2假设取出的目标存在,则先要把等候重用行列中的这个视图移除,再将该视图增加到正在运用的行列傍边,回来这个视图作为可重用的视图
// 进行行列移动
[_waitUseQueue removeObject:view];
[_usingQueue addObject:view];
return view;
}
}
#pragma mark - 向重用池傍边增加一个视图
- (void)addUsingView:(UIView *)view {
// 3.1假设增加的view为空,return
if (view == nil) {
return;
}
// 3.2假设增加的视图不为空,增加视图到运用中的行列
[_usingQueue addObject:view];
}
#pragma mark - 重置办法,将当时运用中的视图移动到可重用行列傍边
- (void)reset {
// 4.1界说局部变量
UIView *view = nil;
// 4.2 将运用中行列中的视图目标悉数移动到等候运用行列傍边
while ((view = [_usingQueue anyObject])) {
// 4.2-1 从运用中的行列移除
[_usingQueue removeObject:view];
// 4.2-2 参加等候运用的行列
[_waitUseQueue addObject:view];
}
}
界说类IndexedTableView
是UITableView的带索引条的子类。IndexedTableView.h
中内容如下,界说了一个数据源协议,来获取一个tableView的字母索引条数据:
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
// 数据源协议
@protocol IndexedTableViewDataSource <NSObject>
// 获取一个tableView的字母索引条数据的办法
- (NSArray <NSString *> *)indexTitlesForIndexTableView:(UITableView *)tableView;
@end
@interface IndexedTableView : UITableView
@property (nonatomic, weak) id <IndexedTableViewDataSource>indexedDataSource;
@end
IndexedTableView.m
中对应的完成,界说一个容器containerView
用于装载一切的字母索引控件,界说一个ViewReusePool
类型的重用池reusePool
,完成重用机制。然后重写reloadData
办法,懒加载字母索引容器和重用池,将重用池重置,行将一切视图标记为可重用状态。在加载字母索引条进程中,先判别当时数据源是否呼应数据源办法indexTitlesForIndexTableView:
,假设呼应则经过数据源办法向数据源供给方获取字母索引数组,获取字母索引条的显现内容,判别字母索引条是否为空,为空时,不显现索引条。接着从重用池傍边取一个button
出来,假设没有可重用的button
从头创立一个,一同注册button
到重用池中,假设有可重用的button
则重用。然后将button
增加到父视图中。
#import "IndexedTableView.h"
#import "ViewReusePool.h"
@interface IndexedTableView() {
// 容器:装载一切的字母索引控件
UIView *containerView;
// 重用池
ViewReusePool *reusePool;
}
@end
@implementation IndexedTableView
- (void)reloadData {
[super reloadData];
// 懒加载字母索引容器
if (containerView == nil) {
containerView = [[UIView alloc] initWithFrame:CGRectZero];
containerView.backgroundColor = [UIColor whiteColor];
//防止索引条随着table翻滚
[self.superview insertSubview:containerView aboveSubview:self];
}
// 懒加载重用池
if (reusePool == nil) {
reusePool = [[ViewReusePool alloc] init];
}
// 标记一切视图为可重用状态
[reusePool reset];
// reload字母索引条
[self reloadIndexedBar];
}
- (void)reloadIndexedBar {
// 获取字母索引条的显现内容
NSArray <NSString *> *arrayTitles = nil;
// 判别当时数据源是否呼应数据源办法
if ([self.indexedDataSource respondsToSelector:@selector(indexTitlesForIndexTableView:)]) {
// 经过数据源办法向数据源供给方获取字母索引数组
arrayTitles = [self.indexedDataSource indexTitlesForIndexTableView:self];
}
// 判别字母索引条是否为空
if (!arrayTitles || arrayTitles.count <= 0) { // 没有字母索引条
[containerView setHidden:YES];
return;
}
NSUInteger count = arrayTitles.count;
CGFloat buttonWidth = 60;
CGFloat buttonHeight = self.frame.size.height / count;
for (int i = 0; i < [arrayTitles count]; i++) {
NSString *title = [arrayTitles objectAtIndex:i];
// 从重用池傍边取一个Button出来
UIButton *button = (UIButton *)[reusePool dequeueReusableView];
// 假设没有可重用的Button从头创立一个
if (button == nil) {
button = [[UIButton alloc] initWithFrame:CGRectZero];
button.backgroundColor = [UIColor blueColor];
// 注册button到重用池中
[reusePool addUsingView:button];
NSLog(@"新创立了一个button");
} else {
NSLog(@"button重用了");
}
// 增加button到父视图控件
[containerView addSubview:button];
[button setTitle:title forState:UIControlStateNormal];
[button setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
// 设置button的坐标
[button setFrame:CGRectMake(0, i * buttonHeight, buttonWidth, buttonHeight)];
}
[containerView setHidden:NO];
containerView.frame = CGRectMake(self.frame.origin.x + self.frame.size.width - buttonWidth, self.frame.origin.y, buttonWidth, self.frame.size.height);
}
@end
在ViewController.m
中运用带索引条的IndexedTableView
。界说带有索引条的tableView
、改写索引条的按钮button
、数据源dataSource
。接着创立IndexedTableView
类型的tableView
,设置tableVie w
的索引数据源,创立改写按钮button
,初始化数据源dataSource
。然后恪守数据源协议回来索引条数据,完成UITableView
数据源和署理协议。
#import "ViewController.h"
#import "IndexedTableView.h"
@interface ViewController ()<UITableViewDataSource,UITableViewDelegate,IndexedTableViewDataSource> {
// 带有索引条的tableView
IndexedTableView *tableView;
// 改写button
UIButton *button;
// 数据源
NSMutableArray *dataSource;
}
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 创立一个TableView
tableView = [[IndexedTableView alloc] initWithFrame:CGRectMake(0, 60, self.view.frame.size.width, self.view.frame.size.height - 60) style:UITableViewStylePlain];
tableView.delegate = self;
tableView.dataSource = self;
// 设置tableView的索引数据源
tableView.indexedDataSource = self;
[self.view addSubview:tableView];
// 改写按钮
button = [[UIButton alloc] initWithFrame:CGRectMake(0, 20, self.view.frame.size.width, 40)];
button.backgroundColor = [UIColor redColor];
[button setTitle:@"readTable" forState:UIControlStateNormal];
[button addTarget:self action:@selector(doAction:) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:button];
// 数据源
dataSource = [NSMutableArray array];
for (int i = 0; i < 100; i++) {
[dataSource addObject:@(i+1)];
}
}
#pragma mark **- IndexedTableViewDataSource**
- (NSArray<NSString *> *)indexTitlesForIndexTableView:(UITableView *)tableView {
// 奇数次调用回来6个字母,偶数次调用回来11个字母
static BOOL change = NO;
if (change) {
change = NO;
return @[@"A",@"B",@"C",@"D",@"E",@"F",@"G",@"H",@"I",@"J",@"K"];
} else {
change = YES;
return @[@"A",@"B",@"C",@"D",@"E",@"F"];
}
}
#pragma mark **- UITableViewDataSource**
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return [dataSource count];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *identifier = @"reuseID";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier];
// 假设重用池傍边没有可重用的cell,则创立一个cell
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier];
}
// 案牍设置
cell.textLabel.text = [[dataSource objectAtIndex:indexPath.row] stringValue];
return cell;
}
#pragma mark **- UITableViewDelegate**
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return 40;
}
- (void)doAction:(UIButton *)button {
NSLog(@"reloadData");
[tableView reloadData];
}
@end
2、数据源同步问题
当咱们开启子线程处理数据源的时分,主线程的操作并没有记载在子线程中。这样就会导致子线程处理完数据回来主线程改写UI后数据紊乱。
例如,在tableView中显现新闻数据和广告,子线程进行网络恳求,数据解析等操作的一同,在主线程中删去了广告,并更新UI。然后子线程处理完数据,终究也在主线程中更新UI,子线程没有记载主线程的删去操作,导致数据源没有同步的问题。数据源同步解决计划有两种:
(1)并发拜访、数据复制
比方当时存在主线程和子线程,在主线程中进行数据复制,给子线程运用。在子线程中进行新数据的网络恳求、数据解析、预排版等,与此一同,在主线程中删去一行数据,reload UI后此数据UI消失,接着进行其他的作业。接着,子线程回来恳求的成果,并再进行一次reload UI。整个进程中为了确保数据源的同步,需要注意两步操作:一是在主线程删去数据时记载此删去操作;二是在子线程回来恳求数据之前同步进行删去操作,然后确保子线程在回来数据时的数据源和主线程数据源同步
。
时序图:
(2)串行拜访
串行拜访的原理是经过GCD拓荒一条串行行列,把数据操作的使命放到串行行列上面操作,这样能够同步主线程和子线程对数据源的操作。
时序图:
- 并行拜访数据复制:望文生义是要对操作进行记载并复制到子线程中,这样需要拓荒内存空间,对内存消耗较大。
- 串行拜访:当线程有耗时操作时,就会导致对数据源的增删改查操作有延时。
二.事情传递&呼应
问题一:UIView和CALayer相关问题
UIView包含layer、backgroundColor特点,layer为CALayer类型,backgroundColor实践是对CALayer中同名特点的包装,UIView的显现部分由CALayer中contents特点决议的,contents中的backing store实践是一个bitmap位图。
UIView为CALayer供给内容,以及担任处理接触等事情,参与事情呼应链; CALayer担任显现内容contents; 如上符合单一责任的规划准则。
问题二:事情传递机制
如图,View A中包含View B1、View B2,View B2中包含View C1,View C2既包含View C1的一部分,由包含View B2的一部分,View C1中包含View D。当点击View C2的空白区域时,体系怎么找到事情呼应者为View C2?
事情传递和如下2个办法相关
:
// 哪个视图呼应事情回来哪个
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;
// 点击方位是否在当时视图范围
-(BOOL)pointInside(CGPoint)point withEvent:(UIEvent *)event;
(1)事情传递流程
当用户点击屏幕
的某个方位,该事情会被传递给UIApplication
,UIApplication
又传递给当时的UIWindow
,UIWindow
会经过hitTest:WithEvent
办法回来呼应的视图。hi tTest:WithEvent:
办法内部经过pointInside:withEvent:
办法判别点击point
是否在当时UIWindow
范围内,假设在,则会遍历其间的一切子视图SubViews
来查找终究呼应此事情的视图,遍历方式为倒序遍历,即终究增加到UIWindow
的视图最优先被遍历到,依次遍历,能够看作是递归调用。每个UIView
中又都会调用其对应hitTest:WithEvent:
办法,终究回来呼应视图hit
,假设hit
有值,则hit
视图就作为该事情的呼应视图被回来,假设hit
没有值,但在当时UIWindow
范围内,则当时UIWindow
作为事情的呼应视图。
(2)hitTest:WithEvent:体系内部完成
首要在hitTest:WithEvent:
办法内部先判别当时视图的hidden特点、是否可交互、透明度是否大于0.01。假设该视图不满足上述3个条件,则回来nil,当时视图不作为事情的呼应视图,当时视图的父视图持续遍历其他的子视图;假设该视图没有躲藏、用户可交互、透明度大于0.01,则会经过pointInside:WithEvent:
办法判别点击的点是否在当时视图范围内,假设不在,则同样回来nil,当时视图仍不作为事情的呼应者;假设在,则会经过倒序遍历当时视图的子视图,调用其子视图对应的hitTest:WithEvent:
办法,假设某个视图回来了事情呼应视图,则该回来的视图被作为事情的呼应者,反之则持续遍历判别。假设遍历完后没有任何视图呼应此事情,由于此事情点击的范围在当时视图范围内,则将当时视图作为事情呼应者回来。
(3)视图事情呼应流程
上述叙述了视图事情的传递流程,当视图事情传递后,终究事情由谁来呼应呢,这就触及视图的呼应链、呼应链的机制和流程
。
如图,页面存在一个UILabel
、一个UITextField
、一个UIButton
,实线箭头表示下一个呼应者。
视图事情呼应链相关的办法有:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
例如,当点击View C2
的空白处时,事情由谁来呼应呢?首要由View C2
接收事情,假设它不处理,就会把事情传递给View B2
,假设View B2
还不呼应这个事情,View B2
会经过呼应链将事情传递给它的父视图View A
,假设还不呼应,则会沿着呼应链一直向上传递,直到传递到UIApplicationDelegate依然不对事情进行处理,则会忽略此事情
。
三.UI图画显现原理
CPU
和GPU
经过总线连接起来,在CPU
中输出的成果往往是一个位图,经由总线在适宜的时刻点传给GPU
,GPU
拿到位图会进行位图图层的烘托包含纹路的组成,然后将成果放到帧缓存区域frame Buffer
中,由视图控制器依据VSync信号
在指定时刻之内去提取对应帧缓存区域中屏幕显现内容,终究显现在手机屏幕上。
1、图画显现原理
首要创立一个UIView控件后,它的显现部分是由CALayer
担任的,CALayer
中有一个con tents
特点,便是要制作到屏幕的位图。比方要用UILabel显现”Hello world”,则contents
的内容便是关于”Hello world”的文字位图,体系会在适宜的时分回调drawRect
办法,在此基础上还能够制作一些自界说的内容。制作好的位图会经过Core Animation
框架提交给GPU
部分的OpenGL
烘托管线,进行位图的烘托包含纹路的组成,终究显现在屏幕之上。
2、CPU和GPU的作业
CPU作业:
Layout: UI布局和文本核算。如对应每一个控件frame的设置、文字、size等的核算。
Display: 制作进程。如drawRect办法.
Prepare:图片编解码。如UIImageView,图片无法直接显现,需要对图片解码。
Commit: 提交位图。
GPU烘托管线:
极点上色:对位图的处理。
图元安装
光栅化
片段上色
片段处理
将处理好的位图提交到帧缓冲区FrameBuffer中
四.UI卡顿&掉帧
1、UI卡顿、掉帧的原因
一般说页面滑动的流畅性是60FPS,指每一秒中有60帧画面更新,人眼看到的便是流畅的作用。基于此,相当于每隔六十分之一秒,即16.7ms就要发生一帧画面,在这16.7ms的时刻内需要CPU和GPU共同协作发生这一帧数据,比方CPU文本布局、UI核算、视图制作、图片解码等,把发生的位图提交给GPU,GPU再进行图层组成、纹路烘托等,准备好下一帧画面,鄙人一帧的VSync信号到来时显现画面。假设CPU完成作业运用的时长较长时,留给GPU的时刻就会较少,完成作业所用的总时刻就或许会超越16.7ms,所以鄙人一帧VSync到来时没有准备好当下画面,由此发生掉帧,看到的作用便是滑动卡顿。总结来说便是:在规定的16.7ms之内,鄙人一帧VSync信号到来之前,CPU和GPU没有完成下一帧画面的组成,导致卡顿/掉帧
。
2、滑动优化计划
-
CPU
1.目标创立、调整、毁掉放到子线程中,节省时刻;
2.预排版,将布局核算、文本核算放到子线程中,让主线程有更多时刻呼应用户的交互;
3.预烘托,对文本等进行异步制作,对图片等进行编解码。 -
GPU
1.纹路烘托,尽量防止离屏烘托,减少纹路烘托用时;
2.视图混合,减少视图层级的复杂度,异步制作等。
五.UIView的制作原理&异步制作
1、UIView的制作原理
当UIView调用setNeedsDisplay后并没有立刻开始当时视图制作作业,而是在之后的某一机遇才进行制作
。当UIView调用setNeedsDisplay
时,体系会当即调用view的layer同名setNeeds Display
,相当于在当时视图layer
上打了一个脏标记,在当时runloop即将完毕时才会调用CALayer
的display
办法,然后进入到当时视图真实的制作作业流程中。CALayer的display
办法内部会先判别layer的delegate
是否呼应displayLayer:
办法,假设不呼应就进入到体系制作流程,假设呼应,就相当于供给了异步制作入口。
体系的制作流程
CALayer内部会创立一个backing store
,能够理解为CGConetextRef
,然后判别layer是否有delegate,假设没有署理,CALayer调用drawInContext办法,假设有署理,就调用署理的drawLayer:inContext:办法,做视图的制作作业,此进程存在于体系内部,再在适宜的机遇供给drawRect:
回调办法做其他制作作业。终究经过CALayer上传backing store
(位图)到GPU
,完毕体系默认的制作流程。
2、异步制作
假设layer的delegate呼应displayLayer办法,就能够进入异步制作的流程中,署理担任生成对应的bitmap,设置该bitmap作为layer.contents特点的值
。
[layer.delegate displayLayer]
如图,存在主行列和大局并发行列,在某个机遇,View
调用了setNeedsDispla y
办法,在当时runloop
即将完毕时体系调用视图对应CALayer
的display
办法,假设View.layer
的署理完成了displayLayer
办法,再经过线程的切换,在子线程中CGBitmapContextCreate
创立绘图的上下文,经过CoreGraphic API
做当时UI控件的制作作业CGBitmapContextCreatelmage
依据当时所制作的上下文制作一张image图片。然后回到主行列中提交这个位图,设置给CALayer
的Contents
。这便是异步制作的整个进程。
六.离屏烘托
1、On-Screen Rendering
意为当时屏幕烘托,指的是GPU
的烘托操作是在当时用于显现的屏幕缓冲区中进行;
2、Off-Screen Rendering
意为离屏烘托,指的是GPU
在当时屏幕缓冲区以外新拓荒一个缓冲区进行烘托操作。
3、什么状况会触发离屏烘托?
当咱们指定了UI视图的某些特点,标记为它在未预组成之前不能用于当时屏幕上直接显现时,就会触发离屏烘托。有如下状况:
- 圆角(当和maskToBounds一同运用时)
- 图层蒙版
- 阴影
- 光栅化
4、为何要防止离屏烘托?
- 在触发离屏烘托时,会增加GPU的作业量,这样很有或许导致CPU和GPU作业加起来的时刻超越16.7ms,就会导致UI的卡顿和掉帧,所以要尽量防止离屏烘托。
- 离屏烘托会创立新的烘托缓冲区,还要进行上下文切换。
本文总结
1.体系的UI事情传递机制是怎样的(hitTestWithEvent、pointInside);
2.使UITableView翻滚更流畅得计划或思路都有哪些(CPU、GPU方面);
3.什么是离屏烘托(GPU),为什么要防止离屏烘托;
4.UIView和CALayer之间的关系是怎样的;
有任何问题,欢迎各位谈论指出!觉得博主写的还不错的费事点个赞喽