第十一章:iOS性能优化、测试与发布

本章覆盖 iOS 开发的最后一公里:SwiftUI 渲染性能优化、ARC 内存管理、Instruments 性能分析、XCTest 单元/UI 测试、TestFlight 内测发布和 App Store 发布流程。


11.1 SwiftUI 渲染性能优化

视图重建原理

swift 复制代码
// SwiftUI 重建规则:
// 当 @State / @Observable / @Binding 变化时,持有这些状态的 View.body 重新执行
// 子视图:若参数(让 Equatable)没有变化,则不重建

// ❌ 不良写法:所有子视图随父视图重建
struct PoorPerformanceView: View {
    @State private var counter = 0
    
    var body: some View {
        VStack {
            Text("计数:\(counter)")
            Button("增加") { counter += 1 }
            
            // 每次 counter 变化,以下全部重建(即使数据没变)
            ExpensiveListView(data: staticData)   // 不必要的重建!
            ComplexGridView(items: staticItems)    // 不必要的重建!
        }
    }
}

// ✅ 拆分子视图:独立状态,精准更新
struct BetterPerformanceView: View {
    var body: some View {
        VStack {
            CounterSectionView()                 // 只有这个视图有 @State
            ExpensiveListView(data: staticData)  // 不受 counter 影响
            ComplexGridView(items: staticItems)  // 不受 counter 影响
        }
    }
}

struct CounterSectionView: View {
    @State private var counter = 0
    var body: some View {
        Text("计数:\(counter)")
        Button("增加") { counter += 1 }
    }
}

使用 Equatable

swift 复制代码
// 遵循 Equatable + .equatable() 防止不必要重建
struct ArticleCard: View, Equatable {
    let article: Article
    
    // 只比较 id 和 isBookmarked,减少不必要重建
    static func == (lhs: ArticleCard, rhs: ArticleCard) -> Bool {
        lhs.article.id == rhs.article.id
        && lhs.article.isBookmarked == rhs.article.isBookmarked
    }
    
    var body: some View {
        // 复杂视图内容...
    }
}

// 在父视图中使用
ArticleCard(article: article).equatable()

LazyStack 与 List

swift 复制代码
// ❌ Stack(把所有视图一次性渲染)
ScrollView {
    VStack {
        ForEach(0..<1000) { index in
            HeavyRowView(index: index)  // 1000个全部创建!
        }
    }
}

// ✅ LazyVStack(只渲染可见区域)
ScrollView {
    LazyVStack(spacing: 8) {
        ForEach(0..<1000) { index in
            HeavyRowView(index: index)  // 按需创建,滚动时回收
        }
    }
}

// ✅ List(更高性能,支持滑动交互)
List(items) { item in
    ItemRow(item: item)
}

// ✅ 图片使用 drawingGroup(复杂视图合并渲染)
ComplexView()
    .drawingGroup()  // 离屏渲染为单个位图,减少 GPU 合成层

避免频繁创建视图

swift 复制代码
// ❌ 在 body 中创建闭包/对象(每次渲染都创建)
struct BadView: View {
    var body: some View {
        let formatter = DateFormatter()   // 每次 body 执行都创建!
        formatter.dateStyle = .medium
        return Text(formatter.string(from: Date()))
    }
}

// ✅ 在类型层面定义静态实例
struct GoodView: View {
    private static let dateFormatter: DateFormatter = {
        let f = DateFormatter()
        f.dateStyle = .medium
        return f
    }()
    
    var body: some View {
        Text(Self.dateFormatter.string(from: Date()))
    }
}

11.2 内存管理

ARC 与循环引用

swift 复制代码
// ARC:自动引用计数,引用计数 = 0 时释放内存

// ❌ 循环引用(内存泄漏)
class Owner {
    var pet: Pet?
    deinit { print("Owner 释放") }
}

class Pet {
    var owner: Owner?   // 强引用 → 循环!
    deinit { print("Pet 释放") }
}

var owner: Owner? = Owner()
var pet: Pet? = Pet()
owner?.pet = pet
pet?.owner = owner

