iOS 开发面试 50 个高频易混淆知识点详解

一、Swift 语言基础与进阶(1-10)

1. letvar的本质区别(不是 "常量 / 变量" 这么简单)

核心混淆点 :很多人认为let只是值不可变,var是值可变,但忽略了引用类型的特殊行为和内存语义差异。

详细解释

  • let声明的是不可变绑定 ,不是 "不可变值"。对于值类型(Int、String、Array、Struct),let绑定后值和内存地址都不可变;对于引用类型(Class),let仅保证指针地址不可变,但指针指向的对象内部属性可以修改。
  • var声明的是可变绑定,既可以修改值,也可以重新绑定到新的内存地址。
  • 性能差异:Swift 编译器对let有更激进的优化,会将其视为编译期常量(如果可能),减少运行时开销。

示例

复制代码
// 值类型:let完全不可变
let array1 = [1,2,3]
// array1.append(4) ❌ 编译错误:Cannot use mutating member on immutable value
var array2 = [1,2,3]
array2.append(4) // ✅ 正确

// 引用类型:let仅指针不可变,对象内部可变
class Person {
    var name: String
    init(name: String) { self.name = name }
}
let person = Person(name: "张三")
person.name = "李四" // ✅ 正确!修改的是对象内部属性
// person = Person(name: "王五") ❌ 编译错误:Cannot assign to value: 'person' is a 'let' constant

2. structclass的核心区别(面试必问)

核心混淆点:很多人只知道 "值类型 / 引用类型",但忽略了继承、内存管理、方法派发、Copy-On-Write 等关键差异。

详细解释

表格

特性 struct(值类型) class(引用类型)
类型本质 栈上分配(小对象),自动内存管理 堆上分配,需要 ARC 管理引用计数
继承 不支持继承,只能遵守协议 支持单继承
方法派发 静态派发(编译期确定),性能更高 默认动态派发(运行时确定),支持多态
拷贝行为 赋值时自动深拷贝(实际是 Copy-On-Write 优化) 赋值时仅拷贝指针,共享同一对象
生命周期 作用域结束自动销毁 引用计数为 0 时销毁
self修改 必须用mutating标记方法 无需标记,可直接修改属性

示例(Copy-On-Write 机制)

复制代码
var array1 = [1,2,3]
var array2 = array1 // 此时并未真正拷贝,共享同一块内存
print(array1 === array2) // true(指针相同)

array2.append(4) // 触发Copy-On-Write,array2获得独立拷贝
print(array1 === array2) // false(指针不同)
print(array1) // [1,2,3]
print(array2) // [1,2,3,4]

3. =====的区别

核心混淆点:很多人混用这两个运算符,不清楚它们分别比较的是什么。

详细解释

  • ==值相等比较 ,比较两个对象的内容 是否相等。对于值类型,比较的是存储的值;对于引用类型,默认比较的是指针地址(但可以通过重写==运算符自定义内容比较)。
  • ===引用相等比较 ,仅适用于引用类型,比较两个指针是否指向同一个内存地址(即是否是同一个对象实例)。
  • 注意:===不能用于值类型,因为值类型赋值时会拷贝,永远不会有两个值类型变量指向同一个内存地址。

示例

复制代码
class Person: Equatable {
    var name: String
    init(name: String) { self.name = name }
    
    static func == (lhs: Person, rhs: Person) -> Bool {
        return lhs.name == rhs.name
    }
}

let p1 = Person(name: "张三")
let p2 = Person(name: "张三")
let p3 = p1

print(p1 == p2) // true(内容相等)
print(p1 === p2) // false(不同对象)
print(p1 === p3) // true(同一个对象)

4. optional(可选型)的本质与!?的使用

核心混淆点 :很多人滥用!强制解包,不清楚可选型的底层实现和安全解包方式。

详细解释

  • 可选型本质是一个枚举:enum Optional<T> { case none, case some(T) },用于表示 "有值" 或 "无值" 两种状态,解决了 Objective-C 中nil只能用于对象的问题。
  • ?:可选型标记,也用于可选链 (Optional Chaining),如果可选值为nil,整个表达式返回nil,不会崩溃。
  • !强制解包 ,告诉编译器 "我确定这个可选值一定有值",如果为nil会触发运行时崩溃(Fatal Error)。
  • 安全解包方式:if letguard letswitchnil coalescing operator(??)

示例

复制代码
var name: String? = "张三"

// 不安全:强制解包,name为nil时崩溃
print(name!.count) // 4

// 安全:if let解包
if let name = name {
    print(name.count) // 4
} else {
    print("name is nil")
}

// 安全:guard let解包(提前退出)
guard let name = name else {
    fatalError("name is nil")
}
print(name.count) // 4

// 安全:nil合并运算符
let unwrappedName = name ?? "默认值"
print(unwrappedName) // 张三

5. guardif的使用场景区别

核心混淆点 :很多人认为guard只是if的另一种写法,不清楚它们的设计目的和最佳实践。

详细解释

  • if:用于条件分支 ,当条件满足时执行某个代码块,否则执行else块。适合处理 "两种情况都需要执行逻辑" 的场景。
  • guard:用于提前退出 (Early Exit),当条件不满足时立即退出当前作用域(函数、循环、代码块)。它的设计目的是减少嵌套,提高代码可读性,将错误处理放在最前面。
  • 关键区别:guardelse块必须包含退出语句(returnbreakcontinuethrow),且guard解包的可选值在后续整个作用域中都可用。

示例

复制代码
// 不好的写法:多层if嵌套
func validateUser1(username: String?, password: String?) -> Bool {
    if let username = username {
        if !username.isEmpty {
            if let password = password {
                if password.count >= 6 {
                    return true
                }
            }
        }
    }
    return false
}

// 好的写法:guard提前退出,无嵌套
func validateUser2(username: String?, password: String?) -> Bool {
    guard let username = username, !username.isEmpty else {
        return false
    }
    guard let password = password, password.count >= 6 else {
        return false
    }
    return true
}

6. enum的高级用法(关联值、原始值、递归枚举)

核心混淆点:很多人只知道枚举用于表示有限状态,不清楚 Swift 枚举的强大功能(关联值、方法、协议遵守等)。

详细解释

  • Swift 枚举是一等公民,可以拥有属性、方法、初始化器,遵守协议,支持泛型。
  • 原始值(Raw Value):枚举的每个 case 都有一个预先定义的原始值(Int、String、Character 等),在编译期确定。
  • 关联值(Associated Value):枚举的每个 case 可以携带不同类型的关联数据,在运行时赋值,非常适合表示不同状态下的不同数据。
  • 递归枚举 :枚举的 case 关联值类型是自身,需要用indirect标记。

示例(关联值)

复制代码
enum Result<T> {
    case success(T)
    case failure(Error)
}

func fetchData() -> Result<String> {
    if let data = try? Data(contentsOf: URL(string: "https://example.com")!) {
        return .success(String(data: data, encoding: .utf8)!)
    } else {
        return .failure(NSError(domain: "NetworkError", code: -1))
    }
}

// 使用
switch fetchData() {
case .success(let data):
    print("获取数据成功:\(data)")
case .failure(let error):
    print("获取数据失败:\(error)")
}

7. protocolclass-only限制与AnyObject

核心混淆点 :很多人不清楚protocol: AnyObject的作用,以及它与class协议的区别。

详细解释

  • 默认情况下,协议可以被classstructenum遵守。
  • 如果协议声明为protocol P: AnyObject,则该协议只能被类遵守,不能被值类型遵守。
  • 作用:
    1. 告诉编译器该协议的实现者一定是引用类型,可以使用===比较引用相等性。
    2. 可以在协议中声明weak属性(因为weak只能用于引用类型)。
  • 注意:AnyObject是所有类的隐式父类,代表任意引用类型。

示例

复制代码
// 只能被类遵守的协议
protocol DataManagerDelegate: AnyObject {
    func didFetchData(data: String)
}

class DataManager {
    // 必须用weak,否则会循环引用
    weak var delegate: DataManagerDelegate?
    
