iOS:响应链和事件传递&图片的解压缩及渲染过程

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)响应响应链,手势识别和触摸事件是两个不同的事件,手势的设置可能会影响触摸事件。

手势的三个属性:

  1. cancelsTouchesInView
    默认为YES,这种情况下当手势识别器识别到touch之后,会发送touchesCancelled给hit-testview以取消hit-test view对touch的响应,这个时候只有手势识别器响应touch。
    当设置成NO时,手势识别器识别到touch之后不会发送touchesCancelled给hit-test,这个时候手势识别器和hit-test view均响应touch。
  2. delaysTouchesBegan
    默认是NO,这种情况下当发生一个touch时,手势识别器先捕捉到到touch,然后发给hit-testview,两者各自做出响应。如果设置为YES,手势识别器在识别的过程中(注意是识别过程),不会将touch发给hit-test view,即hit-testview不会有任何触摸事件。只有在识别失败之后才会将touch发给hit-testview,这种情况下hit-test view的响应会延迟约0.15ms。
  3. 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的手势设置了delaysTouchesBegantap.cancelsTouchesInView

    tap.delaysTouchesBegan = YES;
    tap.cancelsTouchesInView = NO;

那么响应结果如下:

 -[AView tapclick]
 -[BView touchesEnded:withEvent:]

因为最佳响应者找到后,调用他的touchesBegan方法(前提是没有手势,或者手势的delaysTouchesBeganYES),然后向上找nextResponder,有没有绑定手势,有就响应,没有就在往上,直到抛弃,然后响应touchesEnded(前提是没有手势,或者手势的cancelsTouchesInViewNO

三、View相关的方法调用

layoutSubviews调用时机
  1. init不会触发layoutSubviews
  2. 调用addSubview会触发layoutSubviews
  3. UIViewFrame改变时(frame的值设置前后发生了变化),会触发layoutSubviews
  4. UIScrollView滚动时,UIView的重新布局会触发layoutSubviews
  5. 直接调用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)>

  1. UIView初始化时没有设置rect,那么drawRect不被自动调用。因为drawRect 是在Controller->loadView, Controller->viewDidLoad 两方法之后调用的
  2. 该方法在调用sizeToFit后被调用,所以可以先调用sizeToFit计算出size。然后系统自动调用drawRect方法。
  3. 直接调用setNeedsDisplaysetNeedsDisplayInRect都会触发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,清理

对于普通应用来说,图片是最占用手机内存的资源,将一张图片从磁盘中加载出来,并最终显示到屏幕上,中间其实经过了一系列复杂的处理过程。

  1. 假设我们使用 +imageWithContentsOfFile: 方法从磁盘中加载一张图片,这个时候的图片并没有解压缩;
  2. 然后将生成的 UIImage 赋值给 UIImageView
  3. 接着一个隐式的 CATransaction 捕获到了 UIImageView 图层树的变化;
  4. 在主线程的下一个 runloop 到来时,Core Animation 提交了这个隐式的 transaction ,这个过程可能会对图片进行 copy 操作,而受图片是否字节对齐等因素的影响,这个 copy 操作可能会涉及以下部分或全部步骤:
  • 分配内存缓冲区用于管理文件 IO 和解压缩操作;
  • 将文件数据从磁盘读到内存中;
  • 将压缩的图片数据解码成未压缩的位图形式,这是一个非常耗时的 CPU 操作;
  • 最后 Core Animation 中CALayer使用未压缩的位图数据渲染 UIImageView 的图层。
  • CPU计算好图片的Frame,对图片解压之后.就会交给GPU来做图片渲染
  1. 渲染流程
  • 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];

具体流程

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值