天天看点

iOS 2023最新面试题(上)-KVO KVC Runloop

作者:有一说一哥哥
iOS 2023最新面试题(上)-KVO KVC Runloop

一.KVO和KVC

KVO(Key-Value Observing)是一种观察者模式,允许对象对其他对象特定属性的更改进行监听,当这个属性发生变化时,KVO 机制会自动通知观察者进行相应的处理。KVO 是基于 runtime 实现的,通过动态创建一个新的子类,在子类中重写被观察的属性的 setter 方法,在 setter 方法中调用 Foundation 框架提供的方法,从而实现对属性的监听。

KVO 的运用主要是在 iOS 开发中,比如可以监听 UIView 中的 frame 和 bounds 等属性的变化,用于自适应布局;也可以用于观察网络请求的状态变化等。使用 KVO 需要注意以下几点:

  1. 被观察对象需要遵守 KVO 的一些规范,如属性必须使用 @objc dynamic 修饰符声明,否则 KVO 监听不到属性的变化。
  2. 观察者需要实现 observeValue(forKeyPath:of:change:context:) 方法,在这个方法中处理被观察对象属性变化时的逻辑。
  3. 观察者在添加监听之后,需要在适当的时候手动移除监听,否则会导致内存泄漏和潜在的崩溃问题。
  4. 不要过度使用 KVO,因为过多的属性监听会导致程序性能下降和难以维护。如果可能的话,可以使用其他方式替代 KVO,如回调、通知等。

综上,KVO 是一种强大的机制,可以用于实现对对象属性的监听,但使用时需要注意一些规范和细节,以确保程序的正确性和稳定性。

KVC(Key-Value Coding)是一种允许开发者通过属性名称字符串来访问对象属性值的机制。它是 Objective-C 运行时特性的一部分,允许开发者访问和修改对象属性的值,而无需调用明确的存取方法。KVC 的本质是通过运行时提供的函数实现对于 Objective-C 对象属性值的读取和写入。

KVC 的核心是 NSKeyValueCoding 协议,该协议定义了一些标准的方法来实现 KVC 机制,包括 value(forKey:)、setValue(:forKey:)、value(forKeyPath:)、setValue(:forKeyPath:) 等方法。这些方法允许开发者使用字符串类型的键值来访问对象属性。

通过 KVC,开发者可以很方便地对一个对象的属性进行批量设置、获取、监听等操作,使得代码更加简洁、可读性更高。同时,KVC 还可以实现一些高级功能,例如基于键值路径的属性访问、KVO 的实现等。

KVC 的优点:

  • 可以快速地获取或设置对象的属性值,不需要编写大量的 getter 和 setter 方法。
  • 能够避免直接使用属性访问器带来的一些副作用,例如自动触发 KVO。
  • 方便实现属性的批量赋值,提高代码的可读性和可维护性。

KVC 的缺点:

  • 当对象的属性名发生变化时,编译器无法检测到错误,需要开发人员手动调整代码。
  • 当对象的属性值不存在时,会抛出运行时异常。

KVO 的优点:

  • 可以在对象属性发生变化时,自动通知观察者进行更新,避免了手动更新 UI 界面的繁琐操作。
  • 通过观察者模式,能够实现对象之间的解耦,提高了代码的灵活性和可维护性。

KVO 的缺点:

  • 使用起来相对复杂,需要在适当的时候添加和移除观察者,避免引起内存泄漏和崩溃。
  • 需要遵守一定的编程规范,例如被观察的属性需要使用 @objc dynamic 关键字修饰,否则无法触发 KVO。
  • 只能观察对象的属性变化,无法观察对象的其他状态变化,例如方法的调用等。

二.什么是 RunLoop? RunLoop 作用有哪些?

RunLoop 是 iOS 中非常重要的一个机制,它是一个事件处理循环,用于处理事件(包括用户输入、定时器、网络请求等)并且在空闲的时候休眠以节省 CPU 资源。

RunLoop 的主要作用包括:

  1. 处理事件:RunLoop 可以监听输入源和定时器事件,当事件发生时自动通知相应的处理函数进行处理。
  2. 节省资源:RunLoop 可以在没有事件需要处理时自动休眠,避免 CPU 空转浪费资源。
  3. 多线程协作:RunLoop 可以在多线程间传递消息,协调线程之间的任务处理。
  4. 实现定时器等功能:RunLoop 可以实现延时调用、周期性调用等定时器功能。