    func fetchData() {
        delegate?.didFetchData(data: "数据")
    }
}

// 正确:类遵守协议
class ViewController: DataManagerDelegate {
    func didFetchData(data: String) {
        print(data)
    }
}

// 错误:结构体不能遵守AnyObject协议
// struct StructDataManager: DataManagerDelegate {} ❌

8. extension的使用限制与能力

核心混淆点:很多人不清楚 Swift 扩展能做什么、不能做什么,以及它与 Objective-C 分类的区别。

详细解释

  • Swift 扩展可以:
    1. 添加计算属性(不能添加存储属性)
    2. 添加方法、初始化器、下标
    3. 遵守协议
    4. 为泛型类型添加条件扩展
  • Swift 扩展不能
    1. 添加存储属性(会改变对象的内存布局)
    2. 重写已有方法(与 Objective-C 分类不同)
    3. 添加指定初始化器(只能添加便利初始化器)
  • 与 Objective-C 分类的关键区别:Swift 扩展没有名字,不会冲突;如果多个扩展添加了同名方法,编译时会报错。

示例(条件扩展)

复制代码
// 为所有遵守Equatable协议的数组添加去重方法
extension Array where Element: Equatable {
    func unique() -> [Element] {
        var result: [Element] = []
        for element in self {
            if !result.contains(element) {
                result.append(element)
            }
        }
        return result
    }
}

let numbers = [1,2,2,3,3,3]
print(numbers.unique()) // [1,2,3]

9. throwstrydo-catch错误处理机制

核心混淆点 :很多人不清楚 Swift 错误处理的本质,以及try?try!的区别。

详细解释

  • Swift 错误处理基于Error 协议,任何遵守 Error 协议的类型都可以作为错误抛出。
  • throws:标记函数可能抛出错误,调用者必须处理错误。
  • try:用于调用可能抛出错误的函数,必须放在do-catch块中。
  • try?:将错误转换为可选型,如果函数抛出错误,返回nil;否则返回结果。
  • try!:强制解包结果,如果函数抛出错误,会触发运行时崩溃。
  • 最佳实践:尽量避免使用try!,除非你确定函数绝对不会抛出错误。

示例

复制代码
enum MathError: Error {
    case divisionByZero
}

func divide(_ a: Int, by b: Int) throws -> Int {
    guard b != 0 else {
        throw MathError.divisionByZero
    }
    return a / b
}

// 完整的do-catch处理
do {
    let result = try divide(10, by: 2)
    print(result) // 5
} catch MathError.divisionByZero {
    print("除数不能为0")
} catch {
    print("其他错误:\(error)")
}

// try?转换为可选型
let result1 = try? divide(10, by: 0) // nil
let result2 = try? divide(10, by: 2) // Optional(5)

// try!强制解包(危险)
// let result3 = try! divide(10, by: 0) ❌ 崩溃

10. inout参数的工作原理

核心混淆点 :很多人认为inout是 "传引用",但实际上它是 "值传递 + 拷贝回写"(Copy-In Copy-Out)。

详细解释

  • inout参数的工作流程:
    1. 函数调用时,将参数的值拷贝到一个临时变量中。
    2. 函数内部修改这个临时变量。
    3. 函数返回时,将临时变量的值拷贝回原始变量。
  • 这与真正的传引用不同:
    1. inout参数不能是let常量,因为需要修改它的值。
    2. inout参数不能有默认值。
    3. 函数内部对inout参数的修改,直到函数返回时才会反映到原始变量上。
  • 注意:Swift 编译器会对inout参数进行优化,当参数是引用类型或大的值类型时,可能会直接使用引用传递来避免拷贝,但这是优化,不是语义上的改变。

示例

复制代码
func swapTwoInts(_ a: inout Int, _ b: inout Int) {
    let temp = a
    a = b
    b = temp
}

var x = 10
var y = 20
swapTwoInts(&x, &y)
print("x: \(x), y: \(y)") // x: 20, y: 10

二、内存管理(11-18)

11. ARC 的工作原理与自动引用计数

核心混淆点:很多人认为 ARC 是垃圾回收,不清楚它是编译期技术,以及它如何管理内存。

详细解释

  • ARC(Automatic Reference Counting)是 Swift 和 Objective-C 的内存管理机制,它在编译期 自动插入retainreleaseautorelease代码,不需要开发者手动管理引用计数。
  • 工作原理:每个对象都有一个引用计数,当有一个新的强引用指向对象时,引用计数 + 1;当强引用被销毁时,引用计数 - 1;当引用计数变为 0 时,对象被销毁。
  • ARC 不是垃圾回收:垃圾回收是运行时技术,会周期性地扫描内存,回收不再使用的对象;而 ARC 是编译期技术,在编译时就确定了对象的生命周期,没有运行时开销。
  • 注意:ARC 只能管理引用类型的内存,值类型(struct、enum)不需要 ARC 管理,因为它们在栈上分配,作用域结束自动销毁。

示例

复制代码
class Person {
    deinit {
        print("Person被销毁")
    }
}

var p1: Person? = Person() // 引用计数=1
var p2 = p1 // 引用计数=2
p1 = nil // 引用计数=1
p2 = nil // 引用计数=0,对象被销毁,打印"Person被销毁"

12. 强引用(strong)、弱引用(weak)、无主引用(unowned)的区别

核心混淆点 :很多人分不清weakunowned的使用场景,以及它们的区别。

详细解释

表格

引用类型 引用计数变化 对象销毁时行为 是否为可选型 使用场景
strong +1 不会自动置为 nil 默认引用类型,对象生命周期由它控制
weak 不 + 1 自动置为 nil 两个对象互相引用,其中一个生命周期更短
unowned 不 + 1 不会自动置为 nil,访问已销毁对象会崩溃 两个对象互相引用,生命周期相同或更长
  • 关键区别:weak是安全的,访问已销毁对象会得到nilunowned是不安全的,访问已销毁对象会触发运行时崩溃(类似!强制解包)。
  • 最佳实践:优先使用weak,只有当你确定引用的对象在访问时一定不会被销毁时,才使用unowned

示例(循环引用)

复制代码
class Person {
    var name: String
    var car: Car?
    deinit { print("Person \(name)被销毁") }
    init(name: String) { self.name = name }
}

class Car {
    var brand: String
    weak var owner: Person? // 用weak打破循环引用
    deinit { print("Car \(brand)被销毁") }
    init(brand: String) { self.brand = brand }
}

var person: Person? = Person(name: "张三")
var car: Car? = Car(brand: "宝马")
person?.car = car
car?.owner = person

person = nil
car = nil
// 打印:Person 张三被销毁;Car 宝马被销毁
// 如果owner不用weak,会形成循环引用,两个对象都不会被销毁

13. 循环引用的产生原因与解决方法

核心混淆点:很多人只知道 delegate 需要用 weak,不清楚其他可能产生循环引用的场景。

详细解释

  • 循环引用产生的根本原因:两个或多个对象互相持有强引用,导致它们的引用计数永远不会变为 0,内存泄漏。
  • 常见场景:
    1. delegate 模式:如果 delegate 是强引用,会导致控制器和代理对象互相持有。
    2. 闭包:如果闭包捕获了 self,而 self 又持有闭包,会形成循环引用。
    3. 定时器:Timer会强引用它的 target,而 target 如果又持有Timer,会形成循环引用。
  • 解决方法:
    1. 使用weakunowned打破循环引用。
    2. 对于闭包,使用捕获列表[weak self][unowned self]
    3. 对于定时器,在合适的时机调用invalidate()方法。

示例(闭包循环引用)

复制代码
class ViewController {
    var name = "ViewController"
    var completion: (() -> Void)?
    
    func setup() {
        // 错误:闭包强引用self,self强引用闭包,循环引用
        // completion = {
        //     print(self.name)
        // }
        
        // 正确:使用[weak self]捕获列表
        completion = { [weak self] in
            guard let self = self else { return }
            print(self.name)
        }
    }
    
    deinit {
        print("ViewController被销毁")
    }
}

