Inter view question - result0924/iosLearning GitHub Wiki

技術問題

多隻 API 的情境

問題:在 iOS 如何處理多支 API 的情境?

點擊查看答案

在 iOS 開發中,處理多個 API 的情境可以使用不同的方法,包括 GCD、Combine 和 async/await。這裡提供三種常用的實現方式:

1. 使用 GCD(Grand Central Dispatch)

步驟:

  1. 創建 DispatchGroup

    • 使用 DispatchGroup 來追踪多個異步任務,確保所有 API 都完成後再處理結果。
  2. 執行 API 請求:

    • 對於每個 API 請求,使用 dispatchGroup.enter() 來標記進入群組,然後在請求完成時使用 dispatchGroup.leave() 標記離開群組。
  3. 處理結果:

    • 使用 dispatchGroup.notify(queue:) 方法,在所有請求完成後執行結果處理的代碼。

代碼範例:

import Foundation

// 1. 創建 DispatchGroup
let dispatchGroup = DispatchGroup()

// 2. 執行 API 請求
dispatchGroup.enter()
performAPICall1 { result1 in
    // 處理 API 1 的結果
    print("API 1 完成")
    dispatchGroup.leave()
}

dispatchGroup.enter()
performAPICall2 { result2 in
    // 處理 API 2 的結果
    print("API 2 完成")
    dispatchGroup.leave()
}

dispatchGroup.enter()
performAPICall3 { result3 in
    // 處理 API 3 的結果
    print("API 3 完成")
    dispatchGroup.leave()
}

// 3. 在所有 API 完成後處理結果
dispatchGroup.notify(queue: .main) {
    print("所有 API 請求已完成")
    // 處理合併後的結果或更新 UI
}

// 模擬的 API 請求方法
func performAPICall1(completion: @escaping (String) -> Void) {
    DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
        completion("Result from API 1")
    }
}

func performAPICall2(completion: @escaping (String) -> Void) {
    DispatchQueue.global().asyncAfter(deadline: .now() + 2) {
        completion("Result from API 2")
    }
}

func performAPICall3(completion: @escaping (String) -> Void) {
    DispatchQueue.global().asyncAfter(deadline: .now() + 1.5) {
        completion("Result from API 3")
    }
}

2. 使用 Combine

步驟:

  1. 創建 Publishers:

    • 每個 API 請求可以封裝成一個 Publisher。
  2. 使用 combineLatestzip

    • 如果 API 的結果之間有依賴性,可以使用 zipcombineLatest 操作符來等待所有請求完成後再處理。
  3. 訂閱結果:

    • 使用 sink 來訂閱合併後的結果並處理。

代碼範例:

import Combine
import Foundation

var cancellables = Set<AnyCancellable>()

let publisher1 = performAPICall1().eraseToAnyPublisher()
let publisher2 = performAPICall2().eraseToAnyPublisher()
let publisher3 = performAPICall3().eraseToAnyPublisher()

Publishers.Zip3(publisher1, publisher2, publisher3)
    .sink(receiveCompletion: { completion in
        switch completion {
        case .finished:
            print("所有 API 請求已完成")
        case .failure(let error):
            print("發生錯誤:\(error)")
        }
    }, receiveValue: { result1, result2, result3 in
        // 處理合併後的結果
        print("API 1: \(result1), API 2: \(result2), API 3: \(result3)")
    })
    .store(in: &cancellables)

// 模擬的 API 請求方法
func performAPICall1() -> Future<String, Error> {
    return Future { promise in
        DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
            promise(.success("Result from API 1"))
        }
    }
}

func performAPICall2() -> Future<String, Error> {
    return Future { promise in
        DispatchQueue.global().asyncAfter(deadline: .now() + 2) {
            promise(.success("Result from API 2"))
        }
    }
}

func performAPICall3() -> Future<String, Error> {
    return Future { promise in
        DispatchQueue.global().asyncAfter(deadline: .now() + 1.5) {
            promise(.success("Result from API 3"))
        }
    }
}

3. 使用 async/await

步驟:

  1. 將 API 請求封裝為 async 函數:

    • 利用 Swift 5.5 引入的 async/await 特性,可以將 API 請求封裝為 async 函數。
  2. 使用 async let 並行執行:

    • 使用 async let 關鍵字來並行執行多個 API 請求。
  3. 等待所有結果:

    • 使用 await 關鍵字等待所有請求完成後再處理結果。

代碼範例:

import Foundation

func fetchAllData() async {
    async let result1 = performAPICall1()
    async let result2 = performAPICall2()
    async let result3 = performAPICall3()

    do {
        let (response1, response2, response3) = try await (result1, result2, result3)
        print("API 1: \(response1), API 2: \(response2), API 3: \(response3)")
    } catch {
        print("發生錯誤:\(error)")
    }
}

// 模擬的 API 請求方法
func performAPICall1() async throws -> String {
    try await Task.sleep(nanoseconds: 1 * 1_000_000_000)
    return "Result from API 1"
}

func performAPICall2() async throws -> String {
    try await Task.sleep(nanoseconds: 2 * 1_000_000_000)
    return "Result from API 2"
}

func performAPICall3() async throws -> String {
    try await Task.sleep(nanoseconds: 1.5 * 1_000_000_000)
    return "Result from API 3"
}

結論:

  • GCD:適合處理傳統的異步操作,對於需要手動控制任務流程的場景非常有效。
  • Combine:適合用於響應式編程,特別是在需要處理多個 Publisher 的場景下,可以更方便地組合和處理異步結果。
  • async/await:Swift 最新的並行處理特性,提供了更直觀和簡潔的語法來處理異步操作,適合現代 iOS 開發。

問題:在 iOS 中,如何處理需要互相等待的多支 API?

點擊查看答案

當多個 API 需要依賴彼此的結果時,可以採用以下三種方法來處理:

1. 使用 GCD(Grand Central Dispatch)

步驟:

  1. 順序執行 API 請求:

    • 使用異步的方式執行第一個 API 請求,在其完成的回調中執行下一個 API。
  2. 處理每個 API 的結果:

    • 每個 API 完成後,將結果傳遞給下一個 API,直到所有請求完成。

代碼範例:

import Foundation

performAPICall1 { result1 in
    // 使用 result1 作為下一個 API 的輸入
    performAPICall2(input: result1) { result2 in
        // 使用 result2 作為下一個 API 的輸入
        performAPICall3(input: result2) { result3 in
            // 所有 API 請求完成,處理結果
            print("API 1: \(result1), API 2: \(result2), API 3: \(result3)")
        }
    }
}

// 模擬的 API 請求方法
func performAPICall1(completion: @escaping (String) -> Void) {
    DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
        completion("Result from API 1")
    }
}

func performAPICall2(input: String, completion: @escaping (String) -> Void) {
    DispatchQueue.global().asyncAfter(deadline: .now() + 1.5) {
        completion("Processed \(input) and Result from API 2")
    }
}

func performAPICall3(input: String, completion: @escaping (String) -> Void) {
    DispatchQueue.global().asyncAfter(deadline: .now() + 2) {
        completion("Processed \(input) and Result from API 3")
    }
}

2. 使用 Combine

步驟:

  1. 順序執行 Publishers:

    • 使用 flatMap 來處理一個 API 的結果,並將其傳遞給下一個 API。
  2. 訂閱最終結果:

    • 使用 sink 來處理最終 API 的結果。

代碼範例:

import Combine
import Foundation

var cancellables = Set<AnyCancellable>()

performAPICall1()
    .flatMap { result1 in
        performAPICall2(input: result1)
    }
    .flatMap { result2 in
        performAPICall3(input: result2)
    }
    .sink(receiveCompletion: { completion in
        switch completion {
        case .finished:
            print("所有 API 請求已完成")
        case .failure(let error):
            print("發生錯誤:\(error)")
        }
    }, receiveValue: { result3 in
        print("最終結果: \(result3)")
    })
    .store(in: &cancellables)

// 模擬的 API 請求方法
func performAPICall1() -> Future<String, Error> {
    return Future { promise in
        DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
            promise(.success("Result from API 1"))
        }
    }
}

func performAPICall2(input: String) -> Future<String, Error> {
    return Future { promise in
        DispatchQueue.global().asyncAfter(deadline: .now() + 1.5) {
            promise(.success("Processed \(input) and Result from API 2"))
        }
    }
}

func performAPICall3(input: String) -> Future<String, Error> {
    return Future { promise in
        DispatchQueue.global().asyncAfter(deadline: .now() + 2) {
            promise(.success("Processed \(input) and Result from API 3"))
        }
    }
}

3. 使用 async/await

步驟:

  1. 順序執行 async 函數:

    • 使用 await 來順序執行每個 API 請求,並將前一個結果傳遞給下一個。
  2. 等待結果:

    • 使用 try await 處理每個 API 的返回值,直到所有請求完成。

代碼範例:

import Foundation

func fetchAllData() async {
    do {
        let result1 = try await performAPICall1()
        let result2 = try await performAPICall2(input: result1)
        let result3 = try await performAPICall3(input: result2)
        print("API 1: \(result1), API 2: \(result2), API 3: \(result3)")
    } catch {
        print("發生錯誤:\(error)")
    }
}

// 模擬的 API 請求方法
func performAPICall1() async throws -> String {
    try await Task.sleep(nanoseconds: 1 * 1_000_000_000)
    return "Result from API 1"
}

func performAPICall2(input: String) async throws -> String {
    try await Task.sleep(nanoseconds: 1.5 * 1_000_000_000)
    return "Processed \(input) and Result from API 2"
}

func performAPICall3(input: String) async throws -> String {
    try await Task.sleep(nanoseconds: 2 * 1_000_000_000)
    return "Processed \(input) and Result from API 3"
}

結論:

  • GCD:適合傳統的異步操作,適合需要明確控制任務順序的場景。
  • Combine:使用響應式編程來順序處理 API 請求,適合需要依賴鏈式操作的情境。
  • async/await:現代的並行處理方法,提供直觀的語法來處理順序 API 調用,特別適合複雜的依賴關係。

Closure Capture list / Retain Cycle / @escaping @autoclosure 說明

問題:Closure Capture list 是什麼?

點擊查看答案

1. Closure Capture List 的定義

在 Swift 中,閉包(Closure)是一種自包含的功能塊,可以捕捉並存儲其周圍範圍內的變量和常量。這個過程稱為“捕捉”(capture)。當閉包在捕捉的過程中會導致循環引用時,我們可以使用“Capture List”來打破這種循環。

Capture List 是在閉包表達式的開頭使用的列表,通過它可以明確指定閉包如何捕捉外部變量(使用強引用或弱引用)。

2. Capture List 的語法

Capture List 是一組在方括號 [] 內的變量或常量,通常在閉包的參數列表之前。每個項目都是一個變量名稱後跟一個修飾符(例如 weakunowned),用來明確指定閉包應該如何捕捉這些變量。

範例

class SomeClass {
    var value: Int = 0
}

var instance = SomeClass()

let closure = { [weak instance] in
    print(instance?.value ?? 0)
}

在這個範例中,instance 是用 weak 捕捉的,這樣可以避免閉包和 instance 之間的強引用循環。

3. Capture List 的應用場景

  • 避免強引用循環

    • 當閉包和它捕捉的對象之間形成強引用循環時,對象不會被釋放,導致內存洩漏。使用 Capture List 可以通過捕捉弱引用或無主引用來打破這個循環。

    範例

    class ViewController {
        var buttonTitle: String = "Click me"
        lazy var buttonAction: () -> Void = { [unowned self] in
            print(self.buttonTitle)
        }
    }

    在這個範例中,self 是用 unowned 來捕捉的,以避免 ViewControllerbuttonAction 之間的強引用循環。

  • 控制變量捕捉方式

    • 當需要確保變量在閉包執行時不會被釋放或變更時,可以使用 Capture List 強制閉包捕捉一個強引用或特定的值。

    範例

    var count = 0
    let incrementer = { [count] in
        print("Count is \(count)")
    }
    count = 10
    incrementer() // 輸出:Count is 0

    在這個範例中,count 被捕捉為初始值 0,即使在閉包外將其更改為 10,閉包仍然使用的是當前捕捉時的值。