owner = nil   // Owner 不会释放!
pet = nil     // Pet 也不会释放!

// ✅ 使用 weak 打破循环
class Pet {
    weak var owner: Owner?   // 弱引用:不增加引用计数
    deinit { print("Pet 释放") }
}

// ✅ ViewModel 中的异步任务
@Observable
class DataViewModel {
    var items: [Item] = []
    
    func load() {
        Task { [weak self] in   // 避免 Task 延长 self 生命周期
            guard let self else { return }
            let items = try? await DataService.shared.fetchItems()
            self.items = items ?? []
        }
    }
}

Instruments 内存分析

复制代码
步骤:
1. Xcode → Product → Profile(⌘I)
2. 选择 "Leaks" 模板
3. 运行 App,操作各种功能
4. 观察:
   - Leaks:检测"红色!"= 内存泄漏点
   - Allocations:跟踪内存分配,找峰值
   - VM Tracker:查看虚拟内存占用

常见泄漏原因:
① 循环引用(class A → class B → class A)
② 闭包强捕获 self(忘记 [weak self])
③ Timer 未 invalidate
④ NotificationCenter 观察者未移除(Swift 可自动处理)
⑤ Delegate 用 strong 而非 weak

11.3 启动性能优化

swift 复制代码
// 测量启动时间:Xcode → Product → Profile → "App Launch"

// ① App 入口轻量化
@main
struct iOSDemosApp: App {
    init() {
        // ✅ 只做必要的同步初始化(< 400ms)
        AppConfig.configure()
        Logger.general.info("App 启动")
        
        // ❌ 避免在 init() 中做:
        // - 网络请求
        // - 大文件读取
        // - 复杂数据库初始化
        // - 第三方 SDK 的重型初始化
    }
    
    var body: some Scene {
        WindowGroup {
            ContentView()
                .task {
                    // ② App 显示后,并行初始化其他模块
                    await AppInitializer.shared.initialize()
                }
        }
    }
}

// ② 并行初始化(减少总等待时间)
actor AppInitializer {
    static let shared = AppInitializer()
    private var initialized = false
    
    func initialize() async {
        guard !initialized else { return }
        initialized = true
        
        // 三个初始化任务并行执行
        async let authTask: Void = AuthManager.shared.restoreSession()
        async let configTask: Void = RemoteConfig.shared.fetch()
        async let analyticsTask: Void = AnalyticsManager.shared.setup()
        
        _ = await (authTask, configTask, analyticsTask)
    }
}

// ③ 懒加载重型资源
class ResourceManager {
    // 不在初始化时加载,首次访问时才加载
    lazy var largeDataSet: [ComplexModel] = {
        loadLargeDataSet()
    }()
}

11.4 XCTest 单元测试

swift 复制代码
import XCTest
@testable import iOS_demos   // 访问内部类型

// ViewModel 单元测试
@MainActor
final class ArticleViewModelTests: XCTestCase {
    var viewModel: ArticleListViewModel!
    var mockRepository: MockArticleRepository!
    
    override func setUp() async throws {
        mockRepository = MockArticleRepository()
        viewModel = ArticleListViewModel(repository: mockRepository)
    }
    
    override func tearDown() async throws {
        viewModel = nil
        mockRepository = nil
    }
    
    // 测试:加载成功
    func testLoadArticlesSuccess() async throws {
        mockRepository.mockArticles = Article.mocks(count: 10)
        
        await viewModel.loadFirstPage()
        
        XCTAssertFalse(viewModel.isLoading, "加载完成后 isLoading 应为 false")
        XCTAssertNil(viewModel.errorMessage, "成功时不应有错误信息")
        XCTAssertEqual(viewModel.articles.count, 10)
        XCTAssertEqual(viewModel.articles.first?.title, "Article 1")
    }
    
    // 测试:加载失败
    func testLoadArticlesFailure() async {
        mockRepository.shouldThrowError = NetworkError.serverError
        
        await viewModel.loadFirstPage()
        
        XCTAssertFalse(viewModel.isLoading)
        XCTAssertNotNil(viewModel.errorMessage)
        XCTAssertTrue(viewModel.articles.isEmpty)
    }
    