var vc: ViewController? = ViewController()
vc?.setup()
vc = nil // 打印"ViewController被销毁"

14. autoreleasepool的作用与使用场景

核心混淆点 :很多人认为 Swift 不需要autoreleasepool,不清楚它的作用和使用时机。

详细解释

  • autoreleasepool用于管理自动释放对象 的生命周期。当对象调用autorelease方法时,会被添加到最近的autoreleasepool中,当autoreleasepool结束时,会向池中的所有对象发送release消息。
  • 在 Swift 中,ARC 会自动管理大部分对象的生命周期,但在以下场景中,手动创建autoreleasepool可以显著降低内存峰值:
    1. 循环中创建大量临时对象。
    2. 处理大文件、大图片等占用大量内存的操作。
  • 注意:Swift 的autoreleasepool语法是autoreleasepool { ... },与 Objective-C 的@autoreleasepool类似。

示例

复制代码
// 不好的写法:循环中创建大量临时对象,内存峰值很高
for _ in 0..<10000 {
    let image = UIImage(named: "large_image")
    // 处理图片
}

// 好的写法:每次循环创建一个autoreleasepool,及时释放临时对象
for _ in 0..<10000 {
    autoreleasepool {
        let image = UIImage(named: "large_image")
        // 处理图片
    }
}

15. deinit的调用时机与限制

核心混淆点 :很多人不清楚deinit什么时候会被调用,以及它能做什么、不能做什么。

详细解释

  • deinit是类的析构方法,当对象的引用计数变为 0 时,会自动调用deinit方法。
  • deinit的作用:
    1. 释放手动管理的资源(如文件句柄、网络连接、Core Foundation 对象)。
    2. 移除观察者(如 KVO、NotificationCenter)。
    3. 停止定时器。
  • deinit的限制:
    1. 不能接受参数,不能有返回值。
    2. 不能主动调用deinit,只能由系统自动调用。
    3. 子类的deinit会自动调用父类的deinit
    4. deinit中不能使用weakunowned引用的 self,因为此时对象已经在销毁过程中。

示例

复制代码
class ViewController {
    var timer: Timer?
    
    init() {
        timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
            print("定时器触发")
        }
    }
    
    deinit {
        // 必须停止定时器,否则会内存泄漏
        timer?.invalidate()
        timer = nil
        print("ViewController被销毁")
    }
}

16. 值类型的内存管理与 Copy-On-Write

核心混淆点:很多人认为值类型赋值时会立即深拷贝,不清楚 Copy-On-Write 优化机制。

详细解释

  • 值类型(struct、enum、Array、Dictionary 等)在赋值时,理论上会进行深拷贝,每个变量都有自己独立的副本。但为了提高性能,Swift 标准库中的大部分值类型都实现了 **Copy-On-Write(写时复制)** 机制。
  • Copy-On-Write 工作原理:
    1. 当值类型被赋值时,并不会立即拷贝,而是共享同一块内存。
    2. 当其中一个变量修改值时,才会真正进行拷贝,生成独立的副本。
  • 好处:减少不必要的内存拷贝,提高性能,特别是对于大的数组和字典。
  • 注意:自定义的 struct 默认没有实现 Copy-On-Write,需要手动实现。

示例

复制代码
var array1 = [1,2,3,4,5]
var array2 = array1

// 此时array1和array2共享同一块内存
print(array1.withUnsafeBufferPointer { $0.baseAddress })
print(array2.withUnsafeBufferPointer { $0.baseAddress }) // 相同地址

array2.append(6) // 触发Copy-On-Write,array2获得独立副本

// 此时array1和array2指向不同的内存
print(array1.withUnsafeBufferPointer { $0.baseAddress })
print(array2.withUnsafeBufferPointer { $0.baseAddress }) // 不同地址

