本文首要内容

一.UITableView相关
二.事情传递&视图呼应
三.UI图画显现原理
四.UI卡顿&掉帧
五.UIView的制作原理&异步制作
六.离屏烘托

全方位剖析iOS高级技术问题(一)之UI视图

一.UITableView相关

1、重用机制

cell = [tableView dequeueReusableCellWithIdentifier:identifier];

如图,虚线部分为屏幕所显现内容,其间A3-A5都完全显现在屏幕傍边,A2和A6只要一部分显现在屏幕中,假设当时UITableView是将屏幕向上滑动的中间状态成果,此时A1会被参加到重用池中(由于A1已经被滚出到屏幕之外了),再持续向上滑动UITableView,A7就会从重用池中依据指定的identifier标识符取出一个可重用的cell。假设A1-A7都用同一个标识符,A7就能够复用A1所创立的cell的内存或许说是空间,然后达到cell的重用(复用)的目的。

全方位剖析iOS高级技术问题(一)之UI视图

实例

字母索引
在工程中首要界说了类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,子线程没有记载主线程的删去操作,导致数据源没有同步的问题。数据源同步解决计划有两种:

全方位剖析iOS高级技术问题(一)之UI视图

(1)并发拜访、数据复制
比方当时存在主线程和子线程,在主线程中进行数据复制,给子线程运用。在子线程中进行新数据的网络恳求、数据解析、预排版等,与此一同,在主线程中删去一行数据,reload UI后此数据UI消失,接着进行其他的作业。接着,子线程回来恳求的成果,并再进行一次reload UI。整个进程中为了确保数据源的同步,需要注意两步操作:一是在主线程删去数据时记载此删去操作;二是在子线程回来恳求数据之前同步进行删去操作,然后确保子线程在回来数据时的数据源和主线程数据源同步

时序图:

全方位剖析iOS高级技术问题(一)之UI视图

(2)串行拜访
串行拜访的原理是经过GCD拓荒一条串行行列,把数据操作的使命放到串行行列上面操作,这样能够同步主线程和子线程对数据源的操作。 时序图:

全方位剖析iOS高级技术问题(一)之UI视图

  1. 并行拜访数据复制:望文生义是要对操作进行记载并复制到子线程中,这样需要拓荒内存空间,对内存消耗较大。
  2. 串行拜访:当线程有耗时操作时,就会导致对数据源的增删改查操作有延时。

二.事情传递&呼应

问题一:UIView和CALayer相关问题
UIView包含layer、backgroundColor特点,layer为CALayer类型,backgroundColor实践是对CALayer中同名特点的包装,UIView的显现部分由CALayer中contents特点决议的,contents中的backing store实践是一个bitmap位图

UIView为CALayer供给内容,以及担任处理接触等事情,参与事情呼应链; CALayer担任显现内容contents; 如上符合单一责任的规划准则。

全方位剖析iOS高级技术问题(一)之UI视图

问题二:事情传递机制
如图,View A中包含View B1、View B2,View B2中包含View C1,View C2既包含View C1的一部分,由包含View B2的一部分,View C1中包含View D。当点击View C2的空白区域时,体系怎么找到事情呼应者为View C2?

全方位剖析iOS高级技术问题(一)之UI视图

事情传递和如下2个办法相关

// 哪个视图呼应事情回来哪个  
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;     
// 点击方位是否在当时视图范围  
-(BOOL)pointInside(CGPoint)point withEvent:(UIEvent *)event; 

(1)事情传递流程

当用户点击屏幕的某个方位,该事情会被传递给UIApplicationUIApplication又传递给当时的UIWindow,UIWindow会经过hitTest:WithEvent办法回来呼应的视图。hi tTest:WithEvent:办法内部经过pointInside:withEvent:办法判别点击point是否在当时UIWindow范围内,假设在,则会遍历其间的一切子视图SubViews来查找终究呼应此事情的视图,遍历方式为倒序遍历,即终究增加到UIWindow的视图最优先被遍历到,依次遍历,能够看作是递归调用。每个UIView中又都会调用其对应hitTest:WithEvent:办法,终究回来呼应视图hit,假设hit有值,则hit视图就作为该事情的呼应视图被回来,假设hit没有值,但在当时UIWindow范围内,则当时UIWindow作为事情的呼应视图。

全方位剖析iOS高级技术问题(一)之UI视图

(2)hitTest:WithEvent:体系内部完成

首要在hitTest:WithEvent:办法内部先判别当时视图的hidden特点、是否可交互、透明度是否大于0.01。假设该视图不满足上述3个条件,则回来nil,当时视图不作为事情的呼应视图,当时视图的父视图持续遍历其他的子视图;假设该视图没有躲藏、用户可交互、透明度大于0.01,则会经过pointInside:WithEvent:办法判别点击的点是否在当时视图范围内,假设不在,则同样回来nil,当时视图仍不作为事情的呼应者;假设在,则会经过倒序遍历当时视图的子视图,调用其子视图对应的hitTest:WithEvent:办法,假设某个视图回来了事情呼应视图,则该回来的视图被作为事情的呼应者,反之则持续遍历判别。假设遍历完后没有任何视图呼应此事情,由于此事情点击的范围在当时视图范围内,则将当时视图作为事情呼应者回来。

