Skip to content

UI视图

面试点

UITableView相关, 事件传递&视图响应,图像显示原理,卡顿&掉帧,绘制原理&异步绘制,离屏渲染

UITableView

1. 重用机制

 UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:<#(nonnull NSString *)#> forIndexPath:indexPath];

示例:字母索引条


#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN

// 实现重用机制类
@interface ViewReusePool : NSObject

// 从重用池中取出一个可重用的 view
- (UIView *)dequeueReusableView;

// 像重用池中添加一个视图
- (void)addUsingView:(UIView *)view;

// 重置方法,将当前使用中的视图移动到可重用队列中
- (void)reset;

@end

NS_ASSUME_NONNULL_END

#import "ViewReusePool.h"

@interface ViewReusePool ()
@property (nonatomic, strong) NSMutableSet *waitUsedQueue;
@property (nonatomic, strong) NSMutableSet *usingQueue;
@end

@implementation ViewReusePool

- (instancetype)init
{
    self = [super init];
    if (self) {
        _waitUsedQueue = [NSMutableSet set];
        _usingQueue = [NSMutableSet set];
    }
    return self;
}

- (UIView *)dequeueReusableView
{
    UIView *view = [_waitUsedQueue anyObject];

    if (view == nil) {
        return nil;
    } else {
        // 从重用队列移除
        [_waitUsedQueue removeObject:view];
        // 添加到使用中的队列中
        [_usingQueue addObject:view];
        return  view;
    }
}

- (void)addUsingView:(UIView *)view
{
    if (view == nil) {
        return;
    }
    // 添加视图到使用中的队列
    [_usingQueue addObject:view];
}

- (void)reset
{
    UIView *view = nil;
    while ((view = [_usingQueue anyObject])) {
        // 从使用中队列移除
        [_usingQueue removeObject:view];
        // 加入等待使用的队列
        [_waitUsedQueue addObject:view];
    }
}

@end

#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN


@protocol IndexTableViewDataSource <NSObject>

// 索引数据
- (NSArray <NSString *> *)indexTitleForIndexTableView:(UITableView *)tableView;

@end

@interface IndexTableView : UITableView

@property (nonatomic, weak) id<IndexTableViewDataSource> indexedDataSource;

@end

NS_ASSUME_NONNULL_END

#import "IndexTableView.h"
#import "ViewReusePool.h"

@interface IndexTableView ()
{
    UIView *containerView;
    ViewReusePool *reusePool;
}
@end

@implementation IndexTableView

- (void)reloadData {
    [super reloadData];

    // 懒加载
    if (containerView == nil) {
        containerView = [[UIView alloc] initWithFrame:CGRectZero];
        containerView.backgroundColor = UIColor.whiteColor;
        // 插入索引条,层级关系,避免 tableview 滚动,索引条跟着滚动
        [self.superview insertSubview:containerView aboveSubview:self];
    }

    if (reusePool == nil) {
        reusePool = [[ViewReusePool alloc] init];
    }

    // 标记所有视图为可重用状态
    [reusePool reset];

    // reload 字母索引条
    [self reloadIndexedBar];
}

- (void)reloadIndexedBar
{
    // 获取字母索引显示内容
    NSArray <NSString *> *arrayTitles = nil;
    if ([self.indexedDataSource respondsToSelector:@selector(indexTitleForIndexTableView:)]) {
        arrayTitles = [self.indexedDataSource indexTitleForIndexTableView:self];
    }

    // 判断
    if (!arrayTitles || arrayTitles.count <= 0) {
        [containerView setHidden:YES];
        return;
    }

    NSUInteger count = arrayTitles.count;
    CGFloat buttonWidth = 60;
    CGFloat buttonHeight = self.frame.size.height / count;

    for (int i = 0; i < arrayTitles.count; i ++) {
        NSString *title = arrayTitles[i];

        UIButton *button = (UIButton *)[reusePool dequeueReusableView];
        if (button == nil) {
            button = [UIButton buttonWithType:UIButtonTypeSystem];
            button.backgroundColor = UIColor.whiteColor;

            [reusePool addUsingView:button];
            NSLog(@"新创建一个 button");
        } else {
            NSLog(@"button 重用了");
        }

        [containerView addSubview:button];
        [button setTitle:title forState:UIControlStateNormal];
        [button setTitleColor:UIColor.blackColor forState:UIControlStateNormal];

        button.frame = CGRectMake(0, i * buttonHeight, buttonWidth, buttonHeight);
    }

    [containerView setHidden: NO];
    containerView.frame = CGRectMake(self.frame.origin.x + self.frame.size.width - buttonWidth, self.frame.origin.y, buttonWidth, self.frame.size.height);
}

