面試攻略:2023 年 iOS 技術問題集錦[In Progress]

Reed Hsin
12 min readFeb 16, 2024

--

我統整了在 2023 年的面試過程中,遇到關於 iOS 技術的問題,這些問題大多數是 iOS 開發中常見的話題,但也包括了一些較新的技術,例如 Actor

不同於過去,這次關於 Objective-C 的深入應用和底層實現的問題較少出現,即使提到,也多是概念性的討論,不再深入到具體的類別和函數

我想這樣的變化可能與遇到的面試官都是西方國家的人,以及 Swift 逐漸取代 Objective-C 成為主流趨勢有關吧 🙌

4. Sync 和 Async 的比較

解釋何謂 progress 和 thread,再解釋 Sync 和 Async

💡 可以先解釋 progress 和 thread

Thread 是 iOS 系統可用來執行 code 的最小單元,一個 iOS app 本身運行於一個 Single Progress 中,其中一個 Progress 可以有多個 thread

在 iOS 中,默認會有一個最高優先級別的 Main Thread,而其他的 background Threads 的優先級都會小於 Main Thread

我們可以把計算繁雜或請求資源的異步 task 交由 background thread 去做處理,善用多線程的優勢,增加 app 的體驗

💡 在個別解釋 Sync 和 Async

Sync

當前 thread 將 task 提交到 queue 後,會堵塞當前 thread

代表當前 thread 會等待此 task 完成(return) 後,才會繼續往下進行

Async

當前 thread 將 task 提交到 queue 後,不會堵塞當前 thread

代表不需等待此 task 完成與否,當前 thread 可以直接往下繼續進行

5. SerialQueue 和 ConcurrentQueue

可分別講解兩種 Queue 的定義、適合的場景、使用案例、相關 API

Serial Queue

  1. 一次只能執行一個 task,執行及完成的順序為 task 被加入的順序 (first in first out)
  2. 適合的場景

當任務需要按特定順序執行時,或者當你需要防止同時訪問共享資源時使用

3. 使用案例

UserDefault/Dictionary/Array 的單讀單寫

read: serial queue + sync, write: serial queue + sync

4. API

DispatchQueue(label: "com.example.mySerialQueue")

Concurrent Queue

  1. 多個任務可以同時執行。所以執行 task 的順序和時間點都無法確定
  2. 適合的場景

適用於提高效率和響應速度的情境,尤其當每個任務都相對獨立

3. 使用案例

UserDefault/Dictionary/Array 的多讀單寫

read: concurrent queue + sync

write: concurrent queue + async(flag: .barrier))

4. API

DispatchQueue(label: "com.example.myConcurrentQueue", attributes: .concurrent)

6. Concurrent Programming (並發編程) 的三大議題

講解 Race Condition, Priority Inversion, Dead Lock 的成因、導致的後果、在 iOS 可用什麼方法防範

💡重點可放在如何防範 Race Condition 以及 Dead Lock

Race Condition

指兩個或以上的 thread 同時對 data 進行讀寫操作時

將導致無法確定 data 真正的值,而已發預期外的錯誤

確保讀寫操作滿足 thread safe,以防範此問題的發生

Priority Inversion

指低優先級的 task 會因為某些原因,優先於高優先級的 task 執行

可能導致性能下降或者應用響應性變差

Swift 透過設定隊列的 Quality of Service (QoS) 屬性來防範此問題

Dead Lock

每個 thread 都在等待其他 thread 所握有的 resource,才可以完成手上的 task,近而釋放手上握有的 resource 給其他 thread 使用

在未改變此狀態前,所有的 thread 都會互相等待對方,造成 tasks 都無法往下進行,進而發生 dead lock

可能導致的後果

當 main thread(UI thread)陷入死鎖時,app 的用戶界面會完全凍結,無法響應任何用戶操作

若在 background thread 發生 dead lock,雖然用戶界面可能仍然響應,但特定的功能或服務可能會失效

長時間的死鎖可能導致系統終止 app progress。iOS系統監控應用的響應性,如果檢測到應用在規定時間內無響應,可能會強制終止 app,導致 app crash

防範

  1. 小心使用 thread lock
  2. serial queue 只要嵌套用 sync 提交任務便會 dead lock
// Dead lock
let que = DispatchQueue.init(label: "com.example.serialQueue")
que.async {
que.sync {
print("4")
}
}

// Dead lock
let que = DispatchQueue.init(label: "com.example.serialQueue")
que.sync {
que.sync {
print("4")
}
}

3. concurrent queue 不論如何嵌套提交任務都不會 dead lock

// No Dead lock
let queue = DispatchQueue(label: "com.example.myConcurrentQueue", attributes: .concurrent)
que.async {
que.sync {
print("4")
}
}

// No Dead lock
let queue = DispatchQueue(label: "com.example.myConcurrentQueue", attributes: .concurrent)
que.sync {
que.sync {
print("4")
}
}

7. Thread Safe 的討論

可從為什麼要確保 Thread Safe 開始切入,並解釋 iOS 如何做到 thread safe

x為何要確保 Thread Safe

防止 Race Condition 以及 Dead Lock

