天天看點

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中,這樣就能保證在滑動過程中定時器正常運作了。

繼續閱讀