第一章 UI相关面试题

本文深入探讨了iOS中UIView与CALayer的区别和联系,讲解了事件传递和视图响应链的工作原理,以及如何处理触摸事件。此外,介绍了离屏渲染的概念、性能影响及常见优化策略。最后,阐述了如何通过RunLoop检测应用卡顿,并提供了相关代码实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

  • 欲练此功,挥刀自宫。若不自宫,功起热生。热从身起,身燃而生。由下窜上,燥乱不定。即便热止,身伤不止。自宫以后,真气自生。汇入丹田,无有制碍。气生值法,思色是苦。厌苦舍离,以达性静。性静以后,手若拈花。
一、 UIView和CALayer的区别和联系
  1. 每个UIView内部都有一个CALayer在背后提供内容的绘制和显示,并且UIView的尺寸样式都由内部的Layer所提供。两者都有树状层级结构,layer内部有SubLayers,View内部有SubViews。但是Layer比View多了个AnchorPoint。
  2. 在View显示的时候,UIView做为Layer的CALayerDelegate,View的显示内容取决于内部的CALayer的display。
  3. CALayer是默认修改属性支持隐式动画的,在给UIView的Layer做动画的时候,View作为Layer的代理,Layer通过actionForLayer:forKey:向View请求相应的action(动画行为)。
  4. layer内部维护着三分layer tree,分别是presentLayer Tree(动画树),modeLayer Tree(模型树),Render Tree (渲染树)。在做iOS动画的时候,我们修改动画的属性,在动画的其实是Layer的 presentLayer的属性值,而最终展示在界面上的其实是提供View的modelLayer。
  5. 两者最明显的区别是View可以接受并处理事件,而 Layer 不可以。
二、iOS 事件传递和视图响应链
1、事件的分类

multitouch events:所谓的多点触摸事件,即用户触摸屏幕交互产生的事件类型;
motion events:所谓的移动事件。是指用户在摇晃、移动和倾斜手机的时候产生的事件称为移动事件。这类事件依赖于iPhone手机里边的加速器,陀螺仪等传感器;
remote control events:所谓的远程控制事件。指的是用户在操作多媒体的时候产生的事件。比如,播放音乐,视频等。

2、触摸事件
  • UIEvent
    iOS将触摸事件定义第一个手指开始触摸屏幕到最后一个手指离开屏幕为一个触摸事件。用类UIEvent表示。
  • UITouch
    一个手指第一次点屏幕,会形成一个UITouch对象,知道离开销毁,表示触碰。UITouch对象能表明了当前手指触碰屏幕的位置,状态。状态分为开始触碰,移动和离开。
    根据定义,UIEvent实际包括了多个UITouch对象。有几个手指触碰,就会有几个UITouch对象。

UITouch中phase表明了手指移动的状态,包括 1.开始点击;2.移动;3.保持; 4.离开;5.被取消(手指没有离开屏幕,但是系统不再跟踪它了)

综上,UIEvent就是一组UITouch。每当该组中任何一个UITouch对象的phase发生变化,系统都会产生一条TouchMessage。也就是说每次用户手指的移动和变化,UITouch都会形成状态改变,系统变回会形成Touch message进行传递和派发。

3、Responder

有个很重要的属性,nextResponder,表明响应是一个链表结构,通过nextResponder找到下一个responder。这里是从第一个responder开始通过nextResponder传递事件,直到有responder响应了事件就停止传递;如果传递到最后一个responder都没有被响应,那么该事件就被抛弃。

  • 程序启动
    UIApplication会生成一个单例,并会关联一个APPDelegate。APPDelegate作为整个响应链的根建立起来,而UIApplication会将自己与这个单例链接,即UIApplication的nextResponder(下一个事件处理者)为APPDelegate
  • 创建UIWindow
    程序启动后,任何的UIWindow被创建时,UIWindow内部都会把nextResponser设置为UIApplication单例。
    UIWindow初始化rootViewController, rootViewController的nextResponser会设置为UIWindow
  • UIViewController初始化
    loadView, VC的view的nextResponser会被设置为VC.
  • addSubView
    addSubView操作过程中,如果子subView不是VC的View,那么subView的nextResponser会被设置为superView。如果是VC的View,那就是 subView -> subView.VC ->superView
4、事件的传递
4.1 事件传递的流程

触摸事件的传递是从父控件传递到子控件
也就是从UIApplicaiton->window->寻找处理事件的最合适的view
注意:如果父控件不能接受触摸事件,那么子控件就不可能接收到触摸事件。

4.2 如何寻找最合适的控件来处理事件