@end

#import "ViewController.h"
#import "IndexTableView.h"

@interface ViewController ()<UITableViewDelegate, UITableViewDataSource, IndexTableViewDataSource>
@property (nonatomic, strong) IndexTableView *tableView;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.

    self.view.backgroundColor = [UIColor whiteColor];
    self.navigationItem.title = @"重用机制";
    self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemRefresh target:self action:@selector(reload)];


    _tableView = [[IndexTableView alloc] initWithFrame:self.view.bounds];
    _tableView.dataSource = self;
    _tableView.delegate = self;
    _tableView.rowHeight = 50;
    [_tableView registerClass:UITableViewCell.class forCellReuseIdentifier:@"CELL"];
    _tableView.indexedDataSource = self;
    [self.view addSubview:_tableView];
}

- (void)reload {
    [self.tableView reloadData];
}


#pragma mark - UITableViewDataSource

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return 50;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"CELL" forIndexPath:indexPath];
    cell.textLabel.text = @"hello";
    return cell;
}


- (NSArray<NSString *> *)indexTitleForIndexTableView:(UITableView *)tableView {

    static BOOL flag = NO;

    if (flag) {
        flag = NO;
        return @[@"a",@"b",@"c"];
    } else {
        flag = YES;
        return @[@"1",@"2",@"3",@"4",@"5",@"6"];
    }
}

@end

2. 数据源同步

示例场景: 新闻咨询类的 app,删除广告,同时加载更多在进行。


  • 并发访问,数据拷贝

数据源是主线程拷贝过去的,删除数据的时候做个标记,子线程返回新数据进行拼接的时候要将标记的数据移除掉,之后在更新 UI。


  • 串行访问

事件传递&响应

1. UIView & CALayer

每个 UIView 都包含一个 CALayer 实例,这个层称为视图的主层(backing layer)。

UIView 负责处理事件和响应用户交互,参与响应连,而 CALayer 负责管理和绘制视图的内容 contents。

设计原则,职责分工,单一职责。

2. 事件传递

// 这个方法用于确定触摸事件的最终响应者视图,即用户触摸的最终视图。
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;  

// 这个方法用于确定触摸点是否在当前视图的边界内。
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;   

hitTest:withEvent: 方法首先调用 pointInside:withEvent: 方法来确定给定点是否在视图的范围内。如果 pointInside:withEvent: 返回 NO,则 hitTest:withEvent: 返回 nil。如果返回 YES,则它会递归调用子视图的 hitTest:withEvent:方法,直到找到最终的响应者视图。

// 简化的内部实现
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event {

    // 如果视图不可交互、隐藏或透明度接近0,返回nil
    if (!self.userInteractionEnabled || self.hidden || self.alpha <= 0.01) {
        return nil;
    }

    // 检查触摸点是否在当前视图内
    if ([self pointInside:point withEvent:event]) {
        // 逆序遍历子视图数组(从最上层的子视图开始)
        for (UIView *subview in [self.subviews reverseObjectEnumerator]) {
            // 将触摸点转换为子视图的坐标系
            CGPoint convertedPoint = [subview convertPoint:point fromView:self];
            // 调用子视图的hitTest:withEvent:方法
            UIView *hitTestView = [subview hitTest:convertedPoint withEvent:event];
            // 如果找到了最终的响应者视图,返回它
            if (hitTestView != nil) {
                return hitTestView;
            }
        }
        // 如果没有子视图包含触摸点,返回当前视图
        return self;
    }
    // 如果触摸点不在当前视图内,返回nil
    return nil;
}

示例:方形按钮指定区域内接受事件响应(方形里面有个最大圆,圆形接受事件,四个角忽略事件)

#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN

@interface CustomButton : UIButton

@end

NS_ASSUME_NONNULL_END


#import "CustomButton.h"

@implementation CustomButton


- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    if (!self.userInteractionEnabled || self.hidden || 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 convertPoint = [self convertPoint:point toView:obj];
            // 调用子视图的 hittest方法
            hit = [obj hitTest:convertPoint withEvent:event];
            if (hit) {
                *stop = YES;
            }
        }];

        if (hit) {
            return  hit;
        }
        return self;
    }
    return nil;
}


// 直角三角形 a*a + b*b = c*c
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
    CGFloat x1 = point.x;
    CGFloat y1 = point.y;

    CGFloat x2 = self.frame.size.width / 2.0;
    CGFloat y2 = self.frame.size.height / 2.0;

    double dis = sqrt( (x1-x2)*(x1-x2) + (y1-y2)*(y1-y2) );

    if (dis <= self.frame.size.width / 2.0) {
        return YES;
    }
    return NO;
}

@end


3. 响应链

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {}
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {}
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {}
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {}

在 iOS 开发中,响应链(Responder Chain)是事件处理系统的重要组成部分。它决定了用户触摸事件(例如点击、滑动等)如何在视图层次结构中传播和处理。UIView 是响应链中的关键参与者。理解响应链对于实现自定义用户交互和处理复杂的事件分发逻辑至关重要。

概念: 响应链是一个对象链,这些对象有能力响应和处理事件。主要的响应者包括:

  • UIView:所有视图对象。
  • UIViewController:视图控制器。
  • UIWindow:窗口对象。
  • UIApplication:应用程序对象。

事件首先由视图(通常是用户直接触摸的视图)处理,如果该视图无法处理事件,则事件沿着响应链向上传递,直到找到能够处理该事件的对象或者链的终点。如果没有处理者则忽略这个事件,并不会引起闪退。

触摸事件的传递:触摸事件首先由最前面的视图(触摸点所在的视图)接收,然后通过以下方法传递:

  1. hitTest:withEvent::确定触摸点所在的最终视图。
  2. pointInside:withEvent::检查触摸点是否在当前视图的范围内。

一旦找到触摸点所在的视图,事件传递过程如下:

  • touchesBegan:withEvent:
  • touchesMoved:withEvent:
  • touchesEnded:withEvent:
  • touchesCancelled:withEvent:

这些方法可以在 UIView 子类中重写以处理触摸事件。

响应链的查找和转发:如果视图无法处理事件,它会将事件传递给下一个响应者,通常是其父视图或视图控制器。可以通过以下方法确定下一个响应者:

  • nextResponder:返回下一个响应者。

对于 UIViewnextResponder 通常是其父视图或拥有它的视图控制器。

示例:假设我们有一个自定义视图和视图控制器,它们处理触摸事件并转发给下一个响应者:

@interface CustomView : UIView
@end

@implementation CustomView

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [super touchesBegan:touches withEvent:event];
    NSLog(@"CustomView touchesBegan");
    // 如果不处理,传递给下一个响应者
    [[self nextResponder] touchesBegan:touches withEvent:event];
}

@end
@interface CustomViewController : UIViewController
@end

@implementation CustomViewController

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [super touchesBegan:touches withEvent:event];
    NSLog(@"CustomViewController touchesBegan");
    // 可以在这里处理事件
}

@end

响应链示例:

  1. 视图层次结构

    • UIWindow
      • CustomViewController
        • CustomView
  2. 事件传递

    • 用户触摸 CustomView
    • CustomViewtouchesBegan:withEvent: 方法被调用。
    • 如果 CustomView 不处理事件,它会调用 nextResponder 将事件传递给 CustomViewController
    • CustomViewControllertouchesBegan:withEvent: 方法被调用。

响应链的实际应用:

响应链的设计使得事件处理更加灵活和模块化。以下是一些实际应用场景:

  • 事件分发:视图可以选择处理事件或将事件传递给父视图或控制器。
  • 手势识别:手势识别器(如 UITapGestureRecognizer)可以添加到视图上,视图可以选择处理特定手势,而将其他手势传递给上层视图或控制器。
  • 自定义事件传递:通过覆盖 nextResponder 方法,可以创建自定义的事件传递逻辑。

自定义 nextResponder:

你可以在自定义视图或视图控制器中重写 nextResponder 方法,以改变响应链的默认行为。例如:

@interface CustomView : UIView
@end

@implementation CustomView

