iOS 响应链和事件传递
当我们点击了屏幕发生了什么?
两件事,第一找到点击的view(事件传递),第二响应对应的事件(响应链)。
一、事件传递
发生触摸事件后,系统会将事件加入到UIApplication管理的一个任务队列
(比如滑动事件就是多个UIEvent事件,放入一个队列中,取出队列的头部事件进行处理)中,UIApplication
将事件传递给UIWindow
继续向下分发给UIView
。
UIView
首先做hitTest
检测 触摸点是否在自己身上。如果不在,那么继续寻找子视图。
以下代码就是hitTesting
测试
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
// 当前控件能否接收事件
if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) {
return nil;
}
// 点击点是否在当前控件
if ([self pointInside:point withEvent:event] == NO) {
return nil;
}
// 从后往前遍历自己的子控件
for (NSInteger i = self.subviews.count - 1; i >= 0; i--) {
UIView *subView = self.subviews[i];
// 把当前控件上的坐标系转换成子控件上的坐标系
CGPoint subPoint = [self convertPoint:point toView:subView];
UIView *fitView = [subView hitTest:subPoint withEvent:event];
// 找到最终符合条件的view
if (fitView) {
return fitView;
}
}
// 循环结束,表示没有比自己更合适的view
return self;
}
最终找到了fitView
,然后进入事件响应链
注意:可以自己实现
pointInside:withEvent:
方法,控制其点击区域
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
if (我想要他扩大点击范围)
return YES;
else {
return NO;
}
}
二、响应链
1:触摸事件响应链
找到了点击的View,先判断当前的view能否处理该事件,如果不能,事件将会传递给其nextResponder
(可能是控制器),若没有控制器则传给其superView,最后传给UIWindow,UIApplication。若UIApplication还是没处理则将事件传给nil。即不做处理
2:手势识别(UIGestureRecognizer)响应响应链
找到最佳响应者后,事件响应有两条线,第一条就是上面的触摸事件响应链,第二条就是手势(UIGestureRecognizer)响应响应链,手势识别和触摸事件是两个不同的事件,手势的设置可能会影响触摸事件。
手势的三个属性:
- cancelsTouchesInView
默认为YES,这种情况下当手势识别器识别到touch之后,会发送touchesCancelled给hit-testview以取消hit-test view对touch的响应,这个时候只有手势识别器响应touch。
当设置成NO时,手势识别器识别到touch之后不会发送touchesCancelled给hit-test,这个时候手势识别器和hit-test view均响应touch。 - delaysTouchesBegan
默认是NO,这种情况下当发生一个touch时,手势识别器先捕捉到到touch,然后发给hit-testview,两者各自做出响应。如果设置为YES,手势识别器在识别的过程中(注意是识别过程),不会将touch发给hit-test view,即hit-testview不会有任何触摸事件。只有在识别失败之后才会将touch发给hit-testview,这种情况下hit-test view的响应会延迟约0.15ms。 - delaysTouchesEnded
默认为YES。这种情况下发生一个touch时,在手势识别成功后,发送touchesCancelled消息给hit-testview,手势识别失败时,会延迟大概0.15ms,期间没有接收到别的touch才会发送touchesEnded。如果设置为NO,则不会延迟,即会立即发送touchesEnded以结束当前触摸。
View上重写的touchesBegan、touchesEnd,还有tap手势的action,
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapclick)];
self.userInteractionEnabled = YES;
[self addGestureRecognizer:tap];
// tap.cancelsTouchesInView = NO;
// tap.delaysTouchesBegan = YES;
// tap.delaysTouchesEnded = NO;
}
return self;
}
- (void)tapclick {
NSLog(@"%s",__func__);
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"%s",__func__);
}
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"%s",__func__);
}
打印
//tap.cancelsTouchesInView = YES;
2020-07-27 15:57:25.195197+0800 -[OBViewB touchesBegan:withEvent:]
2020-07-27 15:57:25.202015+0800 -[OBViewB tapclick]
//tap.cancelsTouchesInView = NO;
2020-07-27 15:57:59.705265+0800 -[OBViewB touchesBegan:withEvent:]
2020-07-27 15:57:59.714245+0800 -[OBViewB tapclick]
2020-07-27 15:57:59.714455+0800 -[OBViewB touchesEnded:withEvent:]
//tap.delaysTouchesBegan = YES;如果设置为YES,手势识别器在识别的过程中(注意是识别过程),不会将touch发给hit-test view,即hit-testview不会有任何触摸事件
2020-07-27 15:57:59.714245+0800 -[OBViewB tapclick]
找到最佳响应者的view后,检测view有没有绑定手势,没有就沿着nextResponser检测,直到UIApplication,没有手势就不管了,有就响应手势,并不在找nextResponser了。
Aview中添加手势,并在Aview中添加Bview,点击Bview,Aview的手势会响应,就算Bview重写了touch相关的方法也一样,因为手势识别和触摸事件是两个不同的事件
此时,如果Bview实现了touchBegin和touchEnd,那么,响应顺序是:
-[BView touchesBegan:withEvent:]
-[AView tapclick]
-[BView touchesEnded:withEvent:]
如果AView的手势设置了delaysTouchesBegan
和tap.cancelsTouchesInView
tap.delaysTouchesBegan = YES;
tap.cancelsTouchesInView = NO;
那么响应结果如下:
-[AView tapclick]
-[BView touchesEnded:withEvent:]
因为最佳响应者找到后,调用他的touchesBegan
方法(前提是没有手势,或者手势的delaysTouchesBegan
为YES
),然后向上找nextResponder
,有没有绑定手势,有就响应,没有就在往上,直到抛弃,然后响应touchesEnded
(前提是没有手势,或者手势的cancelsTouchesInView
为NO
)
三、View相关的方法调用
layoutSubviews调用时机
init
不会触发layoutSubviews
- 调用
addSubview
会触发layoutSubviews
UIView
的Frame
改变时(frame的值设置前后发生了变化),会触发layoutSubviews
UIScrollView
滚动时,UIView
的重新布局会触发layoutSubviews
- 直接调用
setNeedsLayout
或者layoutIfNeeded
drawRect调用时机
每一个UIView都有一个layer,每一个layer都有个content,这个content指向的是一块缓存,叫做backing store。默认情况下,CALayer的content为空。若UIView的子类重写了drawRect,则UIView执行完drawRect后,系统会为器layer的content开辟一块缓存,缓存大小为size = widthheightscale,用来存放drawRect绘制的内容。
即使重写的drawRect啥也没做,也会开辟缓存,消耗内存,所以尽量不要随便重写drawRect却啥也不做。
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
OBView * v1 = [[OBView alloc] initWithFrame:CGRectMake(100, 90, 100, 80)];
v1.backgroundColor = [UIColor cyanColor];
[self.view addSubview:v1];
}
//重写drawRect
- (void)drawRect:(CGRect)rect {
NSLog(@"before contents %@",self.layer.contents);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"scale:%0.0f",[[UIScreen mainScreen] scale]);
NSLog(@"after contents %@",self.layer.contents);
});
}
打印:
2020-07-23 14:35:44.562681+0800 before contents (null)
2020-07-23 14:35:45.586855+0800 scale:2
2020-07-23 14:35:45.587201+0800 after contents <CABackingStore 0x15f03a5f0 (buffer [200 160] BGRX8888)>
UIView
初始化时没有设置rect
,那么drawRect
不被自动调用。因为drawRect
是在Controller->loadView, Controller->viewDidLoad 两方法之后调用的- 该方法在调用
sizeToFit
后被调用,所以可以先调用sizeToFit
计算出size。然后系统自动调用drawRect
方法。 - 直接调用
setNeedsDisplay
和setNeedsDisplayInRect
都会触发drawRect
(rect不能为0)
四、iOS 中图片的解压缩到渲染过程
通常计算机在显示是CPU与GPU协同合作完成一次渲染.接下来我们了解一下CPU/GPU等在这样一次渲染过程中,具体的分工是什么?
- CPU: 计算视图frame,图片解码,需要绘制纹理图片通过数据总线交给GPU
- GPU: 纹理混合,顶点变换与计算,像素点的填充计算,渲染到帧缓冲区。
- 时钟信号:垂直同步信号V-Sync / 水平同步信号H-Sync。
- iOS设备双缓冲机制:显示系统通常会引入两个帧缓冲区,双缓冲机制
图片显示到屏幕上是CPU与GPU的协作完成
我们提到了图片的解压缩是一个非常耗时的 CPU 操作,并且它默认是在主线程中执行的。那么当需要加载的图片比较多时,就会对我们应用的响应性造成严重的影响,尤其是在快速滑动的列表上,这个问题会表现得更加突出。
其中用到Image I/O
中的函数,静态图的解码,基本可以分为以下步骤:
- 创建CGImageSource
- 读取图像格式元数据(可选)
- 解码得到CGImage
- 生成上层的UIImage,清理
对于普通应用来说,图片是最占用手机内存的资源,将一张图片从磁盘中加载出来,并最终显示到屏幕上,中间其实经过了一系列复杂的处理过程。
- 假设我们使用
+imageWithContentsOfFile:
方法从磁盘中加载一张图片,这个时候的图片并没有解压缩; - 然后将生成的
UIImage
赋值给UIImageView
; - 接着一个隐式的
CATransaction
捕获到了UIImageView
图层树的变化; - 在主线程的下一个
runloop
到来时,Core Animation
提交了这个隐式的transaction
,这个过程可能会对图片进行copy
操作,而受图片是否字节对齐等因素的影响,这个copy
操作可能会涉及以下部分或全部步骤:
- 分配内存缓冲区用于管理文件 IO 和解压缩操作;
- 将文件数据从磁盘读到内存中;
- 将压缩的图片数据解码成未压缩的位图形式,这是一个非常耗时的 CPU 操作;
- 最后
Core Animation
中CALayer使用未压缩的位图数据渲染UIImageView
的图层。 CPU
计算好图片的Frame
,对图片解压之后.就会交给GPU
来做图片渲染
- 渲染流程
GPU
获取获取图片的坐标- 将坐标交给顶点着色器(顶点计算)
- 将图片光栅化(获取图片对应屏幕上的像素点)
- 片元着色器计算(计算每个像素点的最终显示的颜色值)
- 从帧缓存区中渲染到屏幕上
由于图片解压是非常消耗性能的,所以图片文件只有在确认要显示时,CPU才会对其进行解压缩.解压过的图片就不会重复解压,会缓存起来.
图片渲染到屏幕的过程: 读取文件->计算Frame
->图片解码->解码后纹理图片位图数据通过数据总线交给GPU
->GPU
获取图片Frame
->顶点变换计算->光栅化->根据纹理坐标获取每个像素点的颜色值(如果出现透明值需要将每个像素点的颜色*透明度值)->渲染到帧缓存区->渲染到屏幕
#import <CoreText/CoreText.h>
@implementation AsyncLabel
- (void)displayLayer:(CALayer *)layer {
/**
除了在drawRect方法中, 其他地方获取context需要自己创建[https://www.jianshu.com/p/86f025f06d62]
coreText用法简介:[https://www.cnblogs.com/purple-sweet-pottoes/p/5109413.html]
*/
CGSize size = self.bounds.size;;
CGFloat scale = [UIScreen mainScreen].scale;
///异步绘制:切换至子线程
dispatch_async(dispatch_get_global_queue(0, 0), ^{
UIGraphicsBeginImageContextWithOptions(size, NO, scale);
///获取当前上下文
CGContextRef context = UIGraphicsGetCurrentContext();
///将坐标系反转
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
///文本沿着Y轴移动
CGContextTranslateCTM(context, 0, size.height);
///文本反转成context坐标系
CGContextScaleCTM(context, 1.0, -1.0);
///创建绘制区域
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, CGRectMake(0, 0, size.width, size.height));
///创建需要绘制的文字
NSMutableAttributedString *attStr = [[NSMutableAttributedString alloc] initWithString:self.asynText];
[attStr addAttribute:NSFontAttributeName value:self.asynFont range:NSMakeRange(0, self.asynText.length)];
[attStr addAttribute:NSBackgroundColorAttributeName value:self.asynBGColor range:NSMakeRange(0, self.asynText.length)];
///根据attStr生成CTFramesetterRef
CTFramesetterRef frameSetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attStr);
CTFrameRef frame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, attStr.length), path, NULL);
///将frame的内容绘制到content中
CTFrameDraw(frame, context);
UIImage *getImg = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
///子线程完成工作, 切换到主线程展示
dispatch_async(dispatch_get_main_queue(), ^{
self.layer.contents = (__bridge id)getImg.CGImage;
});
});
}
@end
调用
AsyncLabel *asLabel = [[AsyncLabel alloc] initWithFrame:CGRectMake(50, 500, 300, 100)];
asLabel.backgroundColor = [UIColor cyanColor];
asLabel.asynBGColor = [UIColor greenColor];
asLabel.asynFont = [UIFont systemFontOfSize:16 weight:20];
asLabel.asynText = @"异步绘制";
[self.view addSubview:asLabel];
///不调用的话不会触发 displayLayer方法
[asLabel.layer setNeedsDisplay];