①.首先判断主窗口(keyWindow)自己是否能接受触摸事件
②.判断触摸点是否在自己身上
③.子控件数组中从后往前遍历子控件,重复前面两个步骤(所谓从后往前遍历子控件,就是首先查找子控件数组中最后一个元素,然后执行1、2步骤)
④.如果没有符合条件的子控件,那么就认为自己最合适处理这个事件,也就是自己是最合适的view。

4.3 两个重要的方法

view会调用hitTest:withEvent:方法,hitTest:withEvent:方法底层会调用pointInside:withEvent:方法判断触摸点是不是在这个view的坐标上。如果在坐标上,会分发事件给这个view的子view。然后每个子view重复以上步骤,直至最底层的一个合适的view。

  • UIView不能接收触摸事件的三种情况:
  1. 不允许交互:userInteractionEnabled = NO
  2. 隐藏:如果把父控件隐藏,那么子控件也会隐藏,隐藏的控件不能接受事件
  3. 透明度:如果设置一个控件的透明度<0.01,会直接影响子控件的透明度。alpha:0.0~0.01为透明。
5、事件的响应
5.1 触摸事件处理的整体过程

1 用户点击屏幕后产生的一个触摸事件,经过一系列的传递过程后,会找到最合适的视图控件来处理这个事件
2 找到最合适的视图控件后,就会调用控件的touches方法来作具体的事件处理touchesBegan…touchesMoved…touchedEnded…
3 这些touches方法的默认做法是将事件顺着响应者链条向上传递(也就是touch方法默认不处理事件,只传递事件),将事件交给上一个响应者进行处理

5.2 响应者链条

在iOS程序中无论是最后面的UIWindow还是最前面的某个按钮,它们的摆放是有前后关系的,一个控件可以放到另一个控件上面或下面,那么用户点击某个控件时是触发上面的控件还是下面的控件呢,这种先后关系构成一个链条就叫响应者链。也可以说,响应者链是由多个响应者对象连接起来的链条。

事件响应会先从底层最合适的view开始,然后随着上一步找到的链一层一层响应touch事件。默认touch事件会传递给上一层。如果到了viewcontroller的view,就会传递给viewcontroller。如果viewcontroller不能处理,就会传递给UIWindow。如果UIWindow无法处理,就会传递给UIApplication。如果UIApplication无法处理,就会传递给UIApplicationDelegate。如果UIApplicationDelegate不能处理,则会丢弃该事件。

三、如何让UIButton点击范围变大

1.比较土的方法,在UIButton上覆盖一个稍微大一点的透明的Button
2.定义一个MyButton类,继承UIButton, 重写pointInside方法,扩大热区

override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
        var bounds = self.bounds
        
        let widthDelta = max(44 - bounds.size.width, 0)
        let heightDelta = max(44 - bounds.size.height, 0)
        
        bounds = bounds.insetBy(dx: -widthDelta, dy: -heightDelta)
        return bounds.contains(point)
        
    }
四、离屏渲染
  1. 离屏渲染的原理
    <1> 一般情况下,GPU渲染一个普通的视图,会先把绘制好的数据存储在一个frame buffer(帧缓冲区)
    <2> 在一些特殊的情况下,比如阴影、光栅化等效果下,GPU无法一次性扫描出图像,并且直接存放到frame buffer中,因为GPU采用的是图层叠加的“油画算法”,就是一层一层地绘制,然后再叠加起来。
    <3> 当一层无法绘制最终的图片的时候,就需要额外开辟一块控件来存放临时图层,我们称之为Off Screen buffer(离屏缓冲区),直到最后再存放到frame buffer中。

  2. 离屏渲染性能分析
    a. 优点
    (1) 有些后续经常用到的图层数据,可以先缓存在离屏缓冲区,用的时候直接使用。
    (2)一些特殊效果,正常使用一个帧缓冲区是无法完成,不得不用离屏缓冲区,才能完成渲染。
    b. 缺点
    (1)既然多开一个缓冲区,那么肯定是会带来空间上的消耗。(相对来说影响较小)
    (2)触发离屏渲染,从帧缓冲区切换到离屏缓冲区,渲染完毕后再切换会帧缓冲区,这个上下文来回切换的过程是比较耗费性能。
    (3)数据从离屏缓冲区取出,再存入帧缓冲区,也是需要耗费时间。
    (4)离屏缓冲区存在空间限制,是屏幕像素的2.5倍,当大于这一值时候,不会触发离屏渲染。
    c. 优化方向
    (1)尽量避免裁切masksToBounds
    (2)如果无法避免裁切,那么久尽量对单个子view进行裁切,裁切好了再加到需要的view里面去
    (3)尽量可以让UI同事做好圆角图片
    (4)用贝塞尔曲线去画代替裁切

  3. 离屏渲染的场景分析总结:
    前言,一般的View都有三层:background层,content层,border层
    (1)UILabel三个条件同时满足即可
    - 必须设置一: contents层
    - 必须设置二: 圆角
    - 必须设置三:(两个任选一个:layer.backgroundColor或layer.border层设置一个)

        let label = UILabel(frame: CGRect(x: 30, y: 200, width: 300, height: 150))
        // 必须要设置的有2个
        // 必须设置一: contents层
        label.text = "我是一个文本我是一个文本我是一个文本我是一个文本我是一个文本我是一个文本我是一个文本我是一个文本"
