本章覆盖 iOS 数据层完整技术栈:URLSession 异步网络请求、Codable 序列化、UserDefaults 键值存储、SwiftData 本地数据库、URLSessionWebSocketTask 实时通信、AsyncImage/Kingfisher 图片缓存。
7.1 URLSession + async/await 网络请求
基础网络请求
// 原生 URLSession(无需第三方库)
func fetchArticle(id: String) async throws -> Article {
let url = URL(string: "https://api.example.com/articles/\(id)")!
// 自动在后台线程执行
let (data, response) = try await URLSession.shared.data(from: url)
// 状态码校验
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
throw NetworkError.serverError
}
return try JSONDecoder().decode(Article.self, from: data)
}
封装 NetworkClient
// 可复用的网络层封装
actor NetworkClient {
static let shared = NetworkClient()
private let session: URLSession
private let baseURL: URL
private let decoder: JSONDecoder
init(baseURL: String = "https://api.example.com") {
let config = URLSessionConfiguration.default
config.timeoutIntervalForRequest = 30
config.timeoutIntervalForResource = 300
config.requestCachePolicy = .returnCacheDataElseLoad
self.session = URLSession(configuration: config)
self.baseURL = URL(string: baseURL)!
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
decoder.keyDecodingStrategy = .convertFromSnakeCase
self.decoder = decoder
}
// GET 请求
func get<T: Decodable>(
_ path: String,
queryItems: [URLQueryItem]? = nil
) async throws -> T {
var components = URLComponents(url: baseURL.appendingPathComponent(path),
resolvingAgainstBaseURL: true)!
components.queryItems = queryItems
var request = URLRequest(url: components.url!)
request.setValue("application/json", forHTTPHeaderField: "Accept")
addAuthHeader(to: &request)
return try await execute(request)
}
// POST 请求
func post<T: Decodable, B: Encodable>(
_ path: String,
body: B
) async throws -> T {
var request = URLRequest(url: baseURL.appendingPathComponent(path))
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = try JSONEncoder().encode(body)
addAuthHeader(to: &request)
return try await execute(request)
}
// PUT / PATCH / DELETE 类似...
// 文件上传(multipart)
func upload<T: Decodable>(
_ path: String,
fileData: Data,
fileName: String,
mimeType: String
) async throws -> T {
var request = URLRequest(url: baseURL.appendingPathComponent(path))
request.httpMethod = "POST"
let boundary = "Boundary-\(UUID().uuidString)"
request.setValue("multipart/form-data; boundary=\(boundary)",
forHTTPHeaderField: "Content-Type")
var body = Data()
body.append("--\(boundary)\r\n")
body.append("Content-Disposition: form-data; name=\"file\"; filename=\"\(fileName)\"\r\n")
body.append("Content-Type: \(mimeType)\r\n\r\n")
body.append(fileData)
body.append("\r\n--\(boundary)--\r\n")
request.httpBody = body
return try await execute(request)
}
// 执行请求 + 带重试
private func execute<T: Decodable>(_ request: URLRequest,
retryCount: Int = 3) async throws -> T {
var lastError: Error?
for attempt in 1...retryCount {
do {
let (data, response) = try await session.data(for: request)
guard let httpResponse = response as? HTTPURLResponse else {
throw NetworkError.invalidResponse
}
switch httpResponse.statusCode {
case 200...299:
return try decoder.decode(T.self, from: data)
case 401:
throw NetworkError.unauthorized
case 404:
throw NetworkError.notFound
case 429:
// 限流:等待后重试
try await Task.sleep(for: .seconds(Double(attempt)))
continue
case 500...599:
throw NetworkError.serverError
default:
throw NetworkError.httpError(httpResponse.statusCode)
}
} catch is CancellationError {
throw NetworkError.cancelled
} catch let error as NetworkError {
throw error // 业务错误不重试
} catch {
lastError = error
if attempt < retryCount {
try await Task.sleep(for: .seconds(1))
}
}
}
throw lastError ?? NetworkError.unknown
}
private func addAuthHeader(to request: inout URLRequest) {
if let token = KeychainManager.shared.get("accessToken") {
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
}
}
}
7.2 Codable - JSON 序列化
// 基础 Codable 模型
struct Article: Codable, Identifiable {
let id: Int
let title: String
let content: String
let author: Author
let tags: [String]
let publishedAt: Date
var isLiked: Bool
var likeCount: Int
// 字段名映射(JSON snake_case → Swift camelCase)
// 使用 decoder.keyDecodingStrategy = .convertFromSnakeCase 时可省略
enum CodingKeys: String, CodingKey {
case id, title, content, author, tags
case publishedAt = "published_at"
case isLiked = "is_liked"
case likeCount = "like_count"
}
}
struct Author: Codable {
let id: Int
let name: String
let avatarURL: URL?
let bio: String?
}
// 通用分页响应包装
struct PagedResponse<T: Codable>: Codable {
let data: [T]
let total: Int
let page: Int
let perPage: Int
let totalPages: Int
var hasMore: Bool { page < totalPages }
}
// 自定义解码(处理复杂 JSON)
struct FlexibleArticle: Codable {
let id: Int
let title: String
let status: ArticleStatus
// 自定义解码:status 可能是 String 或 Int
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(Int.self, forKey: .id)
title = try container.decode(String.self, forKey: .title)
// 尝试解码为字符串
if let statusStr = try? container.decode(String.self, forKey: .status) {
status = ArticleStatus(rawValue: statusStr) ?? .draft
} else if let statusInt = try? container.decode(Int.self, forKey: .status) {
status = statusInt == 1 ? .published : .draft
} else {
status = .draft
}
}
}
enum ArticleStatus: String, Codable {
case draft = "draft"
case published = "published"
case archived = "archived"
}
7.3 UserDefaults - 键值存储
// 类型安全的 UserDefaults 封装
@propertyWrapper
struct UserDefault<Value> {
let key: String
let defaultValue: Value
var container: UserDefaults = .standard
var wrappedValue: Value {
get { container.object(forKey: key) as? Value ?? defaultValue }
set { container.set(newValue, forKey: key) }
}
var projectedValue: Self { self }
}
// 集中管理 App 配置
enum AppStorage {
@UserDefault("hasCompletedOnboarding", defaultValue: false)
static var hasCompletedOnboarding: Bool
@UserDefault("selectedTheme", defaultValue: "system")
static var selectedTheme: String
@UserDefault("notificationsEnabled", defaultValue: true)
static var notificationsEnabled: Bool
@UserDefault("lastSyncDate", defaultValue: Date.distantPast)
static var lastSyncDate: Date
static func reset() {
UserDefaults.standard.removePersistentDomain(
forName: Bundle.main.bundleIdentifier!
)
}
}
// 存储 Codable 对象
extension UserDefaults {
func setCodable<T: Codable>(_ value: T, forKey key: String) {
let data = try? JSONEncoder().encode(value)
set(data, forKey: key)
}
func codable<T: Codable>(forKey key: String) -> T? {
guard let data = data(forKey: key) else { return nil }
return try? JSONDecoder().decode(T.self, from: data)
}
}
// SwiftUI 中使用(@AppStorage = 对 UserDefaults 的封装)
struct SettingsView: View {
@AppStorage("isDarkMode") var isDarkMode = false
@AppStorage("fontSize") var fontSize = 16.0
@AppStorage("selectedLanguage") var selectedLanguage = "zh-Hans"
var body: some View {
Form {
Toggle("深色模式", isOn: $isDarkMode)
Slider(value: $fontSize, in: 12...24, step: 1) {
Text("字体大小:\(Int(fontSize))pt")
}
}
}
}
7.4 SwiftData - 现代本地数据库(iOS 17)
import SwiftData
// 定义数据模型
@Model
class Task {
var id: UUID
var title: String
var notes: String
var isCompleted: Bool
var priority: TaskPriority
var dueDate: Date?
var createdAt: Date
// 一对多关系
@Relationship(deleteRule: .cascade)
var subtasks: [SubTask] = []
// 多对一关系
var project: Project?
init(title: String, priority: TaskPriority = .medium) {
self.id = UUID()
self.title = title
self.notes = ""
self.isCompleted = false
self.priority = priority
self.createdAt = Date()
}
}
@Model
class SubTask {
var title: String
var isCompleted: Bool
var parentTask: Task?
init(title: String) {
self.title = title
self.isCompleted = false
}
}
enum TaskPriority: Int, Codable {
case low = 0
case medium = 1
case high = 2
var label: String {
switch self {
case .low: "低"
case .medium: "中"
case .high: "高"
}
}
}
// App 入口配置数据容器
@main
struct iOSDemosApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
// 自动创建 SQLite 数据库,支持迁移
.modelContainer(for: [Task.self, SubTask.self, Project.self])
}
}
// 视图中的 CRUD 操作
struct TaskListView: View {
@Environment(\.modelContext) private var context
// 查询:自动监听数据变化,实时更新
@Query(sort: \Task.createdAt, order: .reverse)
private var allTasks: [Task]
// 带过滤的查询
@Query(
filter: #Predicate<Task> { task in
task.priority == .high && !task.isCompleted
},
sort: \Task.dueDate
)
private var urgentTasks: [Task]
@State private var newTaskTitle = ""
var body: some View {
List {
Section("紧急任务") {
ForEach(urgentTasks) { TaskRow(task: $0) }
}
Section("所有任务(\(allTasks.count))") {
ForEach(allTasks) { task in
TaskRow(task: task)
.swipeActions {
Button(role: .destructive) {
context.delete(task) // 删除
} label: { Label("删除", systemImage: "trash") }
}
}
}
}
.toolbar {
Button("添加测试任务") {
let task = Task(title: "任务 \(allTasks.count + 1)", priority: .medium)
context.insert(task) // 插入
}
}
}
}
// 动态查询(响应用户筛选)
struct FilterableTaskView: View {
@State private var selectedPriority: TaskPriority?
@State private var showCompletedOnly = false
var body: some View {
DynamicTaskList(
priority: selectedPriority,
completedOnly: showCompletedOnly
)
}
}
struct DynamicTaskList: View {
@Environment(\.modelContext) private var context
let priority: TaskPriority?
let completedOnly: Bool
// 使用 FetchDescriptor 动态查询
var tasks: [Task] {
let predicate = #Predicate<Task> { task in
(priority == nil || task.priority == priority!)
&& (!completedOnly || task.isCompleted)
}
let descriptor = FetchDescriptor<Task>(
predicate: predicate,
sortBy: [SortDescriptor(\.createdAt, order: .reverse)]
)
return (try? context.fetch(descriptor)) ?? []
}
var body: some View {
List(tasks) { TaskRow(task: $0) }
}
}
7.5 WebSocket 实时通信
// WebSocket 管理器
@Observable
class WebSocketManager {
var messages: [ChatMessage] = []
var connectionState = ConnectionState.disconnected
private var task: URLSessionWebSocketTask?
private let session = URLSession.shared
enum ConnectionState { case connecting, connected, disconnected }
func connect(url: URL) {
connectionState = .connecting
task = session.webSocketTask(with: url)
task?.resume()
connectionState = .connected
receiveMessage()
}
private func receiveMessage() {
task?.receive { [weak self] result in
guard let self else { return }
switch result {
case .success(.string(let text)):
if let message = try? JSONDecoder().decode(ChatMessage.self,
from: Data(text.utf8)) {
DispatchQueue.main.async { self.messages.append(message) }
}
self.receiveMessage() // 继续监听
case .failure:
self.handleDisconnect()
default: break
}
}
}
func send(_ text: String) async throws {
try await task?.send(.string(text))
}
func disconnect() {
task?.cancel(with: .goingAway, reason: nil)
connectionState = .disconnected
}
private func handleDisconnect() {
connectionState = .disconnected
// 3秒后重连
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
self.connect(url: URL(string: "wss://api.example.com/ws")!)
}
}
}
7.6 图片缓存与加载
// 1. AsyncImage(内置,iOS 15+)
AsyncImage(url: URL(string: "https://example.com/photo.jpg")) { phase in
switch phase {
case .empty:
// 加载中
ZStack {
Rectangle().fill(.gray.opacity(0.1))
ProgressView()
}
case .success(let image):
image
.resizable()
.scaledToFill()
.transition(.opacity.animation(.easeIn(duration: 0.3)))
case .failure:
Image(systemName: "photo")
.foregroundStyle(.secondary)
@unknown default:
EmptyView()
}
}
.frame(width: 200, height: 200)
.clipShape(RoundedRectangle(cornerRadius: 12))
// 2. Kingfisher(第三方,功能更强)
// 添加:https://github.com/onevcat/Kingfisher
import Kingfisher
KFImage(URL(string: imageURLString))
.placeholder { ProgressView() }
.fade(duration: 0.3)
.cacheMemoryOnly() // 仅内存缓存
.resizable()
.scaledToFill()
.frame(width: 100, height: 100)
.clipShape(Circle())
// 3. 预加载图片
ImagePrefetcher(urls: imageURLs).start()
// 4. 缓存管理
KingfisherManager.shared.cache.clearMemoryCache()
KingfisherManager.shared.cache.clearDiskCache { print("磁盘缓存已清理") }
章节总结
| 技术 |
API/框架 |
适用场景 |
| HTTP 请求 |
URLSession + async/await |
所有网络通信 |
| JSON 序列化 |
Codable |
数据模型与 JSON 互转 |
| 键值存储 |
UserDefaults / @AppStorage |
偏好设置、轻量配置 |
| 本地数据库 |
SwiftData (iOS 17) |
复杂本地数据 |
| 安全存储 |
Keychain |
Token、密码等敏感数据 |
| 实时通信 |
URLSessionWebSocketTask |
聊天、实时推送 |
| 图片缓存 |
AsyncImage / Kingfisher |
网络图片展示 |
Demo 说明
| 文件 |
演示内容 |
URLSessionDemo.swift |
GET/POST/上传/重试 |
CodableDemo.swift |
模型解码、自定义解码 |
LocalStorageDemo.swift |
UserDefaults + @AppStorage + SwiftData CRUD |
WebSocketDemo.swift |
实时聊天室 + 断线重连 |
ImageCacheDemo.swift |
AsyncImage / Kingfisher 对比 |
📎 扩展内容补充
来源:第七章_性能优化.md
本章概述:学习 iOS 视图渲染性能、内存管理(ARC/weak/unowned)、async/await 并发优化,以及 Instruments 性能分析工具的使用。
7.1 SwiftUI 渲染性能优化
概念讲解
避免不必要的视图重建
// ❌ 反例:整个视图树都会重建
struct BadPerformanceView: View {
@State private var count = 0
var body: some View {
VStack {
// 这个计数改变时,下面所有视图都会重建
Button("增加 \(count)") { count += 1 }
// 这些昂贵视图本不需要重建
ExpensiveListView() // 每次都重建!
ComplexGridView() // 每次都重建!
}
}
}
// ✅ 正例:拆分子视图,精准更新
struct GoodPerformanceView: View {
@State private var count = 0
var body: some View {
VStack {
CounterButton(count: count, onTap: { count += 1 })
ExpensiveListView() // count 变化不会触发重建
ComplexGridView() // count 变化不会触发重建
}
}
}
// 子视图只在 count 变化时重建
struct CounterButton: View {
let count: Int // 值类型参数
let onTap: () -> Void
var body: some View {
Button("增加 \(count)", action: onTap)
}
}
equatable 精准控制重建
// EquatableView - 标记视图满足 Equatable,相同 input 不重建
struct ExpensiveView: View, Equatable {
let data: completeData
static func == (lhs: ExpensiveView, rhs: ExpensiveView) -> Bool {
lhs.data.id == rhs.data.id // 只比较 id
}
var body: some View {
// 复杂渲染...
}
}
// .equatable() Modifier
ExpensiveView(data: data).equatable()
LazyStack vs Stack
// ❌ 一次性渲染所有元素
VStack {
ForEach(0..<1000) { index in
ItemView(index: index) // 1000 个视图全部创建
}
}
// ✅ 懒加载,只渲染可见区域(类比 Flutter 的 ListView.builder)
LazyVStack {
ForEach(0..<1000) { index in
ItemView(index: index) // 按需创建
}
}
图片性能优化
// 使用合适的图片大小
Image("largePoster")
.resizable()
.scaledToFill()
.frame(width: 100, height: 100)
// ✅ 在适当时候降采样
.drawingGroup() // 离屏渲染合并为单张位图
// 预加载图片
let prefetcher = ImagePrefetcher(urls: imageURLs)
prefetcher.start()
7.2 内存管理与 ARC
概念讲解
ARC(自动引用计数)是 Swift 的内存管理机制,开发者需要理解循环引用问题。
强引用、弱引用、无主引用
class Teacher {
var name: String
var student: Student? // strong 强引用
init(name: String) { self.name = name }
deinit { print("Teacher \(name) 释放") }
}
class Student {
var name: String
weak var teacher: Teacher? // weak 弱引用(可选,teacher 释放后为 nil)
init(name: String) { self.name = name }
deinit { print("Student \(name) 释放") }
}
// ✅ 使用 weak 打破循环引用
var teacher: Teacher? = Teacher(name: "张老师")
var student: Student? = Student(name: "小明")
teacher?.student = student
student?.teacher = teacher // weak,不增加引用计数
teacher = nil // 释放 Teacher(引用计数 → 0)
student = nil // 释放 Student
// unowned - 无主引用(非可选,确保对象不会先释放)
class Card {
let number: String
unowned let customer: Customer // 必须在 customer 之前销毁
init(number: String, customer: Customer) {
self.number = number
self.customer = customer
}
}
闭包中的循环引用
// ❌ 闭包强捕获 self,造成循环引用
class ProfileViewModel {
var name = "Alice"
var updateAction: (() -> Void)?
func setup() {
// self → updateAction → self,循环引用!
updateAction = {
print(self.name) // 强引用
}
}
}
// ✅ 使用 [weak self] 打破循环引用
class ProfileViewModel {
var name = "Alice"
var updateAction: (() -> Void)?
func setup() {
updateAction = { [weak self] in
guard let self = self else { return }
print(self.name) // 弱引用,self 可能已释放
}
}
deinit { print("ViewModel 已释放") }
}
// ViewModel 中的网络请求(Task 捕获)
@Observable
class DataViewModel {
var data: [Item] = []
func loadData() {
Task { [weak self] in // 避免 Task 延长 self 生命周期
guard let self = self else { return }
let result = try await NetworkClient.shared.get("/items")
self.data = result
}
}
}
Instruments 内存分析
Xcode → Product → Profile (⌘I)
选择 Leaks 模板:
- Leaks:检测内存泄漏
- Allocations:追踪内存分配
- VM Tracker:虚拟内存使用
常见内存问题:
1. 循环引用(Retain Cycles)→ 使用 weak/unowned
2. 闭包强捕获 self → 使用 [weak self]
3. Timer 未销毁 → 在 deinit 中调用 timer.invalidate()
4. NotificationCenter 未移除观察者(Swift 可自动处理)
7.3 异步性能优化
概念讲解
Task Group - 并行任务
// 并行执行多个网络请求(类比 Flutter 的 Future.wait)
func loadDashboardData() async throws -> DashboardData {
async let userProfile = NetworkClient.shared.get("/profile")
async let notifications = NetworkClient.shared.get("/notifications")
async let statistics = NetworkClient.shared.get("/stats")
// 等待所有并行请求完成
let (profile, notifs, stats) = try await (userProfile, notifications, statistics)
return DashboardData(profile: profile, notifications: notifs, stats: stats)
}
// withTaskGroup - 动态数量的并行任务
func downloadImages(urls: [URL]) async -> [UIImage] {
var images: [UIImage] = []
await withTaskGroup(of: UIImage?.self) { group in
for url in urls {
group.addTask {
try? await URLSession.shared.downloadImage(from: url)
}
}
for await image in group {
if let image { images.append(image) }
}
}
return images
}
Actor - 线程安全的状态管理
// Actor 保证内部状态的线程安全(类比 Dart 的 Isolate)
actor DataCache {
private var cache: [String: Data] = [:]
func get(_ key: String) -> Data? {
cache[key]
}
func set(_ key: String, value: Data) {
cache[key] = value
}
func remove(_ key: String) {
cache.removeValue(forKey: key)
}
var count: Int { cache.count }
}
// 使用 actor(必须用 await 调用)
let cache = DataCache()
await cache.set("key", value: Data())
let data = await cache.get("key")
主线程保护
// @MainActor - 确保在主线程执行 UI 操作
@Observable
@MainActor
class HomeViewModel {
var articles: [Article] = []
var isLoading = false
func loadArticles() async {
isLoading = true
// 网络请求在后台线程执行
let data = try? await Task.detached(priority: .background) {
try await NetworkClient.shared.get("/articles")
}.value
// 回到主线程更新 UI(@MainActor 保证)
articles = data ?? []
isLoading = false
}
}
章节总结
| 优化点 |
技巧 |
效果 |
| 视图重建 |
拆分子视图/equatable |
减少不必要渲染 |
| 列表性能 |
LazyVStack/LazyHStack |
按需加载 |
| 内存泄漏 |
weak self/打破循环引用 |
防止内存溢出 |
| 并行请求 |
async let / withTaskGroup |
减少等待时间 |
| 线程安全 |
Actor / @MainActor |
避免竞争条件 |
Demo 说明
| Demo 文件 |
演示内容 |
RenderPerformanceDemo.swift |
视图拆分 / LazyStack 性能对比 |
MemoryManagementDemo.swift |
ARC / 循环引用检测与修复 |
AsyncOptimizationDemo.swift |
并行请求 / Actor / @MainActor |