UI相关面试题

263 阅读3分钟

UI相关

  • 重用机制
    重用机制示意图
    • 通过一个自定义控件字母索引条讲解
  • 数据源同步
    数据源同步问题
    • 并发访问,数据拷贝
      解决方法1
    • 串行访问
      解决方法2

事件传递&视图响应

  • UIView和CALayer关系

    关系图

    • UIView为其提供内容,以及负责处理触摸等事件,参与响应链
    • CALayer负责显示内容contents
    • 设计目的是单一职责原则
  • 事件传递机制

    事件传递流程

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
  • 事件响应流程
    事件响应
    事件响应示例
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;
  • 示例代码
//实现一个button,只有内切圆点击能响应点击事件
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    if (!self.userInteractionEnabled ||
        [self isHidden] ||
        self.alpha <= 0.01) {
        return nil;
    }
    
    if ([self pointInside:point withEvent:event]) {
        //遍历当前对象的子视图
        __block UIView *hit = nil;
        [self.subviews enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(__kindof UIView * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
            // 坐标转换
            CGPoint vonvertPoint = [self convertPoint:point toView:obj];
            //调用子视图的hittest方法
            hit = [obj hitTest:vonvertPoint withEvent:event];
            // 如果找到了接受事件的对象,则停止遍历
            if (hit) {
                *stop = YES;
            }
        }];
        
        if (hit) {
            return hit;
        }
        else{
            return self;
        }
    }
    else{
        return nil;
    }
}

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
    CGFloat x1 = point.x;
    CGFloat y1 = point.y;
    
    CGFloat x2 = self.frame.size.width / 2;
    CGFloat y2 = self.frame.size.height / 2;
    //平方差公式,算出与圆心距离
    double dis = sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
    // 67.923
    if (dis <= self.frame.size.width / 2) {
        return YES;
    }
    else{
        return NO;
    }
}

图像显示原理

  • 一般来说,计算机系统中 CPU、GPU、屏幕是以上面这种方式协同工作的。CPU 计算好显示内容提交到 GPU,GPU 渲染完成后将渲染结果放入帧缓冲区,随后视频控制器会按照 VSync 信号逐行读取帧缓冲区的数据,经过可能的数模转换传递给屏幕显示。

    图像显示1
    图像显示2

  • cpu工作

    • layout
      • UI布局
      • 文本计算
    • display
      • 绘制
    • prepare
      • 图片编解码
    • commit
      • 提交位图
  • gpu渲染管线

    • 顶点着色
    • 图元装配
    • 光栅化
    • 片段着色
    • 片段处理
  • cpu和gpu处理后提交到 FrameBuffer/帧缓存区

卡顿&掉帧

卡顿原因

  • 滑动优化方案
    • cpu
      • 对象创建、调整、销毁
      • 预排版(布局、文本计算)
      • 预渲染(文本等异步绘制,图片编解码等)
    • gpu
      • 纹理渲染:尽量减少在短时间内大量图片的显示,尽可能将多张图片合成为一张进行显示
      • 视图混合:应用应当尽量减少视图数量和层次,并在不透明的视图里标明 opaque 属性 = YES,以避免GPU进行无用的 Alpha 通道合成
      • 离屏渲染:layer的圆角、阴影、遮罩触发

绘制原理&异步绘制

  • 绘制原理,异步绘制入口

    绘制原理1

  • 系统默认绘制流程

    系统默认绘制流程

  • 如何实现异步绘制

    异步绘制实现

  • 异步绘制核心示例代码

//- [layer.delegate displayLayer:]
//代理负责生成对应的bitmap
//设置该bitmap作为layer.contents属性的值
- (void)display {
    dispatch_async(backgroundQueue, ^{
        CGContextRef ctx = CGBitmapContextCreate(...);
        // draw in context...
        CGImageRef img = CGBitmapContextCreateImage(ctx);
        CFRelease(ctx);
        dispatch_async(mainQueue, ^{
            layer.contents = img;
        });
    });
}

离屏渲染

  • 什么是离屏渲染
    • 在屏幕渲染,指的是gpu的渲染操作是在当前用于显示屏幕缓冲去中进行
    • 离屏渲染,指的是gpu在当前屏幕缓冲区以外新开辟的一个缓冲区进行渲染操作
  • 何时会触发离屏渲染
    • 圆角(当和maskToBounds一起使用时)
    • 图层蒙版
    • 阴影
    • 光栅化(CALayer.shouldRasterize 属性,但这会把原本离屏渲染的操作转嫁到 CPU 上去)
  • 为何要避免
    • UI卡顿和掉帧
    • 创建新的渲染缓冲区
    • 上下文切换

相关问题

  • 系统的UI时间传递机制是怎样?
  • 使UITableView滚动更流畅的方案或思路有哪些?
  • 什么是离屏渲染?
  • UIView和CALayer之间的关系是怎样的?

参考资料

iOS 保持界面流畅的技巧
iOS UIView绘制&异步绘制
YYAsyncLayer 源码剖析