17. Core Foundation 对象的内存管理(__bridge__bridge_retained__bridge_transfer

核心混淆点:很多人不清楚 Foundation 和 Core Foundation 之间的桥接转换,以及三个桥接关键字的区别。

详细解释

  • Foundation 框架是 Objective-C 的面向对象框架,Core Foundation 框架是 C 语言的面向过程框架,它们之间可以互相转换,称为Toll-Free Bridging
  • 三个桥接关键字的区别:
    1. __bridge:仅转换类型,不改变引用计数。Foundation 转 Core Foundation 时,不需要手动释放;Core Foundation 转 Foundation 时,也不需要手动释放。
    2. __bridge_retained(也叫CFBridgingRetain):将 Foundation 对象转换为 Core Foundation 对象,同时引用计数 + 1,需要手动调用CFRelease释放。
    3. __bridge_transfer(也叫CFBridgingRelease):将 Core Foundation 对象转换为 Foundation 对象,同时将引用计数的管理权交给 ARC,不需要手动释放。

示例

复制代码
// Foundation转Core Foundation
let nsString: NSString = "Hello"
let cfString: CFString = __bridge_retained CFStringRef(nsString)
// 使用cfString
CFRelease(cfString) // 必须手动释放

// Core Foundation转Foundation
let cfString2: CFString = CFStringCreateWithCString(kCFAllocatorDefault, "World", kCFStringEncodingUTF8)
let nsString2: NSString = __bridge_transfer NSString(cfString2)
// 不需要手动释放cfString2,ARC会管理

18. 内存泄漏的检测方法

核心混淆点:很多人不知道如何检测内存泄漏,只知道用 Instruments,但不清楚具体步骤。

详细解释

  • 内存泄漏是指对象不再被使用,但引用计数不为 0,无法被销毁,导致内存无法释放。
  • 常见的内存泄漏检测方法:
    1. Instruments Leaks:Xcode 自带的内存泄漏检测工具,可以精确找到泄漏的对象和代码位置。
    2. Debug Memory Graph:Xcode 8 及以上版本提供的内存图调试工具,可以直观地查看对象的引用关系,快速找到循环引用。
    3. 手动打印 deinit :在类的deinit方法中打印日志,看对象是否被正确销毁。
    4. MLeaksFinder:腾讯开源的内存泄漏检测工具,可以在运行时自动检测内存泄漏,并弹出提示。

使用 Debug Memory Graph 的步骤

  1. 运行项目,进入可能存在内存泄漏的页面。
  2. 点击 Xcode 导航栏中的 "Debug Memory Graph" 按钮(三个圆圈的图标)。
  3. 在左侧的对象列表中,找到应该被销毁但仍然存在的对象。
  4. 查看右侧的引用关系图,找到导致循环引用的强引用。

三、多线程与 GCD(19-27)

19. 进程与线程的区别

核心混淆点:很多人分不清进程和线程的概念,不清楚它们的关系。

详细解释

  • 进程:是操作系统分配资源的基本单位,每个进程都有独立的内存空间、文件句柄、网络连接等资源。一个应用程序就是一个进程。
  • 线程:是 CPU 调度的基本单位,一个进程可以包含多个线程,这些线程共享进程的资源(内存、文件句柄等)。
  • 关键区别:
    1. 资源开销:进程的创建和销毁开销大,线程的创建和销毁开销小。
    2. 通信:进程间通信复杂(管道、消息队列、共享内存等),线程间通信简单(共享内存)。
    3. 安全性:进程之间相互独立,一个进程崩溃不会影响其他进程;线程之间共享资源,一个线程崩溃可能导致整个进程崩溃。
  • 在 iOS 中,每个应用程序都是一个进程,主线程(UI 线程)是进程的第一个线程,负责处理 UI 事件和更新 UI。

20. 串行队列与并发队列的区别

核心混淆点:很多人分不清串行队列和并发队列的执行方式,以及它们与同步 / 异步的关系。

详细解释

  • 串行队列(Serial Queue):队列中的任务按顺序执行,一个任务执行完毕后,才会执行下一个任务。同一时间只有一个任务在执行。
  • 并发队列(Concurrent Queue):队列中的任务可以同时执行,系统会根据 CPU 的核心数和负载情况,自动创建多个线程来执行任务。
  • 注意:队列只是决定了任务的执行顺序和并发度,而同步 / 异步决定了任务是否会阻塞当前线程。

示例

复制代码
// 串行队列
let serialQueue = DispatchQueue(label: "com.example.serial")
serialQueue.async {
    print("任务1")
    sleep(1)
}
serialQueue.async {
    print("任务2")
    sleep(1)
}
serialQueue.async {
    print("任务3")
}
// 输出:任务1 -> 任务2 -> 任务3(按顺序执行)

// 并发队列
let concurrentQueue = DispatchQueue(label: "com.example.concurrent", attributes: .concurrent)
concurrentQueue.async {
    print("任务A")
    sleep(1)
}
concurrentQueue.async {
    print("任务B")
    sleep(1)
}
concurrentQueue.async {
    print("任务C")
}
// 输出:任务A、任务B、任务C(顺序不确定,并发执行)

21. 同步(sync)与异步(async)的区别

核心混淆点:很多人认为同步是在主线程执行,异步是在子线程执行,这是完全错误的。

详细解释

  • 同步(sync) :提交任务到队列后,会阻塞当前线程,直到任务执行完毕。
  • 异步(async) :提交任务到队列后,不会阻塞当前线程,立即返回,任务在后台执行。
  • 关键误区:同步 / 异步与线程没有直接关系!同步任务可以在子线程执行,异步任务也可以在主线程执行。
  • 例如:在子线程中同步提交任务到主队列,任务会在主线程执行,并且子线程会阻塞直到任务执行完毕。

示例

复制代码
print("当前线程:\(Thread.current)") // 主线程

// 异步提交到主队列:不会阻塞主线程,任务在主线程执行
DispatchQueue.main.async {
    print("异步主队列任务,线程:\(Thread.current)") // 主线程
}

// 同步提交到全局并发队列:会阻塞主线程,任务在子线程执行
DispatchQueue.global().sync {
    print("同步全局队列任务,线程:\(Thread.current)") // 子线程
}

print("主线程继续执行")

22. 主队列与全局队列的区别

核心混淆点:很多人不清楚主队列和全局队列的特性,以及它们的使用场景。

详细解释

  • 主队列(Main Queue)
    1. 是系统提供的串行队列,所有任务都在主线程执行。
    2. 负责处理 UI 事件和更新 UI,所有 UI 操作必须在主队列执行。
    3. 优先级最高。
  • 全局队列(Global Queue)
    1. 是系统提供的并发队列,有四个优先级:userInteractiveuserInitiatedutilitybackground
    2. 任务在子线程执行,适合执行耗时操作(如网络请求、数据处理)。
    3. 系统会根据优先级自动调整线程数量。

示例

复制代码
// 耗时操作放在全局队列
DispatchQueue.global().async {
    // 模拟网络请求
    sleep(2)
    let data = "网络数据"
    
    // UI更新必须放在主队列
    DispatchQueue.main.async {
        print("更新UI:\(data)")
    }
}

23. DispatchGroup的使用方法与场景

核心混淆点 :很多人不清楚DispatchGroup的作用,以及如何等待多个异步任务完成。

详细解释

  • DispatchGroup用于管理一组异步任务,可以等待所有任务完成后再执行某个操作。
  • 常用方法:
    1. enter():标记一个任务开始,组内任务数 + 1。
    2. leave():标记一个任务结束,组内任务数 - 1。
    3. wait():阻塞当前线程,直到所有任务完成。
    4. notify(queue:execute:):当所有任务完成后,在指定队列执行回调。
  • 使用场景:多个网络请求并发执行,等待所有请求完成后再更新 UI。

示例

复制代码
let group = DispatchGroup()
let queue = DispatchQueue.global()

queue.async(group: group) {
    sleep(1)
    print("任务1完成")
}

queue.async(group: group) {
    sleep(2)
    print("任务2完成")
}

// 所有任务完成后,在主队列执行回调
group.notify(queue: DispatchQueue.main) {
    print("所有任务完成,更新UI")
}

// 或者阻塞当前线程等待所有任务完成
// group.wait()
// print("所有任务完成")

24. DispatchSemaphore的使用方法与场景

核心混淆点:很多人不清楚信号量的作用,以及如何控制并发数。

详细解释

  • DispatchSemaphore(信号量)是一种同步机制,用于控制同时访问共享资源的线程数量。
  • 常用方法:
    1. init(value:):初始化信号量,value 表示最大并发数。
    2. wait():信号量值 - 1,如果值小于 0,阻塞当前线程。
    3. signal():信号量值 + 1,如果值大于等于 0,唤醒一个等待的线程。
  • 使用场景:
    1. 控制并发数(如限制同时最多 3 个网络请求)。
    2. 解决生产者 - 消费者问题。
    3. 保证线程安全。

示例(控制并发数)

复制代码
// 最大并发数为2
let semaphore = DispatchSemaphore(value: 2)
let queue = DispatchQueue.global()

for i in 1...5 {
    queue.async {
        semaphore.wait() // 信号量-1
        defer { semaphore.signal() } // 任务完成后信号量+1
        
        print("任务\(i)开始")
        sleep(1)
        print("任务\(i)完成")
    }
}
// 输出:任务1、任务2开始 -> 1秒后任务1、2完成,任务3、4开始 -> 1秒后任务3、4完成,任务5开始 -> 1秒后任务5完成

25. DispatchBarrier的使用方法与场景

核心混淆点:很多人不清楚栅栏的作用,以及如何实现读写锁。

详细解释

  • DispatchBarrier(栅栏)用于在并发队列中插入一个 "栅栏",当栅栏任务执行时,会等待队列中之前的所有任务完成,然后再执行栅栏任务,栅栏任务完成后,才会执行队列中之后的任务。
  • 常用方法:async(flags: .barrier, execute:)
  • 使用场景:实现读写锁(多读单写),即多个线程可以同时读,但写操作必须互斥,且写操作时不能有读操作。

示例(读写锁)

复制代码
class SafeArray<T> {
    private var array: [T] = []
    private let queue = DispatchQueue(label: "com.example.safeArray", attributes: .concurrent)
    
    // 读操作:并发执行
    func get(at index: Int) -> T {
        return queue.sync {
            array[index]
        }
    }
    
    // 写操作:栅栏执行,互斥
    func append(_ element: T) {
        queue.async(flags: .barrier) {
            self.array.append(element)
        }
    }
}

26. OperationQueue与 GCD 的区别

核心混淆点 :很多人分不清OperationQueue和 GCD 的使用场景,不清楚它们的优缺点。

详细解释

  • GCD 是基于 C 语言的底层 API,轻量级,性能高,适合简单的多线程任务。
  • OperationQueue是基于 GCD 的 Objective-C 封装,更面向对象,提供了更多高级功能:
    1. 可以设置任务的优先级。
    2. 可以添加任务之间的依赖关系。
    3. 可以取消任务。
    4. 可以设置最大并发数。
  • 选择建议:
    • 简单的异步任务,使用 GCD。
    • 复杂的任务(需要依赖、取消、优先级),使用OperationQueue

示例(任务依赖)

复制代码
let queue = OperationQueue()

let op1 = BlockOperation {
    print("任务1")
}

let op2 = BlockOperation {
    print("任务2")
}

let op3 = BlockOperation {
    print("任务3")
}

// op3依赖op1和op2,必须等op1和op2完成后才能执行
op3.addDependency(op1)
op3.addDependency(op2)

queue.addOperations([op1, op2, op3], waitUntilFinished: false)
// 输出:任务1、任务2(顺序不确定) -> 任务3

27. 线程安全的实现方法

核心混淆点:很多人不清楚什么是线程安全,以及如何保证线程安全。

详细解释

  • 线程安全是指多个线程同时访问共享资源时,不会出现数据不一致或崩溃的情况。
  • 常见的线程安全问题:
    1. 多个线程同时写同一个变量。
    2. 一个线程读,另一个线程写同一个变量。
  • 实现线程安全的方法:
    1. 串行队列:所有访问共享资源的任务都放在同一个串行队列中执行。
    2. 并发队列 + 栅栏:读操作并发执行,写操作用栅栏互斥执行(读写锁)。
    3. 信号量:控制同时访问共享资源的线程数量为 1。
    4. @synchronized :Objective-C 中的同步锁,Swift 中没有直接对应的语法,可以用objc_sync_enterobjc_sync_exit实现。

示例(串行队列实现线程安全)

复制代码
class Counter {
    private var count = 0
    private let queue = DispatchQueue(label: "com.example.counter")
    
    func increment() {
        queue.sync {
            count += 1
        }
    }
    
    func getCount() -> Int {
        return queue.sync {
            count
        }
    }
}

四、UIKit 与界面开发(28-36)

28. framebounds的区别

核心混淆点 :很多人分不清framebounds的坐标系,不清楚它们的作用。

详细解释

  • frame :是视图在父视图坐标系中的位置和大小。它的原点是父视图的左上角。
  • bounds :是视图在自身坐标系中的位置和大小。它的原点是自身的左上角。
  • 关键区别:
    1. 修改frame会改变视图在父视图中的位置。
    2. 修改bounds会改变视图的可见区域,不会改变视图在父视图中的位置。
    3. 当视图发生旋转时,frame会变化,但bounds不会变化。

示例

复制代码
let view = UIView(frame: CGRect(x: 100, y: 100, width: 200, height: 200))
view.backgroundColor = .red
self.view.addSubview(view)

print("frame: \(view.frame)") // (100.0, 100.0, 200.0, 200.0)
print("bounds: \(view.bounds)") // (0.0, 0.0, 200.0, 200.0)

// 修改bounds的origin,视图的可见区域会变化,但位置不变
view.bounds = CGRect(x: 50, y: 50, width: 200, height: 200)
print("修改后frame: \(view.frame)") // 仍然是(100.0, 100.0, 200.0, 200.0)
print("修改后bounds: \(view.bounds)") // (50.0, 50.0, 200.0, 200.0)

29. UIView的生命周期方法

核心混淆点 :很多人分不清UIViewUIViewController的生命周期方法,不清楚它们的调用顺序。

详细解释

  • UIView的生命周期方法:
    1. init(frame:):通过代码创建视图时调用。
    2. init(coder:):通过 xib 或 storyboard 创建视图时调用。
    3. awakeFromNib():从 xib 或 storyboard 加载完成后调用,此时视图的 frame 还没有确定。
    4. layoutSubviews():视图的 frame 发生变化时调用,用于布局子视图。
    5. draw(_ rect:):绘制视图内容时调用,不要手动调用,通过setNeedsDisplay()触发。
    6. deinit:视图被销毁时调用。
  • 调用顺序:init(frame:)/init(coder:) -> awakeFromNib() -> layoutSubviews() -> draw(_ rect:) -> deinit

示例

复制代码
class CustomView: UIView {
    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    
    private func commonInit() {
        // 初始化代码
        backgroundColor = .red
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        // 布局子视图
        subviews.forEach { $0.frame = bounds }
    }
    
    override func draw(_ rect: CGRect) {
        super.draw(rect)
        // 绘制内容
        let path = UIBezierPath(ovalIn: rect)
        UIColor.blue.setFill()
        path.fill()
    }
}

30. UIViewController的生命周期方法

核心混淆点 :很多人不清楚UIViewController各个生命周期方法的调用时机和作用。

详细解释

  • UIViewController的生命周期方法:
    1. init(nibName:bundle:):初始化控制器。
    2. viewDidLoad():视图加载完成后调用,此时视图的 frame 还没有确定,适合做一次性的初始化工作。
    3. viewWillAppear(_:):视图即将显示时调用,每次显示都会调用。
    4. viewDidAppear(_:):视图已经显示时调用。
    5. viewWillLayoutSubviews():视图即将布局子视图时调用。
    6. viewDidLayoutSubviews():视图已经布局子视图时调用,此时视图的 frame 已经确定。
    7. viewWillDisappear(_:):视图即将消失时调用。
    8. viewDidDisappear(_:):视图已经消失时调用。
    9. deinit:控制器被销毁时调用。
  • 调用顺序:init -> viewDidLoad() -> viewWillAppear(_:) -> viewWillLayoutSubviews() -> viewDidLayoutSubviews() -> viewDidAppear(_:) -> viewWillDisappear(_:) -> viewDidDisappear(_:) -> deinit

示例

复制代码
class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        // 一次性初始化工作
        view.backgroundColor = .white
    }
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        // 每次显示都需要做的工作,如刷新数据
        print("视图即将显示")
    }
    
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        // 布局子视图,此时frame已经确定
        let button = UIButton(frame: CGRect(x: 100, y: 100, width: 100, height: 50))
        button.setTitle("按钮", for: .normal)
        button.backgroundColor = .blue
        view.addSubview(button)
    }
}