- (UIResponder *)nextResponder {
    // 返回一个特定的响应者
    return [super nextResponder];
}

@end

通过掌握响应链的机制和灵活运用其传递逻辑,可以设计出复杂而优雅的用户交互和事件处理系统。

图像显示原理

理解图像显示原理从 CPU 和 GPU 的角度出发,是深入掌握 iOS 图像处理和优化的关键。

1. CPU 和 GPU 的角色

Layout: UI布局,文本计算,Display: 绘制,drawRect, Prepare: 图片编解码,Commit: 提交位图

  1. CPU(中央处理器)

    • 负责图像的解码、处理和计算。
    • 处理与视图层级结构相关的布局计算和更新。
    • 执行应用程序的主逻辑。
  2. GPU(图形处理器)

    • 专门用于图像渲染和图形计算。
    • 处理纹理绘制、图形变换、抗锯齿、阴影和其他复杂的图形运算。
    • 将图像渲染到屏幕上。

2. 图像显示流程

  1. 图像加载与解码(CPU)
    • 从文件或网络加载图像数据(例如 PNG 或 JPEG 格式)。
    • 解码图像数据,将其转换为可供渲染的位图格式。
    • 使用 UIImage 类进行图像加载和解码。
UIImage *image = [UIImage imageNamed:@"example.png"];
  1. 视图布局与更新(CPU)
    • 计算视图层级的布局和位置。
    • 调用 layoutSubviewssetNeedsLayout 方法触发视图层次的更新。
[self.view setNeedsLayout];
[self.view layoutIfNeeded];
  1. 图像绘制与渲染(GPU)
    • 将解码后的图像数据上传到 GPU。
    • 使用 OpenGL ES 或 Metal 等图形框架进行图像渲染。
    • 通过 CALayerUIViewdrawRect: 方法将图像绘制到屏幕上。
- (void)drawRect:(CGRect)rect {
    UIImage *image = [UIImage imageNamed:@"example.png"];
    [image drawInRect:rect];
}

3. 性能优化

为了优化图像显示性能,我们需要尽量减少 CPU 和 GPU 的负载,确保流畅的用户体验。

减少 CPU 负载

  • 异步解码:避免在主线程上进行图像解码。
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
    UIImage *image = [UIImage imageWithData:[NSData dataWithContentsOfURL:url]];
    dispatch_async(dispatch_get_main_queue(), ^{
        imageView.image = image;
    });
});
  • 图像缓存:使用内存和磁盘缓存来减少重复解码。
[imageView sd_setImageWithURL:url placeholderImage:[UIImage imageNamed:@"placeholder"]];
  • 预先计算布局:尽量在后台线程计算视图布局,避免频繁调用 layoutSubviews
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    [self.view setNeedsLayout];
    [self.view layoutIfNeeded];
});

减少 GPU 负载

  • 合并绘制操作:尽量减少视图层级,合并多个绘制操作为一个。