💡 iOS 中有哪些方法可以做到保護

  1. 使用 Actor
  2. 使用 Serial Queue,確保同一時間只有一個 thread 會訪問 shared resource
  3. 使用 Synchronized Locks,如 NSLock, Spin Lock, Semaphore 來確保同一時間只有一個 thread 來訪問 shared resource
  4. 使用 GCD 的 Barrier 如 DsipatchQueue.async(flags: .barrier)
  5. Property Wrappers 在 Swift5.1 以上,可以用 Property Wrappers 來創建 thread safe 的 mode,並用此 wrapper 來修飾共享資源

8. 你使用過 Actor 嗎?什麼是 Actor?

可從說明 Actor 的定義、使用方式、優點切入,並解釋在 Actor 內部如何定義及後續如何調用異步任務

💡 如何在 Actor 的定義、優點

Actor 主要降低了 concurrency programming 的複雜度,它保證了 class 的 method 和 property 的訪問是 serialized,這保證了在 concurrency programming 的環境下,也確保了 thread safe,同時也解決了調用異步任務後的 call back hell 的 issue(藉由 Task 及 await 的 keyword,把異步任務當作同步任務一樣調用)

💡 如何在 Actor 內部定義異步任務

透過 async 將 method 標記為異步任務,並使用 Task 及 await 調用這些異步 method

// Define an Actor
actor DataFetcher {
var data: String = ""

// Asynchronous method for fetching data
func fetchData() async {
// Simulate an asynchronous operation, such as fetching data from the network
data = await downloadDataFromNetwork()
}

// Another asynchronous method for processing data
func processData() async {
// Use the await keyword here because processData might need to wait for other asynchronous operations
let processedData = await processDataAsync(data)
// Update data
data = processedData
}

// Assume this is a function for asynchronously downloading data from the network
private func downloadDataFromNetwork() async -> String {
// This is just a placeholder, in reality you would perform a network request here
// and wait for the result using try await
return "Raw Data from Network"
}

// Assume this is a function for asynchronously processing data
private func processDataAsync(_ data: String) async -> String {
// This is just a placeholder, in reality you would perform data processing here
// and wait for the result using try await
return "Processed Data"
}
}

// Usage
let fetcher = DataFetcher()

// Use the 'async' keyword to run asynchronous tasks
Task {
// Use 'await' to wait for the asynchronous tasks to complete
await fetcher.fetchData()
await fetcher.processData()

// Retrieve the updated data
let updatedData = await fetcher.data
print(updatedData) // Outputs "Processed Data"
}

9. 如何不用 Actor 實作 list 的單讀單寫以及多讀單寫

這一題其實在問你如何在 multi thread 的環境下,確保讀取與寫入時的資料ㄧ致性

💡 單讀單寫的方法

使用 serial queue,read 和 write 皆為 sync, 無論讀寫一次只能進行一個操作

read: Serial Queue + sync , write: Serial Queue + sync

struct Item {}
private var _items: [Item] = []
let serialQueue = DispatchQueue(label: "com.serial.items")
private(set) var items: [Item] {
get { serialQueue.sync { self._items } }
set(newValue) { serialQueue.async { self._items.append(newValue) } }
}

也要記得解釋為和上述 code 可以做到 thread safe

由於 serialQueue 的性質,所有的讀取和寫入操作都會按照它們被加入到隊列的順序逐一執行

所以這可以自然地保證 _items 的線程安全性,而不需要使用 barrier 或其他額外的同步機制

💡 再提供多讀單寫

使用 concurrent queue,sync read , async(flags: .barrier) write , 達到多讀單寫的效果

read: Concurrent Queue + sync , write: Concurrent Queue + async

struct Item {}
private var _items: [Item] = []
let concurrentQueue = DispatchQueue(label: "com.current.items", attributes:.concurrent)

private(set) var items: [Item] {
get { concurrentQueue.sync { self._items } }
set(newValue) { concurrentQueue.async(flags: .barrier) { self._items.append(newValue) } }
}

可以主動解釋為何這邊的 read 要在 concurrentQueue 的狀態下,使用 sync

因為它是在一個 concurrent queue 中運行的,所以可以多個 thread 同時讀取 _items

使用 sync 可以確保在讀取 _items 時不會有其他任何寫入操作同時發生,從而保證了讀取操作的數據一致性

這是因為 sync 會在當前執行緒中阻塞,直到隊列中的塊被執行完畢,這期間不會有新的寫入操作進入到這個隊列中(至少是對 _items 的操作)

可以主動解釋為何這邊的 write 要使用 async(flags: .barrier)

因為確保在寫入操作期間,沒有其他的讀操作和寫操作可以同時進行,從而避免了 race condition 和可能的數據不一致

且因為寫操作是耗時操作,所以可以使用 async 在 background thread 異步執行寫操作,不會阻塞當前 thread

10. 請解釋何謂 run loop

--

--

Reed Hsin

在電商、直撥串流、加密貨幣、社交媒體等領域都有豐富經驗的資深 iOS 工程師,曾在 KKBox、Pinkoi、17Live、Crypto.com,目前於 TikTok 任職。 多年來,我帶領許多非電資背景的學生轉職成為 iOS 工程師,也幫助資深 iOS 工程師轉職到外商 🙌 希望能的經驗,能讓大家在職場上閃閃發光✨