    // 测试:搜索过滤
    func testSearchFiltering() async {
        mockRepository.mockArticles = [
            Article(title: "Swift 入门"),
            Article(title: "SwiftUI 进阶"),
            Article(title: "Objective-C 基础"),
        ]
        await viewModel.loadFirstPage()
        
        viewModel.searchText = "Swift"
        
        XCTAssertEqual(viewModel.filteredArticles.count, 2)
        XCTAssertTrue(viewModel.filteredArticles.allSatisfy {
            $0.title.contains("Swift")
        })
    }
    
    // 测试:异步加载更多
    func testLoadMore() async {
        mockRepository.mockArticles = Article.mocks(count: 20)
        await viewModel.loadFirstPage()
        
        mockRepository.mockArticles = Article.mocks(count: 10)
        await viewModel.loadMore()
        
        XCTAssertEqual(viewModel.articles.count, 30)
        XCTAssertEqual(viewModel.currentPage, 2)
    }
    
    // 性能测试
    func testLoadingPerformance() {
        measure {
            let exp = expectation(description: "loading")
            Task {
                await viewModel.loadFirstPage()
                exp.fulfill()
            }
            wait(for: [exp], timeout: 5)
        }
    }
}

// Mock 依赖
final class MockArticleRepository: ArticleRepositoryProtocol {
    var mockArticles: [ArticleEntity] = []
    var shouldThrowError: Error?
    var searchCallCount = 0
    
    func fetchAll(page: Int, pageSize: Int) async throws -> [ArticleEntity] {
        if let error = shouldThrowError { throw error }
        return mockArticles
    }
    
    func fetchById(_ id: String) async throws -> ArticleEntity {
        guard let article = mockArticles.first(where: { $0.id == id }) else {
            throw NetworkError.notFound
        }
        return article
    }
    
    func bookmark(_ id: String) async throws {}
    
    func search(query: String) async throws -> [ArticleEntity] {
        searchCallCount += 1
        return mockArticles.filter { $0.title.contains(query) }
    }
}

11.5 UI 测试(XCUITest)

swift 复制代码
final class LoginUITests: XCTestCase {
    var app: XCUIApplication!
    
    override func setUp() {
        super.setUp()
        continueAfterFailure = false
        app = XCUIApplication()
        app.launchArguments = ["--uitesting"]
        app.launchEnvironment = ["MOCK_API": "1"]
        app.launch()
    }
    
    func testLoginFlow() throws {
        // 验证初始状态
        XCTAssertTrue(app.navigationBars["登录"].exists)
        XCTAssertFalse(app.buttons["login_button"].isEnabled)
        
        // 输入用户名
        let usernameField = app.textFields["username_field"]
        XCTAssertTrue(usernameField.exists)
        usernameField.tap()
        usernameField.typeText("test@example.com")
        
        // 输入密码
        let passwordField = app.secureTextFields["password_field"]
        passwordField.tap()
        passwordField.typeText("password123")
        
        // 验证按钮可用
        XCTAssertTrue(app.buttons["login_button"].isEnabled)
        
        // 点击登录
        app.buttons["login_button"].tap()
        
        // 等待首页出现(最多 10 秒)
        let homeNav = app.navigationBars["首页"]
        XCTAssertTrue(homeNav.waitForExistence(timeout: 10))
    }
    
    // 截图记录
    func testScreenshot() {
        let screenshot = XCTAttachment(screenshot: app.screenshot())
        screenshot.name = "登录页面"
        screenshot.lifetime = .keepAlways
        add(screenshot)
    }
}

// View 中添加 accessibilityIdentifier(供 UI 测试定位)
TextField("邮箱", text: $email)
    .accessibilityIdentifier("username_field")

SecureField("密码", text: $password)
    .accessibilityIdentifier("password_field")

Button("登录") { login() }
    .accessibilityIdentifier("login_button")

11.6 TestFlight 与 App Store 发布

发布前检查清单