UIGraphicsBeginImageContext(view.bounds.size);
[view.layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
  • 使用 Core Animation:利用 Core Animation 的图层特性,减少复杂视图层级。
CALayer *layer = [CALayer layer];
layer.contents = (id)image.CGImage;
[self.view.layer addSublayer:layer];
  • 避免过度的离屏渲染:尽量减少使用阴影、圆角、透明度等属性,避免触发离屏渲染。
view.layer.shadowOpacity = 0;  // 禁用阴影
view.layer.cornerRadius = 0;   // 禁用圆角

4. 深入理解

  • 离屏渲染(Offscreen Rendering):某些效果(如阴影、圆角、遮罩等)会导致 GPU 进行离屏渲染,这是非常耗费性能的。尽量避免使用这些效果,或者尽量合并这些操作。

  • 纹理上传:频繁地将图像纹理从 CPU 上传到 GPU 是非常昂贵的操作。尽量减少图像的频繁更新,尤其是在动画中。

  • 帧率:保持高帧率(60FPS),确保界面流畅。监控和优化应用的性能,使用工具如 Instruments 的 Core Animation 来检测性能瓶颈。

UI卡顿,掉帧原因

为了实现流畅的动画和用户体验,iOS 设备通常每秒钟需要渲染 60 帧(60 FPS)。这意味着每帧的渲染时间需要在 16.7 毫秒(1000ms / 60 ≈ 16.7ms)内完成。这包括 CPU 处理视图布局、图像解码、逻辑处理以及 GPU 进行图像渲染和显示。

  1. CPU

    • 对象的创建,调整,销毁
    • 预排班(布局计算,文本计算)
    • 预渲染(文本等异步绘制,图片编解码等)
  2. GPU

    • 纹理渲染
    • 视图混合

  1. CPU 处理

    • 布局计算:计算视图层次结构的位置和大小。
    • 事件处理:处理用户交互和响应。
    • 图像解码:将图像数据从磁盘或网络中加载并解码为位图。
    • 准备渲染指令:将需要渲染的内容准备好并传递给 GPU。
  2. GPU 渲染

    • 接收渲染指令:从 CPU 接收要渲染的内容。
    • 图像渲染:将位图图像渲染到屏幕上。
    • 图形变换:处理各种图形效果,如阴影、圆角、渐变等。

UI 卡顿和掉帧的主要原因是 CPU 或 GPU 不能在 16.7ms 内完成它们的工作。以下是一些常见的原因:

  1. CPU 过载

    • 复杂的布局计算:视图层次结构过于复杂,需要大量的计算。
    • 频繁的视图更新:频繁调用 setNeedsLayoutlayoutSubviews,导致大量的布局计算。
    • 图像解码:大尺寸或复杂图像的解码在主线程上进行,阻塞了主线程。
    • 复杂的事件处理:处理复杂的用户交互逻辑或长时间运行的任务。
  2. GPU 过载

    • 复杂的图形效果:使用大量的阴影、圆角、透明度、渐变等效果,导致 GPU 渲染负担过重。
    • 离屏渲染:某些效果(如阴影、圆角、遮罩等)会触发 GPU 的离屏渲染,这是一种昂贵的操作。
    • 大纹理处理:处理高分辨率的图像纹理,会增加 GPU 的负担。

UIView绘制原理

1. 绘制流程

UIView 的绘制过程主要包括以下几个步骤:

  1. 布局(Layout)

    • 在视图层次结构中,每个视图都会按照布局约束和位置属性进行布局。这是在视图树中从上到下进行的。
  2. 绘制(Drawing)

    • 每个 UIView 都有一个 CALayer 对象作为其图层(layer),CALayer 负责实际的图形内容的显示。
    • UIView 提供了一个 drawRect: 方法,用于在视图中绘制自定义内容。

绘制的核心方法

setNeedsDisplay 是一个用来标记视图需要重绘的方法。调用这个方法会将视图标记为“需要重绘”,但不会立即绘制。系统会在下一次屏幕刷新周期调用视图的 drawRect: 方法来重绘视图。

[self.view setNeedsDisplay];

drawRect: 方法是在需要重绘视图时由系统调用的。你可以在这个方法中执行自定义绘制代码。

- (void)drawRect:(CGRect)rect {
    // 获取绘图上下文
    CGContextRef context = UIGraphicsGetCurrentContext();

    // 绘制一个红色矩形
    CGContextSetFillColorWithColor(context, [UIColor redColor].CGColor);
    CGContextFillRect(context, rect);
}

绘制过程的底层实现 Core Graphics

drawRect: 方法通常使用 Core Graphics 框架(也称为 Quartz 2D)进行绘制。Core Graphics 提供了强大的 2D 绘图功能。

- (void)drawRect:(CGRect)rect {
    CGContextRef context = UIGraphicsGetCurrentContext();

    // 设置绘制颜色
    CGContextSetFillColorWithColor(context, [UIColor blueColor].CGColor);

    // 绘制一个圆
    CGContextFillEllipseInRect(context, rect);
}

CALayer

每个 UIView 都有一个关联的 CALayer

CALayer 是 Core Animation 框架的一部分,它负责处理视图的内容和动画效果。CALayer 提供了高效的绘制和动画支持,并通过 GPU 进行加速。

UIViewCALayer 的关系

  • 每个 UIView 都有一个 CALayer 实例,称为其“主图层”。
  • UIView 负责处理用户交互和管理视图层次,而 CALayer 负责内容绘制和动画。

CALayer 的绘制

CALayer 的内容可以通过以下几种方式设置:

直接设置内容

可以直接设置 CALayercontents 属性,这通常用于静态图像。

CALayer *layer = [CALayer layer];
layer.contents = (id)[UIImage imageNamed:@"example.png"].CGImage;
[self.view.layer addSublayer:layer];

使用 drawRect:

UIViewdrawRect: 方法中使用 Core Graphics 绘制内容。

- (void)drawRect:(CGRect)rect {
    CGContextRef context = UIGraphicsGetCurrentContext();
    CGContextSetFillColorWithColor(context, [UIColor redColor].CGColor);
    CGContextFillRect(context, rect);
}

使用 drawLayer:inContext:

自定义的 CALayer 可以重写 drawLayer:inContext: 方法进行绘制。

@interface CustomLayer : CALayer
@end

@implementation CustomLayer
- (void)drawInContext:(CGContextRef)ctx {
    CGContextSetFillColorWithColor(ctx, [UIColor blueColor].CGColor);
    CGContextFillRect(ctx, self.bounds);
}
@end

绘制流程

  1. 标记重绘

    • 当调用 setNeedsDisplaysetNeedsDisplayInRect: 时,视图会被标记为需要重绘,系统会在下一次屏幕刷新时调用 drawRect: 方法。
  2. 准备绘制上下文

    • drawRect: 中,系统提供一个图形上下文(CGContextRef),可以在这个上下文中执行绘图操作。
  3. 执行绘制代码

    • drawRect: 方法中执行自定义绘制代码,使用 Core Graphics API 绘制内容到上下文中。
  4. 提交绘制内容

    • 系统将绘制内容提交给 CALayerCALayer 再将其交给 GPU 进行最终渲染。

性能优化

为了确保流畅的用户体验,需要优化绘制过程,以避免 UI 卡顿和掉帧。

优化策略

  1. 减少视图层次结构复杂度

    • 避免嵌套过深的视图层次结构,以减少布局计算和绘制开销。
  2. 异步绘制

    • 使用后台线程进行图像解码和复杂的绘制操作,然后将结果提交到主线程。
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
    UIGraphicsBeginImageContext(size);
    CGContextRef context = UIGraphicsGetCurrentContext();
    // 执行复杂绘制操作
    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    dispatch_async(dispatch_get_main_queue(), ^{
        imageView.image = image;
    });
});
  1. 使用离屏渲染
    • 离屏渲染会导致额外的性能开销,尽量避免使用不必要的阴影、圆角、透明度等效果。
