一、Swift 语言基础与进阶(1-10)
1. let与var的本质区别(不是 "常量 / 变量" 这么简单)
核心混淆点 :很多人认为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. struct与class的核心区别(面试必问)
核心混淆点:很多人只知道 "值类型 / 引用类型",但忽略了继承、内存管理、方法派发、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 let、guard let、switch、nil 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. guard与if的使用场景区别
核心混淆点 :很多人认为guard只是if的另一种写法,不清楚它们的设计目的和最佳实践。
详细解释:
if:用于条件分支 ,当条件满足时执行某个代码块,否则执行else块。适合处理 "两种情况都需要执行逻辑" 的场景。guard:用于提前退出 (Early Exit),当条件不满足时立即退出当前作用域(函数、循环、代码块)。它的设计目的是减少嵌套,提高代码可读性,将错误处理放在最前面。- 关键区别:
guard的else块必须包含退出语句(return、break、continue、throw),且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. protocol的class-only限制与AnyObject
核心混淆点 :很多人不清楚protocol: AnyObject的作用,以及它与class协议的区别。
详细解释:
- 默认情况下,协议可以被
class、struct、enum遵守。 - 如果协议声明为
protocol P: AnyObject,则该协议只能被类遵守,不能被值类型遵守。 - 作用:
- 告诉编译器该协议的实现者一定是引用类型,可以使用
===比较引用相等性。 - 可以在协议中声明
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 扩展可以:
- 添加计算属性(不能添加存储属性)
- 添加方法、初始化器、下标
- 遵守协议
- 为泛型类型添加条件扩展
- Swift 扩展不能 :
- 添加存储属性(会改变对象的内存布局)
- 重写已有方法(与 Objective-C 分类不同)
- 添加指定初始化器(只能添加便利初始化器)
- 与 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. throws、try、do-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参数的工作流程:- 函数调用时,将参数的值拷贝到一个临时变量中。
- 函数内部修改这个临时变量。
- 函数返回时,将临时变量的值拷贝回原始变量。
- 这与真正的传引用不同:
inout参数不能是let常量,因为需要修改它的值。inout参数不能有默认值。- 函数内部对
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 的内存管理机制,它在编译期 自动插入
retain、release、autorelease代码,不需要开发者手动管理引用计数。 - 工作原理:每个对象都有一个引用计数,当有一个新的强引用指向对象时,引用计数 + 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)的区别
核心混淆点 :很多人分不清weak和unowned的使用场景,以及它们的区别。
详细解释:
表格
| 引用类型 | 引用计数变化 | 对象销毁时行为 | 是否为可选型 | 使用场景 |
|---|---|---|---|---|
| strong | +1 | 不会自动置为 nil | 否 | 默认引用类型,对象生命周期由它控制 |
| weak | 不 + 1 | 自动置为 nil | 是 | 两个对象互相引用,其中一个生命周期更短 |
| unowned | 不 + 1 | 不会自动置为 nil,访问已销毁对象会崩溃 | 否 | 两个对象互相引用,生命周期相同或更长 |
- 关键区别:
weak是安全的,访问已销毁对象会得到nil;unowned是不安全的,访问已销毁对象会触发运行时崩溃(类似!强制解包)。 - 最佳实践:优先使用
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,内存泄漏。
- 常见场景:
- delegate 模式:如果 delegate 是强引用,会导致控制器和代理对象互相持有。
- 闭包:如果闭包捕获了 self,而 self 又持有闭包,会形成循环引用。
- 定时器:
Timer会强引用它的 target,而 target 如果又持有Timer,会形成循环引用。
- 解决方法:
- 使用
weak或unowned打破循环引用。 - 对于闭包,使用捕获列表
[weak self]或[unowned self]。 - 对于定时器,在合适的时机调用
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可以显著降低内存峰值:- 循环中创建大量临时对象。
- 处理大文件、大图片等占用大量内存的操作。
- 注意: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的作用:- 释放手动管理的资源(如文件句柄、网络连接、Core Foundation 对象)。
- 移除观察者(如 KVO、NotificationCenter)。
- 停止定时器。
deinit的限制:- 不能接受参数,不能有返回值。
- 不能主动调用
deinit,只能由系统自动调用。 - 子类的
deinit会自动调用父类的deinit。 deinit中不能使用weak或unowned引用的 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 工作原理:
- 当值类型被赋值时,并不会立即拷贝,而是共享同一块内存。
- 当其中一个变量修改值时,才会真正进行拷贝,生成独立的副本。
- 好处:减少不必要的内存拷贝,提高性能,特别是对于大的数组和字典。
- 注意:自定义的 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。
- 三个桥接关键字的区别:
__bridge:仅转换类型,不改变引用计数。Foundation 转 Core Foundation 时,不需要手动释放;Core Foundation 转 Foundation 时,也不需要手动释放。__bridge_retained(也叫CFBridgingRetain):将 Foundation 对象转换为 Core Foundation 对象,同时引用计数 + 1,需要手动调用CFRelease释放。__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,无法被销毁,导致内存无法释放。
- 常见的内存泄漏检测方法:
- Instruments Leaks:Xcode 自带的内存泄漏检测工具,可以精确找到泄漏的对象和代码位置。
- Debug Memory Graph:Xcode 8 及以上版本提供的内存图调试工具,可以直观地查看对象的引用关系,快速找到循环引用。
- 手动打印 deinit :在类的
deinit方法中打印日志,看对象是否被正确销毁。 - MLeaksFinder:腾讯开源的内存泄漏检测工具,可以在运行时自动检测内存泄漏,并弹出提示。
使用 Debug Memory Graph 的步骤:
- 运行项目,进入可能存在内存泄漏的页面。
- 点击 Xcode 导航栏中的 "Debug Memory Graph" 按钮(三个圆圈的图标)。
- 在左侧的对象列表中,找到应该被销毁但仍然存在的对象。
- 查看右侧的引用关系图,找到导致循环引用的强引用。
三、多线程与 GCD(19-27)
19. 进程与线程的区别
核心混淆点:很多人分不清进程和线程的概念,不清楚它们的关系。
详细解释:
- 进程:是操作系统分配资源的基本单位,每个进程都有独立的内存空间、文件句柄、网络连接等资源。一个应用程序就是一个进程。
- 线程:是 CPU 调度的基本单位,一个进程可以包含多个线程,这些线程共享进程的资源(内存、文件句柄等)。
- 关键区别:
- 资源开销:进程的创建和销毁开销大,线程的创建和销毁开销小。
- 通信:进程间通信复杂(管道、消息队列、共享内存等),线程间通信简单(共享内存)。
- 安全性:进程之间相互独立,一个进程崩溃不会影响其他进程;线程之间共享资源,一个线程崩溃可能导致整个进程崩溃。
- 在 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) :
- 是系统提供的串行队列,所有任务都在主线程执行。
- 负责处理 UI 事件和更新 UI,所有 UI 操作必须在主队列执行。
- 优先级最高。
- 全局队列(Global Queue) :
- 是系统提供的并发队列,有四个优先级:
userInteractive、userInitiated、utility、background。 - 任务在子线程执行,适合执行耗时操作(如网络请求、数据处理)。
- 系统会根据优先级自动调整线程数量。
- 是系统提供的并发队列,有四个优先级:
示例:
// 耗时操作放在全局队列
DispatchQueue.global().async {
// 模拟网络请求
sleep(2)
let data = "网络数据"
// UI更新必须放在主队列
DispatchQueue.main.async {
print("更新UI:\(data)")
}
}
23. DispatchGroup的使用方法与场景
核心混淆点 :很多人不清楚DispatchGroup的作用,以及如何等待多个异步任务完成。
详细解释:
DispatchGroup用于管理一组异步任务,可以等待所有任务完成后再执行某个操作。- 常用方法:
enter():标记一个任务开始,组内任务数 + 1。leave():标记一个任务结束,组内任务数 - 1。wait():阻塞当前线程,直到所有任务完成。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(信号量)是一种同步机制,用于控制同时访问共享资源的线程数量。- 常用方法:
init(value:):初始化信号量,value 表示最大并发数。wait():信号量值 - 1,如果值小于 0,阻塞当前线程。signal():信号量值 + 1,如果值大于等于 0,唤醒一个等待的线程。
- 使用场景:
- 控制并发数(如限制同时最多 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 封装,更面向对象,提供了更多高级功能:- 可以设置任务的优先级。
- 可以添加任务之间的依赖关系。
- 可以取消任务。
- 可以设置最大并发数。
- 选择建议:
- 简单的异步任务,使用 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。
@synchronized:Objective-C 中的同步锁,Swift 中没有直接对应的语法,可以用objc_sync_enter和objc_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. frame与bounds的区别
核心混淆点 :很多人分不清frame和bounds的坐标系,不清楚它们的作用。
详细解释:
- frame :是视图在父视图坐标系中的位置和大小。它的原点是父视图的左上角。
- bounds :是视图在自身坐标系中的位置和大小。它的原点是自身的左上角。
- 关键区别:
- 修改
frame会改变视图在父视图中的位置。 - 修改
bounds会改变视图的可见区域,不会改变视图在父视图中的位置。 - 当视图发生旋转时,
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的生命周期方法
核心混淆点 :很多人分不清UIView和UIViewController的生命周期方法,不清楚它们的调用顺序。
详细解释:
UIView的生命周期方法:init(frame:):通过代码创建视图时调用。init(coder:):通过 xib 或 storyboard 创建视图时调用。awakeFromNib():从 xib 或 storyboard 加载完成后调用,此时视图的 frame 还没有确定。layoutSubviews():视图的 frame 发生变化时调用,用于布局子视图。draw(_ rect:):绘制视图内容时调用,不要手动调用,通过setNeedsDisplay()触发。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的生命周期方法:init(nibName:bundle:):初始化控制器。viewDidLoad():视图加载完成后调用,此时视图的 frame 还没有确定,适合做一次性的初始化工作。viewWillAppear(_:):视图即将显示时调用,每次显示都会调用。viewDidAppear(_:):视图已经显示时调用。viewWillLayoutSubviews():视图即将布局子视图时调用。viewDidLayoutSubviews():视图已经布局子视图时调用,此时视图的 frame 已经确定。viewWillDisappear(_:):视图即将消失时调用。viewDidDisappear(_:):视图已经消失时调用。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. UIButton的addTarget与closure的区别
核心混淆点 :很多人不清楚 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 进行复用。- 重用机制的原理:
- 当
UITableView需要显示一个 cell 时,会调用dequeueReusableCell(withIdentifier:for:)方法。 - 如果重用池中有可用的 cell,就取出这个 cell,调用
prepareForReuse()方法重置 cell 的状态。 - 如果重用池中没有可用的 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 Layout与Frame布局的区别
核心混淆点:很多人不清楚 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 及以上版本引入的布局容器,用于管理一组子视图的布局,自动为子视图添加约束。- 优势:
- 减少约束的数量,简化布局代码。
- 动态添加、删除、隐藏子视图时,自动更新布局。
- 支持水平和垂直布局,支持不同的对齐方式和分布方式。
- 常用属性:
axis:布局方向(水平 / 垂直)。alignment:子视图的对齐方式。distribution:子视图的分布方式。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. CALayer与UIView的区别
核心混淆点 :很多人分不清CALayer和UIView的关系,不清楚它们的职责分工。
详细解释:
UIView:是 UIKit 中的视图类,负责处理用户交互事件(触摸、手势等),是CALayer的管理器。CALayer:是 Core Animation 中的图层类,负责绘制内容和动画,不处理用户交互事件。- 关系:每个
UIView都有一个对应的CALayer,UIView的 frame、bounds、backgroundColor 等属性实际上都是转发给CALayer的。 - 关键区别:
UIView继承自UIResponder,可以处理用户交互;CALayer继承自NSObject,不能处理用户交互。UIView的动画是基于CALayer的动画实现的。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 在当前屏幕缓冲区之外,开辟一个新的缓冲区进行渲染操作。
- 产生原因:当视图的某些属性需要提前渲染时,就会触发离屏渲染。常见的触发条件:
shouldRasterize = true(光栅化)。mask(遮罩)。shadow(阴影)。cornerRadius + masksToBounds = true(iOS 13 及以上版本优化了圆角,不会触发离屏渲染)。allowsEdgeAntialiasing = true(边缘抗锯齿)。
- 性能问题:离屏渲染需要多次切换上下文(从屏幕缓冲区到离屏缓冲区,再回到屏幕缓冲区),会消耗大量的 GPU 资源,导致帧率下降。
- 优化方法:
- 尽量避免使用触发离屏渲染的属性。
- 对于静态视图,可以使用
shouldRasterize = true将渲染结果缓存起来。 - 使用 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)。
- 使用限制:
- 不适合存储大量数据,因为
UserDefaults会将所有数据加载到内存中,数据量大会导致内存占用过高。 - 不适合存储敏感数据,因为
UserDefaults的数据是以明文形式存储在 plist 文件中的,容易被破解。 - 不适合存储频繁修改的数据,因为每次修改都会写入磁盘,性能较差。
- 不适合存储大量数据,因为
- 最佳实践:
- 只用于存储用户偏好设置(如主题、语言、是否首次启动)。
- 敏感数据使用 Keychain 存储。
- 大量数据使用 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 提供的安全存储方案,用于存储敏感数据(如密码、令牌、证书等)。- 优势:
- 数据加密存储,安全性高。
- 应用卸载后,Keychain 中的数据不会被删除,重新安装应用后可以恢复。
- 支持应用间共享数据(通过 Keychain Group)。
- 注意:iOS 原生的 Keychain API 是基于 C 语言的,使用起来比较繁琐,通常使用第三方库如
SAMKeychain或KeychainSwift。
示例(使用 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 Data与SQLite的区别
核心混淆点 :很多人分不清Core Data和SQLite的关系,不清楚它们的优缺点。
详细解释:
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 的使用。- 优势:
- 语法简洁,代码量少。
- 自动管理数据库连接,避免了手动管理的麻烦。
- 支持线程安全的操作(
FMDatabaseQueue)。
- 常用类:
FMDatabase:表示一个 SQLite 数据库,用于执行 SQL 语句。FMResultSet:表示查询结果集。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的使用方法与优势
核心混淆点 :很多人不清楚URLSession与NSURLConnection的区别,以及URLSession的优势。
详细解释:
URLSession是 iOS 7 及以上版本引入的网络编程框架,替代了之前的NSURLConnection。- 优势:
- 支持后台下载和上传,即使应用退出到后台,网络请求仍然可以继续。
- 支持配置不同的会话类型(默认、临时、后台)。
- 支持任务的暂停、恢复、取消。
- 支持 SSL/TLS 加密,安全性高。
- 性能更好,支持 HTTP/2。
- 常用类:
URLSession:表示一个会话,用于创建和管理网络任务。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. GET与POST请求的区别
核心混淆点:很多人只知道 GET 参数在 URL 中,POST 参数在请求体中,不清楚其他关键区别。
详细解释:
表格
| 特性 | GET | POST |
|---|---|---|
| 参数位置 | URL 中 | 请求体中 |
| 参数长度 | 有限制(不同浏览器和服务器限制不同,一般为 2KB-8KB) | 无限制 |
| 安全性 | 低(参数明文显示在 URL 中) | 高(参数在请求体中,不会被缓存) |
| 缓存 | 可以被浏览器缓存 | 默认不被浏览器缓存 |
| 书签 | 可以保存为书签 | 不可以保存为书签 |
| 后退 / 刷新 | 无害 | 会重新提交请求 |
| 幂等性 | 是(多次请求结果相同) | 否(多次请求可能产生不同结果) |
| 使用场景 | 获取数据 | 提交数据 |
44. HTTP 状态码的含义
核心混淆点:很多人只知道 200、404、500,不清楚其他常见状态码的含义。
详细解释:
- HTTP 状态码分为 5 类:
- 1xx(信息性):表示请求已接收,继续处理。
- 2xx(成功):表示请求已成功被接收、理解、接受。
- 3xx(重定向):表示需要进一步操作才能完成请求。
- 4xx(客户端错误):表示请求有语法错误或无法实现。
- 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用于指定网络请求的缓存策略,常见选项:useProtocolCachePolicy:默认策略,使用 HTTP 协议的缓存机制。reloadIgnoringLocalCacheData:忽略本地缓存,直接从网络加载数据。returnCacheDataElseLoad:如果本地有缓存,就使用缓存;否则从网络加载。returnCacheDataDontLoad:只使用本地缓存,如果没有缓存,请求失败。
- HTTP 缓存机制:通过
Cache-Control、Expires、Last-Modified、ETag等响应头来控制缓存。
示例:
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是懒加载的,并且在初始化时是原子操作。 -
优点:
- 提供全局访问点,方便使用。
- 节省资源,避免重复创建实例。
-
缺点:
- 耦合度高,不利于测试。
- 生命周期长,容易导致内存泄漏。
- 不支持继承。
47. 代理模式的实现与优缺点
核心混淆点:很多人不清楚代理模式的作用,以及如何正确实现代理模式。
详细解释:
- 代理模式是指一个对象将自己的部分职责委托给另一个对象来完成。
- 实现步骤:
- 定义一个协议,声明代理需要实现的方法。
- 在委托类中声明一个
weak的代理属性。 - 在适当的时机,调用代理的方法。
- 代理类遵守协议,实现协议中的方法。
- 优点:
- 解耦,委托类和代理类之间通过协议通信,不需要知道对方的具体实现。
- 灵活,可以动态更换代理。
- 缺点:
- 代码量增加,需要定义协议和实现代理方法。
- 调试困难,需要跟踪代理的调用流程。
示例:
// 定义协议
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. 观察者模式的实现与优缺点
核心混淆点:很多人不清楚观察者模式的作用,以及它与代理模式的区别。
详细解释:
- 观察者模式是指一个对象(被观察者)的状态发生变化时,会通知所有订阅了它的对象(观察者)。
- 实现方式:
NotificationCenter:系统提供的通知中心,实现了观察者模式。- KVO(Key-Value Observing):键值观察,用于观察对象属性的变化。
- 与代理模式的区别:
- 代理模式是一对一的通信,观察者模式是一对多的通信。
- 代理模式是同步的,观察者模式是异步的。
- 代理模式需要代理遵守协议,观察者模式不需要。
示例(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 可以显示的数据。
- 优点:
- 解耦,View 和 Model 之间通过 ViewModel 通信,不需要知道对方的存在。
- 可测试性好,ViewModel 可以单独测试。
- 代码复用性高,ViewModel 可以被多个 View 复用。
- 缺点:
- 学习曲线陡峭,需要理解数据绑定。
- 简单的应用使用 MVVM 会增加代码量。
50. 工厂模式的实现与优缺点
核心混淆点:很多人不清楚工厂模式的作用,以及简单工厂、工厂方法、抽象工厂的区别。
详细解释:
- 工厂模式是一种创建型设计模式,用于封装对象的创建过程,将对象的创建和使用分离。
- 三种工厂模式:
- 简单工厂模式:由一个工厂类根据参数创建不同的产品对象。
- 工厂方法模式:定义一个创建对象的接口,让子类决定实例化哪个类。
- 抽象工厂模式:提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。
- 优点:
- 解耦,对象的创建和使用分离。
- 易于扩展,添加新产品时只需要修改工厂类或添加新的工厂子类。
- 缺点:
- 增加了系统的复杂度,需要定义更多的类。
- 简单工厂模式违反了开闭原则,添加新产品时需要修改工厂类的代码。
示例(简单工厂模式):
// 产品协议
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() // 画矩形