4. 使用 Capture List 的最佳實踐

  • 避免循環引用: 當閉包需要捕捉某個對象並且該對象可能會在閉包內被引用時,使用 Capture List 可以防止強引用循環,確保內存能夠被正確釋放。
  • 明確捕捉行為: 使用 Capture List 可以使捕捉行為變得更清晰,從而提高代碼的可讀性和可預測性。

總結

  • Capture List 是 Swift 中閉包的一個重要特性,用於控制閉包如何捕捉外部變量。
  • 它可以幫助我們避免強引用循環,從而防止內存洩漏。
  • 在涉及到引用類型時,理解並正確使用 Capture List 是確保代碼高效和穩定的關鍵。

問題:如何避免 Retain Cycle?

點擊查看答案

1. Retain Cycle 的定義

在 Swift 中,當兩個或多個對象之間存在強引用,且它們互相保持對方的引用時,就會導致 Retain Cycle,也稱為“強引用循環”。這種情況下,涉及到的對象都無法被釋放,導致內存洩漏。

2. 避免 Retain Cycle 的方法

  • 使用弱引用(weak:

    • weak 是一種不會增加引用計數的引用。當對象的引用計數降至 0 時,該對象會被釋放,而 weak 變量會自動設置為 nil

    範例

    class Person {
        var name: String
        weak var spouse: Person?
    
        init(name: String) {
            self.name = name
        }
    }
    
    var john: Person? = Person(name: "John")
    var jane: Person? = Person(name: "Jane")
    
    john?.spouse = jane
    jane?.spouse = john
    
    john = nil
    jane = nil

    在這個範例中,spouse 屬性被聲明為 weak,因此不會形成強引用循環,johnjane 會在釋放後正確地被回收。

  • 使用無主引用(unowned:

    • unowned 也是一種不會增加引用計數的引用,不同於 weakunowned 在對象被釋放後不會設置為 nil。因此,如果在對象被釋放後訪問 unowned 變量,會導致崩潰。

    範例

    class Customer {
        var name: String
        var card: CreditCard?
    
        init(name: String) {
            self.name = name
        }
    }
    
    class CreditCard {
        let number: UInt64
        unowned var customer: Customer
    
        init(number: UInt64, customer: Customer) {
            self.number = number
            self.customer = customer
        }
    }
    
    var john: Customer? = Customer(name: "John")
    john?.card = CreditCard(number: 1234_5678_9012_3456, customer: john!)
    
    john = nil

    在這個範例中,CreditCard 類中的 customer 屬性被聲明為 unowned,這樣可以避免循環引用,並且確保在 Customer 被釋放時,CreditCard 不會保留對它的強引用。

  • 使用 Closure Capture List:

    • 當閉包內部捕捉了外部對象時,應使用 Capture List 明確指定閉包對這些對象的引用方式。通常使用 weakunowned 來避免強引用循環。

    範例

    class ViewController {
        var title: String = "ViewController"
        lazy var showTitle: () -> Void = { [weak self] in
            print(self?.title ?? "No title")
        }
    }
    
    var vc: ViewController? = ViewController()
    vc?.showTitle()
    vc = nil

    在這個範例中,使用了 [weak self] 捕捉 self,這樣可以避免 ViewController 和閉包之間形成循環引用,從而防止內存洩漏。

3. 避免 Retain Cycle 的最佳實踐

  • 優先使用 weak 引用: 在多數情況下,當對象之間不應該互相擁有時,應該使用 weak 引用。這適用於大多數 delegate、父子層級關係等場景。
  • 在必須確保對象存在時使用 unowned 引用: 當你確信引用的對象在整個應用程序的生命周期內都會存在時,可以使用 unowned,這樣可以避免循環引用的同時不需要處理可選值(Optional)。
  • 使用 Capture List: 當閉包內捕捉外部對象時,務必使用 Capture List 來明確指定對象的引用方式,避免形成循環引用。

總結

  • 避免 Retain Cycle 的關鍵在於正確管理對象之間的引用關係。
  • 使用 weakunowned 來打破強引用循環,並使用 Closure Capture List 來避免閉包引發的引用循環,是保持內存健康的重要手段。

問題:@escaping 的用途和使用情境?

點擊查看答案

1. @escaping 的定義

在 Swift 中,閉包(Closure)默認是非逃逸(non-escaping)的,這意味著它們必須在函數返回之前執行完畢。如果閉包在函數返回後才被執行,那麼它就是“逃逸的”(escaping)。為了表示一個閉包是逃逸的,需要在閉包的參數類型前面加上 @escaping 關鍵字。

2. @escaping 的用途

@escaping 的主要用途是在閉包需要在函數返回後執行時使用。這種情況通常出現在需要將閉包保存起來,稍後在某個異步操作完成後再執行的場景。

3. 使用情境

  • 異步操作:

    • 當你需要在異步操作(如網絡請求、讀寫文件等)完成後執行某個閉包,該閉包通常需要標記為 @escaping,因為這些操作會在函數返回後才結束。

    範例

    func fetchData(completion: @escaping (String) -> Void) {
        DispatchQueue.global().async {
            // 模擬網絡請求
            let data = "Fetched Data"
            DispatchQueue.main.async {
                completion(data)
            }
        }
    }
    
    fetchData { data in
        print(data) // 當異步操作完成後,這裡才會被執行
    }

    在這個範例中,completion 閉包被標記為 @escaping,因為它在 fetchData 函數返回後才會執行。

  • 將閉包存儲起來供以後使用:

    • 當你需要將閉包存儲在某個屬性或變量中,以便稍後在某個特定時刻執行,它也必須標記為 @escaping

    範例

    class TaskManager {
        var task: (() -> Void)?
    
        func setTask(_ task: @escaping () -> Void) {
            self.task = task
        }
    
        func executeTask() {
            task?()
        }
    }
    
    let manager = TaskManager()
    manager.setTask {
        print("Task executed")
    }
    manager.executeTask() // 輸出:Task executed

    在這個範例中,task 被保存為 TaskManager 的屬性,因此 task 閉包需要標記為 @escaping

4. 注意事項

  • 內存管理: 使用 @escaping 時要特別注意內存管理,因為閉包可能會捕捉對象並導致強引用循環(Retain Cycle)。這時候可以使用 weakunowned 來避免循環引用。

    範例

    class SomeClass {
        var value: String = "Hello"
        func performTask(completion: @escaping () -> Void) {
            DispatchQueue.global().async {
                [weak self] in
                print(self?.value ?? "No value")
                completion()
            }
        }
    }

    在這個範例中,使用 [weak self] 來避免 SomeClass 和閉包之間形成強引用循環。

5. @escaping 的最佳實踐

  • 僅在必要時使用 @escaping: 如果閉包在函數返回之前執行完畢,則不需要使用 @escaping。這可以使代碼更加簡潔並減少內存管理的複雜性。
  • 正確處理內存管理: 在需要捕捉引用類型對象時,使用 weakunowned 來避免強引用循環,確保內存不會洩漏。

總結

  • @escaping 用於標記那些可能在函數返回後執行的閉包,這些閉包通常與異步操作或延遲執行有關。
  • 理解和正確使用 @escaping 能夠讓你的 Swift 代碼在處理閉包時更加靈活和安全。

問題:@autoclosure 的用途和使用情境?

點擊查看答案

1. @autoclosure 的定義

@autoclosure 是 Swift 中的一個屬性,用於自動將表達式包裝為閉包。這使得在呼叫函數時,可以省略顯式的閉包語法,而是直接傳入一個表達式。@autoclosure 主要用於延遲求值(Lazy Evaluation),即只有在閉包真正被執行時,表達式才會被求值。

2. @autoclosure 的用途

@autoclosure 的主要用途是簡化函數接口,讓函數的使用者可以傳入一個表達式,而不是明確地寫出一個閉包。這通常用於有條件的求值或延遲求值的場景,特別是在處理布爾表達式、斷言和錯誤處理時。

3. 使用情境

  • 簡化布爾表達式的函數調用:

    • @autoclosure 可以用來簡化布爾條件判斷函數的調用,使代碼更加易讀。

    範例

    func check(condition: @autoclosure () -> Bool) {
        if condition() {
            print("Condition is true")
        } else {
            print("Condition is false")
        }
    }
    
    let isValid = true
    check(condition: isValid) // 這裡的 `isValid` 被自動包裝為閉包

    在這個範例中,check 函數接受一個 @autoclosure 參數,這樣你可以直接傳入 isValid 而不需要明確寫出 { isValid }

  • 斷言或錯誤處理:

    • @autoclosure 常用於斷言函數中,使得斷言消息或條件在出錯時才會被計算。

    範例

    func assert(condition: @autoclosure () -> Bool, message: @autoclosure () -> String) {
        if !condition() {
            print("Assertion failed: \(message())")
        }
    }
    
    let number = 5
    assert(condition: number > 10, message: "Number is too small") // 只有當條件為 false 時,才會計算訊息

    在這個範例中,@autoclosure 使得 message 只有在 conditionfalse 時才會被計算,避免了不必要的性能開銷。

  • ??&&/|| 等操作符一起使用:

    • @autoclosure 經常用於實現操作符的函數,例如 Swift 標準庫中 ?? 操作符的實現。

    範例

    func ??<T>(optional: T?, defaultValue: @autoclosure () -> T) -> T {
        switch optional {
        case .some(let value):
            return value
        case .none:
            return defaultValue()
        }
    }
    
    let name: String? = nil
    let displayName = name ?? "Default Name" // "Default Name" 只有在 `name` 為 nil 時才會被計算

    在這個範例中,?? 操作符利用 @autoclosure 延遲計算默認值,只有在 optionalnil 時才會計算 defaultValue()

4. 注意事項

  • 避免濫用 @autoclosure: 儘管 @autoclosure 可以讓代碼更簡潔,但過度使用可能會降低代碼的可讀性,特別是當使用者不了解其工作原理時。

  • @escaping 一起使用: 默認情況下,@autoclosure 是非逃逸的(non-escaping),但如果你需要讓自動閉包逃逸,可以使用 @autoclosure @escaping

    範例

    func storeClosure(closure: @escaping @autoclosure () -> Bool) {
        let storedClosure = closure
        print("Closure stored: \(storedClosure())")
    }
    
    let isActive = true
    storeClosure(closure: isActive)

5. @autoclosure 的最佳實踐

  • 合理使用 @autoclosure 來簡化函數調用: 當你的函數需要延遲求值且希望保持接口簡潔時,可以考慮使用 @autoclosure
  • 保持代碼可讀性: 使用 @autoclosure 時,確保代碼的邏輯清晰,以免未來的開發者難以理解閉包是何時執行的。

總結

  • @autoclosure 用於將傳入的表達式自動轉換為閉包,常用於延遲求值的場景。
  • 這使得函數接口更加簡潔,但使用時需謹慎,以免降低代碼的可讀性。

問題:weakunowned 的差別?

點擊查看答案

1. 基本定義

在 Swift 中,weakunowned 都是用來解決強引用循環(Retain Cycle)問題的引用類型修飾符。它們用於修飾對象之間的引用,以防止對象無法被釋放,導致內存洩漏。然而,weakunowned 在使用方式和適用場景上有一些重要的差異。

  • weak 引用

    • 定義weak 是一種可選(Optional)的引用,當所引用的對象被釋放時,weak 引用會自動設置為 nil
    • 特性
      • 必須聲明為可選類型(Optional)。
      • 不增加引用計數。
      • 適用於引用可能會變為 nil 的情況。
  • unowned 引用

    • 定義unowned 是一種非可選(Non-Optional)的引用,當所引用的對象被釋放後,unowned 引用不會變為 nil,但如果試圖訪問已釋放的對象,會導致運行時錯誤(Crash)。
    • 特性
      • 不需要聲明為可選類型。
      • 不增加引用計數。
      • 適用於引用在其生命週期內始終存在的情況。

2. 使用場景比較

  • 使用 weak 的場景

    • 可選引用:當引用的對象可能在引用存在期間被釋放,需要安全地處理引用變為 nil 的情況。
    • 避免強引用循環:常見於 delegate 模式中,子對象引用父對象,且父對象不應被子對象強引用。

    範例

    class Person {
        var name: String
        weak var spouse: Person?
        
        init(name: String) {
            self.name = name
        }
    }
    
    var john: Person? = Person(name: "John")
    var jane: Person? = Person(name: "Jane")
    
    john?.spouse = jane
    jane?.spouse = john
    
    john = nil
    jane = nil
    // 此時 john 和 jane 都會被正確釋放,因為 spouse 是弱引用
  • 使用 unowned 的場景

    • 非可選引用:當你確信引用的對象在引用存在期間不會被釋放。
    • 父子對象關係:父對象持有子對象的強引用,而子對象持有父對象的無主引用,確保子對象不會延長父對象的生命週期。

    範例

    class Customer {
        var name: String
        var card: CreditCard?
        
        init(name: String) {
            self.name = name
        }
    }
    
    class CreditCard {
        let number: UInt64
        unowned let customer: Customer
        
        init(number: UInt64, customer: Customer) {
            self.number = number
            self.customer = customer
        }
    }
    
    var john: Customer? = Customer(name: "John")
    john?.card = CreditCard(number: 1234_5678_9012_3456, customer: john!)
    
    john = nil
    // 當 john 被設為 nil 時,CreditCard 也會被釋放,因為 customer 是無主引用

3. 主要差異點

特性 weak unowned
可選性 必須是可選類型(Optional) 非可選類型(Non-Optional)
引用對象釋放後的行為 自動設置為 nil 不會設置為 nil,試圖訪問會導致崩潰
適用場景 引用可能會變為 nil,需要安全處理引用的消失 引用在生命週期內始終存在,且不會變為 nil
常見用途 Delegate 模式、父子對象中子對象對父對象的引用 父子對象中子對象對父對象的無主引用(確保父對象存在)

4. 具體範例解析

  • weak 的範例

    class Teacher {
        var name: String
        weak var student: Student?
        
        init(name: String) {
            self.name = name
        }
    }
    
    class Student {
        var name: String
        var teacher: Teacher
        
        init(name: String, teacher: Teacher) {
            self.name = name
            self.teacher = teacher
        }
    }
    
    var mrSmith: Teacher? = Teacher(name: "Mr. Smith")
    var alice: Student? = Student(name: "Alice", teacher: mrSmith!)
    
    mrSmith?.student = alice
    
    mrSmith = nil
    // 因為 teacher 對 student 是強引用,而 student 對 teacher 是弱引用,mrSmith 被釋放後,teacher 會自動設為 nil,不會造成強引用循環
  • unowned 的範例

    class Owner {
        var name: String
        var vehicle: Vehicle?
        
        init(name: String) {
            self.name = name
        }
    }
    
    class Vehicle {
        let model: String
        unowned let owner: Owner
        
        init(model: String, owner: Owner) {
            self.model = model
            self.owner = owner
        }
    }
    
    var owner: Owner? = Owner(name: "Alice")
    owner?.vehicle = Vehicle(model: "Tesla", owner: owner!)
    
    owner = nil
    // 當 owner 被設為 nil 時,vehicle 也會被釋放,因為 owner 是無主引用

5. 使用上的注意事項

  • 選擇合適的引用類型

    • 如果不確定引用對象是否會在引用存在期間被釋放,應選擇 weak
    • 如果確信引用對象在引用存在期間不會被釋放,並且希望避免可選類型帶來的額外處理,可以選擇 unowned
  • 避免使用 unowned 引用可變為 nil 的對象

    • 使用 unowned 引用的對象在引用存在期間應該始終存在,否則會導致運行時錯誤。
  • 理解引用週期

    • 在設計類之間的引用關係時,應仔細考慮使用 weakunowned 來避免強引用循環,確保內存能夠被正確釋放。

6. 總結

  • weakunowned 都是用來避免強引用循環的工具,但它們適用於不同的情境。
  • weak 引用適用於引用可能會變為 nil 的情況,並且必須是可選類型。
  • unowned 引用適用於引用在其生命週期內始終存在的情況,並且不需要是可選類型。
  • 正確選擇和使用這兩種引用類型,有助於有效管理內存,避免內存洩漏和運行時錯誤。

Race Condition

問題:Race Condition 什麼時候會發生?怎麼解決?

點擊查看答案

1. Race Condition 是什麼?

Race Condition 是一種 並發問題,當多個執行緒同時訪問和修改共享資源時,如果訪問和修改的順序未被正確控制,可能導致不一致的或不正確的結果。

發生時機:
  • Race Condition 發生於多個執行緒(或進程) 同時 存取同一個資源(如變數、資料結構、文件等),而未被正確同步的情況下。
  • 特別是在 多線程編程 中,如果兩個或多個線程競爭同一資源,並且順序不確定,這會導致資源處於競爭狀態,進而出現 Race Condition。
舉例:

假設有一個共享變數 count,兩個執行緒都試圖對它進行增減操作。如果兩個執行緒同時讀取該變數並執行加法操作,最後的結果可能是不正確的。

var count = 0

DispatchQueue.global().async {
    count += 1 // 線程 A 增加 count
}

DispatchQueue.global().async {
    count += 1 // 線程 B 增加 count
}

// 結果可能不會是我們預期的 2,而是 1,甚至是 0

2. 如何解決 Race Condition?

解決 Race Condition 的關鍵在於 正確同步 多個執行緒對共享資源的訪問。這可以通過使用同步機制來避免多個線程同時修改共享資源。以下是一些常見的解決方法:

2.1 使用互斥鎖 (Mutex)

互斥鎖是最常見的並發控制工具,它允許一次只有一個線程進入臨界區(即對共享資源的訪問區域)。當一個線程獲得互斥鎖,其他線程就必須等待,直到鎖被釋放。

var count = 0
let lock = NSLock()

DispatchQueue.global().async {
    lock.lock()
    count += 1  // 線程 A 獲得鎖,進行操作
    lock.unlock()
}

DispatchQueue.global().async {
    lock.lock()
    count += 1  // 線程 B 獲得鎖,進行操作
    lock.unlock()
}
2.2 使用串行隊列 (Serial Queue)

串行隊列保證任務按順序執行,從而避免 Race Condition。當多個任務被提交到同一個串行隊列時,它們會依次執行,避免了同時訪問共享資源的問題。

var count = 0
let serialQueue = DispatchQueue(label: "com.example.serialQueue")

serialQueue.async {
    count += 1 // 線程 A 在串行隊列中執行
}

serialQueue.async {
    count += 1 // 線程 B 也在串行隊列中執行
}
2.3 使用原子操作 (Atomic Operations)

原子操作是無需加鎖即可保證操作完整性的操作,它們通常是針對簡單變數的操作。Swift 中可以使用 OSAtomicIncrement 等函數來實現原子操作,但現在更推薦使用 DispatchQueue.syncNSLock 等同步機制。

2.4 使用 DispatchQueue.sync

DispatchQueue.sync 會同步執行任務,確保對共享資源的訪問是有序且不會產生 Race Condition。

var count = 0
let queue = DispatchQueue(label: "com.example.syncQueue")

queue.sync {
    count += 1
}

queue.sync {
    count += 1
}

3. 總結

  • Race Condition 發生時機:當多個線程同時訪問和修改共享資源而沒有同步控制時。
  • 解決方法:使用互斥鎖、串行隊列、原子操作或 DispatchQueue.sync 等同步機制來保證多線程之間的正確性。

Property Wrapper 說明

問題:Property Wrapper 是什麼?有什麼應用場景?

點擊查看答案

在 Swift 中,Property Wrapper 是一種強大的工具,允許你在不改變原有屬性行為的情況下,為屬性添加額外的邏輯或功能。通過使用 Property Wrapper,你可以重複使用屬性邏輯,減少代碼重複,並提升代碼的可讀性和可維護性。

1. Property Wrapper 的定義

Property Wrapper 是一個使用 @propertyWrapper 修飾的結構體或類,它封裝了屬性值的讀取和寫入邏輯。當你應用 Property Wrapper 到一個屬性時,該屬性將使用 Property Wrapper 提供的邏輯來管理其值。

範例

@propertyWrapper
struct Capitalized {
    private var value: String = ""

    var wrappedValue: String {
        get { value }
        set { value = newValue.capitalized }
    }
}

struct Person {
    @Capitalized var name: String
}

var person = Person(name: "john doe")
print(person.name) // 輸出:John Doe

在這個範例中,Capitalized 是一個 Property Wrapper,它將字符串的首字母大寫。在 Person 結構體中,@Capitalized 被應用於 name 屬性,因此無論 name 被賦予什麼值,最終都會被自動轉換為首字母大寫的形式。

2. Property Wrapper 的應用場景

  • 數據校驗與格式化:

    • 你可以使用 Property Wrapper 自動地對屬性進行數據校驗或格式化。例如,你可以創建一個 Property Wrapper 來確保數值屬性永遠不會為負數,或將字符串屬性自動轉換為特定格式。

    範例

    @propertyWrapper
    struct NonNegative {
        private var value: Int
    
        init(wrappedValue: Int) {
            self.value = max(0, wrappedValue)
        }
    
        var wrappedValue: Int {
            get { value }
            set { value = max(0, newValue) }
        }
    }
    
    struct Account {
        @NonNegative var balance: Int
    }
    
    var account = Account(balance: -10)
    print(account.balance) // 輸出:0
  • 數據持久化:

    • Property Wrapper 可以用來將屬性值與用戶預設、檔案系統或資料庫中的數據同步。例如,使用 Property Wrapper 自動將屬性值保存到 UserDefaults

    範例

    @propertyWrapper
    struct UserDefault<T> {
        let key: String
        let defaultValue: T
    
        var wrappedValue: T {
            get { UserDefaults.standard.object(forKey: key) as? T ?? defaultValue }
            set { UserDefaults.standard.set(newValue, forKey: key) }
        }
    }
    
    struct Settings {
        @UserDefault(key: "isDarkMode", defaultValue: false) var isDarkMode: Bool
    }
    
    var settings = Settings()
    settings.isDarkMode = true
    print(UserDefaults.standard.bool(forKey: "isDarkMode")) // 輸出:true
  • 行為綁定與依賴注入:

    • Property Wrapper 還可以用來將依賴項注入到屬性中,或在屬性改變時觸發特定行為。例如,當屬性值改變時自動更新界面,或在屬性設置時觸發數據更新。

    範例

    @propertyWrapper
    struct Logged<T> {
        private var value: T
    
        init(wrappedValue: T) {
            self.value = wrappedValue
            print("Initial value set to \(wrappedValue)")
        }
    
        var wrappedValue: T {
            get { value }
            set {
                print("Value changed from \(value) to \(newValue)")
                value = newValue
            }
        }
    }
    
    struct User {
        @Logged var username: String
    }
    
    var user = User(username: "JohnDoe")
    user.username = "JaneDoe" 
    // 輸出:
    // Initial value set to JohnDoe
    // Value changed from JohnDoe to JaneDoe

總結:

  • Property Wrapper 提供了一種簡潔而強大的方式來封裝屬性的邏輯,使得屬性的處理更加可重用和一致。
  • 它們在數據校驗、格式化、持久化以及行為綁定等應用場景中非常有用,能顯著提高代碼的可維護性。

透過了解和使用 Property Wrapper,你可以創建更乾淨、更具表達力的代碼,並在保持代碼簡潔的同時實現複雜的功能。

Concurrency (async/await)

  • 如何在 Swift 中使用 async/await 處理 Concurrency?

用過 Combine / RxSwift 嗎?

  • 你有使用過 Combine 或 RxSwift 嗎?有什麼經驗?

用過 SwiftUI 嗎?

  • 你有使用過 SwiftUI 嗎?分享你的經驗?

如何解析 JSON?

  • 怎麼 parse JSON?使用 Codable 還是其他套件?

用過 local 資料庫嗎?

  • 你有使用過本地資料庫嗎?使用什麼工具或框架?

問題:如何知道面試者了解 Core Data 的流程呢?

點擊查看答案

1. 基本概念

  • 詢問關於 Core Data 的基本概念
    • 什麼是 Core Data?
      確認面試者是否了解 Core Data 是一個對象圖和持久化框架,用於在 iOS 和 macOS 應用中管理模型層數據。
    • Core Data 與 SQLite 的區別是什麼?
      檢查面試者是否理解 Core Data 是一個高層次的框架,並且可以在其背後使用 SQLite 作為其存儲引擎。

2. 核心流程的理解

  • 詢問面試者如何在項目中設置 Core Data
    • 如何設置 Core Data Stack?
      評估面試者是否理解 Core Data Stack 的組成部分,如 NSPersistentContainer、NSManagedObjectContext、NSPersistentStoreCoordinator 和 NSManagedObjectModel。
    • 如何使用 NSManagedObjectContext 來進行數據操作?
      確認面試者是否知道如何使用 NSManagedObjectContext 來插入、刪除、更新和查詢數據。

3. 性能和優化

  • 詢問面試者如何優化 Core Data 的性能
    • 如何優化批量操作?
      確認面試者是否了解批量插入、刪除和更新操作的優化策略,例如使用 NSBatchInsertRequestNSBatchDeleteRequest
    • 如何處理大量數據的查詢?
      評估面試者是否知道如何使用 NSFetchRequestfetchBatchSizepredicatesortDescriptors 來進行高效的數據查詢。

4. 數據同步與合併

  • 詢問面試者如何處理數據同步和合併
    • 如何解決多個上下文之間的數據衝突?
      確認面試者是否了解 Core Data 的合併策略,如 .mergeByPropertyStoreTrump.mergeByPropertyObjectTrump
    • 如何實現本地數據與服務器數據的同步?
      評估面試者是否能解釋如何設計本地數據與服務器同步的機制,如使用 NSManagedObjectNSMergePolicy 或手動管理合併。

5. 錯誤處理

  • 詢問面試者如何處理 Core Data 的錯誤
    • 如何處理 Core Data 的錯誤和崩潰?
      評估面試者是否了解如何處理 Core Data 中可能發生的錯誤,例如數據庫損壞、合併衝突等,並能提供有效的錯誤處理策略。

6. 範例問題

  • 實際操作與代碼問題
    • 要求面試者寫一段代碼來插入新數據到 Core Data 中
      這有助於確認面試者是否具備基本的代碼實作能力。
    • 給出一個場景,要求面試者解釋如何在該場景中設計 Core Data 模型和操作流程
      例如,設計一個筆記應用,需要管理筆記的創建、編輯和刪除操作,並支持數據的同步和合併。

問題:如何使用 NSManagedObjectContext 來進行數據操作?

點擊查看答案

1. NSManagedObjectContext 概述

NSManagedObjectContext 是 Core Data 的核心組件之一,它用於管理對象的生命周期和與持久化存儲的交互。通過 NSManagedObjectContext,你可以執行以下操作:

  • 插入數據
  • 查詢數據
  • 更新數據
  • 刪除數據
  • 保存和回滾更改

2. 插入數據

要插入新數據,你需要創建一個新的 NSManagedObject 實例,設置其屬性,然後將其保存到上下文中。

// 獲取 NSEntityDescription 對象
let entity = NSEntityDescription.entity(forEntityName: "EntityName", in: context)!

// 創建新的 NSManagedObject 實例
let newObject = NSManagedObject(entity: entity, insertInto: context)

// 設置屬性
newObject.setValue("Some Value", forKey: "attributeName")

// 保存上下文以持久化數據
do {
    try context.save()
} catch {
    print("Failed to save context: \(error)")
}

3. 查詢數據

使用 NSFetchRequest 可以查詢存儲在 Core Data 中的數據。你可以指定實體名稱、過濾條件和排序方式。

// 創建 NSFetchRequest 對象
let fetchRequest = NSFetchRequest<NSManagedObject>(entityName: "EntityName")

// 設定過濾條件
fetchRequest.predicate = NSPredicate(format: "attributeName == %@", "Some Value")

// 設定排序
fetchRequest.sortDescriptors = [NSSortDescriptor(key: "attributeName", ascending: true)]

do {
    let results = try context.fetch(fetchRequest)
    for result in results {
        // 處理查詢結果
    }
} catch {
    print("Failed to fetch data: \(error)")
}

4. 更新數據

要更新數據,首先需要查詢到對象,然後修改其屬性,最後保存上下文。

// 查詢數據
let fetchRequest = NSFetchRequest<NSManagedObject>(entityName: "EntityName")
fetchRequest.predicate = NSPredicate(format: "attributeName == %@", "Some Value")

do {
    let results = try context.fetch(fetchRequest)
    if let objectToUpdate = results.first {
        // 更新屬性
        objectToUpdate.setValue("New Value", forKey: "attributeName")
        
        // 保存上下文
        try context.save()
    }
} catch {
    print("Failed to update data: \(error)")
}

5. 刪除數據

刪除數據的過程包括查詢要刪除的對象,從上下文中刪除對象,然後保存上下文。

// 查詢數據
let fetchRequest = NSFetchRequest<NSManagedObject>(entityName: "EntityName")
fetchRequest.predicate = NSPredicate(format: "attributeName == %@", "Some Value")

do {
    let results = try context.fetch(fetchRequest)
    if let objectToDelete = results.first {
        // 刪除對象
        context.delete(objectToDelete)
        
        // 保存上下文
        try context.save()
    }
} catch {
    print("Failed to delete data: \(error)")
}

6. 保存和回滾更改

  • 保存更改:使用 context.save() 來持久化上下文中的所有更改。
  • 回滾更改:如果需要撤銷更改,可以使用 context.rollback()
// 保存上下文
do {
    try context.save()
} catch {
    print("Failed to save context: \(error)")
    // 若需要,可以進行回滾
    context.rollback()
}

7. 使用批量操作

對於大規模數據操作,使用 NSBatchInsertRequestNSBatchDeleteRequest 可以提高性能。

  • 批量插入
let batchInsertRequest = NSBatchInsertRequest(entity: entity, objects: objectsArray)
do {
    let result = try context.execute(batchInsertRequest) as? NSBatchInsertResult
    // 處理結果
} catch {
    print("Failed to execute batch insert: \(error)")
}
  • 批量刪除
let batchDeleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
do {
    let result = try context.execute(batchDeleteRequest) as? NSBatchDeleteResult
    // 處理結果
} catch {
    print("Failed to execute batch delete: \(error)")
}

這些操作和技巧將幫助你有效地使用 NSManagedObjectContext 進行數據操作,從而確保數據的管理和持久化符合應用需求。

問題:如何解決多個上下文之間的數據衝突?

點擊查看答案

1. 多個上下文中的數據衝突

在使用 Core Data 時,如果應用中存在多個 NSManagedObjectContext 實例,它們之間可能會發生數據衝突。這種情況通常發生在以下情境中:

  • 多個上下文同時修改相同的數據對象。
  • 一個上下文的更改尚未保存,而另一個上下文已經對相同對象進行了修改。

2. 解決衝突的策略

以下是幾種解決多個上下文之間數據衝突的策略:

  • 合併策略:Core Data 提供了幾種合併策略,可以在上下文之間同步數據時解決衝突。

    • NSMergePolicyNSManagedObjectContext 可以設置合併策略,處理在合併時的數據衝突。

      let context = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
      context.mergePolicy = NSMergePolicy.mergeByPropertyStoreTrump

      常見的合併策略有:

      • mergeByPropertyStoreTrump:使用來自持久化存儲的數據來覆蓋上下文中的數據。
      • mergeByPropertyObjectTrump:使用上下文中的數據來覆蓋來自持久化存儲的數據。
      • mergeByPropertyObjectTrump:使用上下文中的數據來覆蓋來自持久化存儲的數據。
  • 手動合併:在某些情況下,你可能需要手動解決衝突,例如當數據複雜或者需要根據特定業務邏輯進行處理時。

    • 手動合併步驟
      1. 檢測衝突:使用 NSManagedObjectContextupdatedObjectsdeletedObjects 屬性來檢查已更改的對象。
      2. 處理衝突:根據應用需求決定使用哪一個版本的數據,並手動更新上下文中的對象。
      3. 保存上下文:在合併數據後,保存上下文以持久化更改。
  • 使用通知:Core Data 提供了通知機制,讓你能夠在上下文合併時進行自定義操作。

    • 使用通知的步驟
      1. 監聽通知
        NotificationCenter.default.addObserver(self, selector: #selector(handleContextDidSave), name: .NSManagedObjectContextDidSave, object: nil)
      2. 處理通知
        @objc func handleContextDidSave(notification: Notification) {
            guard let context = notification.object as? NSManagedObjectContext else { return }
            if context != self.context {
                self.context.mergeChanges(fromContextDidSave: notification)
            }
        }

3. 合併上下文的使用示例

以下是一個示例,展示如何在多個上下文之間合併數據:

let parentContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
let childContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
childContext.parent = parentContext

// 設置合併策略
childContext.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump
parentContext.mergePolicy = NSMergePolicy.mergeByPropertyStoreTrump

// 在 childContext 中進行操作
childContext.perform {
    let newObject = NSEntityDescription.insertNewObject(forEntityName: "EntityName", into: childContext)
    newObject.setValue("Some Value", forKey: "attributeName")

    do {
        try childContext.save() // 保存 childContext 的更改
    } catch {
        print("Failed to save child context: \(error)")
    }
}

// 在 parentContext 中進行操作
parentContext.perform {
    let fetchRequest = NSFetchRequest<NSManagedObject>(entityName: "EntityName")
    
    do {
        let results = try parentContext.fetch(fetchRequest)
        // 處理 parentContext 的數據
    } catch {
        print("Failed to fetch data: \(error)")
    }
}

4. 優化合併

  • 定期合併:確保定期合併上下文中的更改,以減少衝突的發生。
  • 使用背景上下文:在背景線程中進行數據處理,然後將更改合併到主上下文中,以提升性能和響應性。

通過以上策略和示例,你可以有效地處理多個上下文之間的數據衝突,確保數據的一致性和完整性。

問題:如何實現本地數據與服務器數據的同步?

點擊查看答案

1. 同步的基本概念

實現本地數據與服務器數據的同步通常涉及以下步驟:

  • 獲取數據:從服務器獲取最新的數據。
  • 合併數據:將從服務器獲取的數據與本地存儲的數據進行合併。
  • 處理衝突:如果服務器和本地數據之間存在衝突,則需要解決這些衝突。
  • 更新本地數據:將合併後的數據更新到本地存儲中。

2. 設計同步機制

在設計同步機制時,可以考慮以下方法:

  • 全量同步:每次從服務器獲取所有數據,然後完全覆蓋本地數據。

    • 優點:實現簡單,適合數據量較小的情況。
    • 缺點:數據傳輸量大,效率較低。
  • 增量同步:只獲取自上次同步以來有變更的數據。

    • 優點:數據傳輸量小,效率較高。
    • 缺點:需要追蹤數據的變更,實現相對複雜。

3. 實現增量同步的步驟

  1. 服務器端設計

    • 時間戳:服務器端應該能夠提供每條數據的最後修改時間戳。
    • 變更記錄:服務器端應該能夠記錄數據的變更,包括創建、更新和刪除操作。
  2. 客戶端設計

    • 儲存上次同步時間:在本地儲存上次成功同步的時間。
    • 發送同步請求:將上次同步時間作為參數,向服務器請求自上次同步以來的變更數據。
  3. 處理同步數據

    • 合併數據:根據服務器返回的數據更新本地數據。如果本地數據和服務器數據之間存在衝突,則根據業務邏輯進行合併。
    • 更新本地存儲:將合併後的數據更新到本地存儲(如 Core Data)。
  4. 同步實現示例

以下是一個使用 Core Data 進行增量同步的示例:

// 1. 獲取上次同步的時間戳
let lastSyncDate = UserDefaults.standard.object(forKey: "lastSyncDate") as? Date ?? Date.distantPast

// 2. 向服務器發送同步請求
let url = URL(string: "https://api.example.com/sync?lastModified=\(lastSyncDate.timeIntervalSince1970)")!
let task = URLSession.shared.dataTask(with: url) { data, response, error in
    guard let data = data, error == nil else {
        print("Failed to fetch data from server: \(String(describing: error))")
        return
    }
    
    // 3. 處理從服務器返回的數據
    do {
        if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
           let records = json["records"] as? [[String: Any]] {
            
            // 使用背景上下文進行數據操作
            let backgroundContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
            backgroundContext.parent = mainContext
            
            backgroundContext.perform {
                for record in records {
                    // 根據 record 更新本地數據
                    let id = record["id"] as! String
                    let fetchRequest = NSFetchRequest<NSManagedObject>(entityName: "EntityName")
                    fetchRequest.predicate = NSPredicate(format: "id == %@", id)
                    
                    let results = try? backgroundContext.fetch(fetchRequest)
                    let managedObject: NSManagedObject
                    
                    if let existingObject = results?.first {
                        managedObject = existingObject
                    } else {
                        // 如果對象不存在,則創建新的對象
                        managedObject = NSEntityDescription.insertNewObject(forEntityName: "EntityName", into: backgroundContext)
                    }
                    
                    // 設置對象屬性
                    managedObject.setValue(record["attribute"] as? String, forKey: "attributeName")
                }
                
                // 4. 保存數據
                do {
                    try backgroundContext.save()
                    try mainContext.save()
                    
                    // 更新上次同步時間
                    UserDefaults.standard.set(Date(), forKey: "lastSyncDate")
                } catch {
                    print("Failed to save context: \(error)")
                }
            }
        }
    } catch {
        print("Failed to parse server response: \(error)")
    }
}
task.resume()

4. 處理衝突

  • 優先策略:根據業務需求選擇合適的衝突解決策略,如使用最新的數據覆蓋舊的數據,或者根據數據的來源選擇優先級。

  • 用戶介面:在某些情況下,可能需要通知用戶選擇如何解決衝突,例如在數據編輯衝突時提供選項。

通過以上步驟,你可以實現有效的本地數據與服務器數據同步,確保應用中的數據始終保持最新和一致。

問題:Core Data 中的關聯 (Relationships)

點擊查看答案

1. Core Data 中的關聯類型

Core Data 支持多種類型的關聯,用於建模實體之間的關係。主要包括以下幾種:

  • 一對一關聯(One-to-One Relationship):一個實體只關聯到另一個實體。
  • 一對多關聯(One-to-Many Relationship):一個實體關聯到多個實體。
  • 多對多關聯(Many-to-Many Relationship):多個實體相互關聯,每個實體可以關聯到多個其他實體。

2. 一對一關聯(One-to-One Relationship)

一對一關聯表示一個實體對象只能關聯到另一個實體對象,反之亦然。例如,一個 Person 實體可以有一個 Passport 實體。

  • 設置一對一關聯

    1. 在 Core Data 模型編輯器中,選擇兩個實體。
    2. 添加一個關聯,並設置其 "Destination" 為另一個實體。
    3. 設置關聯的 "Type" 為 "To One" 並且勾選 "Optional" 或 "Required" 根據需求設定。
  • 使用示例

    let person = NSEntityDescription.insertNewObject(forEntityName: "Person", into: context) as! Person
    let passport = NSEntityDescription.insertNewObject(forEntityName: "Passport", into: context) as! Passport
    
    person.passport = passport
    passport.owner = person
    
    do {
        try context.save()
    } catch {
        print("Failed to save context: \(error)")
    }

3. 一對多關聯(One-to-Many Relationship)

一對多關聯表示一個實體對象可以關聯到多個實體對象。例如,一個 Author 實體可以有多個 Book 實體。

  • 設置一對多關聯

    1. 在 Core Data 模型編輯器中,選擇主要實體。
    2. 添加一個關聯,並設置其 "Destination" 為另一個實體。
    3. 設置關聯的 "Type" 為 "To Many" 並設置 "Inverse" 關聯。
  • 使用示例

    let author = NSEntityDescription.insertNewObject(forEntityName: "Author", into: context) as! Author
    let book1 = NSEntityDescription.insertNewObject(forEntityName: "Book", into: context) as! Book
    let book2 = NSEntityDescription.insertNewObject(forEntityName: "Book", into: context) as! Book
    
    author.addToBooks(book1)
    author.addToBooks(book2)
    
    book1.author = author
    book2.author = author
    
    do {
        try context.save()
    } catch {
        print("Failed to save context: \(error)")
    }

4. 多對多關聯(Many-to-Many Relationship)

多對多關聯表示多個實體對象可以相互關聯。例如,Student 實體和 Course 實體之間的關聯。

  • 設置多對多關聯

    1. 在 Core Data 模型編輯器中,選擇兩個實體。
    2. 添加兩個關聯,並設置其 "Destination" 為對方實體。
    3. 設置關聯的 "Type" 為 "To Many" 並設置 "Inverse" 關聯。
  • 使用示例

    let student = NSEntityDescription.insertNewObject(forEntityName: "Student", into: context) as! Student
    let course = NSEntityDescription.insertNewObject(forEntityName: "Course", into: context) as! Course
    
    student.addToCourses(course)
    course.addToStudents(student)
    
    do {
        try context.save()
    } catch {
        print("Failed to save context: \(error)")
    }

5. 設置關聯的屬性

關聯通常包括兩個主要屬性:

  • Inverse Relationship:每個關聯應該有一個對應的反向關聯,以確保數據一致性和完整性。例如,在一對多關聯中,AuthorBook 的關聯應該有一個對應的反向關聯。
  • Optionality:設置關聯是否為可選,這將決定是否必須存在關聯對象。

6. 使用 NSFetchRequest 進行關聯查詢

  • 查詢具有關聯的對象
    let fetchRequest = NSFetchRequest<Person>(entityName: "Person")
    fetchRequest.predicate = NSPredicate(format: "passport != nil")
    
    do {
        let personsWithPassport = try context.fetch(fetchRequest)
        for person in personsWithPassport {
            print("Person with passport: \(person)")
        }
    } catch {
        print("Failed to fetch data: \(error)")
    }

通過理解和使用 Core Data 中的關聯,你可以有效地設計和操作複雜的數據模型,確保數據的一致性和完整性。

問題:Core Data 的遷移(Migration)

點擊查看答案

1. 遷移的基本概念

遷移(Migration)是在 Core Data 中指的是當你的數據模型(NSManagedObjectModel)發生變更時,將舊的數據模型中的數據遷移到新的數據模型中的過程。這是數據持久化的一個重要部分,以確保應用更新數據模型結構時能夠保留現有的數據。

2. 遷移的類型

Core Data 支持以下幾種遷移類型:

  • 輕量級遷移(Lightweight Migration):當數據模型的變更比較簡單時,Core Data 可以自動處理這些變更,而無需手動干預。這包括添加新的屬性、刪除屬性或簡單的屬性類型更改。

  • 手動遷移(Manual Migration):當數據模型變更較為複雜時,輕量級遷移可能無法處理。這時需要手動創建遷移映射(Mapping Model)來處理更複雜的數據結構變更。

3. 輕量級遷移(Lightweight Migration)

輕量級遷移非常簡單,通常只需確保 NSPersistentStoreCoordinator 在創建 NSPersistentStore 時設置適當的選項。

  • 設置輕量級遷移
    let options = [
        NSMigratePersistentStoresAutomaticallyOption: true,
        NSInferMappingModelAutomaticallyOption: true
    ]
    
    do {
        let persistentStoreCoordinator = NSPersistentStoreCoordinator(managedObjectModel: model)
        try persistentStoreCoordinator.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: storeURL, options: options)
    } catch {
        print("Failed to add persistent store: \(error)")
    }

4. 手動遷移(Manual Migration)

如果你的模型變更需要手動遷移,你需要創建一個遷移映射模型並使用 NSMigrationManager 進行遷移。

  • 創建遷移映射模型

    1. 在 Xcode 中,選擇你的 .xcdatamodeld 文件。
    2. 點擊 Editor -> Add Mapping Model,創建新的遷移映射模型。
    3. 在映射模型中,指定源模型和目標模型,並設置屬性映射規則。
  • 執行手動遷移

    let sourceModel = NSManagedObjectModel(contentsOf: sourceModelURL)
    let destinationModel = NSManagedObjectModel(contentsOf: destinationModelURL)
    let mappingModel = NSMappingModel(from: nil, forSourceModel: sourceModel!, destinationModel: destinationModel!)
    
    let migrationManager = NSMigrationManager(sourceModel: sourceModel!, destinationModel: destinationModel!)
    
    do {
        try migrationManager.migrateStore(from: sourceStoreURL, sourceType: NSSQLiteStoreType, options: nil, with: mappingModel, toDestinationURL: destinationStoreURL, destinationType: NSSQLiteStoreType, destinationOptions: nil)
    } catch {
        print("Failed to migrate store: \(error)")
    }

5. 常見遷移問題

  • 模型版本不匹配:確保在進行遷移時,源模型和目標模型的版本是正確的。
  • 屬性類型變更:如果屬性類型變更較為複雜,需要在映射模型中進行詳細配置。
  • 關聯變更:關聯的變更可能需要在映射模型中進行額外配置,以處理對象之間的關聯變化。

6. 自動遷移的限制

  • 複雜變更:如果變更過於複雜,如修改屬性類型或進行數據重組,輕量級遷移可能無法處理這些變更,需要手動遷移。
  • 測試:在正式部署遷移之前,務必在測試環境中進行充分測試,確保遷移過程不會損壞數據。

7. 遷移示例

以下是一個遷移示例,演示如何設置 Core Data 遷移選項:

func setupCoreDataStack() {
    let modelURL = Bundle.main.url(forResource: "Model", withExtension: "momd")!
    let model = NSManagedObjectModel(contentsOf: modelURL)!
    let storeURL = applicationDocumentsDirectory.appendingPathComponent("DataModel.sqlite")
    
    let options = [
        NSMigratePersistentStoresAutomaticallyOption: true,
        NSInferMappingModelAutomaticallyOption: true
    ]
    
    let coordinator = NSPersistentStoreCoordinator(managedObjectModel: model)
    do {
        try coordinator.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: storeURL, options: options)
    } catch {
        print("Failed to initialize the application's saved data: \(error)")
    }
    
    let context = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
    context.persistentStoreCoordinator = coordinator
}

通過了解和使用 Core Data 中的遷移,你可以在數據模型更改時保護和保留現有數據,確保應用的數據一致性和完整性。

Callback 設計

問題:如何決定使用 Closure 或 Delegate 作為 Callback?

點擊查看答案

在 iOS 開發中,Closure 和 Delegate 是常用的回調機制。選擇使用哪一個主要取決於場景的複雜性、回調的頻率和靈活性需求。

1. Closure

Closure 是一個匿名函數,可以捕捉並儲存上下文變量,用於簡單而即時的回調場景。

使用場景:
  • 簡單的回調需求:當你只需要一個單一且輕量級的回調,Closure 是理想的選擇。
  • 短期回調:像網絡請求、按鈕點擊等一次性的操作。
  • 輕鬆傳遞和處理數據:使用 Closure 作為回調更直觀,適合傳遞小量數據。
範例
func fetchData(completion: @escaping (Result<Data, Error>) -> Void) {
    // 執行網絡請求,完成後回調
    completion(.success(Data()))
}
優點
  • 更加簡潔,適合簡單場景。
  • 可以立即使用,不需要額外的協定設計。
  • 更加靈活,允許在函數結構內定義和使用。
缺點
  • 不適合處理多個回調場景。
  • 如果不小心使用過多,會增加代碼的複雜性,難以維護。

2. Delegate

Delegate 是一種設計模式,適合處理多個事件回調或者長期的回調場景。Delegate 通常是通過協定(Protocol)來實現。

使用場景:
  • 多個回調需求:當一個對象需要回調多個事件,例如 UITableViewDelegate 會有多種不同的操作。
  • 長期回調:適用於需要持續不斷交互的對象,像用戶交互、數據源代理等。
  • 組件間解耦:透過 Delegate 讓兩個組件之間的耦合性降低,符合面向對象設計的原則。
範例
protocol DataDelegate: AnyObject {
    func didFetchData(_ data: Data)
}

class DataFetcher {
    weak var delegate: DataDelegate?
    
    func fetchData() {
        // 假設資料已取得
        let data = Data()
        delegate?.didFetchData(data)
    }
}
優點
  • 適合處理多個回調事件。
  • 清晰的結構,特別是在 MVC、MVVM 等設計模式中常用。
  • 有利於代碼的可讀性和可維護性。
缺點
  • 需要額外的協定定義,並實現相關方法。
  • 代碼量較多,對於一些簡單的回調來說可能顯得過於繁瑣。

3. 如何選擇?

  • 簡單單次回調:如果只需要執行一個簡單的回調任務,如網絡請求完成,Closure 是更好的選擇,因為它簡單且不需要額外的協定設計。
  • 複雜且多次回調:當你有多個回調事件需要處理,或是需要與一個對象持續交互,Delegate 更為合適,因為它結構更清晰,適合處理多個回調方法。
  • 長期對象間溝通:當需要不同對象長期通信時,Delegate 更適合因為它減少了對象之間的強耦合,並且更容易管理和維護。

範例比較

Closure 範例:

func performAction(completion: @escaping (Bool) -> Void) {
    // 執行動作,完成後回調
    completion(true)
}

Delegate 範例:

protocol ActionDelegate: AnyObject {
    func actionCompleted(success: Bool)
}

class ActionPerformer {
    weak var delegate: ActionDelegate?
    
    func performAction() {
        // 執行動作
        delegate?.actionCompleted(success: true)
    }
}

Xcode 分析工具

  • 你有使用過什麼 Xcode Analyze 工具?(例如 Zombie,…etc)

Design Pattern 應用

  • 你有應用過什麼設計模式?可以分享一下嗎?

MVVM

  • 什麼是 MVVM?它的主要目的是什么?
  • MVVM 的優點有哪些?
  • 如何實現 Binding?

Coordinator 模式

  • 你有使用過 Coordinator 嗎?如何應用?

平常使用什麼架構?

  • 你通常使用什麼架構來開發應用?

UnitTest / UITest

  • 你會寫 UnitTest 或 UITest 嗎?

問題:Unit Test 中 Mock 和 Stub 之間有什麼差別?

點擊查看答案

在單元測試中,MockStub 都是用來替代真實對象的測試替身,但它們的使用目的和方式略有不同。

1. Stub

Stub 是一個提供固定輸出或行為的對象,它用來替代某些依賴對象,並返回預定義的結果。Stub 通常不會對被測對象的交互進行驗證,它僅僅是用來返回測試所需的數據。

應用場景
  • 當你需要某個依賴對象返回特定數據,但不關心它的內部邏輯時,可以使用 Stub。
  • 簡單的例子是,模擬一個 API 的返回結果,無論如何調用,都返回相同的結果。
範例
class UserServiceStub: UserServiceProtocol {
    func getUserData() -> User {
        // 返回一個預定義的用戶對象
        return User(name: "Stub User", age: 30)
    }
}

// 測試時注入 Stub
let stubService = UserServiceStub()
let viewModel = UserViewModel(userService: stubService)
XCTAssertEqual(viewModel.userName, "Stub User")
特點
  • 作用:為測試提供固定的數據,不驗證交互。
  • 主要用途:簡化測試環境,減少測試的依賴。

2. Mock

Mock 是一個模擬對象,不僅可以提供固定數據,還能記錄與其交互的細節,以便在測試中驗證這些交互是否符合預期。Mock 更加靈活,允許你設置預期行為並驗證具體的調用次數、參數等。

應用場景
  • 當你不僅需要驗證方法的結果,還需要驗證與其他對象的交互(如方法調用次數、參數等)時,使用 Mock 更合適。
  • 常見的場景是模擬網絡請求是否被正確發送,或檢查某些依賴對象的操作是否被正確執行。
範例
class UserServiceMock: UserServiceProtocol {
    var getUserDataCalled = false
    
    func getUserData() -> User {
        getUserDataCalled = true
        return User(name: "Mock User", age: 25)
    }
}

// 測試時使用 Mock
let mockService = UserServiceMock()
let viewModel = UserViewModel(userService: mockService)
viewModel.loadUserData()

XCTAssertTrue(mockService.getUserDataCalled, "UserService should have been called.")
特點
  • 作用:記錄交互細節並提供數據。
  • 主要用途:驗證對象間的交互是否正確。

3. 差異總結

特徵 Stub Mock
行為 返回固定的數據 記錄交互並可以返回數據
驗證交互 不驗證交互 可以驗證與對象的交互
使用場景 測試時只需要固定結果 測試時需要驗證交互行為和結果
複雜度 較簡單,僅返回預定義數據 較複雜,需要設置期望行為並驗證交互

4. 選擇指南

  • 使用 Stub:當只需要特定數據或結果,不關心如何調用這些對象時。
  • 使用 Mock:當你需要驗證對象之間的交互,例如方法是否被正確調用、調用次數、傳遞的參數等。

Debug 方法

  • 你如何進行 Debug?

Objective-C 和 Swift 的差異

問題:Objective-C 和 Swift 有什麼差別?

點擊查看答案

1. 語言特性

  • Objective-C:是一種基於 C 語言的面向對象編程語言,擁有動態語言特性。它大量使用了指針和動態消息傳遞。
  • Swift:是 Apple 為 iOS 和 macOS 開發的一種現代化語言,具有強類型檢查、內存安全和表達能力。Swift 在語法上更簡潔,安全性更高。

2. 語法簡潔性

  • Objective-C 語法冗長,尤其是在方法調用上需要明確寫出參數名稱和類型。例如:

    [myObject doSomethingWithParam1:param1 param2:param2];
  • Swift 語法簡潔並且更接近現代語言的風格,強調可讀性:

    myObject.doSomething(param1: param1, param2: param2)

3. 內存管理

  • Objective-C 使用的是 ARC(Automatic Reference Counting),但開發者仍需處理一些手動內存管理的情況,並且 Objective-C 使用 retainrelease 來管理對象的生命週期。
  • Swift 也使用 ARC 進行內存管理,但 Swift 對內存管理的處理更加自動化,減少了手動處理內存的需求,並且通過結構體(值類型)進一步提高了內存安全性。

4. 類型安全

  • Objective-C 是動態語言,很多操作可以在運行時執行,因此會引入一定的風險,例如消息傳遞給 nil 對象。
  • Swift 是靜態語言,強調類型安全和內存安全,所有變量都必須在編譯時指定類型,防止運行時出現不正確的類型問題。

5. 可選型與空值

  • Objective-C 沒有內建的可選型支援,使用 nil 表示空值,但如果不小心傳遞 nil,可能會導致程序崩潰。

  • Swift 則引入了可選型(Optional),允許變量可以有值或為 nil,並通過安全的解包機制來防止空值崩潰。

    var name: String? // 可選型

6. 動態 vs 靜態調度

  • Objective-C 使用動態消息傳遞,即方法調用在運行時決定。這提供了靈活性,但也增加了風險。
  • Swift 則大多數情況下是靜態調度,方法的調用和分派在編譯時就已確定,因此性能更高,錯誤檢測也更加完善。

7. 互操作性

  • Objective-CSwift 可以相互操作,可以在 Swift 中調用 Objective-C 的代碼,也可以在 Objective-C 中調用 Swift 的代碼,這使得現有的 Objective-C 項目能夠逐步轉換為 Swift。

8. 並發模型

  • Objective-C 使用的是 GCD(Grand Central Dispatch)來進行並發處理,並且可以使用 @synchronized 等關鍵字來處理同步問題。
  • Swift 在 Swift 5.5 引入了 Concurrency,提供了 async/await 和 Actors,讓並發編程更加簡潔且避免 Race Condition。

9. 語言發展與社區支持

  • Objective-C 是一個相對老舊的語言,最早於 1980 年代發展。隨著 Swift 的推出,它的發展逐漸減緩。
  • Swift 是一個快速發展的語言,有著活躍的社區支持和不斷更新的版本,是 Apple 強烈推動的現代語言。

範例對比

Objective-C 範例

@interface Person : NSObject
@property (nonatomic, strong) NSString *name;
- (void)sayHello;
@end

@implementation Person
- (void)sayHello {
    NSLog(@"Hello, %@", self.name);
}
@end

Swift 範例

class Person {
    var name: String
    init(name: String) {
        self.name = name
    }
    func sayHello() {
        print("Hello, \(name)")
    }
}

總結

  • Swift 更現代、安全,語法簡潔,強類型系統帶來更好的編譯時檢查,適合開發新的 iOS 應用。
  • Objective-C 更靈活,適合維護舊項目或需要與已有 Objective-C 庫互操作的情境。

class 和 struct 的差異

問題:在 iOS 中,值類型 (Value Types) 和參考類型 (Reference Types) 有什麼不同?

點擊查看答案

在 iOS 開發中,值類型 (Value Types) 和參考類型 (Reference Types) 是兩種不同的數據存儲和操作方式,了解它們的區別對於編寫高效和正確的代碼非常重要。

  1. 值類型 (Value Types):

    • 定義: 當你將一個值類型賦值給另一個變量或常量時,它會產生一個副本,兩者之間不會相互影響。Swift 中的結構體 (struct)、枚舉 (enum)、以及基本數據類型如 IntDoubleBoolString 都是值類型。
    • 特性:
      • 值類型的變量各自擁有獨立的數據,這意味著在多線程環境下使用時,它們更具安全性,因為不會產生競爭條件 (Race Conditions)。
      • 在傳遞值類型時,會進行值拷貝 (Pass by Copying),即每次傳遞時都會複製一份新的副本,而不是傳遞原始數據的引用。因此,修改副本不會影響原始數據。
    • 存儲方式: 值類型的數據通常存儲在堆疊 (Stack) 中,這是一種快速且高效的記憶體管理方式。由於每個變量都有自己的副本,值類型不會因為變量之間的賦值而共享記憶體。
    • 應用場景: 使用值類型的場合通常是數據較為簡單,且你希望確保每個實例彼此獨立不受影響時。

    範例:

    struct Point {
        var x: Int
        var y: Int
    }
    
    var point1 = Point(x: 0, y: 0)
    var point2 = point1 // 這裡 point2 是 point1 的拷貝
    point2.x = 10 // 修改 point2 不會影響 point1
    
    print(point1.x) // 輸出 0
    print(point2.x) // 輸出 10
  2. 參考類型 (Reference Types):

    • 定義: 當你將一個參考類型賦值給另一個變量或常量時,兩者將共享同一塊內存,修改其中一個變量會影響到另一個。Swift 中的類 (class) 是參考類型。
    • 特性:
      • 參考類型通過引用來共享相同的數據,因此更適合用於需要共享或互相影響的數據結構。它們適用於表示需要擁有單一共享實例的對象,例如在應用中需要維持狀態的管理器或控制器。
      • 在傳遞參考類型時,會進行引用傳遞 (Pass by Reference),即傳遞的是對內存中實例的引用,而不是實例的副本。因此,對引用的修改會直接影響到原始實例。
    • 存儲方式: 參考類型的實例通常存儲在堆積 (Heap) 中。堆積是一個較大的記憶體區域,用於存儲動態分配的數據。變量之間的賦值只會傳遞內存地址,而不會複製整個數據。當一個參考類型的變量不再被任何參考時,系統會通過自動引用計數 (ARC) 來回收這部分記憶體。
    • 應用場景: 當你希望不同的對象共享數據,或需要表示一個具有身份的對象時,使用參考類型會比較合適。

    範例:

    class Point {
        var x: Int
        var y: Int
    
        init(x: Int, y: Int) {
            self.x = x
            self.y = y
        }
    }
    
    var point1 = Point(x: 0, y: 0)
    var point2 = point1 // 這裡 point2 只是引用了 point1 的內存
    point2.x = 10 // 修改 point2 也會影響 point1
    
    print(point1.x) // 輸出 10
    print(point2.x) // 輸出 10

總結來說,值類型傾向於使用於不可變和簡單數據的場景,而參考類型則適合複雜且需要共享狀態的對象。Swift 的 structclass 是這兩種不同概念的主要代表。理解它們在記憶體中的存儲方式以及值的傳遞方式,有助於你做出更好的架構決策。

問題:Heap memory allocation 與 Stack memory allocation 有什麼不同?

點擊查看答案

在計算機科學中,堆積記憶體 (Heap Memory) 和堆疊記憶體 (Stack Memory) 是兩種主要的記憶體管理方式,各有其特點和用途。理解它們的差異對於有效地管理記憶體和編寫高效的代碼非常重要。

  1. 堆積記憶體 (Heap Memory):

    • 定義: 堆積記憶體是一個動態分配的記憶體區域,通常用於存儲需要長期存在或大小不固定的數據。記憶體的分配和釋放由程式員或自動垃圾回收機制管理。
    • 特性:
      • 動態分配: 堆積記憶體可以在運行時動態分配和釋放,這意味著你可以在程序執行過程中根據需要分配內存。
      • 管理: 需要手動或自動 (如自動引用計數 ARC) 管理內存的分配和釋放。不正確的管理可能會導致內存洩漏 (Memory Leak)。
      • 速度: 分配和釋放內存的速度通常較慢,因為它涉及到內存的動態管理和搜索空閒區域。
      • 分配時機: 堆積記憶體的分配是在運行時進行的。當程序需要存儲一個大小不確定或壽命較長的數據時(例如,對象實例或大型數據結構),堆積記憶體會在運行時進行分配。
    • 多線程: 在多線程環境中,堆積記憶體對不同線程是共享的。這意味著如果多個線程對同一個堆積記憶體區域進行讀寫操作,可能需要額外的同步機制來避免競爭條件和數據不一致。使用鎖 (Locks) 或其他同步技術來保護對堆積記憶體的訪問是常見的做法。
    • 應用場景: 適合用於需要存儲大量數據或具有不確定大小的數據,如大型對象或需要共享的實例。

    範例:

    class Node {
        var value: Int
        var next: Node?
    
        init(value: Int) {
            self.value = value
        }
    }
    
    var head = Node(value: 1)
    head.next = Node(value: 2) // Node 和其鏈表結構存儲在堆積記憶體中
  2. 堆疊記憶體 (Stack Memory):

    • 定義: 堆疊記憶體是一個靜態分配的記憶體區域,主要用於存儲局部變量和函數調用的資訊。記憶體的分配和釋放由系統自動管理,遵循先進後出 (LIFO) 原則。
    • 特性:
      • 靜態分配: 堆疊記憶體的大小和生命周期在編譯時已知,且內存的分配和釋放是自動進行的。每當函數被調用時,相關的局部變量會被壓入堆疊中,當函數結束時,變量會被自動釋放。
      • 管理: 系統會自動管理堆疊記憶體的分配和釋放,不需要額外的管理工作。
      • 速度: 分配和釋放內存的速度非常快,因為它只是簡單地更新堆疊指針。
      • 分配時機: 堆疊記憶體的分配是靜態的,通常在編譯時期就確定了內存的大小。每當函數調用時,相關的局部變量會自動分配到堆疊上,函數返回後,這些變量會自動釋放。
    • 多線程: 每個線程擁有自己的堆疊記憶體,因此線程之間的堆疊記憶體不會相互影響。每個線程獨立管理自己的堆疊,這有助於避免多線程之間的競爭條件。由於堆疊記憶體是線程私有的,所以不需要進行同步。
    • 應用場景: 適合用於存儲簡單數據和局部變量,如函數參數和返回值。

    範例:

    func sum(a: Int, b: Int) -> Int {
        let result = a + b // result 變量存儲在堆疊記憶體中
        return result
    }
    
    let total = sum(a: 5, b: 10) // 當函數執行結束,局部變量 result 會被自動釋放

總結:

  • 堆積記憶體 (Heap Memory) 用於動態分配的長期存在的數據,速度較慢,需要手動管理內存。分配是在運行時進行的。
  • 堆疊記憶體 (Stack Memory) 用於靜態分配的局部變量,速度快,由系統自動管理內存。分配是在編譯時期進行的。

理解這兩種記憶體的分配和管理方式有助於你更有效地控制內存使用,避免常見的內存管理問題。

Heap Stack
Dynamic memory allocation Static memory allocation
Allocated at run time Allocated at compile time
Complex search for an available block to allocate Simply increment and decrement the stack pointer
Work across multiple threads at the same time Each threads has it's own Stack

問題:如何在數據建模時選擇使用 struct 還是 class

點擊查看答案

在 Swift 中,struct(結構體)和 class(類)是兩種基本的數據類型,用於建模和管理數據。它們各自有不同的特性,理解這些特性有助於在設計數據模型時做出明智的選擇。

1. struct(結構體)

  • 特性:

    • 值類型: struct 是值類型。當你將一個 struct 的實例賦值給另一個變量或傳遞給函數時,會進行複製,兩者之間的變化互不影響。
    • 內存分配: 結構體通常分配在堆疊記憶體中,這使得它們的內存管理更高效,特別是對於小型或短期存在的數據。
    • 性能: 由於結構體是值類型,通常在性能上比類型更優,特別是在頻繁創建和傳遞小型實例的情境中。
    • 初始化: 結構體自動生成成員初始化器,使得創建和初始化實例變得簡單。
    • 不可繼承: struct 不能被繼承,這意味著它們不能有子類別。
  • 適用場景:

    • 用於建模簡單的數據結構,如坐標、範圍、點、大小等。
    • 當你需要值的複製,而不是對同一實例的引用時。
    • 適合於不需要繼承或多態的場景。

範例:

struct Point {
    var x: Int
    var y: Int
}

var point1 = Point(x: 10, y: 20)
var point2 = point1 // point2 是 point1 的複製品
point2.x = 30
// point1 的 x 值仍然是 10

2. class(類)

  • 特性:

    • 引用類型: class 是引用類型。當你將一個 class 的實例賦值給另一個變量或傳遞給函數時,實際上是傳遞對實例的引用,兩者之間的變化會互相影響。
    • 內存分配: 類的實例通常分配在堆積記憶體中,這意味著內存管理可能會比結構體更複雜,特別是對於長期存在的實例。
    • 性能: 由於類是引用類型,傳遞引用的開銷可能比值複製的開銷更高,尤其是對於大型數據結構。
    • 繼承: class 支持繼承,這意味著你可以創建子類並覆蓋或擴展基類的功能。
    • 初始化: 類不會自動生成成員初始化器,這意味著需要顯式定義初始化方法。
  • 適用場景:

    • 當你需要共享實例或對象的狀態時,例如在 MVC 或 MVVM 模式中共享 ViewModel。
    • 當你需要使用繼承來擴展功能時。
    • 適合於需要多態和引用語義的情況。

範例:

class Rectangle {
    var width: Int
    var height: Int

    init(width: Int, height: Int) {
        self.width = width
        self.height = height
    }
}

var rect1 = Rectangle(width: 10, height: 20)
var rect2 = rect1 // rect2 和 rect1 引用同一個實例
rect2.width = 30
// rect1 的 width 也變成了 30

總結:

  • 選擇 struct

    • 當你的數據模型是簡單的且不需要繼承。
    • 當你需要值的複製,而不是對實例的引用。
    • 當你需要較高的性能,尤其是在頻繁創建和傳遞數據時。
  • 選擇 class

    • 當你的數據模型需要繼承和多態。
    • 當你需要對象共享和引用語義。
    • 當你需要更靈活的初始化和管理功能。

了解這些特性可以幫助你在設計數據模型時做出最佳選擇,從而提高代碼的效率和可維護性。

問題:Static memory allocation 與 Dynamic memory allocation 有什麼不同?

點擊查看答案

在計算機科學中,靜態記憶體分配 (Static Memory Allocation) 和動態記憶體分配 (Dynamic Memory Allocation) 是兩種主要的記憶體管理技術。了解這些技術的差異有助於你更好地管理內存和設計高效的程序。

1. 靜態記憶體分配 (Static Memory Allocation)

  • 定義: 靜態記憶體分配是在編譯時期進行的內存分配。程序在編譯時就確定了所需的內存量,並在程序執行前分配好記憶體。

  • 特性:

    • 固定大小: 內存的大小在編譯時已經確定,不能在運行時改變。
    • 生命周期: 內存的生命周期與程序的生命周期一致,直到程序結束,內存才會被釋放。
    • 速度: 由於內存分配在編譯時已經確定,這使得靜態記憶體分配的速度非常快。
    • 管理: 內存的管理由編譯器自動處理,無需額外的手動管理。
  • 使用場景:

    • 用於全局變量和靜態變量,這些變量的大小和位置在編譯時已知且不會改變。
    • 適合於內存需求固定且預測的情境。

範例:

var globalVariable: Int = 10 // 靜態記憶體分配,編譯時確定

2. 動態記憶體分配 (Dynamic Memory Allocation)

  • 定義: 動態記憶體分配是在運行時期進行的內存分配。程序可以在執行過程中根據需要分配和釋放內存。

  • 特性:

    • 可變大小: 內存的大小在運行時決定,這使得程序可以靈活地根據實際需求進行內存分配。
    • 生命周期: 內存的生命周期由程序控制,可以在需要時分配,並在不再需要時釋放。
    • 速度: 由於需要在運行時進行內存分配和管理,相對靜態記憶體分配的速度較慢。
    • 管理: 需要程序員或垃圾回收機制(如 ARC)手動管理內存的分配和釋放。
  • 使用場景:

    • 用於需要在運行時動態調整大小或數量的數據結構,例如動態數組、鏈表、樹等。
    • 適合於內存需求不確定或需要靈活管理的情境。

範例:

class Node {
    var value: Int
    var next: Node?

    init(value: Int) {
        self.value = value
    }
}

var head = Node(value: 1) // 動態記憶體分配,運行時確定
head.next = Node(value: 2)

靜態 vs 動態記憶體分配的比較

  • 分配時機:

    • 靜態: 在編譯時分配,內存大小和位置已知且固定。
    • 動態: 在運行時分配,內存大小和位置可變,根據需求調整。
  • 內存管理:

    • 靜態: 由編譯器自動管理,不需手動操作。
    • 動態: 需要程序員手動管理或依賴垃圾回收機制,可能導致內存洩漏或碎片化。
  • 靈活性:

    • 靜態: 靈活性差,適合內存需求固定的情況。
    • 動態: 靈活性高,適合內存需求變化多端的情況。
  • 性能:

    • 靜態: 分配和釋放速度快,效率高。
    • 動態: 分配和釋放速度較慢,性能受內存管理策略影響。

總結:

  • 靜態記憶體分配 適合於內存需求固定且預測的情況,具有更快的分配速度和簡單的內存管理。
  • 動態記憶體分配 適合於內存需求變化多端或需要靈活管理的情況,提供了更大的靈活性,但需要更複雜的內存管理。

理解這些差異可以幫助你在編程時選擇合適的記憶體分配方式,以提高程序的性能和穩定性。

離線功能設計

問題:如果要實現離線功能,你會如何設計?

點擊查看答案

1. 確定離線功能需求

實現離線功能的設計需要根據應用的具體需求來確定,這通常包括:

  • 哪些數據需要在離線狀態下訪問?
  • 離線期間用戶能執行哪些操作?
  • 這些操作如何與服務器進行同步?

2. 選擇適當的本地存儲技術

為了在沒有網絡的情況下保留數據,你需要選擇一個適當的本地存儲技術。常見選擇包括:

  • Core Data:適合結構化數據存儲,支持對象關係圖和查詢功能。
  • Realm:類似於 Core Data,但更易用且性能更佳,適合複雜數據結構。
  • SQLite:低層次的數據庫解決方案,適合需要手動管理數據庫結構和查詢的情況。
  • UserDefaults:適合存儲小量的鍵值對數據,通常用於存儲簡單的設置或標誌位。
  • 文件存儲:適合存儲較大且不常變動的數據,例如圖片或文件。

3. 數據同步與衝突解決策略

在離線模式下,用戶可能會對數據進行修改,這就需要一個有效的數據同步機制來確保數據的正確性和一致性:

  • 本地隊列:將用戶的離線操作保存在本地隊列中,當網絡恢復時自動執行這些操作。
  • 背景同步:應用進入前台或後台時自動與服務器同步數據。
  • 衝突解決策略:當離線修改與線上數據產生衝突時,可以根據時間戳、用戶優先權或其他業務邏輯來解決衝突。

4. 用戶體驗的設計

為了提升用戶在離線模式下的體驗,你應該考慮以下設計:

  • 離線提示:當應用處於離線狀態時,應明確告知用戶,如在 UI 中顯示「目前處於離線狀態」的提示。
  • 緩存機制:緩存最近訪問過的數據,允許用戶在無網絡的情況下仍然可以查看這些數據。
  • 增量同步:當網絡恢復時,只同步改變過的數據,以減少流量消耗和同步時間。

5. 範例設計

假設你正在設計一個筆記應用,該應用支持用戶在離線時編輯和查看筆記:

  1. 存儲方式:使用 Core Data 來存儲筆記數據,因為它支持複雜的對象圖和查詢。
  2. 數據同步:當用戶創建或編輯筆記時,將這些操作記錄到本地的「同步隊列」中。應用恢復網絡連接後,自動將隊列中的操作同步到服務器。
  3. 衝突解決:如果用戶在線上和離線期間都編輯了同一筆記,則根據編輯時間或用戶手動選擇來解決衝突。
  4. 用戶體驗:當用戶處於離線狀態時,顯示「離線模式」的提示,並允許用戶查看本地緩存的筆記。當應用重新連接網絡後,自動進行數據同步。

這種設計可以確保應用在無網絡時仍能正常運行,並且在網絡恢復後能夠自動同步數據,提供無縫的用戶體驗。

問題:在 iOS 不同 App 溝通的方式

點擊查看答案

1. URL Schemes

  • 簡介:URL Schemes 是 iOS 應用之間通信的常用方式之一,允許應用通過特定的 URL 調用其他應用,並可以傳遞數據。

  • 應用場景:打開另一個應用並執行特定操作,例如打開地圖應用並導航到指定地點。

  • 實現方式

    1. 在應用的 Info.plist 中定義自定義的 URL Scheme。
    2. 使用 UIApplication.shared.open(URL) 在應用內部調用另一個應用。
    if let url = URL(string: "otherAppScheme://somePath") {
        UIApplication.shared.open(url)
    }

2. Universal Links

  • 簡介:Universal Links 是更現代的應用間通信方式,它允許應用通過 HTTP 或 HTTPS 連結打開指定的應用,並且如果應用未安裝,連結將會打開對應的網頁。
  • 應用場景:分享可打開應用或網頁的連結,在不同情境下打開相應的內容。
  • 實現方式
    1. 配置應用的 Associated Domains,在 Info.plist 中添加對應的 Associated Domain 條目。
    2. 在應用的後端服務上設置 Apple App Site Association 文件,指定 Universal Links 的對應處理邏輯。

3. App Extensions

  • 簡介:App Extensions 允許開發者將應用的一部分功能擴展到其他應用或 iOS 系統功能中,例如分享擴展、今日擴展(Today Extension)。
  • 應用場景:在用戶從其他應用中分享內容時,直接利用該應用的擴展處理分享內容。
  • 實現方式
    1. 創建 App Extension Target,如 Share Extension。
    2. 在 Extension 的 ViewController 中實現相應的邏輯來處理數據。

4. UIPasteboard

  • 簡介:UIPasteboard 是 iOS 中的剪貼板,用於在應用之間傳遞簡單的數據,如文字、圖片、URL 等。

  • 應用場景:用戶在一個應用中複製文本,然後切換到另一個應用並粘貼該文本。

  • 實現方式

    1. 使用 UIPasteboard.general 讀取和寫入數據。
    let pasteboard = UIPasteboard.general
    pasteboard.string = "Copied text"

5. Keychain 共享(Keychain Sharing)

  • 簡介:Keychain 共享允許同一開發者帳戶下的多個應用共享敏感數據,如登入憑證。
  • 應用場景:同一開發者的多個應用之間共享用戶憑證或其他敏感數據。
  • 實現方式
    1. 在每個需要共享數據的應用中啟用 Keychain Sharing。
    2. 使用帶有共享訪問組(Access Group)的 Keychain API 來讀寫數據。

6. Darwin Notifications

  • 簡介:Darwin Notifications 是一種低層次的方式,用於在同一個設備上的不同應用或系統組件之間傳遞通知。

  • 應用場景:在不同的應用間傳遞通知,而不需要經過網絡或持久化存儲。

  • 實現方式

    1. 使用 CFNotificationCenterPostNotification 發送通知。
    2. 使用 CFNotificationCenterAddObserver 註冊通知觀察者。
    CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), CFNotificationName("com.example.notification" as CFString), nil, nil, true)