view.layer.shadowOpacity = 0;  // 禁用阴影
view.layer.cornerRadius = 0;   // 禁用圆角
  1. 优化绘制区域
    • 使用 setNeedsDisplayInRect: 仅标记需要重绘的区域,减少不必要的绘制开销。
[self setNeedsDisplayInRect:CGRectMake(0, 0, 50, 50)];

总结

UIView 的绘制原理涉及到 CPU 和 GPU 的协同工作,通过 UIViewCALayer 的紧密配合,实现高效的图形渲染。理解和优化这个过程,对于提升应用性能至关重要。以下是关键点:

  • UIView 负责视图层次和用户交互,CALayer 负责内容绘制和动画。
  • 使用 setNeedsDisplay 标记视图需要重绘,系统会在合适的时机调用 drawRect: 方法。
  • Core Graphics 是绘制的核心工具,提供了强大的 2D 绘图功能。
  • 优化绘制过程,通过减少视图层次、异步绘制、避免离屏渲染等方法,确保流畅的用户体验。

这些知识不仅能帮助你在面试中展示对 iOS 绘制机制的深刻理解,还能在实际开发中应用,提高应用的性能和用户体验。

2. 异步绘制

离屏渲染

1. 概念

  • On-Screen Rendering

    意为当前屏幕渲染,指的是GPU的渲染操作是在当前用于显示的屏幕缓冲区中进行

  • Off-Screen Rendering

    意为离屏渲染,指的是GPU在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作

2. 何时触发

  • 圆角(当和maskToBounds一起使用时)
  • 图层蒙版
  • 阴影
  • 光栅化

3. 为何要避免?

离屏渲染会增加 GPU 的工作量,导致 cpu+gpu 的工作耗时超过 16.7ms, 导致 UI 卡顿和掉帧,所以要避免离屏渲染。

  • 会创建新的渲染缓冲区,内存有开销
  • 上下文切换导致 GPU 额外开销

常见面试题

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