31. UIButtonaddTargetclosure的区别

核心混淆点 :很多人不清楚 Swift 中UIButton的两种点击事件处理方式的区别,以及循环引用问题。

详细解释

  • addTarget(_:action:for:) :Objective-C 风格的事件处理方式,通过 selector 调用方法。
    • 优点:不会产生循环引用,因为UIButton对 target 是弱引用。
    • 缺点:语法繁琐,需要单独写一个方法。
  • addAction(for:handler:) :iOS 14 及以上版本新增的闭包风格的事件处理方式。
    • 优点:语法简洁,代码集中。
    • 缺点:闭包会强引用 self,如果 self 又持有 button,会形成循环引用。

示例

复制代码
// addTarget方式
let button1 = UIButton(frame: CGRect(x: 100, y: 100, width: 100, height: 50))
button1.setTitle("按钮1", for: .normal)
button1.addTarget(self, action: #selector(button1Tapped), for: .touchUpInside)
view.addSubview(button1)

@objc private func button1Tapped() {
    print("按钮1被点击")
}

// addAction方式(iOS 14+)
let button2 = UIButton(frame: CGRect(x: 100, y: 200, width: 100, height: 50))
button2.setTitle("按钮2", for: .normal)
button2.addAction(UIAction { [weak self] _ in
    guard let self = self else { return }
    print("按钮2被点击")
}, for: .touchUpInside)
view.addSubview(button2)

32. UITableView的重用机制

核心混淆点 :很多人不清楚UITableView重用机制的原理,以及为什么会出现内容错乱的问题。

详细解释

  • UITableView的重用机制是为了提高性能,减少内存占用。它不会为每一行数据都创建一个UITableViewCell,而是创建有限的几个UITableViewCell,当 cell 滑出屏幕时,会被放入重用池,当新的 cell 滑入屏幕时,会从重用池中取出一个 cell 进行复用。
  • 重用机制的原理:
    1. UITableView需要显示一个 cell 时,会调用dequeueReusableCell(withIdentifier:for:)方法。
    2. 如果重用池中有可用的 cell,就取出这个 cell,调用prepareForReuse()方法重置 cell 的状态。
    3. 如果重用池中没有可用的 cell,就创建一个新的 cell。
  • 内容错乱的原因:复用 cell 时,没有正确重置 cell 的状态,导致旧的内容被保留。
  • 解决方法:在cellForRowAt方法中,为 cell 的所有属性赋值,或者在prepareForReuse()方法中重置 cell 的状态。

示例

复制代码
class CustomCell: UITableViewCell {
    let titleLabel = UILabel()
    
    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        contentView.addSubview(titleLabel)
        titleLabel.frame = contentView.bounds
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func prepareForReuse() {
        super.prepareForReuse()
        // 重置cell的状态
        titleLabel.text = nil
        titleLabel.textColor = .black
    }
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "CustomCell", for: indexPath) as! CustomCell
    // 为cell的所有属性赋值
    cell.titleLabel.text = dataArray[indexPath.row]
    return cell
}

33. Auto LayoutFrame布局的区别

核心混淆点:很多人不清楚 Auto Layout 的原理,以及它与 Frame 布局的优缺点。