7. Shared Group Container

  • 簡介:共享組容器允許多個應用程序共享文件和數據,這些應用必須屬於同一個應用組(App Group)。

  • 應用場景:多個應用之間共享文件、用戶設置或其他持久化數據。

  • 實現方式

    1. 在應用中配置 App Groups。
    2. 使用 FileManagerUserDefaults 的共享容器來訪問共享數據。
    let sharedDefaults = UserDefaults(suiteName: "group.com.example.appgroup")
    sharedDefaults?.set("Shared data", forKey: "sharedKey")

通過這些不同的技術,你可以實現 iOS 應用之間的多種通信方式,根據具體需求選擇最合適的方式。

mutating 和 lazy 的使用時機

問題:什麼時候會使用 mutating?什麼時候會使用 lazy

點擊查看答案

1. mutating

mutating 是用在 值類型(如 structenum)的方法上,當你需要修改這個值類型的屬性時,必須將方法聲明為 mutating

應用場景:
  • 當需要在 結構體枚舉 的方法中修改實例的屬性值或自己時,使用 mutating
  • 例如結構體的默認行為是不可變的,這意味著你不能在方法中改變自身的屬性。如果你需要修改,就必須使用 mutating 關鍵字。
範例
struct Counter {
    var count = 0
    
    mutating func increment() {
        count += 1
    }
}

