- Core Animation:核心动画; Core Graphics:核心绘制 ;UIkit:ios基础视图框架。
- UIkit可以看成是对Core Animation 和Core Graphics两者的高度集成,方便开发者直接应用。
- UIView通过内部图层layer显示在屏幕上,本身并不能显示。当UIView需要显示到屏幕上时,会调用drawRect:方法进行绘图,并且会将所有内容绘制在自己的layer图层上,绘图完毕后,系统会将图层拷贝到屏幕上,于是就完成了UIView的显示。
- layer层的重要性:Core Animation所有的动画都是操作的uiview的layer层,而所有的Core Graphics绘制的内容,都是绘制在layer层上。
图像显示原理
图像显示2大硬件,通过总线连接起来
CPU:生成位图,合适的时机通过总线上传给GPU。
GPU:得到位图后,负责对位图进行图层渲染,包括纹理的合成,之后就把结果放到帧缓冲区(Frame Buffer)中
视频控制器:根据信号在指定时间片段之前去帧缓冲区中提取屏幕显示内容。然后最终显示到手机屏幕上。
提供内容:UIView负责提供显示内容。
生成位图:CALayer中的contents就是对应的显示内容的位图,并在合适的时机显示到drawRect方法中,可以在这个方法中进行额外的加工显示处理。
提交位图:位图由Core Animation提交给OPenGL(ES)
位图渲染:OPenGL(ES)负责位图的渲染
屏幕显示:渲染完成后显示到屏幕上。
CPU工作
GPU渲染管线
UI卡顿与掉帧原因
卡顿的现象
卡顿是因为cpu和gpu由于执行的任务操作比较繁重,不能在视频同步信号到达前合成显示内容到 视频缓冲区中,导致无数据显示,
所以界面还是显示之前的内容,假如1秒内的总视频帧过少,就导致肉眼看起来明显的卡顿;
优化方案
CPU方面
GPU方面
纹理渲染
避免离屏渲染(比如:)都会触发离屏渲染,这样会导致GPU的工作量非常大,可以用CPU的异步绘制来减轻GPU的压力
避免多视图混合(很多视图图层的话,要计算每一个视图的合成,就会加大CPU或者CPU的压力)
防止离屏渲染
当我们指定了UI视图的某些属性标记为不能在当前屏幕显示的时候,就会触发离屏渲染
离屏渲染概念是起源GPU中,GPU如果在当前屏幕缓冲区外新开辟一个缓冲区进行渲染操作就是离屏渲染。
触发离屏渲染的场景
为什么要避免呢?
触发离屏渲染会导致GPU的消耗,因为需要创建新的GPU缓冲区,和上下文切换,导致额外开销;因为出现多通道渲染管线,需要把多通道渲染结果进行合成,需要涉及上下文切换,会有额外的开销,有可能导致cpu和GPU的总耗时超过16.7ms,这样会导致界面的卡顿与掉帧现象。
UIView绘制流程
UI界面绘制
如果打印App启动之后的主线程RunLoop
可以发现另外一个callout为_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv 的Observer,这个监听专门负责UI变化后的更新,比如修改了frame、调整了UI层级(UIView/CALayer)或者手动设置了setNeedsDisplay/setNeedsLayout之后就会将这些操作提交到全局容器。
而这个Observer监听了主线程RunLoop的即将进入休眠和退出状态,一旦进入这两种状态则会遍历所有的UI更新并提交进行实际绘制更新。
通常情况下这种方式是完美的,因为除了系统的更新
还可以利用setNeedsDisplay等方法手动触发下一次RunLoop运行的更新。
但是如果当前正在执行大量的逻辑运算可能UI的更新就会比较卡,因此facebook推出了AsyncDisplayKit来解决这个问题。
AsyncDisplayKit其实是将UI排版和绘制运算尽可能放到后台,将UI的最终更新操作放到主线程(这一步也必须在主线程完成)。
同时提供一套类UIView或CALayer的相关属性,尽可能保证开发者的开发习惯。
这个过程中AsyncDisplayKit在主线程RunLoop中增加了一个Observer监听,即将进入休眠和退出RunLoop两种状态,收到回调时遍历队列中的待处理任务一一执行
刷新时机都在主线程上
是因为 安全+效率:因为UIKit框架不是线程安全的框架,当在多个线程进行UI操作,有可能出现资源抢夺,导致问题。
系统的绘制流程
异步绘制流程
[- layer.delegate displayLayer:];
某个时机调用setNeedsDisplay;
runloop将要结束的时候调用[CALayer display]
如果代理实现了dispalyLayer将会调用此方法, 在子线程中去做异步绘制的工作;
子线程中做的工作:创建上下文, 控件的绘制, 生成图片;
转到主线程, 设置layer.contents, 将生成的视图展示在layer上面;
异步绘制实现代码:
时序图
视图坐标与转换
- frame
frame是一个CGRect结构体,描述该视图在父视图的坐标系下的位置以及视图大小;
- bounds
bounds是一个CGRect结构,描述视图自己的坐标系以及视图大小;
- center
center是一个CGPoint结构,描述了该视图中心位于父视图坐标下的位置;需要修改中心点时,可以修改该属性
- transform
transform是一个CGAffineTransform结构,描述了该视图的形变状态;视图需要变形(缩放、旋转)时,可以修改该属性
假如在旋转时,视图旋转只影响视图本身以及子视图的视觉效果 视图旋转改变了其在父视图中的位置但并未改变自身尺寸,也没有改变子视图在其坐标系的位置,另外该旋转是围绕center为中心进行的 只有旋转的视图自身frame发生改变、bounds和center不受影响,子视图的坐标系均不受影响(bounds、center、frame)
常见的frame变化的情景
直接修改了frame属性时,bounds:会变化,center会变化,transform会变化 直接修改bounds属性时,frame会变化,center不会变化,transform不会变化 直接修改center属性时, frame会变化,bounds不会变化,transform不会变化 直接修改transform属性时,frame会变化,bounds不会变化,center不会变化
视图的frame
,bounds
和center
属性仅仅是存取方法,当操纵视图的frame
,实际上是在改变位于视图下方CALayer
的frame
,不能够独立于图层之外改变视图的frame
。
对于视图或者图层来说,frame
并不是一个非常清晰的属性,它其实是一个虚拟属性,是根据bounds
,position
和transform
计算而来,所以当其中任何一个值发生改变,frame都会变化。相反,改变frame的值同样会影响到他们当中的值
记住当对图层做变换的时候,比如旋转或者缩放,frame
实际上代表了覆盖在图层旋转之后的整个轴对齐的矩形区域,也就是说frame
的宽高可能和bounds
的宽高不再一致了(图3.2)
图3.2 旋转一个视图或者图层之后的frame
属性
坐标转换相关函数
转换point相关 - convertPoint: toWindow: - convertPoint**: fromWindow: - convertPoint: toView: - convertPoint: fromView**: |
转换rect相关 - convertRect**: **toWindow: - convertRect**: fromWindow: - convertRect: toView**: - convertRect**: fromView:** |
---|---|
比如**convertPoint: toWindow**:,在调用时,[view convertPoint:xxx toWindow:window]; xxx位置,是相对于view到坐标的,接下来,将把xxx位置映射到window坐标体系中,返回一个坐标是参考window视图的; **
CGPoint newPoint = [subview convertPoint:point fromView:self]; //self中point转换成subview中坐标点
演示代码
摘自YYText
// 转换point
- (CGPoint)yy_convertPoint:(CGPoint)point toViewOrWindow:(UIView *)view {
if (!view) {
if ([self isKindOfClass:[UIWindow class]]) {
return [((UIWindow *)self) convertPoint:point toWindow:nil];
} else {
return [self convertPoint:point toView:nil];
}
}
UIWindow *from = [self isKindOfClass:[UIWindow class]] ? (id)self : self.window;
UIWindow *to = [view isKindOfClass:[UIWindow class]] ? (id)view : view.window;
if ((!from || !to) || (from == to)) return [self convertPoint:point toView:view];
point = [self convertPoint:point toView:from];
point = [to convertPoint:point fromWindow:from];
point = [view convertPoint:point fromView:to];
return point;
}
// 转换rect
- (CGRect)yy_convertRect:(CGRect)rect toViewOrWindow:(UIView *)view {
if (!view) {
if ([self isKindOfClass:[UIWindow class]]) {
return [((UIWindow *)self) convertRect:rect toWindow:nil];
} else {
return [self convertRect:rect toView:nil];
}
}
UIWindow *from = [self isKindOfClass:[UIWindow class]] ? (id)self : self.window;
UIWindow *to = [view isKindOfClass:[UIWindow class]] ? (id)view : view.window;
if (!from || !to) return [self convertRect:rect toView:view];
if (from == to) return [self convertRect:rect toView:view];
rect = [self convertRect:rect toView:from];
rect = [to convertRect:rect fromWindow:from];
rect = [view convertRect:rect fromView:to];
return rect;
}
- (CGPoint)yy_convertPoint:(CGPoint)point fromViewOrWindow:(UIView *)view {
if (!view) {
if ([self isKindOfClass:[UIWindow class]]) {
return [((UIWindow *)self) convertPoint:point fromWindow:nil];
} else {
return [self convertPoint:point fromView:nil];
}
}
UIWindow *from = [view isKindOfClass:[UIWindow class]] ? (id)view : view.window;
UIWindow *to = [self isKindOfClass:[UIWindow class]] ? (id)self : self.window;
if ((!from || !to) || (from == to)) return [self convertPoint:point fromView:view];
point = [from convertPoint:point fromView:view];
point = [to convertPoint:point fromWindow:from];
point = [self convertPoint:point fromView:to];
return point;
}
- (CGRect)yy_convertRect:(CGRect)rect fromViewOrWindow:(UIView *)view {
if (!view) {
if ([self isKindOfClass:[UIWindow class]]) {
return [((UIWindow *)self) convertRect:rect fromWindow:nil];
} else {
return [self convertRect:rect fromView:nil];
}
}
UIWindow *from = [view isKindOfClass:[UIWindow class]] ? (id)view : view.window;
UIWindow *to = [self isKindOfClass:[UIWindow class]] ? (id)self : self.window;
if ((!from || !to) || (from == to)) return [self convertRect:rect fromView:view];
rect = [from convertRect:rect fromView:view];
rect = [to convertRect:rect fromWindow:from];
rect = [self convertRect:rect fromView:to];
return rect;
}
绘制与刷新
sizeToFit
- sizeToFit会自动调用sizeThatFits方法;
- sizeToFit不应该在子类中被重写,应该重写sizeThatFits
- sizeThatFits传入的参数是receiver当前的size,返回一个适合的size
- sizeToFit可以被手动直接调用
- sizeToFit和sizeThatFits方法都没有递归,对subviews也不负责,只负责自己
setNeedLayout
标记为需要重新布局,异步调用layoutIfNeeded
刷新布局,不立即刷新,但layoutSubviews
一定会被调用;
setNeedsLayout在receiver标上一个需要被重新布局的标记,在系统runloop的下一个周期自动调用layoutSubviewslayoutIfNeed
如果有需要刷新的标记,立即调用layoutSubviews进行布局,比如使用了masonry时,有时需要用到layoutIfNeeded,来立即刷新布局得到视图的frame值
另外,layoutIfNeeded遍历的不是superview链,应该是subviews链;
**如果要立即刷新视图,要先调用[view setNeedsLayout],把视图标记设为需要布局,然后马上调用[view layoutIfNeeded],实现布局。 注意:在视图第一次显示之前,视图默认标记总是“需要刷新”的,可以直接调用[view layoutIfNeeded] 进行立马刷新;
结合这个博客理解:https://www.jianshu.com/p/a84f85729952
setNeedDisplay
标记为需要重绘,异步调用drawRect
重新绘制,会触发drawRect的调用,或者第一次设置frame属性时也会触发drawRect的调用
setNeedDisplay在receiver标上一个需要被重新绘图的标记,在下一个draw周期自动重绘,
iphone device的刷新频率是60hz,也就是1/60秒后重绘;在runloop即将休眠前调用CALayer的绘制工作中,[CALayer display]显示
drawRect
重写此方法,执行重绘任务 ,drawRect是对receiver的重绘,能获得context,苹果规定,不允许程序员直接调用此方法
间接调用!
1、UIView通过frame初始化时设置。rect的width和height不能为0;
2、直接调用setNeedsDisplay,或者setNeedsDisplayInRect:触发drawRect:但是有个前提条件是rect的width和height不能为0;
调用顺序:
drawRect调用是在Controller->loadView, Controller->viewDidLoad 两方法之后调用的。所以不用担心在viewDidLoad中,这些View的drawRect就开始画了。这样可以在viewDidLoad中设置一些值给View(如果这些View draw的时候需要用到某些变量值)。
drawRect方法使用注意点:
1、若使用UIView绘图,只能在drawRect:方法中获取相应的contextRef并绘图。如果在其他方法中获取将获取到一个invalidate的ref并且不能用于画图。drawRect:方法不能手动显示调用,必须通过调用setNeedsDisplay 或者 setNeedsDisplayInRect,让系统自动调该方法。
2、若使用CALayer绘图,只能在drawLayer:inContext: 方法中(类似于drawRect)绘制,或者在delegate中的相应方法绘制。同样也是调用setNeedDisplay等间接调用以上方法
3、若要实时画图,不能使用gestureRecognizer,只能使用touchbegan等方法来调用setNeedsDisplay实时刷新屏幕。
layoutSubviews
layoutSubviews是对subviews重新布局。这个方法,默认没有做任何事情,需要子类进行重写;
layoutSubviews方法调用先于drawRect(毕竟要调好位置才绘制嘛),另外,不能直接调用layoutSubviews对子视图进行重新布局;
以下情况,会自动触发layoutSubviews方法:
addSubview
的时候触发layoutSubviews
。- 当
view
的frame
发生改变的时候触发layoutSubviews
。 UIScrollView
滑动的时候触发layoutSubviews
。(过程中不断remove或add视图)- 旋转
Screen
会触发父UIView
上的layoutSubviews
事件。(bounds发生变化)
常见的frame变化的情景**
直接修改了frame属性时,bounds:会变化,center会变化,transform会变化 直接修改bounds属性时,frame会变化,center不会变化,transform不会变化 直接修改center属性时, frame会变化,bounds不会变化,transform不会变化 直接修改transform属性时,frame会变化,bounds不会变化,center不会变化
延伸布局
translucent
UINavigationBar/UITabBar的translucent属性
能控制UITabBar/UINavigationBar的半透明效果,默认为YES,即默认情况下为半透明效果.
往往会导致一些“注意事项”,主要体现为被半透明效果处理后产生的色差和添加的类view控件的坐标变动。
解释:默认为YES,可以通过设置NO来强制使用非透明背景,如果导航条使用自定义背景图片,那么默认情况该属性的值由图片的alpha(透明度)决定,如果alpha的透明度小于1.0值为YES。如果手动设置translucent为YES并且使用自定义不透明图片,那么会自动设置系统透明度(小于1.0)在这个图片上。如果手动设置translucent为NO并且使用自定义带透明度(透明度小于0)的图片,那么系统会展示这张背景图片,只不过这张图片会使用事先确定的barTintColor进行不透明处理,若barTintColor为空,则会使用UIBarStyleBlack(黑色)或者UIBarStyleDefault(白色)。
extendedLayoutIncludesOpaqueBars
extendedLayoutIncludesOpaqueBars控制器属性
通常配合translucent来使用,额外布局是否包括不透明的Bar,默认为NO,意味着如果导航条或者TabBar非透明,view内容不会被他们遮挡,如果该属性设置为YES,那么在导航条或者TabBar非透明的情况下,view的内容将会被他们遮挡(原点为0,0),该属性仅仅对非透明的Bar控件有效。
示例展示如下图所示(代码设置 navigationBar.translucent = NO 并且 extendedLayoutIncludesOpaqueBars = YES)
edgesForExtendedLayout
edgesForExtendedLayout控制器属性
在iOS7以后 UIViewController 开始使用全屏布局,而且是默认的属性。
通常涉及到布局,就离不开这个属性 edgesForExtendedLayout,它是一个类型为UIExtendedEdge的属性,指定UIViewController上的根视图self.view边缘要延伸的方向。由于iOS7鼓励全屏布局,所以它的默认值是UIRectEdgeAll,四周边缘均延伸,就是说,如果即使视图中上有UINavigationBar,下有UITabBar,那么视图仍会延伸覆盖到四周的区域。view是向四周延伸的,所以view的上部分会被Navigationbar遮住。
这里放置了一个frame为(0, 0, 100, 100)的view,backgroundColor设置为redColor,会被遮住一部分。也就是说,此时的self.view是从屏幕顶到屏幕底的。此时我们计算控件frame的y的时候,如果想把控件在导航栏底部开始,那么y就是64。
UIRectEdgeAll![]() |
UIRectEdgeNone:![]() |
---|---|
automaticallyAdjustsScrollViewInsets
automaticallyAdjustsScrollViewInsets控制器属性
默认情况下,如果使用UITabBarController和UINavigationBarController(translucent属性默认为YES),设置一个蓝色的view添加其中并设置距离屏幕边距为(0,0,0,0),展示效果如下,可以看到,UITabBarController和UINavigationBarController被蓝色的view“穿透”了,此时view的边距也正如设置的一样,零点坐标在(0,0)处。这时候,如果将view替换成tableView,展示效果如下:
可以看到,tableView的cell并没有因为“穿透”效果而出现被遮挡的情况,这是由于苹果对滚动视图的特殊性进行处理:对于类ScrollView,系统默认控制器属性automaticallyAdjustsScrollViewInsets默认为YES。
automaticallyAdjustsScrollViewInsets = YES时
scrollView的内容原本没有内边距,但是考虑到导航栏(高度44px)、状态栏(高度20px)、TabBar(高度49px)会挡住后面scrollView所展示的内容,系统自动为scrollView增加上下的内边距,这时候我们打印一下tableView的描述,可以看出,contentOffset(内容坐标偏移量)和contentSize(内容尺寸大小)都发生了变化,结合tableView自身的frame可以看出,系统自动为scrollView增加了顶部64px的内边距以及底部49px的内边距,正好是导航栏高度+状态栏高度以及TabBar高度。
- 一旦手动在系统布局页面之前设置automaticallyAdjustsScrollViewInsets = NO,将会取消上述操作;
- 请注意:上述的情况仅仅对UIScrollView或者子类(如UITableView)有效。
- 注意:iOS11开始,苹果摒弃了automaticallyAdjustsScrollViewInsets属性,改由contentInsetAdjustmentBehavior(枚举值)控制
contentInsetAdjustmentBehavior
/*
中文解析:该属性用来配置UIScrollView调整内边距的行为,其值为枚举值
默认值是UIScrollViewContentInsetAdjustmentAutomatic,就是自动调整。
*/
@property(nonatomic) UIScrollViewContentInsetAdjustmentBehavior contentInsetAdjustmentBehavior API_AVAILABLE(ios(11.0),tvos(11.0));
// 以下是该枚举的具体值(选项)
typedef NS_ENUM(NSInteger, UIScrollViewContentInsetAdjustmentBehavior) {
// 中文解析:与UIScrollViewContentInsetAdjustmentScrollableAxes相似,但为了向后兼容(向低版本兼容),当scroll view被view controller管理,且该view controller的automaticallyAdjustsScrollViewInsets = YES并在导航条控制器栈内,该枚举也会调整顶部(导航条)和底部(Tabbar)的内边距,无论该scroll view是否可滚动。
UIScrollViewContentInsetAdjustmentAutomatic,
// 中文解析:滚动轴的边缘会被调整(例如contentSize.width/height > frame.size.width/height 或 alwaysBounceHorizontal/Vertical = YES)
UIScrollViewContentInsetAdjustmentScrollableAxes,
// 中文解析:内边距不会被调整
UIScrollViewContentInsetAdjustmentNever,
// 中文解析:内边距总是被scroll view的safeAreaInsets所调整,safeAreaInsets顾名思义就是safeArea的内边距,safeArea下面会有一个概括性的解释。
UIScrollViewContentInsetAdjustmentAlways,
} API_AVAILABLE(ios(11.0),tvos(11.0));
safeArea
safeArea概括性解释:表示一个区域,该区域避开了导航条、Tabbar等可能挡住view controller的view的控件,如果添加一个view控件是相对于它父控件的safeAreaLayoutGuide做布局,则不用担心被导航条、Tabbar等控件“挡住”