//        label.textAlignment = .center
        
        // 必须设置二: 圆角
        label.layer.cornerRadius = 10
        label.layer.masksToBounds = true
        
        // 必须设置(两个任选一个:layer.backgroundColor或layer.border层设置一个)
//        label.backgroundColor = .red // 设置UILabel的backgroundColor是不会触发离屏渲染的
//        label.layer.backgroundColor = UIColor.red.cgColor
        label.layer.borderWidth = 1

(2)UIButton/UIImageView三个条件同时满足即可
- 必须设置一: contents层
- 必须设置二: 圆角
- 必须设置三:(三个任选一个: backgroundColor或layer.backgroundColor或layer.border层设置一个)

        let btn = UIButton(frame: CGRect(x: 30, y: 200, width: 300, height: 150))
      // 必须设置的:
      // 1. contents层
      btn.setTitle("我是标题", for: .normal)
      // 2. 圆角
      btn.layer.cornerRadius = 10
      btn.layer.masksToBounds = true
//        btn.clipsToBounds = true
      
      // 3.必须选择一个设置: backgroundColor 或 layer.backgroundColor 或 layer.borderWidth
      // backgroundColor层
//        btn.backgroundColor = .red // 会触发离屏渲染
      btn.layer.backgroundColor = UIColor.red.cgColor // 也会触发离屏渲染
      
      // border层
//        btn.layer.borderWidth = 1

(3) 光栅化
条件一:对于background层、content层、 border层,任意设置一层
条件二:开启光栅化即可

        let imgV = UIImageView(frame: CGRect(x: 30, y: 200, width: 300, height: 150))
        
//        imgV.backgroundColor = .red
//        imgV.layer.borderWidth = 1
        // contents层
        imgV.image = UIImage(named: "book")
//        imgV.adjustsImageSizeForAccessibilityContentSizeCategory = true
        
        // 光栅化,静态图片下,其实是有优化作用
        // 缓存100ms
        // 屏幕像素的2.5倍以内,缓存空间有限
        imgV.layer.shouldRasterize = true

(4) 遮罩,和光栅化类似

        let imgV = UIImageView(frame: CGRect(x: 30, y: 200, width: 300, height: 150))
       
       // contents层
       imgV.image = UIImage(named: "book")
       imgV.adjustsImageSizeForAccessibilityContentSizeCategory = true
       
       // 遮罩,触发离屏渲染
       var layer1 = CALayer()
       layer1.frame = CGRect(x: 30, y: 30, width: imgV.bounds.size.width, height: imgV.bounds.size.height)
       layer1.backgroundColor = UIColor.red.cgColor
       imgV.layer.mask = layer1

(5) 阴影

        let imgV = UIImageView(frame: CGRect(x: 30, y: 200, width: 300, height: 150))
       imgV.backgroundColor = .blue
       
       // contents层
       imgV.image = UIImage(named: "book")
       imgV.adjustsImageSizeForAccessibilityContentSizeCategory = true

       // 阴影
       imgV.layer.shadowColor = UIColor.red.cgColor
       imgV.layer.shadowOpacity = 1
       imgV.layer.shadowRadius = 15
       imgV.layer.shadowOffset = CGSize(width: 0, height: 3)
       imgV.layer.masksToBounds = false // 注意:这里要设置为false,阴影才可见
       
       // 优化方向:增加shadowPath,增加这个就不会触发离屏渲染
       // 因为shadowPath,不需要依赖view,可以独立渲染,使用了贝塞尔曲线绘制
//        imgV.layer.shadowPath = UIBezierPath(rect: CGRect(x: 0, y: 0, width: imgV.bounds.size.width + 20, height: imgV.bounds.size.height + 20)).cgPath

(6) 抗锯齿

        let imgV = UIImageView(frame: CGRect(x: 30, y: 200, width: 300, height: 150))
        imgV.backgroundColor = .blue
        
        // contents层
        imgV.image = UIImage(named: "book")

        // 抗锯齿
        imgV.layer.allowsEdgeAntialiasing = true