var counter = Counter()
counter.increment()  // 因為使用了 mutating,方法內可以修改 counter 的 count 屬性
特點
  • 修改值類型的實例:由於 structenum 是值類型,修改它們的屬性或方法需要使用 mutating
  • 允許在方法內變更自身屬性:如果你希望值類型的屬性可以在方法中改變,那麼這個方法必須標記為 mutating

2. lazy

lazy 是用來延遲初始化一個屬性的,只有當該屬性第一次被訪問時,才會被實例化。這對於那些計算昂貴或需要大量內存的屬性來說非常有用。

應用場景:
  • 當你有些屬性不需要馬上初始化,或者初始化的成本比較高,希望在真正使用時再初始化。
  • 尤其適合那些需要依賴其他屬性才能確定值的情況,或需要延遲執行以提高性能的情境。
範例
class DataManager {
    lazy var data = loadData()
    
    func loadData() -> [String] {
        // 假設這個方法很耗時
        return ["Data1", "Data2", "Data3"]
    }
}

let manager = DataManager()
// data 只有在第一次被訪問時才會調用 loadData 方法
print(manager.data)
特點
  • 延遲初始化:當一個屬性不一定在每次對象創建時都會用到,可以使用 lazy 來避免不必要的開銷。
  • 存儲屬性lazy 只能用於類型的 存儲屬性,不能用於常量或計算屬性。