全方位剖析iOS高级技术问题(一)之UI视图

(3)视图事情呼应流程

上述叙述了视图事情的传递流程,当视图事情传递后,终究事情由谁来呼应呢,这就触及视图的呼应链、呼应链的机制和流程。 如图,页面存在一个UILabel一个UITextField、一个UIButton,实线箭头表示下一个呼应者。

全方位剖析iOS高级技术问题(一)之UI视图

视图事情呼应链相关的办法有:

- (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依然不对事情进行处理,则会忽略此事情

全方位剖析iOS高级技术问题(一)之UI视图

三.UI图画显现原理

CPUGPU经过总线连接起来,在CPU中输出的成果往往是一个位图,经由总线在适宜的时刻点传给GPUGPU拿到位图会进行位图图层的烘托包含纹路的组成,然后将成果放到帧缓存区域frame Buffer中,由视图控制器依据VSync信号在指定时刻之内去提取对应帧缓存区域中屏幕显现内容,终究显现在手机屏幕上。

全方位剖析iOS高级技术问题(一)之UI视图

1、图画显现原理

首要创立一个UIView控件后,它的显现部分是由CALayer担任的,CALayer中有一个con tents特点,便是要制作到屏幕的位图。比方要用UILabel显现”Hello world”,则contents的内容便是关于”Hello world”的文字位图,体系会在适宜的时分回调drawRect办法,在此基础上还能够制作一些自界说的内容。制作好的位图会经过Core Animation框架提交给GPU部分的OpenGL烘托管线,进行位图的烘托包含纹路的组成,终究显现在屏幕之上。

全方位剖析iOS高级技术问题(一)之UI视图

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没有完成下一帧画面的组成,导致卡顿/掉帧

全方位剖析iOS高级技术问题(一)之UI视图

2、滑动优化计划

  • CPU

    1.目标创立、调整、毁掉放到子线程中,节省时刻;
    2.预排版,将布局核算、文本核算放到子线程中,让主线程有更多时刻呼应用户的交互;
    3.预烘托,对文本等进行异步制作,对图片等进行编解码。

  • GPU

    1.纹路烘托,尽量防止离屏烘托,减少纹路烘托用时;
    2.视图混合,减少视图层级的复杂度,异步制作等。

五.UIView的制作原理&异步制作

1、UIView的制作原理

当UIView调用setNeedsDisplay后并没有立刻开始当时视图制作作业,而是在之后的某一机遇才进行制作。当UIView调用setNeedsDisplay时,体系会当即调用view的layer同名setNeeds Display,相当于在当时视图layer上打了一个脏标记,在当时runloop即将完毕时才会调用CALayerdisplay办法,然后进入到当时视图真实的制作作业流程中。CALayer的display办法内部会先判别layer的delegate是否呼应displayLayer:办法,假设不呼应就进入到体系制作流程,假设呼应,就相当于供给了异步制作入口。

全方位剖析iOS高级技术问题(一)之UI视图

体系的制作流程
CALayer内部会创立一个backing store,能够理解为CGConetextRef,然后判别layer是否有delegate,假设没有署理,CALayer调用drawInContext办法,假设有署理,就调用署理的drawLayer:inContext:办法,做视图的制作作业,此进程存在于体系内部,再在适宜的机遇供给drawRect:回调办法做其他制作作业。终究经过CALayer上传backing store(位图)到GPU,完毕体系默认的制作流程。

全方位剖析iOS高级技术问题(一)之UI视图

2、异步制作

假设layer的delegate呼应displayLayer办法,就能够进入异步制作的流程中,署理担任生成对应的bitmap,设置该bitmap作为layer.contents特点的值

[layer.delegate displayLayer]

如图,存在主行列和大局并发行列,在某个机遇,View调用了setNeedsDispla y办法,在当时runloop即将完毕时体系调用视图对应CALayerdisplay办法,假设View.layer的署理完成了displayLayer办法,再经过线程的切换,在子线程中CGBitmapContextCreate创立绘图的上下文,经过CoreGraphic API做当时UI控件的制作作业CGBitmapContextCreatelmage依据当时所制作的上下文制作一张image图片。然后回到主行列中提交这个位图,设置给CALayerContents。这便是异步制作的整个进程。

全方位剖析iOS高级技术问题(一)之UI视图

六.离屏烘托

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之间的关系是怎样的;

有任何问题,欢迎各位谈论指出!觉得博主写的还不错的费事点个赞喽