详细解释

  • Frame 布局 :直接设置视图的 frame 和 bounds,是绝对布局。
    • 优点:简单直观,性能高。
    • 缺点:适配不同屏幕尺寸和方向困难,代码量大。
  • Auto Layout :通过约束(Constraint)来描述视图之间的关系,是相对布局。
    • 优点:适配性好,自动适应不同屏幕尺寸和方向,代码量少。
    • 缺点:学习曲线陡峭,性能比 Frame 布局稍差(但对于大多数应用来说可以忽略)。
  • Auto Layout 的原理:基于约束的布局系统,通过求解线性方程组来确定视图的 frame。每个约束都是一个等式或不等式,描述了视图的位置和大小与其他视图的关系。

示例(Auto Layout)

复制代码
let view = UIView()
view.backgroundColor = .red
view.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(view)

NSLayoutConstraint.activate([
    view.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),
    view.centerYAnchor.constraint(equalTo: self.view.centerYAnchor),
    view.widthAnchor.constraint(equalToConstant: 200),
    view.heightAnchor.constraint(equalToConstant: 200)
])

34. UIStackView的使用方法与优势

核心混淆点 :很多人不清楚UIStackView的作用,以及它与 Auto Layout 的关系。

详细解释

  • UIStackView是 iOS 9 及以上版本引入的布局容器,用于管理一组子视图的布局,自动为子视图添加约束。
  • 优势:
    1. 减少约束的数量,简化布局代码。
    2. 动态添加、删除、隐藏子视图时,自动更新布局。
    3. 支持水平和垂直布局,支持不同的对齐方式和分布方式。
  • 常用属性:
    1. axis:布局方向(水平 / 垂直)。
    2. alignment:子视图的对齐方式。
    3. distribution:子视图的分布方式。
    4. spacing:子视图之间的间距。

示例

复制代码
let stackView = UIStackView()
stackView.axis = .vertical
stackView.alignment = .center
stackView.distribution = .equalSpacing
stackView.spacing = 20
stackView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(stackView)

NSLayoutConstraint.activate([
    stackView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
    stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])

// 添加子视图
for i in 1...3 {
    let label = UILabel()
    label.text = "标签\(i)"
    label.backgroundColor = .lightGray
    stackView.addArrangedSubview(label)
}

35. CALayerUIView的区别

核心混淆点 :很多人分不清CALayerUIView的关系,不清楚它们的职责分工。

详细解释

  • UIView :是 UIKit 中的视图类,负责处理用户交互事件(触摸、手势等),是CALayer的管理器。
  • CALayer:是 Core Animation 中的图层类,负责绘制内容和动画,不处理用户交互事件。
  • 关系:每个UIView都有一个对应的CALayerUIView的 frame、bounds、backgroundColor 等属性实际上都是转发给CALayer的。
  • 关键区别:
    1. UIView继承自UIResponder,可以处理用户交互;CALayer继承自NSObject,不能处理用户交互。
    2. UIView的动画是基于CALayer的动画实现的。
    3. CALayer有更多的属性可以设置,如圆角、阴影、边框、渐变等。

示例(CALayer 设置阴影)

复制代码
let view = UIView(frame: CGRect(x: 100, y: 100, width: 200, height: 200))
view.backgroundColor = .white
self.view.addSubview(view)

// 设置阴影
view.layer.shadowColor = UIColor.black.cgColor
view.layer.shadowOpacity = 0.5
view.layer.shadowOffset = CGSize(width: 0, height: 5)
view.layer.shadowRadius = 10

// 设置圆角
view.layer.cornerRadius = 10

// 设置边框
view.layer.borderWidth = 2
view.layer.borderColor = UIColor.red.cgColor

36. 离屏渲染的产生原因与优化方法

核心混淆点:很多人不清楚什么是离屏渲染,以及为什么会导致性能问题。

详细解释

  • 离屏渲染是指 GPU 在当前屏幕缓冲区之外,开辟一个新的缓冲区进行渲染操作。
  • 产生原因:当视图的某些属性需要提前渲染时,就会触发离屏渲染。常见的触发条件:
    1. shouldRasterize = true(光栅化)。
    2. mask(遮罩)。
    3. shadow(阴影)。
    4. cornerRadius + masksToBounds = true(iOS 13 及以上版本优化了圆角,不会触发离屏渲染)。
    5. allowsEdgeAntialiasing = true(边缘抗锯齿)。
  • 性能问题:离屏渲染需要多次切换上下文(从屏幕缓冲区到离屏缓冲区,再回到屏幕缓冲区),会消耗大量的 GPU 资源,导致帧率下降。
  • 优化方法:
    1. 尽量避免使用触发离屏渲染的属性。
    2. 对于静态视图,可以使用shouldRasterize = true将渲染结果缓存起来。
    3. 使用 Core Graphics 直接绘制圆角和阴影。

示例(Core Graphics 绘制圆角)

复制代码
extension UIImageView {
    func setCornerRadius(_ radius: CGFloat) {
        UIGraphicsBeginImageContextWithOptions(bounds.size, false, UIScreen.main.scale)
        let path = UIBezierPath(roundedRect: bounds, cornerRadius: radius)
        path.addClip()
        draw(bounds)
        let image = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        self.image = image
    }
}

五、数据持久化(37-41)

37. UserDefaults的使用限制与最佳实践

核心混淆点 :很多人滥用UserDefaults存储大量数据,不清楚它的使用限制。

详细解释

  • UserDefaults是 iOS 提供的轻量级数据持久化方案,用于存储少量的用户偏好设置。
  • 存储类型:只能存储基本数据类型(Int、Float、Double、Bool、String)、Array、Dictionary、Data、Date。不能存储自定义对象(除非将其转换为 Data)。
  • 使用限制:
    1. 不适合存储大量数据,因为UserDefaults会将所有数据加载到内存中,数据量大会导致内存占用过高。
    2. 不适合存储敏感数据,因为UserDefaults的数据是以明文形式存储在 plist 文件中的,容易被破解。
    3. 不适合存储频繁修改的数据,因为每次修改都会写入磁盘,性能较差。
  • 最佳实践:
    1. 只用于存储用户偏好设置(如主题、语言、是否首次启动)。
    2. 敏感数据使用 Keychain 存储。
    3. 大量数据使用 Core Data 或 SQLite 存储。

示例

复制代码
// 存储数据
UserDefaults.standard.set("张三", forKey: "username")
UserDefaults.standard.set(25, forKey: "age")
UserDefaults.standard.set(true, forKey: "isFirstLaunch")

// 读取数据
let username = UserDefaults.standard.string(forKey: "username") ?? ""
let age = UserDefaults.standard.integer(forKey: "age")
let isFirstLaunch = UserDefaults.standard.bool(forKey: "isFirstLaunch")

// 删除数据
UserDefaults.standard.removeObject(forKey: "username")

38. Keychain的使用方法与优势

核心混淆点 :很多人不清楚Keychain的作用,以及为什么它适合存储敏感数据。

详细解释

  • Keychain是 iOS 提供的安全存储方案,用于存储敏感数据(如密码、令牌、证书等)。
  • 优势:
    1. 数据加密存储,安全性高。
    2. 应用卸载后,Keychain 中的数据不会被删除,重新安装应用后可以恢复。
    3. 支持应用间共享数据(通过 Keychain Group)。
  • 注意:iOS 原生的 Keychain API 是基于 C 语言的,使用起来比较繁琐,通常使用第三方库如SAMKeychainKeychainSwift

示例(使用 KeychainSwift)

复制代码
import KeychainSwift

let keychain = KeychainSwift()

// 存储数据
keychain.set("password123", forKey: "password")
keychain.set("token123456", forKey: "token")

// 读取数据
let password = keychain.get("password") ?? ""
let token = keychain.get("token") ?? ""

// 删除数据
keychain.delete("password")

39. Core DataSQLite的区别

核心混淆点 :很多人分不清Core DataSQLite的关系,不清楚它们的优缺点。