3. 差異總結

特徵 mutating lazy
使用對象 值類型 (struct, enum) 的方法 延遲初始化的存儲屬性
行為 允許方法修改自身屬性或值類型實例 只有在第一次被訪問時才會初始化
使用場景 當需要在方法內改變結構體或枚舉的屬性時 當初始化屬性代價高且可能不會馬上使用時
應用舉例 計算型方法,增減值等 大型數據加載、依賴其他屬性的屬性初始化

TableView 性能優化

  • 如果你發現 TableView 卡頓,你會如何檢查並改善性能?

團隊合作

工作進度估算

  • 你如何估算自己的工作進度?有參與過 Scrum 嗎?

Code Review

  • 你如何進行 Code Review?

意見不合的處理

  • 當你與同事意見不合時,你會怎麼處理?

最不喜歡的同事類型

  • 你最不喜歡哪種類型的同事?

團隊領導經驗

  • 你有帶過團隊嗎?分享你的經驗?

自我進修

社群與博客

  • 你有參加什麼技術社群?或寫自己的 Blog 嗎?

下班後的學習

  • 下班後你會繼續學習程式相關的知識嗎?

技術 Blog 閱讀

  • 你有閱讀什麼技術 blog 嗎?

個性與經驗

近期重要的 Feature

  • 近三個月你負責的最重要的 feature 是什麼?

學習新知的方法與資源

  • 你通常使用什麼方式與資源來學習新知?

面試動機

  • 為什麼會想來面試我們公司?

使用經驗與改善建議

  • 你有使用過我們的 App 嗎?你覺得我們 App 有什麼需要改善的地方?

技術導入經驗

  • 過去你有在公司導入過什麼技術或活動,影響產品的方向嗎?

好的工程師特質

  • 你認為一個好的工程師需要具備什麼特質?
⚠️ **GitHub.com Fallback** ⚠️