Cocoa Touch
layoutIfNeeded 和 setNeedsLayout 的区别
- 如果不是为了更新动画可以无脑用
setNeedsLayout
setNeedsLayout
没有动画能力,layoutIfNeeded
用来动画。因此,layoutIfNeeded
需要放在动画的 block 里面来触发动画。
通过修改约束值来执行动画
UIView *v1 = [[UIView alloc] init];
v1.backgroundColor = [UIColor redColor];
[self.view addSubview:v1];
v1.translatesAutoresizingMaskIntoConstraints = NO;
NSLayoutConstraint *c1 = [NSLayoutConstraint constraintWithItem:v1 attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeTop multiplier:1 constant:100];
NSLayoutConstraint *c2 = [NSLayoutConstraint constraintWithItem:v1 attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeLeading multiplier:1 constant:30];
NSLayoutConstraint *c3 = [NSLayoutConstraint constraintWithItem:v1 attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1 constant:100];
NSLayoutConstraint *c4 = [NSLayoutConstraint constraintWithItem:v1 attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1 constant:30];
[self.view addConstraints:@[c1, c2, c3, c4]];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[UIView animateWithDuration:1 animations:^{
c1.constant = 200;
[self.view layoutIfNeeded];
}];
});
layoutSubviews 调用时机
- init 初始化不会触发 layoutSubviews
- addSubview 会触发 layoutSubviews
- 设置 view 的 frame 会触发 layoutSubviews
- 滚动一个 UIScrollView 会触发 layoutSubviews
- 旋转屏幕会触发父 view 上的layoutSubviews。
- 改变一个 view 的大小也会触发父 view 上的 layoutSubviews
- 调用 setNeedLayout 和 layoutIfNeeded 会触发
UIView 和 CALayer 的关系和区别
- UIView 是被 CALayer 支持的
- UIView 的绘制需要 CPU 在主线程进行,CALayer 的绘制是由 GPU 在子线程i进行。
- UIView 可以响应事件,有响应链,CALayer 没有。
- UIView 的显示属性修改不会产生隐式动画,CALayer 的属性修改会产生默认 0.25s 的隐式动画。
CALayer 动画
CALayer *layer = [CALayer layer];
layer.frame = CGRectMake(30, 100, 100, 60);
layer.backgroundColor = [UIColor redColor].CGColor;
[self.view.layer addSublayer:layer];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[CATransaction setDisableActions:YES]; // 禁用隐式动画
[CATransaction setAnimationDuration:3]; // 设置持续时间
layer.position = CGPointMake(300, 100);
});
UIView 动画
[UIView animationWithDuration:delay:options:animations:completion]
方法中的 animations block 会立即调用不会等待延迟 dealy 时间后再调用。并且在 animations 执行结束后这个方法才会返回。
/**
begin
animation start
end
animation complete
*/
UIView *v = [[UIView alloc] initWithFrame:CGRectMake(10, 100, 100, 60)];
v.backgroundColor = [UIColor redColor];
[self.view addSubview:v];
NSLog(@"begin");
[UIView animateWithDuration:1 delay:3 options:UIViewAnimationOptionCurveEaseInOut animations:^{
NSLog(@"animation start");
v.frame = CGRectMake(300, 100, 100, 60);
} completion:^(BOOL finished) {
NSLog(@"animation complete");
}];
NSLog(@"end");
drawRect: 什么情况下会被触发
- 调用时机:
layoutView
->viewDidLoad
->drawRect:
- 如果在 UIView 初始化时没有设置 rect 大小,将直接导致 drawRect: 不被自动调用
- 通过设置 contentMode 属性值为
UIViewContentModeRedraw
,那么将在每次设置或更改frame的时候自动调用drawRect:
- 直接调用
setNeedDisplay
或者setNeedsDisplayInRect:
会触发 drawRect:,条件是view 当前的 rect 不能为空。 - 该方法在调用
sizeThatFits
后被调用,所以可以先调用 sizeToFit 计算出 size,然后系统自动调用 drawRect: 方法。
渲染
渲染过程
界面从上到下渲染,每开始一行渲染会发出水平同步信号(HSync)。
从上到下渲染出来就是一帧画面。
一帧渲染完成开始下一帧前会发出垂直同步信号(VSync)。
显示器按照一定时间间隔刷新屏幕(界面)这个频率就是 VSync 信号产生的频率 —— 刷新率。平时说的 60Hz 和 120Hz 表示的就是每秒刷新 60 次和 120 次。
CPU 和 GPU 协同工作
CPU 负责计算显示内容并将计算结果提交到 GPU,GPU 在渲染完成后将结果放入帧缓冲区。随后视频控制器根据 VSync 信号从缓冲区逐帧读取数据并显示到屏幕上。
双(三)缓冲区:如果只有一个缓冲区的话,GPU 只有等到视频控制器将内容读取完才能把下一帧放入缓冲区,这样延迟太大、画面就会卡顿。所以引入了多缓冲区机制,GPU 渲染完一帧放入缓冲区内,让视频控制器读取,当下一帧渲染完成后,GPU 会直接把视频控制器的指针指向第二个缓冲区,这样效率可以得到很大提升。
GPU 的垂直(Vsync)同步:但是双缓存机制会带来新的问题,如果视频控制器还没有读取完前一帧的内容时 GPU 就切换回来了,这样就会出现在一个画面同时出现两帧的内容——画面撕裂。为了解决这个问题 GPU 引入了垂直同步。当开启垂直同步后,GPU 会等待显示器的 VSync 信号发出后,才进行新的一帧渲染和缓冲区更新。但是这样也带来了其他问题。
iOS 系统的垂直同步是保持开启的,所以垂直同步机制是造成画面卡顿的根本原因。当 CPU 和 GPU 的渲染速度慢于屏幕的刷新速度时,就会造成掉帧卡顿。
CPU 消耗资源的原因和解决方案
在界面渲染过程中 CPU 主要的工作:对象创建、布局计算、图片解码、文本绘制,如果这些工作量过大会造成界面卡顿。
- 对象创建:使用更轻量的对象比如 CALayer 代替 UIView;尽量使用缓存池复用对象。
- 对象调整:避免调整视图层次、添加和移除视图。
- 局部计算:视图布局计算是 App 中最为常见的消耗 CPU 的地方,尽量在后台提前计算好布局并进行缓存。
- 文本计算:减少文本宽高计算。
[NSAttributedString boundingRectWithSize:options:context:]
[NSAttributedString drawWithRect:options:context:]
- 文本渲染:屏幕上看到的所有文本内容控件,包括 UIWebView,在底层都是通过 CoreText 排版绘制为 Bitmap 显示的。常见的文本控件(UILabel、UITextView 等)其排版和绘制都是在主线程进行的,当显示大量文本时,CPU 的压力会非常大。解决方法: 自定义文本控件,使用 TextKit 或者 CoreText 对文本异步绘制。
- 图片的解码:使用 UIImage 或 CGImageSource 创建图片时不会立即解码,设置到 UIImageView 或者 CALayer.contents 中且 CALayer 被提交到 GPU 前,CGImage 中的数据才会得到解码。这一步是发生在主线程的,并且不可避免。解决方法:后台线程先把图片绘制到 CGBitmapContext 中,然后从 Bitmap 直接创建图片(SDWebImage 就是这样做的)。
GPU 消耗资源的原因和解决方案
- GPU 干的事情:接受 CPU 提交的纹理(Texture)和顶点描述(三角形),应用变换(transform)、混合并渲染,然后输出到缓冲。
- 纹理的渲染:较短时间显示大量图片(TableView 存在大量图片快速滚动)。解决方案:减少短时间内大量图片的显示,可以在滚动过程中先不显示图片,等到停止滚动的过程中再把图片显示出来。
- 视图的混合:视图结构很复杂时会拖累 GPU 的渲染速度,应尽量减少视图层级。把多个图片合并成一张显示。
- 图形的生成:减少圆角,使用圆角图片覆盖或者把需要显示的图片在后台线程绘制为图片。
主要参考了 ibreme 的 iOS 保持界面流程的技巧
界面优化技巧
预排版
当获取到 API JSON 数据后,把每条 cell 需要的数据在后台线程计算并封装为一个布局对象 cellLayout。并把它们缓存到内存,后面可以直接使用这个对象。
预渲染
为了得到圆角图像,把下载下来的图片在后台线程预先渲染为圆形并单独保存到一个 ImageCache 中。
异步绘制
view 显示到屏幕上的过程
根据上面的渲染原理可以得到这个问题的答案:
- CPU 创建 view 对象并计算位置和文本宽高,之后进行文本的渲染和图片的解码(如果是图片的话)
- GPU 接受 CPU 提交过来的纹理和顶点描述,应用变换、混合和渲染,然后输出到缓冲区
事件传递链
RunloopSource0 -> UIKitCore 的事件队列 -> UIWindow -> UIView … -> 最底下的能够响应的 view
从UIWindow递归寻找子视图,并且对于同一层级的子视图使用倒叙遍历,分别调用每一个 view 的 hittest
方法。
一个 view 在下列情况不能响应事件:
- alpha < 0.01
- userInteractionEnable = NO
- hidden = YES
事件响应链
- RunloopSource0
- UIKitCore 事件队列
- Application
sendEvent
- 触发事件的 UIControl
sendAction:to:forEvent:
- Application
sendAction:from:to:forEvent:
, 这里有参数target如果不为空则直接给target发送action,否则沿着响应链查询能够影响action的UIResponder。查询每个视图的方法是通过调用canPerformAction:withSender
方法,如果当前 view 实现了 action 那么就返回 YES,否则继续沿着 nextResponder 找能成功响应的 responder。