markdown 复制代码
代码质量:
□ 所有单元测试通过(⌘U)
□ 无编译警告
□ 无内存泄漏(Instruments 检测)

配置检查:
□ 版本号(CFBundleShortVersionString)格式:x.y.z
□ Build 号(CFBundleVersion)每次上传递增
□ Bundle ID 与 App Store Connect 一致
□ 签名证书有效(Distribution Certificate)
□ Provisioning Profile 包含 App Store

资源检查:
□ App 图标 1024×1024(PNG, 无 Alpha 通道)
□ 所有尺寸启动页正常
□ 国际化字符串完整

隐私合规:
□ Info.plist 权限描述准确
□ App Privacy Report 中隐私标签与实际一致
□ 不得含有私有 API

构建发布:
□ Product → Archive
□ Validate App(本地验证)
□ Distribute App → App Store Connect
□ TestFlight 内测(邀请测试人员)
□ App Store 提交审核

版本管理自动化

bash 复制代码
# 使用 fastlane 自动化发布流程
# 安装:gem install fastlane

# Fastfile 配置
lane :beta do
  increment_build_number           # 自动递增 build 号
  build_app(scheme: "iOS_demos")  # 编译
  upload_to_testflight             # 上传到 TestFlight
  slack(message: "新版本已上传 TestFlight!")
end

lane :release do
  increment_version_number(bump_type: "minor")
  build_app(scheme: "iOS_demos")
  upload_to_app_store(
    skip_metadata: false,
    skip_screenshots: false,
    submit_for_review: true,
    automatic_release: false   # 手动发布
  )
end

章节总结

主题 关键技术 重要程度
视图性能 拆分子视图/Equatable/LazyStack ⭐⭐⭐⭐⭐
内存管理 weak/unowned/[weak self] ⭐⭐⭐⭐⭐
性能分析 Instruments Leaks/Allocations ⭐⭐⭐⭐
启动优化 轻量 init/并行初始化 ⭐⭐⭐⭐
单元测试 XCTest + Mock 依赖注入 ⭐⭐⭐⭐⭐
UI 测试 XCUITest + accessibilityIdentifier ⭐⭐⭐⭐
发布流程 Archive → TestFlight → App Store ⭐⭐⭐⭐⭐

Demo 说明

文件 演示内容
PerformanceDemo.swift 视图重建对比 / LazyStack vs VStack
MemoryLeakDemo.swift 循环引用检测与修复
XCTestDemo.swift 单元测试 + Mock 完整示例
UITestDemo.swift 登录流程 UI 测试

📎 扩展内容补充

来源:第十一章_进阶原理.md
本章概述:深入理解 Swift 内存模型(ARC 底层原理)、Swift 现代并发模型(async/await/Actor/Sendable)、Core Animation 渲染原理,以及 Swift 5.9 宏(Macros)。


11.1 Swift 内存模型与 ARC 原理

概念讲解

ARC 底层工作原理
swift 复制代码
// ARC 为每个对象维护一个引用计数
// 对象创建:引用计数 = 1
// 赋给新变量:引用计数 +1
// 超出作用域/设为 nil:引用计数 -1
// 引用计数 = 0:释放内存,调用 deinit

class Node {
    var value: Int
    var next: Node?  // 默认是 strong
    
    init(_ value: Int) {
        self.value = value
        print("Node(\(value)) 创建,内存地址:\(Unmanaged.passUnretained(self).toOpaque())")
    }
    
    deinit {
        print("Node(\(value)) 释放")
    }
}

// 引用计数追踪
var a: Node? = Node(1)  // 引用计数 = 1
var b = a               // 引用计数 = 2
a = nil                 // 引用计数 = 1
b = nil                 // 引用计数 = 0 → 释放

// 值类型无需 ARC
struct Point { var x, y: Double }
// Stack 上分配,无引用计数开销
let p1 = Point(x: 1, y: 2)
let p2 = p1  // 深拷贝,完全独立
Copy-on-Write(写时复制)
swift 复制代码
// Swift 的集合类型(Array/Dictionary/Set)采用 COW
// 相同数据共享内存,修改时才实际复制

