1、AutoLayout的原理,性能如何
原理:
Auto Layout 只关注视图之间的关系,通过布局引擎和已有的约束计算出各个视图的frame,每当约束改变时会重新计算各个视图的frame,获得frame的过程,就是根据各个视图已有的约束条件解方程式的过程。
性能:
性能会随着视图数量的增加呈指数级增加,达到一定数量的视图时,布局所需要的时间就会大于16.67ms,超过屏幕的刷新频率时会出现卡顿。
2、UIView & CALayer的区别
@interface UIView : UIResponder
@property(nonatomic,readonly,strong) CALayer *layer; // returns view's layer. Will always return a non-nil value. view is layer's delegate
@end
联系:
view持有layer,view是layer的delegate。
CALayer有一个可选的 delegate 属性,实现了 CALayerDelegate 协议,当 CALayer需要一个内容特定的信息时,就会从协议中请求。
CALayerDelegate是一 个非正式协议,其实就是说没有CALayerDelegate @protocol可以让你在类里面引用啦。你只需要调用你想调用的方法,CALayer会帮你做剩下的。( delegate 属 性被声明为id类型,所有的代理方法都是可选的)。
当需要被重绘时,CALayer会请求它的代理给他一个寄宿图来显示。它通过调用下面这个方法做到的:
- (void)displayLayer:(CALayerCALayer *)layer;
区别:
view 负责事件的响应和UI的交互,为layer 提供内容。
UIView 继承自 UIResponder,可以响应触摸和其他类型的事件,视图可以添加 UIGestureRecognizer 来处理常用手势。
layer 负责内容、动画的显示,但即便没有 UIView,CALayer 也可以正常展示内容。我们可以把 CALayer 称之为 “层”。layer 的主要工作是管理我们提供的视觉内容,但是 layer 本身也含有可以被设置的视觉属性,例如背景色、边框、阴影等。
3、事件响应链
一个事件有多个响应者,就存在寻找最佳响应者的过程。事件传递的目的是为了寻找事件的最佳响应者,是自下而上的传递。
寻找最佳响应者流程:
1、UIApplication首先将事件传递给窗口对象(UIWindow)。
2、窗口不响应传递给其他窗口,能响应传递给自己的子视图。
3、根据视图层级由后向前将事件传递给上一个同级子视图;若能响应,则从后往前询问当前视图的子视图。
4、若没有能够响应的子视图时,自己就是最佳响应者。
hitTest:withEvent:
每个UIView对象都有一个 hitTest:withEvent: 方法,这个方法是Hit-Testing过程中最核心的存在,其作用是询问事件在当前视图中的响应者,同时又是作为事件传递的桥梁。
UIApplication将事件通过调用UIWindow对象的 hitTest:withEvent: 传递给UIWindow对象,如果UIWindow能够响应事件,则调用UIWindow的子视图的hitTest:withEvent: ,一直往最上层的子视图传递找到最佳响应者,最终UIWindow返回一个视图层次中的响应者视图给UIApplication,这个视图就是hit-testing的最佳响应者。
默认实现:
- 若当前视图无法响应事件,则返回nil
- 若当前视图可以响应事件,但无子视图可以响应事件,则返回自身作为当前视图层次中的事件响应者
- 若当前视图可以响应事件,同时有子视图可以响应,则返回子视图层次中的事件响应者
事件传递的目的是为了寻找事件的最佳响应者,是自下而上的传递。响应者做出对事件的响应,这个过程是自上而下的。
响应者对于事件的操作方式:
响应者对于事件的拦截以及传递都是通过 touchesBegan:withEvent: 方法控制的。
响应者对于接收到的事件有3种操作:
1、不拦截,默认操作,事件会自动沿着默认的响应链往下传递。
2、拦截,不再往下分发事件,重写 touchesBegan:withEvent: 进行事件处理,不调用父类的touchesBegan:withEvent:。
3、拦截,继续往下分发事件,重写 touchesBegan:withEvent: 进行事件处理,同时调用父类的 touchesBegan:withEvent: 将事件往下传递。
响应链中的事件传递规则:
每一个响应者对象(UIResponder对象)都有一个 nextResponder 方法,用于获取响应链中当前对象的下一个响应者。因此,一旦事件的最佳响应者确定了,这个事件所处的响应链就确定了。
打印响应链:
- (void)printResponderChain
{
UIResponder *responder = self;
printf("%s",[NSStringFromClass([responder class]) UTF8String]);
while (responder.nextResponder) {
responder = responder.nextResponder;
printf(" --> %s",[NSStringFromClass([responder class]) UTF8String]);
}
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
[self printResponderChain];
[super touchesBegan:touches withEvent:event];
}
UIView
若视图是控制器的根视图,则其nextResponder为控制器对象;否则,其nextResponder为父视图。
UIViewController
若控制器的视图是window的根视图,则其nextResponder为窗口对象;若控制器是从别的控制器present出来的,则其nextResponder为presenting view controller。
UIWindow
nextResponder为UIApplication对象。
UIApplication
若当前应用的app delegate是一个UIResponder对象,且不是UIView、UIViewController或app本身,则UIApplication的nextResponder为app delegate。
Reference:
4、drawrect & layoutsubviews调用时机
drawrect调用时机:
-
drawrect:是在UIViewController的loadView:和ViewDidLoad:方法之后调用. -
当我们调用
[UIView sizeToFit]后,会触发系统自动调用drawRect: -
当设置UIView的contentMode或者Frame后会立即触发触发系统调用
drawRect: -
直接调用
setNeedsDisplay设置标记 或setNeedsDisplayInRect:的时候会触发drawRect:,我们使用setNeedsDisplay或setNeedsDisplayInRect:方法来给 view 打上 dirty 的标记,这两个方法告诉系统 view 的内容已经改变,需要在下一个 drawing cycle 进行重绘。
当一个view第一次显示,或者发生让事件可见部分无效的事件时,将会调用这个方法。
This method is called when a view is first displayed or when an event occurs that invalidates a visible part of the view. You should never call this method directly yourself. To invalidate part of your view, and thus cause that portion to be redrawn, call the
setNeedsDisplayorsetNeedsDisplayInRect:method instead.
layoutsubviews调用时机:
- addSubview会触发layoutSubviews。
- 设置view的Frame会触发layoutSubviews (frame发生变化触发)。
- 滚动一个UIScrollView会触发layoutSubviews。
- 旋转Screen会触发父UIView上的layoutSubviews事件。
- 改变一个UIView大小的时候也会触发父UIView上的layoutSubviews事件。
- 直接调用setLayoutSubviews。
5、UI的刷新原理
苹果注册了一个 Observer 监听 BeforeWaiting(即将进入休眠) 和 Exit (即将退出Loop) 事件,回调去执行一个很长的函数:这个函数里会遍历所有待处理的 UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面。
_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()
QuartzCore:CA::Transaction::observer_callback:
CA::Transaction::commit();
CA::Context::commit_transaction();
CA::Layer::layout_and_display_if_needed();
CA::Layer::layout_if_needed();
[CALayer layoutSublayers];
[UIView layoutSubviews];
CA::Layer::display_if_needed();
[CALayer display];
[UIView drawRect]; //只有初始化frame的时候才会触发,更新界面并不会再次触发。如果想触发,可手动调setNeedsDisplay方法。
[UIView setNeedsDisplay] 并没有发生当前视图立即绘制工作,打上需要重绘的脏标记,最后是在某个时机完成。
[UIView setLayoutIfNeed] 立即重新布局视图(下一个Runloop)。
[view layouIfNeeded] 当前RunLoop休眠前更新。
在iOS中是双缓冲机制,有前帧缓存、后帧缓存,即GPU会预先渲染好一帧放入一个缓冲区内(前帧缓存),让视频控制器读取,当下一帧渲染好后,GPU会直接把视频控制器的指针指向第二个缓冲器(后帧缓存)。当你视频控制器已经读完一帧,准备读下一帧的时候,GPU会等待显示器的VSync信号发出后,前帧缓存和后帧缓存会瞬间切换,后帧缓存会变成新的前帧缓存,同时旧的前帧缓存会变成新的后帧缓存。
Reference:
roadmap.isylar.com/iOS/UIKit/U…
6、隐式动画 & 显示动画区别
1、显式动画
是指用户自己通过beginAnimations:context:和commitAnimations创建的动画,显示动画需要手动创建。
2、隐式动画
是指通过UIView的animateWithDuration:animations:方法创建的动画,隐式动画是系统框架自动完成的。
Core Animation在每个runloop周期中自动开始一次新的事务,即使你不显式的用[CATransaction begin]开始一次事务,任何在一次runloop循环中属性的改变都会被集中起来,然后做一次0.25秒的动画。
7、什么是离屏渲染
如果你无法仅仅使用frame buffer来画出最终结果,那就只能另开一块内存空间来储存中间结果。
离屏渲染的定义:
如果要在显示屏上显示内容,我们至少需要一块与屏幕像素数据量一样大的frame buffer,作为像素数据存储区域,而这也是GPU存储渲染结果的地方。如果有时因为面临一些限制,无法把渲染结果直接写入frame buffer,而是先暂存在另外的内存区域,之后再写入frame buffer,那么这个过程被称之为离屏渲染。
为什么会触发离屏渲染:
通常对于每一层layer,Render Server会遵循“画家算法”,按次序输出到frame buffer,后一层覆盖前一层,就能得到最终的显示结果,作为“画家”的GPU虽然可以一层一层往画布上进行输出,但是无法在某一层渲染完成之后,再回过头来擦除/改变其中的某个部分——因为在这一层之前的若干层layer像素数据,已经在渲染中被永久覆盖了。这就意味着,对于每一层layer,要么能找到一种通过单次遍历就能完成渲染的算法,要么就不得不另开一块内存,借助这个临时中转区域来完成一些更复杂的、多次的修改/剪裁操作。
GPU离屏渲染的例子:
例子1:父视图上多个子视图被裁减圆角
1、将一个layer的内容裁剪成圆角,可能不存在一次遍历就能完成的方法。
2、容器的子layer因为父容器有圆角,那么也会需要被裁剪,而这时它们还在渲染队列中排队,尚未被组合到一块画布上,自然也无法统一裁剪。
3、此时我们就不得不开辟一块独立于frame buffer的空白内存,先把容器以及其所有子layer依次画好,然后把四个角“剪”成圆形,再把结果画到frame buffer中。这就是GPU的离屏渲染。
例子2:shadow
1、虽然layer本身是一块矩形区域,但是阴影默认是作用在其中”非透明区域“的,而且需要显示在所有layer内容的下方,因此根据画家算法必须被渲染在先。
2、但矛盾在于此时阴影的本体(layer和其子layer)都还没有被组合到一起,怎么可能在第一步就画出只有完成最后一步之后才能知道的形状呢?
3、这样一来又只能另外申请一块内存,把本体内容都先画好,再根据渲染结果的形状,添加阴影到frame buffer,最后把内容画上去。
避免阴影发生离屏渲染的方法:
我们能够预先告诉CoreAnimation(通过shadowPath属性)阴影的几何形状,那么阴影当然可以先被独立渲染出来,不需要依赖layer本体,也就不再需要离屏渲染了。
例子3:group opacity
alpha并不是分别应用在每一层之上,而是只有到整个layer树画完之后,再统一加上alpha,最后和底下其他layer的像素进行组合。显然也无法通过一次遍历就得到最终结果。
例子 4:mask
mask是应用在layer和其所有子layer的组合之上的,而且可能带有透明度,那么其实和group opacity的原理类似,不得不在离屏渲染中完成。
离屏渲染的优化:
1、既然已经花了不少精力把图片裁出了圆角,如果我能把结果缓存下来,那么下一帧渲染就可以复用这个成果,不需要再重新画一遍了。
2、CALayer为这个方案提供了对应的解法:shouldRasterize。一旦被设置为true,Render Server就会强制把layer的渲染结果(包括其子layer,以及圆角、阴影、group opacity等等)保存在一块内存中,这样一来在下一帧仍然可以被复用,而不会再次触发离屏渲染。
什么时候需要CPU渲染?
1、如文字(CoreText使用CoreGraphics渲染)和图片(ImageIO)渲染,由于GPU并不擅长做这些工作,不得不先由CPU来处理好以后,再把结果作为texture传给GPU。
2、使用CoreGraphics给图片加上圆角(将图片中圆角以外的部分渲染成透明)。
整个过程全部是由CPU完成的。这样一来既然我们已经得到了想要的效果,就不需要再另外给图片容器设置cornerRadius。另一个好处是,我们可以灵活地控制裁剪和缓存的时机,巧妙避开CPU和GPU最繁忙的时段,达到平滑性能波动的目的。
一些优化?
1、对于图片的圆角不经由容器来做剪切,而是预先使用CoreGraphics为图片裁剪圆角。
2、对于视频的圆角,由于实时剪切非常消耗性能,我们会创建四个白色弧形的layer盖住四个角,从视觉上制造圆角的效果。
3、对于view的圆形边框,如果没有backgroundColor,可以放心使用cornerRadius来做
4、对于所有的阴影,使用shadowPath来规避离屏渲染
5、对于特殊形状的view,使用layer mask并打开shouldRasterize来对渲染结果进行缓存
6、对于模糊效果,不采用系统提供的UIVisualEffect,而是另外实现模糊效果(CIGaussianBlur),并手动管理渲染结果。
Reference:
8、imageName & imageWithContentsOfFile区别
1、imageName:
1、imageNamed 会将使用过的图片缓存到内存中。
2、即使生成的对象被 AutoReleasepool 释放了,这份缓存依然存在。
3、imageNamed 会先尝试从缓存中读取,效率更高,但是会额外增加开销 CPU 的时间。
2、imageWithContentsOfFile:
1、imageWithContentsOfFile 直接从文件中加载图片,图片不会缓存,加载速度较慢,但是不会浪费内存。
2、除非某个图片经常使用,否则使用 imageWithContentsOfFile 这种经济的方式。
9、多个相同的图片,会重复加载吗
使用 imageName: 加载图片
- 加载到内存当中后,占据内存空间较大
- 相同的图片,图片不会重复加载
- 加载内存当中之后,会一直停留在内存当中,不会随着对象销毁而销毁
- 加载进去图片之后,占用的内存归系统管理,我们无法管理
使用 imageWithContentsOfFile: 加载图片
- 加载到内存当中后,占据内存空间较小
- 相同的图片会被重复加载内存当中
- 对象销毁的时候,加载到内存中图片会随着一起销毁
结论:
- 图片较小,并且使用频繁,使用
imageName:来加载(按钮图标/主页里面图片) - 图片较大,并且使用较少,使用
imageWithContentsOfFile:来加载(版本新特性/相册)
图片在沙盒中的存在形式
- 部署版本在>=iOS8的时候,打包的资源包中的图片会被放到Assets.car。图片有被压缩; 部署版本在<iOS8的时候,打包的资源包中的图片会被放在MainBudnle里面。图片没有被压缩
- 放在Images.xcassets里面的所有图片会直接暴露在沙盒的资源包(main Bundle), 会压缩到Assets.car文件,被放到MainBudnle里面图片没有被压缩。
结论:
- 小图片\使用频率比较高的图片放在Images.xcassets里面
- 大图片\使用频率比较低的图片(一次性的图片, 比如版本新特性的图片)不要放在Images.xcassets里面
Reference:
10、图片是什么时候解码的,如何优化
首先要搞清楚一个图片显示的流程是怎样的?
- 从磁盘拷贝数据到内核缓冲区(系统调用);
- 从内核缓冲区拷贝数据到用户控件(UIImageView)(进程所在)
- 生成 UIImageView,把图像数据赋值给 UIImageView;
- 如图像未解码,解码为位图数据;
- GPU 处理位图数据,进行渲染。
为什么要解码?
常用的图片格式 PNG、JPEG 等都是压缩格式,而屏幕显示的是位图 bitmap。
怎么解码?
Data Buffer :存储在内存中的原始数据。图像可以以不同格式存储,如 JPEG、PNG。
Image Buffer :图像在内存中的存储方式,每一个元素描述一个像素点。其存储方式与位图相同,存储在内存中。
Frame Buffer :帧缓存,用于显示到显示器上的。存储在 vRAM(video RAM)中。
将 Data Buffer 转换为 Image Buffer 的过程,就可以称为解码。或者说,将未解码的 CGImage 转换为位图。
何时解码?
图片在被设置到 UIImageView.image 或 layer.contents 中之后,在 layer 被提交到 GPU 之前,CGImage 数据才会被解码。这一步发生在主线程中,无可避免。
如何优化?
提前强制解码。
画布重绘解码,将图片用 CGContextDrawImage() 绘制到画布上,然后把画布的数据取出来当作图片。
Reference:
11、图片渲染怎么优化
优化无非从两个方向着手:空间、时间。
空间:
1、限制图片的大小
2、限制图片的缓存,除非使用频繁,否则不要缓存
3、使用 Image Asset Catalogs,压缩率较高(缺点是只能通过 imageNamed: 来加载);
时间:
1、提前强制解码
2、使用符合尺寸的图片:字节对齐
为什么图片要字节对齐?
对齐是为了提高读取的性能。因为处理器读取内存中的数据不是一个一个字节读取的,而是一块一块读取的。
不对齐需要访问两个块然后进行数据处理,会影响读取的性能。
在iOS中,如果这个图像的数据没有字节对齐,那么Core Animation会自动拷贝一份数据做对齐处理。
为了实现高性能的滚动,Core Animation能够使用一个图像而不需要首先创建一个副本,这一点至关重要。Core Animation会创建一个图像的副本的原因之一是图像的底层CGImageRef的字节不对齐。正确对齐的每行字节值必须是
8个像素 × 每个像素的字节。对于一个典型的ARGB图像,每行对齐的字节值是64的倍数。每个image table都是这样配置的,每个图像从一开始就对Core Animation进行正确的字节对齐。因此,当图像从图像表检索时,它们已经处于Core Animation可以直接使用的形式,而不需要创建一个副本。
12、如果GPU的刷新率超过了iOS屏幕60Hz刷新率是什么现象,怎么解决
1、准备画下一帧前,显示器会发出一个垂直同步信号(vertical synchronization),简称 VSync。显示器通常以固定频率进行刷新,这个刷新率就是 VSync 信号产生的频率。
2、在最简单的情况下,帧缓冲区只有一个,这时帧缓冲区的读取和刷新都都会有比较大的效率问题。为了解决效率问题,显示系统通常会引入两个缓冲区,即双缓冲机制。在这种情况下,GPU 会预先渲染好一帧放入一个缓冲区内,让视频控制器读取,当下一帧渲染好后,GPU 会直接把视频控制器的指针指向第二个缓冲器。如此一来效率会有很大的提升。
3、当视频控制器还未读取完成时,即屏幕内容刚显示一半时,GPU 将新的一帧内容提交到帧缓冲区并把两个缓冲区进行交换后,视频控制器就会把新的一帧数据的下半段显示到屏幕上,造成画面撕裂现象。
如果GPU的刷新率超过了iOS屏幕60Hz刷新率是什么现象?
如果GPU的刷新频率高于iOS屏幕的60HZ时,就意味着GPU缓冲区存在还没有显示完成,GPU 将新的一帧内容提交到帧缓冲区,并把两个缓冲区进行交换后,视频控制器就会把新的一帧数据的下半段显示到屏幕上,造成画面撕裂现象。
怎么解决?
为了解决这个问题,GPU 通常有一个机制叫做垂直同步(简写也是 V-Sync),当开启垂直同步后,GPU 会等待显示器的 VSync 信号发出后,才进行新的一帧渲染和缓冲区更新。这样能解决画面撕裂现象,也增加了画面流畅度,但需要消费更多的计算资源,也会带来部分延迟。