详细解释

  • SQLite :是一个轻量级的关系型数据库,使用 SQL 语句进行操作。
    • 优点:性能高,灵活,支持复杂的 SQL 查询。
    • 缺点:需要手动管理数据库连接、表结构、数据映射,代码量大。
  • Core Data :是苹果提供的 ORM(对象关系映射)框架,底层可以使用 SQLite、XML、二进制文件作为存储方式。
    • 优点:面向对象,不需要写 SQL 语句,自动管理数据映射,支持数据缓存、undo/redo、数据验证等高级功能。
    • 缺点:学习曲线陡峭,性能比直接使用 SQLite 稍差,不适合复杂的查询。
  • 选择建议:
    • 简单的数据存储,使用UserDefaults
    • 敏感数据,使用Keychain
    • 大量结构化数据,且需要面向对象操作,使用Core Data
    • 大量结构化数据,且需要复杂的 SQL 查询,使用SQLite(通常使用第三方库如FMDB)。

示例(Core Data)

复制代码
// 假设已经创建了Person实体,有name和age两个属性
let context = persistentContainer.viewContext

// 创建对象
let person = Person(context: context)
person.name = "张三"
person.age = 25

// 保存数据
do {
    try context.save()
} catch {
    print("保存失败:\(error)")
}

// 查询数据
let fetchRequest: NSFetchRequest<Person> = Person.fetchRequest()
do {
    let persons = try context.fetch(fetchRequest)
    for person in persons {
        print("\(person.name!), \(person.age)")
    }
} catch {
    print("查询失败:\(error)")
}

40. FMDB的使用方法与优势

核心混淆点 :很多人不清楚FMDB的作用,以及它与原生 SQLite API 的区别。

详细解释

  • FMDB是对原生 SQLite API 的 Objective-C 封装,提供了面向对象的接口,简化了 SQLite 的使用。
  • 优势:
    1. 语法简洁,代码量少。
    2. 自动管理数据库连接,避免了手动管理的麻烦。
    3. 支持线程安全的操作(FMDatabaseQueue)。
  • 常用类:
    1. FMDatabase:表示一个 SQLite 数据库,用于执行 SQL 语句。
    2. FMResultSet:表示查询结果集。
    3. FMDatabaseQueue:用于多线程操作数据库,保证线程安全。

示例

复制代码
import FMDB

// 获取数据库路径
let documentsPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]
let dbPath = (documentsPath as NSString).appendingPathComponent("test.db")

// 创建数据库
let db = FMDatabase(path: dbPath)
if db.open() {
    print("数据库打开成功")
    
    // 创建表
    let createTableSQL = "CREATE TABLE IF NOT EXISTS person (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, age INTEGER)"
    if db.executeUpdate(createTableSQL, withArgumentsIn: []) {
        print("创建表成功")
    } else {
        print("创建表失败:\(db.lastError())")
    }
    
    // 插入数据
    let insertSQL = "INSERT INTO person (name, age) VALUES (?, ?)"
    if db.executeUpdate(insertSQL, withArgumentsIn: ["张三", 25]) {
        print("插入数据成功")
    } else {
        print("插入数据失败:\(db.lastError())")
    }
    
    // 查询数据
    let querySQL = "SELECT * FROM person"
    if let result = db.executeQuery(querySQL, withArgumentsIn: []) {
        while result.next() {
            let name = result.string(forColumn: "name") ?? ""
            let age = result.int(forColumn: "age")
            print("\(name), \(age)")
        }
    } else {
        print("查询数据失败:\(db.lastError())")
    }
    
    db.close()
} else {
    print("数据库打开失败")
}

41. 数据持久化方案对比

核心混淆点:很多人不知道在不同场景下应该选择哪种数据持久化方案。

详细解释

表格

方案 存储类型 存储大小 安全性 性能 使用场景
UserDefaults 基本数据类型、Array、Dictionary、Data、Date 小(KB 级) 低(明文存储) 用户偏好设置
Keychain Data 小(KB 级) 高(加密存储) 敏感数据(密码、令牌)
文件存储 任意类型(Data、String、UIImage 等) 中(MB 级) 大文件(图片、视频、文档)
Core Data 结构化数据 大(GB 级) 大量结构化数据,面向对象操作
SQLite(FMDB) 结构化数据 大(GB 级) 大量结构化数据,复杂查询

六、网络编程(42-45)

42. URLSession的使用方法与优势

核心混淆点 :很多人不清楚URLSessionNSURLConnection的区别,以及URLSession的优势。

详细解释

  • URLSession是 iOS 7 及以上版本引入的网络编程框架,替代了之前的NSURLConnection
  • 优势:
    1. 支持后台下载和上传,即使应用退出到后台,网络请求仍然可以继续。
    2. 支持配置不同的会话类型(默认、临时、后台)。
    3. 支持任务的暂停、恢复、取消。
    4. 支持 SSL/TLS 加密,安全性高。
    5. 性能更好,支持 HTTP/2。
  • 常用类:
    1. URLSession:表示一个会话,用于创建和管理网络任务。
    2. URLSessionTask:表示一个网络任务,有三个子类:URLSessionDataTask(数据任务)、URLSessionDownloadTask(下载任务)、URLSessionUploadTask(上传任务)。

示例(GET 请求)

复制代码
let url = URL(string: "https://jsonplaceholder.typicode.com/posts/1")!
let task = URLSession.shared.dataTask(with: url) { data, response, error in
    if let error = error {
        print("请求失败:\(error)")
        return
    }
    
    guard let data = data else {
        print("没有数据")
        return
    }
    
    // 解析数据
    if let json = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
        print("请求成功:\(json)")
    }
}
task.resume()

43. GETPOST请求的区别

核心混淆点:很多人只知道 GET 参数在 URL 中,POST 参数在请求体中,不清楚其他关键区别。

详细解释

表格

特性 GET POST
参数位置 URL 中 请求体中
参数长度 有限制(不同浏览器和服务器限制不同,一般为 2KB-8KB) 无限制
安全性 低(参数明文显示在 URL 中) 高(参数在请求体中,不会被缓存)
缓存 可以被浏览器缓存 默认不被浏览器缓存
书签 可以保存为书签 不可以保存为书签
后退 / 刷新 无害 会重新提交请求
幂等性 是(多次请求结果相同) 否(多次请求可能产生不同结果)
使用场景 获取数据 提交数据

44. HTTP 状态码的含义

核心混淆点:很多人只知道 200、404、500,不清楚其他常见状态码的含义。

详细解释

  • HTTP 状态码分为 5 类:
    1. 1xx(信息性):表示请求已接收,继续处理。
    2. 2xx(成功):表示请求已成功被接收、理解、接受。
    3. 3xx(重定向):表示需要进一步操作才能完成请求。
    4. 4xx(客户端错误):表示请求有语法错误或无法实现。
    5. 5xx(服务器错误):表示服务器在处理请求时发生了错误。
  • 常见状态码:
    • 200 OK:请求成功。
    • 201 Created:资源创建成功。
    • 301 Moved Permanently:永久重定向。
    • 302 Found:临时重定向。
    • 304 Not Modified:资源未修改,使用缓存。
    • 400 Bad Request:请求参数错误。
    • 401 Unauthorized:未授权。
    • 403 Forbidden:禁止访问。
    • 404 Not Found:资源不存在。
    • 500 Internal Server Error:服务器内部错误。
    • 502 Bad Gateway:网关错误。
    • 503 Service Unavailable:服务不可用。

45. 网络请求的缓存策略

核心混淆点 :很多人不清楚URLRequest.CachePolicy的各个选项的含义,以及如何设置缓存策略。

详细解释

  • URLRequest.CachePolicy用于指定网络请求的缓存策略,常见选项:
    1. useProtocolCachePolicy:默认策略,使用 HTTP 协议的缓存机制。
    2. reloadIgnoringLocalCacheData:忽略本地缓存,直接从网络加载数据。
    3. returnCacheDataElseLoad:如果本地有缓存,就使用缓存;否则从网络加载。
    4. returnCacheDataDontLoad:只使用本地缓存,如果没有缓存,请求失败。
  • HTTP 缓存机制:通过Cache-ControlExpiresLast-ModifiedETag等响应头来控制缓存。