var array1 = [1, 2, 3, 4, 5]
var array2 = array1  // 未发生实际复制!共享同一块内存

array2.append(6)     // 此时才发生实际复制(Copy-on-Write)

// 自定义 COW
struct MyBuffer {
    private var storage: StorageClass  // 引用类型的存储
    
    mutating func modify() {
        if !isKnownUniquelyReferenced(&storage) {
            storage = StorageClass(copying: storage)  // 复制
        }
        storage.data.append(0)
    }
}

11.2 Swift 现代并发模型

概念讲解

async/await 底层原理
swift 复制代码
// async 函数在 Swift 运行时中是"可挂起函数"
// 遇到 await 时,保存执行上下文,释放线程
// 等待完成后,恢复执行(可能在不同线程)

// 协作式调度(Cooperative Scheduling)
async func demonstrateSuspension() {
    print("1: 开始")                            // 主线程
    
    let result = await someAsyncOperation()      // 挂起,让出线程
    
    print("2: 恢复,结果:\(result)")            // 可能在不同线程恢复
}

// 任务优先级
Task(priority: .high) { ... }
Task(priority: .medium) { ... }  // 默认
Task(priority: .low) { ... }
Task(priority: .background) { ... }
Task(priority: .utility) { ... }

// 结构化并发 - 子任务跟随父任务
func parentTask() async {
    async let child1 = subtask1()  // 启动子任务
    async let child2 = subtask2()  // 启动子任务
    
    let results = await (child1, child2)  // 等待所有子任务
    // 如果父任务被取消,子任务自动取消
}
Sendable - 跨并发域安全
swift 复制代码
// Sendable 标记类型可以安全跨越 Actor/Task 边界
struct SafeMessage: Sendable {
    let id: UUID
    let content: String  // 不可变值类型,天然 Sendable
}

// Class 需要手动保证线程安全才能 Sendable
final class UserInfo: Sendable {
    let name: String   // let + 不可变 = 安全
    let id: UUID
    
    init(name: String, id: UUID = UUID()) {
        self.name = name
        self.id = id
    }
}

// Actor - 隔离可变状态(自动 Sendable)
actor SafeCounter {
    private var value = 0
    
    func increment() { value += 1 }
    func getValue() -> Int { value }
    
    // nonisolated - 不需要隔离的方法,可同步调用
    nonisolated var description: String {
        "Counter"
    }
}
全局 Actor
swift 复制代码
// @MainActor - 主线程 Actor(类比 Dart 的主 Isolate)
@MainActor
class UIUpdater {
    var status: String = ""
    
    func update(status: String) {
        self.status = status  // 总在主线程执行
    }
}

// 自定义全局 Actor
@globalActor
actor DatabaseActor {
    static let shared = DatabaseActor()
}

@DatabaseActor
func writeToDatabase(data: Data) async {
    // 总在 DatabaseActor 上执行,保证数据库访问串行
}

11.3 Core Animation 渲染原理

概念讲解

复制代码
iOS 渲染流程(类比 Flutter 的 Build→Layout→Paint→Composite):

CPU 阶段:
1. 视图树更新(UIView/CALayer 属性变化)
2. 布局计算(layoutSubviews)
3. 显示准备(drawRect/绘制指令)

GPU 阶段:
4. 提交到 Core Animation 渲染服务器
5. 纹理合成(Compositor)
6. Metal/OpenGL 渲染
7. 推送到显示屏(60/120fps)

优化关键点:
- 避免离屏渲染(cornerRadius + clipsToBounds 会触发离屏渲染)
- 使用 CALayer.shouldRasterize 缓存复杂图层
- 减少透明图层叠加(混合)
swift 复制代码
// 检测离屏渲染(Instruments → Core Animation)
// ✅ 避免触发离屏渲染的写法
layer.cornerRadius = 12
layer.masksToBounds = true

// ❌ 更高效但外观相同
// 在 Assets.xcassets 中直接提供带圆角的图片

