响应链:
用户点击屏幕产生事件 -> UIApplication 开始事件分发 -> UIWindow-> Subviews UIWindow的子视图会内部递归调用
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
如果上面的方法返回视图就会调用这个方法
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
- 当view的userInteractionEnabled为NO、hidden为YES或alpha<=0.1时,也不会打印pointInside方法。因此可以推断出在hitTest方法内部会判断如果这些条件一个成立则会返回nil,也不会调用pointInside方法。
- pointInside只是在执行hitTest时,会在hitTest内部调用的一个方法。也就是说pointInside是hitTest的辅助方法。
- 通过重写目标控件的
pointInside: withEvent:方法可以扩大响应范围
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
NSLog(@"%@ -- pointInside",self.class);
CGRect bounds = self.bounds;
//若原热区小于200x200,则放大热区,否则保持原大小不变
//一般热区范围为40x40 ,此处200是为了便于检测
CGFloat widthDelta = MAX(200 - bounds.size.width, 0);
CGFloat heightDelta = MAX(200 - bounds.size.height, 0);
bounds = CGRectInset(bounds, -0.5 * widthDelta, -0.5 * heightDelta);
return CGRectContainsPoint(bounds, point);
}
响应链事件是由上至下的;触摸事件是由由下至上的;
一个典型的响应路线: First Responder-->The Window-->The Application-->App Delegate.
UIView和CALayer
UIView和CALayer的关系
- CALayer 是 UIView 的属性之一,负责动画和视图的绘制/显示。
- UIView 提供了对CALayer部分功能的封装,同时负责交互事件的处理(事件的传递和响应)
UIView和CALayer的同异
- 有相同的层级结构:每个 UIView 都对应 CALayer 负责页面的绘制,所以 CALayer 也具有相应的层级结构。
- 部分效果的设置:因为 UIView 只对 CALayer 的部分功能进行了封装,而另一部分如圆角、阴影、边框等特效都需要通过调用 layer 属性来设置。
- 是否响应点击事件:CALayer 不负责点击事件,所以不响应点击事件,而 UIView 会响应。
- 不同继承关系:CALayer 继承自 NSObject,UIView 由于要负责交互事件,所以继承自 UIResponder。
视图渲染
过程
内部:commit transaction
- Layout: 处理视图的构建和布局
- 调用重载的
layoutSubviews方法 - 创建视图,并通过
addSubview方法添加子视图 - 计算视图布局,即所有的 Layout Constraint
- Display: 绘制视图 有两种情况
- 根据上一阶段 Layout的结果调用Core Graphics进行视图的绘制,得到图元数据(图元primitives:处理到一半的图层数据,后面会交给GPU处理)
- 如果重写了
drawRect:,这一步会调用,得到手动绘制的 bitmap(位图) 数据
正常情况下Display阶段只会得到图元,而位图bitmap是在GPU中根据图元信息绘制得到的。但是如果重写了
drawRect:,那么会直接调用Core Graphics 绘制方法得到 位图bitmap 数据。由于重写了drawRect:,导致绘制过程从GPU转移到了CPU,会额外消耗CPU性能和内存,因此需要高效绘制,否则容易造成CPU卡顿或者内存爆炸。
- Prepare:图像的解码和转码。
- 这部分由Core Animation完成
- Commit:将图层进行打包,并将它们发送至Render Server。
- 该过程会递归执行,因为图层和视图都是以树形结构存在。所以要减少图层数。
这部分是通过监听runloop 在 kCFRunLoopBeforeWaiting 和 kCFRunLoopExit 状态,对图层打包,打完包后,将打包数据发送给一个独立负责渲染的进程 Render Server(外部进程)
外部
- Render Server主要执行OpenGL、Core Graphics相关程序,并调用GPU。
- GPU则在物理层上完成了对图像的渲染。
- GPU通过Frame Buffer、视频控制器等相关部件,将图像显示在屏幕上。
总过程图
如上图所示,开发一般只能影响内部
Commit Transaction这一阶段
理性使用-drawRect: 大家或许感到奇怪,有不少开发者在发有关性能优化的博客当中指出使用-drawRect:来优化性能。但是我这里不太建议大家未经思考的使用-drawRect:方法。原因如下: 当你使用UIImageView在加载一个视图的时候,这个视图虽然依然有CALayer,但是却没有申请到一个后备的存储,取而代之的是使用一个使用屏幕外渲染,将CGImageRef作为内容,并用渲染服务将图片数据绘制到帧的缓冲区,就是显示到屏幕上,当我们滚动视图的时候,这个视图将会重新加载,浪费性能。所以对于使用-drawRect:方法,更倾向于使用CALayer来绘制图层。因为使用CALayer的-drawInContext:,Core Animation将会为这个图层申请一个后备存储,用来保存那些方法绘制进来的位图。那些方法内的代码将会运行在 CPU上,结果将会被上传到GPU。这样做的性能更为好些。 静态界面建议使用-drawRect:的方式,动态页面不建议。**
性能优化与渲染流畅度
在开发和优化iOS应用的过程中,以下几点可以帮助提高视图渲染的性能和流畅度:
- 减少视图层级:复杂的视图层级结构会增加渲染成本,尽量简化视图层级,避免过多嵌套和重叠。
- 异步绘制:对于复杂的绘制操作,可以考虑在后台线程执行,并在主线程更新UI,以避免阻塞主线程造成界面卡顿。
- 利用硬件加速:iOS设备的GPU能够加速图形绘制和动画效果,合理使用Core Animation和Metal等框架可以提高渲染效率。
- 避免过度绘制:只在视图内容变化时进行重绘,避免不必要的绘制操作,减少CPU和GPU的资源消耗。
屏幕显示的原理
- 图像是由电子枪一行一行快速扫描(打)出来的
- 电子枪打之前会发出一个水平同步信号(HSync),其频率就是显示器的刷新频率
- CPU计算好frame等属性后,将计算的好内容交给GPU去做渲染
- 渲染好的“图片”会放在帧缓冲区
- 最后显示器会根据水平同步信号(HSync)逐行读取缓冲区的的数据,然后通过电子枪打出图像
- 由于垂直同步的机制,如果在一个 HSync 时间内,CPU 或者 GPU 没有完成内容提交,则那一帧就会被丢弃,等待下一次机会再显示,而这时显示屏会保留之前的内容不变。这就是界面卡顿的原因。
GPU屏幕渲染方式(离屏渲染&当屏渲染):
1、On-Screen Rendering (当前屏幕渲染) :
- GPU的渲染操作是在当前用于显示的屏幕缓冲区进行。
- 当前屏幕渲染不需要额外创建新的缓存,也不需要开启新的上下文,相对于离屏渲染性能更好。
2、Off-Screen Rendering (离屏渲染):
- 指的是在GPU在当前屏幕缓冲区以外开辟一个缓冲区进行渲染操作。
- 需要创建新缓冲区
- 上下文切换
- 性能差
下面的情况或操作会引发离屏渲染:
- 为图层设置遮罩(layer.mask)、阴影(layer.shadow *)
- 将图层的layer.masksToBounds / view.clipsToBounds属性设置为true
- 将图层layer.allowsGroupOpacity属性设置为YES和layer.opacity小于1.0
- 为图层设置layer.shouldRasterize=true
- 具有layer.cornerRadius,layer.edgeAntialiasingMask,layer.allowsEdgeAntialiasing的图层
- 文本(任何种类,包括UILabel,CATextLayer,Core Text等)。
- shouldRasterize 光栅化:开启光栅化后,会触发离屏渲染,Render Server 会强制将 CALayer 的渲染位图结果bitmap保存下来,这样下次再需要渲染时就可以直接复用,从而提高效率。
重写
drawRect:方法并不会触发离屏渲染。重写drawRect:会将GPU中的渲染操作转移到CPU中完成,并且需要额外开辟内存空间。但这和这和标准意义上的离屏渲染并不一样,并且使用Instrument中的Color offscreen rendered yellow 监测时,也不会被判断为离屏渲染。
优化方案
1、圆角优化
优化方案1:使用贝塞尔曲线UIBezierPath和Core Graphics框架画出一个圆角
UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(100,100,100,100)];
imageView.image = [UIImage imageNamed:@"myImg"];
//开始对imageView进行画图
UIGraphicsBeginImageContextWithOptions(imageView.bounds.size,NO,1.0);
//使用贝塞尔曲线画出一个圆形图
[[UIBezierPath bezierPathWithRoundedRect:imageView.boundscornerRadius:imageView.frame.size.width]addClip];
[imageView drawRect:imageView.bounds];
imageView.image=UIGraphicsGetImageFromCurrentImageContext();
//结束画图
UIGraphicsEndImageContext();
[self.view addSubview:imageView];
优化方案2:使用CAShapeLayer和UIBezierPath设置圆角(推荐)
- 先用UIBezierPath画出圆形
- 将画出的圆形赋值给CAShapeLayer
- 最后CAShapeLayer赋值给View.layer.mask
UIImageView *imageView = [[UIImageView alloc]initWithFrame:CGRectMake(100, 100, 100, 100)];
imageView.image = [UIImage imageNamed:@"myImg"];
UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:imageView.bounds byRoundingCorners:UIRectCornerAllCorners cornerRadii:imageView.bounds.size];
CAShapeLayer *maskLayer = [[CAShapeLayer alloc]init];
//设置大小
maskLayer.frame = imageView.bounds;
//设置图形样子
maskLayer.path = maskPath.CGPath;
imageView.layer.mask = maskLayer;
[self.view addSubview:imageView];
2、shadow优化
对于shadow,如果图层是个简单的几何图形或者圆角图形,我们可以通过UIBezierPath设置shadowPath来优化性能,能大幅提高性能。
imageView.layer.shadowColor=[UIColorgrayColor].CGColor;
imageView.layer.shadowOpacity=1.0;
imageView.layer.shadowRadius=2.0;
UIBezierPath *path=[UIBezierPathbezierPathWithRect:imageView.frame];
//设置shadowPath
imageView.layer.shadowPath=path.CGPath;
3、其他的一些优化建议
- 使用异步进行layer渲染(Facebook开源的异步绘制框架AsyncDisplayKit)
- 设置layer的opaque值为YES,减少复杂图层合成
- 尽量使用不包含透明(alpha)通道的图片资源
- 尽量设置layer的大小值为整型值
- 直接让美工把图片切成圆角进行显示,这是效率最高的一种方案
- 很多情况下用户上传图片进行显示,可以让服务端处理圆角
- 使用代码手动生成圆角Image设置到要显示的View上,利用UIBezierPath(CoreGraphics框架)画出来圆角图片
Core Animation工具检测离屏渲染
对于离屏渲染的检测,苹果为我们提供了一个测试工具Core Animation。可以在Xcode->Open Develeper Tools->Instruments中找到
问题-离屏渲染
- 掉帧问题不在于离屏渲染这个机制,而在离屏渲染的时候, 大量的使用cornerRadius等耗性能的属性会导致计算量过大,导致无法在指定时间内完成绘制,无法提交当前帧,造成卡顿。shapeLayer+贝塞尔曲线这种方式性能更好更快的完成绘制,就不会出现掉帧了...
- view的drawRect(继承于CoreGraphics走的是CPU,消耗的性能较大)
layoutSubviews & setNeedsLayout & layoutIfNeeded & setNeedUpdateLayout
- layoutSubviews:不应该在代码中显式调用这个方法,系统会在任何它需要重新计算视图的 frame 的时候通过RunLoop触发调用这个方法,所以你应该在需要更新 frame 来重新定位或更改大小时重载它。
layoutSubviews被调用后,view所在的vc会触发viewDidLayoutSubviews,所以你应该把所有依赖于布局或者大小的代码放在viewDidLayoutSubviews中,而不是放在viewDidLoad或者viewDidAppear中。 - setNeedsLayout:触发
layoutSubviews调用的最省资源的方法就是在你的视图上调用setNeedsLaylout方法。调用这个方法代表向系统表示视图的布局需要重新计算。setNeedsLayout方法会立刻执行并返回,但在返回前不会真正更新视图。视图会在下一个update cycle中更新,就在系统调用视图们的layoutSubviews以及他们的所有子视图的layoutSubviews方法的时候。 - layoutIfNeeded:
layoutIfNeeded是另一个会让 UIView 触发layoutSubviews的方法。 当视图需要更新的时候,与setNeedsLayout()会让视图在下一周期调用layoutSubviews更新视图不同,layoutIfNeeded会立即调用layoutSubviews方法。