示例

复制代码
var request = URLRequest(url: URL(string: "https://example.com")!)
// 设置缓存策略:忽略本地缓存,直接从网络加载
request.cachePolicy = .reloadIgnoringLocalCacheData
// 设置超时时间
request.timeoutInterval = 10

let task = URLSession.shared.dataTask(with: request) { data, response, error in
    // 处理响应
}
task.resume()

七、设计模式(46-50)

46. 单例模式的实现与优缺点

核心混淆点:很多人不清楚 Swift 中单例模式的正确实现方式,以及单例模式的优缺点。

详细解释

  • 单例模式是指一个类只能有一个实例,提供一个全局访问点。

  • Swift 中单例模式的正确实现方式:

    复制代码
    class Singleton {
        static let shared = Singleton()
        private init() {} // 私有化初始化方法,防止外部创建实例
    }
  • 这种实现方式是线程安全的,因为 Swift 的static let是懒加载的,并且在初始化时是原子操作。

  • 优点:

    1. 提供全局访问点,方便使用。
    2. 节省资源,避免重复创建实例。
  • 缺点:

    1. 耦合度高,不利于测试。
    2. 生命周期长,容易导致内存泄漏。
    3. 不支持继承。

47. 代理模式的实现与优缺点

核心混淆点:很多人不清楚代理模式的作用,以及如何正确实现代理模式。

详细解释

  • 代理模式是指一个对象将自己的部分职责委托给另一个对象来完成。
  • 实现步骤:
    1. 定义一个协议,声明代理需要实现的方法。
    2. 在委托类中声明一个weak的代理属性。
    3. 在适当的时机,调用代理的方法。
    4. 代理类遵守协议,实现协议中的方法。
  • 优点:
    1. 解耦,委托类和代理类之间通过协议通信,不需要知道对方的具体实现。
    2. 灵活,可以动态更换代理。
  • 缺点:
    1. 代码量增加,需要定义协议和实现代理方法。
    2. 调试困难,需要跟踪代理的调用流程。

示例

复制代码
// 定义协议
protocol DataManagerDelegate: AnyObject {
    func didFetchData(data: String)
}

// 委托类
class DataManager {
    weak var delegate: DataManagerDelegate?
    
    func fetchData() {
        DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
            self.delegate?.didFetchData(data: "网络数据")
        }
    }
}

// 代理类
class ViewController: UIViewController, DataManagerDelegate {
    let dataManager = DataManager()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        dataManager.delegate = self
        dataManager.fetchData()
    }
    
    func didFetchData(data: String) {
        DispatchQueue.main.async {
            print("更新UI:\(data)")
        }
    }
}

48. 观察者模式的实现与优缺点

核心混淆点:很多人不清楚观察者模式的作用,以及它与代理模式的区别。

详细解释

  • 观察者模式是指一个对象(被观察者)的状态发生变化时,会通知所有订阅了它的对象(观察者)。
  • 实现方式:
    1. NotificationCenter:系统提供的通知中心,实现了观察者模式。
    2. KVO(Key-Value Observing):键值观察,用于观察对象属性的变化。
  • 与代理模式的区别:
    1. 代理模式是一对一的通信,观察者模式是一对多的通信。
    2. 代理模式是同步的,观察者模式是异步的。
    3. 代理模式需要代理遵守协议,观察者模式不需要。

示例(NotificationCenter)

复制代码
// 发送通知
NotificationCenter.default.post(name: NSNotification.Name("DataUpdated"), object: nil, userInfo: ["data": "新数据"])

// 接收通知
NotificationCenter.default.addObserver(self, selector: #selector(dataUpdated(_:)), name: NSNotification.Name("DataUpdated"), object: nil)

@objc private func dataUpdated(_ notification: Notification) {
    if let data = notification.userInfo?["data"] as? String {
        print("数据更新:\(data)")
    }
}

// 移除通知
deinit {
    NotificationCenter.default.removeObserver(self)
}

49. MVC 与 MVVM 的区别

核心混淆点:很多人分不清 MVC 和 MVVM 的架构,不清楚它们的优缺点。

详细解释

  • MVC(Model-View-Controller)
    • Model:数据模型,负责存储和处理数据。
    • View:视图,负责显示数据和接收用户交互。
    • Controller:控制器,负责协调 Model 和 View,处理业务逻辑。
    • 缺点:Controller 过于臃肿,耦合度高,难以测试。
  • MVVM(Model-View-ViewModel)
    • Model:数据模型,负责存储和处理数据。
    • View:视图,负责显示数据和接收用户交互。
    • ViewModel:视图模型,负责处理业务逻辑,将 Model 的数据转换为 View 可以显示的数据。
    • 优点:
      1. 解耦,View 和 Model 之间通过 ViewModel 通信,不需要知道对方的存在。
      2. 可测试性好,ViewModel 可以单独测试。
      3. 代码复用性高,ViewModel 可以被多个 View 复用。
    • 缺点:
      1. 学习曲线陡峭,需要理解数据绑定。
      2. 简单的应用使用 MVVM 会增加代码量。

50. 工厂模式的实现与优缺点

核心混淆点:很多人不清楚工厂模式的作用,以及简单工厂、工厂方法、抽象工厂的区别。

详细解释

  • 工厂模式是一种创建型设计模式,用于封装对象的创建过程,将对象的创建和使用分离。
  • 三种工厂模式:
    1. 简单工厂模式:由一个工厂类根据参数创建不同的产品对象。
    2. 工厂方法模式:定义一个创建对象的接口,让子类决定实例化哪个类。
    3. 抽象工厂模式:提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。
  • 优点:
    1. 解耦,对象的创建和使用分离。
    2. 易于扩展,添加新产品时只需要修改工厂类或添加新的工厂子类。
  • 缺点:
    1. 增加了系统的复杂度,需要定义更多的类。
    2. 简单工厂模式违反了开闭原则,添加新产品时需要修改工厂类的代码。

示例(简单工厂模式)

复制代码
// 产品协议
protocol Shape {
    func draw()
}

// 具体产品
class Circle: Shape {
    func draw() {
        print("画圆形")
    }
}

class Rectangle: Shape {
    func draw() {
        print("画矩形")
    }
}

// 工厂类
class ShapeFactory {
    static func createShape(type: String) -> Shape? {
        switch type {
        case "circle":
            return Circle()
        case "rectangle":
            return Rectangle()
        default:
            return nil
        }
    }
}

// 使用
let circle = ShapeFactory.createShape(type: "circle")
circle?.draw() // 画圆形

let rectangle = ShapeFactory.createShape(type: "rectangle")
rectangle?.draw() // 画矩形
相关推荐
MonkeyKing2 小时前
iOS 屏幕旋转与多窗口适配原理:横竖屏控制、SizeClasses、iPad分屏终极适配
ios
MonkeyKing2 小时前
iOS 事件传递与响应链全解:hitTest、pointInside 底层流程
ios
人月神话Lee2 小时前
【图像处理】图像直方图——从"频率分布"到"智能决策"
ios·ai编程·图像识别
暗不需求2 小时前
从零实现一个 Vue Todos 任务清单:深入响应式编程与组合式 API
前端·vue.js·面试
Raink老师2 小时前
【AI面试临阵磨枪-90】Skill 之间如何调用、依赖、组合、编排?
面试·职场和发展
2501_916008892 小时前
全面解析常用Web前端开发工具:编辑器、调试工具、性能分析器与框架
android·前端·ios·小程序·uni-app·编辑器·iphone
Raink老师3 小时前
【AI面试临阵磨枪-92】Skill 开发规范:命名、文档、测试、日志、监控、告警?
java·面试·log4j
恋猫de小郭3 小时前
一个 Linux 调度器优化,让 Android 多耗 20% 的电,传音工程师如何发现问题?
android·前端·ios
Raink老师3 小时前
【AI面试临阵磨枪-93】Skill 性能优化:冷启动、并发、内存、IO、缓存?
人工智能·面试·性能优化