RunLoop 的实现基于一个基础的时间循环结构,不断地从事件队列中取出事件进行处理,直到事件队列为空或者接收到退出循环的信号。RunLoop 可以在多种场景下使用,例如 UI 控件的交互响应、网络请求、定时器等。iOS 的 UI 框架要求所有 UI 相关的事件处理都在主线程中完成。

三.app 如何接收到触摸事件的 ?

当用户在 iOS 设备上触摸屏幕时,触摸事件由系统发送给当前应用程序进行处理。具体来说,iOS 中的触摸事件是由 UIResponder 对象处理的,而 UIResponder 是所有视图控制器和视图的基类。iOS 应用程序中的事件处理系统是基于触摸事件和 UIResponder 对象的。

当用户触摸屏幕时,系统会将事件传递给应用程序的主 UIApplication 对象。主 UIApplication 对象会将事件传递给当前应用程序的主窗口 UIWindow 对象。UIWindow 对象会将事件传递给它的子视图,子视图可以是视图控制器或普通视图对象。如果某个视图对象无法处理事件,则事件会继续向上遍历响应者链,直到找到能够处理事件的对象为止。

在响应者链中,每个对象都可以重载一系列方法来处理触摸事件。这些方法包括:

  • touchesBegan(_:with:)
  • touchesMoved(_:with:)
  • touchesEnded(_:with:)
  • touchesCancelled(_:with:)

这些方法可以被重载以处理具体的触摸事件。一般情况下,我们只需要在 UIView 或其子类中实现这些方法即可。

当触摸事件被传递到某个视图对象时,该对象会调用相应的方法来处理事件。如果事件被处理,则该事件不会继续向下传递,否则该事件会继续向下传递,直到找到能够处理事件的对象为止。这样,我们就可以在 iOS 应用程序中实现具体的触摸事件处理逻辑。

四.为什么只在主线程刷新 UI ?

在 iOS 应用程序中,UIKit 是一个重要的 UI 框架,但是它不是线程安全的,也就是说,当多个线程同时操作同一个视图时,可能会引发难以调试和复现的问题。为了避免这种问题,通常建议只在主线程中更新 UI,这是因为主线程是负责处理用户界面的线程,也是唯一与用户界面相关的线程。这意味着只有主线程才能够与应用程序的 UI 元素进行交互,如刷新 UI、接收输入事件等。

当其他线程修改 UI 时,可能会造成界面卡顿、闪屏等不良用户体验,甚至会导致应用程序崩溃。这些问题都是由于多个线程同时操作 UI 元素而引起的。因此,为了避免这些问题,我们需要确保所有的 UI 更新操作都在主线程中执行,以保证线程安全。

五.如何使线程保活?

在 iOS 开发中,我们通常使用 NSThread、GCD、NSOperation 等多线程技术来执行耗时任务,但是有时候我们需要在线程执行完毕后不退出,而是需要一直等待,直到某个条件满足才能退出,这就需要让线程保活。

以下是一些保活线程的方式:

  1. 通过 CFRunLoop 和 NSTimer 组合,使线程一直运行,直到手动调用停止。
  2. 通过 CFRunLoop 和 PerformSelector,定时执行一个 selector,避免线程进入休眠状态。
  3. 在子线程的 run loop 中创建一个 NSPort 对象,将这个 NSPort 添加到 run loop 中,然后使用 port sendBeforeDate 一直阻塞等待端口消息,直到接收到退出指令。

需要注意的是,保活线程可能会导致线程一直处于运行状态,对系统性能和资源消耗会有一定的影响,因此在使用时需要谨慎考虑。

NSPort 是 Foundation 框架提供的一种跨进程通信方式,可以用于进程间传递消息。以下是一个简单的 NSPort 使用实例:

  1. 在发送端创建 NSPort 实例并添加到 NSRunLoop 中:
let sendPort = NSPort() RunLoop.current.add(sendPort, forMode: .common)           
  1. 将 NSPort 实例传递给接收端,并将其添加到 NSRunLoop 中:
let receivePort = NSPort() RunLoop.current.add(receivePort, forMode: .common) let message = "Hello, NSPort!" let data = message.data(using: .utf8)! sendPort.send(before: Date.distantFuture, components: [data], from: nil, reserved: 0)           
  1. 在接收端监听 NSPort 实例并接收传递的消息:
class PortDelegate: NSObject, NSPortDelegate { func handle(_ message: NSPortMessage) { iflet data = message.components.first as? Data, let message = String(data: data, encoding: .utf8) { print("Received message: \(message)") } } } let delegate = PortDelegate() receivePort.delegate = delegate RunLoop.current.run()           

在上述代码中,我们创建了一个发送端的 NSPort 实例并添加到了当前 RunLoop 中,然后在接收端创建了一个 NSPort 实例并添加到了同一个 RunLoop 中,将消息发送给接收端。接收端通过设置代理来监听 NSPort 实例,并在收到消息时进行处理。在接收端的最后,我们通过调用 RunLoop.current.run() 来保持 RunLoop 运行,从而保持线程的保活状态。

NSPort 通常用于实现高性能的线程间通信,以及进程之间的 IPC(进程间通信),比如 macOS 中的 XPC(Cross-Process Communication)和 iOS 中的 Extension(扩展)等。

在使用 NSPort 进行通信时,如果你想要结束通信,则可以调用 invalidate() 方法来使端口无效,从而释放相关的资源。同时,你也可以在 NSPort 所在的线程或进程中调用 exit() 方法来结束该线程或进程,从而间接结束 NSPort 的使用。

六.子线程默认有RunLoop吗?RunLoop创建和销毁的时机又是什么时候呢?

RunLoop 的创建时机一般是在主线程中,在子线程中需要手动创建,并在合适的时候销毁。RunLoop 的创建过程可以在子线程中的入口函数中执行,如 -(void)main 或者 -(void)start 方法,销毁过程可以在任务执行结束时或者手动结束任务时执行。在执行任务时,需要在 RunLoop 中添加事件源或者 Timer,才能保证 RunLoop 持续运行。

需要注意的是,RunLoop 的创建和销毁都需要在同一线程中进行,否则可能会出现异常。另外,RunLoop 的创建和销毁需要成对出现,否则可能会导致内存泄漏或者崩溃问题。

七.RunLoop有哪些 Mode 呢?滑动时发现定时器没有回调,是因为什么原因呢?

1. kCFRunLoopDefaultMode :App的默认Mode,通常主线程是在这个Mode下运行

2. UITrackingRunLoopMode :界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响

3. UIInitializationRunLoopMode : 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用,会切换到kCFRunLoopDefaultMode

4. GSEventReceiveRunLoopMode : 接受系统事件的内部 Mode,通常用不到

5. kCFRunLoopCommonModes : 这是一个占位用的Mode,作为标记kCFRunLoopDefaultMode和UITrackingRunLoopMode用,并不是一种真正的Mode

  1. Default:默认模式,在这个模式下,RunLoop会处理所有注册的事件,包括NSDefaultRunLoopMode、UITrackingRunLoopMode。
  2. UITracking:这个模式是为了滑动等操作保证UI界面的流畅性而添加的,如果滑动过程中有一些UI动画或者其他事件被加入到UITrackingRunLoopMode中,就会将Default模式下的事件暂停,直到滑动结束或者其他耗时事件处理完毕才会继续执行Default模式下的事件。
  3. Common:公用模式,即同步处理所有在Default和Common两个模式下的事件。
  4. Connection:用于接受系统的输入源事件,只能通过内核传递,一般用于NSPort和NSConnection通信。
  5. Modal:用于处理模态的控件,比如弹出框等。在这个模式下,RunLoop不会处理除了Modal类型的事件之外的任何事件,因此可以阻止Modal控件之外的其他操作。
  6. EventTracking:跟踪控件,当我们调用beginTrackingWithTouch方法来处理用户交互事件时,系统会将RunLoop切换到这个模式,以便及时响应手势操作。

关于滑动时定时器没有回调的问题,是因为RunLoop运行在Default模式下,而当我们进行滑动时,RunLoop会自动将模式切换到UITracking模式下,所以此时在Default模式下注册的定时器就不会被处理,从而导致定时器没有回调。解决方法可以将定时器加入到UITrackingRunLoopMode中,这样就能保证在滑动过程中定时器正常运行了。

继续阅读