(7)带有子视图的view,设置不透明

        let imgV = UIImageView(frame: CGRect(x: 30, y: 200, width: 300, height: 150))
        
        // 1. 首先是contents层
        imgV.image = UIImage(named: "book")
        imgV.contentMode = .scaleToFill

        // 2.其次是透明度设置: 0<alpha<1
        imgV.alpha = 0.8
        
        // 3.还要添加子view
        let subV = UIView(frame: CGRect(x: 0, y: 0, width: 30, height: 30))
        subV.backgroundColor = .red
        imgV.addSubview(subV)
        
        // 4.allowsGroupOpacity:默认开启的,允许子视图和父视图的透明度一样
        // 手动关闭,则不会触发离屏渲染
        imgV.layer.allowsGroupOpacity = false // 注意,手动关闭,则不会触发离屏渲染
五、展开讲下利用RunLoop检测卡顿

1.原理思想
(1)runloop是保证一个线程能正常处理事件的
(2)简单理解,runloop被唤醒后,来处理事件,处理完事件后,进入休眠
(3)那么从唤醒到再次休眠的时间,就是处理这次事件的耗时
(4)利用信号量来监听唤醒的状态
2. 代码封装(Swift版本)

import Foundation

class LagMonitor {
    private var sema = DispatchSemaphore(value: 0)
    private var activity: CFRunLoopActivity?
    private var observer: CFRunLoopObserver?
    private var startOb = false
    
    static let shared = LagMonitor()
    
    init() {
        createObserver()
    }
    
    private func createObserver() {
        observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, CFRunLoopActivity.allActivities.rawValue, true, 0) { ob, activity in
            self.activity = activity
            self.sema.signal()
        }
        
        // 添加观察者
        CFRunLoopAddObserver(CFRunLoopGetMain(), observer, .commonModes)
    }
    
    /**
     *   timeOutVal: 设置卡顿时间,单位ms,超过这个值,作为你的卡顿标准
     */
    public func startOB(_ timeOutVal: Int) {
        self.startOb = true
        DispatchQueue.global().async {
            while self.startOb {
                if self.observer != nil {
                    let waitRes = self.sema.wait(timeout: DispatchTime.now() + .milliseconds(timeOutVal))
                    if waitRes == .timedOut {
                        if self.activity == .afterWaiting {
                            print("卡顿了一下",Thread.callStackSymbols)
                        }
                    }
                } else {
                    self.createObserver()
                }
            }
        }
    }
    
    public func endOB() {
        self.startOb = false
        self.observer = nil
        print("停止监控")
    }
}
  1. 代码封装(OC版)
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface LagMonitor : NSObject
+ (instancetype)sharedManager;
- (void)startObserve:(int)timeOutVal;
- (void)endOb;
@end

NS_ASSUME_NONNULL_END
#import "LagMonitor.h"

@interface LagMonitor()
@property(nonatomic, strong) dispatch_semaphore_t sema;
@property(nonatomic, assign) CFRunLoopObserverRef observer;
@property(nonatomic, assign) CFRunLoopActivity activity;
@property(nonatomic, assign) BOOL isStartOB;
@end

@implementation LagMonitor

static LagMonitor *single = nil;

+ (instancetype)sharedManager {
    static dispatch_once_t once;
    dispatch_once(&once, ^{
        single = [[self alloc] init];
    });
    
    return single;
}

- (void)createOB {
    self.observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopBeforeTimers | kCFRunLoopAfterWaiting, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        self.activity = activity;
        dispatch_semaphore_signal(self.sema);
    });
    
    CFRunLoopAddObserver(CFRunLoopGetMain(), self.observer, kCFRunLoopCommonModes);
    
    self.isStartOB = YES;
    self.sema = dispatch_semaphore_create(0);
}

- (void)startObserve:(int)timeOutVal {
    if (self.observer == nil) {
        [self createOB];
    }
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        while (self.isStartOB) {
            long semaphoreWait = dispatch_semaphore_wait(self.sema, dispatch_time(DISPATCH_TIME_NOW, timeOutVal*NSEC_PER_MSEC));
        
            if (semaphoreWait != 0) { // 不等于0,超时
                
                if (self.activity == kCFRunLoopAfterWaiting) {
                    NSLog(@"卡顿--%@",[NSThread callStackSymbols]);
                }
            };
        };
    });
}

- (void)endOb {
    if (self.observer == nil) {
        CFRelease(self.observer);
    }
    self.isStartOB = NO;
    NSLog(@"endOB");
}
@end
六、核心动画

不错的文章

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值