// CALayer 直接操作(比 UIView 更底层、更高性能)
class AnimatedLayer: CALayer {
    override func draw(in ctx: CGContext) {
        ctx.setFillColor(UIColor.blue.cgColor)
        ctx.fillEllipse(in: bounds)
    }
}

// 显示链接(精准的帧动画)
class GameView: UIView {
    private var displayLink: CADisplayLink?
    
    func startAnimation() {
        displayLink = CADisplayLink(target: self, 
                                     selector: #selector(tick))
        displayLink?.preferredFrameRateRange = 
            CAFrameRateRange(minimum: 60, maximum: 120, preferred: 120)
        displayLink?.add(to: .main, forMode: .common)
    }
    
    @objc func tick(displayLink: CADisplayLink) {
        // 每帧调用,精准控制动画
        let timestamp = displayLink.timestamp
        // 更新游戏逻辑...
    }
}

11.4 Swift Macros(Swift 5.9+ 宏)

概念讲解

swift 复制代码
// Swift 宏在编译期展开,类比 C 的宏但类型安全
// @Observable 就是一个宏

// 使用宏(类比 Java 的注解处理器 / Kotlin 的 ksp)
@Model          // SwiftData 模型宏
class Task { ... }

@Observable     // 替代 ObservableObject + @Published
class ViewModel { ... }

// 常见内置宏
#Preview { ... }                    // 预览宏
#warning("待处理")                   // 编译警告
#error("不能这样用")                  // 编译错误
#line, #column, #function, #file   // 调试信息
#if ... #else ... #endif           // 条件编译

// stringify 宏示例(来自官方示例)
@freestanding(expression)
macro stringify<T>(_ value: T) -> (T, String) = 
    #externalMacro(module: "MacrosMacros", type: "StringifyMacro")

// 使用
let (result, code) = #stringify(1 + 2)
print(result)  // 3
print(code)    // "1 + 2"

// 实际项目中常用的宏
// - @CasePathable (TCA):自动生成 case path
// - @ObservableState (TCA):结合 @Observable 的状态
// - @DependencyClient:依赖注入

章节总结

进阶主题 核心概念 重要程度
ARC 原理 引用计数/COW/循环引用 ⭐⭐⭐⭐⭐
async/await原理 可挂起函数/协作调度 ⭐⭐⭐⭐⭐
Actor 并发隔离/线程安全 ⭐⭐⭐⭐⭐
Sendable 跨并发域类型安全 ⭐⭐⭐⭐
Core Animation 渲染流程/性能优化 ⭐⭐⭐⭐
Swift Macros 编译期代码生成 ⭐⭐⭐

Demo 说明

Demo 文件 演示内容
MemoryModelDemo.swift ARC 引用计数/循环引用可视化
ConcurrencyDemo.swift async/await/Actor/TaskGroup 演示
CoreAnimationDemo.swift CALayer/显示链接/性能分析
SwiftMacrosDemo.swift @Observable/@Model 宏展示
相关推荐
iAnMccc3 小时前
Swift Codable 的 5 个生产环境陷阱,以及如何优雅地解决它们
ios
iAnMccc3 小时前
从 HandyJSON 迁移到 SmartCodable:我们团队的实践
ios
kerli3 小时前
基于 kmp/cmp 的跨平台图片加载方案 - 适配 Android View/Compose/ios
android·前端·ios
懋学的前端攻城狮6 小时前
第三方SDK集成沉思录:在便捷与可控间寻找平衡
ios·前端框架
weixin199701080166 小时前
《废旧物资商品详情页前端性能优化实战》
前端·性能优化
MU在掘金916958 小时前
让LLM做选择题而不是问答题:多Agent性能分析的分层架构
性能优化
冰凌时空8 小时前
Swift vs Objective-C:语言设计哲学的全面对比
ios·openai
饭后一颗花生米9 小时前
2026 前端实战:AI 驱动下的性能优化与工程化升级
前端·人工智能·性能优化
花间相见10 小时前
【大模型微调与部署03】—— ms-swift-3.12 命令行参数(训练、推理、对齐、量化、部署全参数)
开发语言·ios·swift