本页定位 · iOS / Swift

iOS 是高价值、强约束的平台:用户付费意愿强,但生态封闭、上架审核严。Swift 是苹果力推的现代语言,SwiftUI 用声明式语法把 UI 写得像搭积木。本页按完整知识链铺开:Swift 语言核心 → SwiftUI 声明式 UI → UIKit 与生命周期 → App 架构 → 持久化 → 打包与性能(Instruments)。我会像讲 Java 进阶那样,把每个知识点拆成"它是什么 → 为什么这样设计 → 怎么用 → 踩过什么坑"。基线:Swift 5.9+ / SwiftUI 5,开发需一台 Mac + Xcode。

1

Swift 语言:可选值、协议、泛型与并发

Optional · Protocol · Generics · async/await · Actor

iOS 开发过去离不开 Objective-C(一门历史包袱很重的语言)。2014 年苹果推出 Swift,今天新项目主推 SwiftUI + Swift。这一章把 Swift 最核心、也最容易"自以为懂"的语言特性讲透:可选值、值类型、面向协议、泛型、结构化并发与 Actor。

可选值:用类型把"空"管起来

Swift 把"可能有值也可能没有"显式写成 String?。编译器逼你处理"空"的情况,把一大类崩溃移到编译期。

var name: String = "MJ" // 一定有值 var nick: String? // 可空,类型后加 ? if let n = nick { print(n) } // 解包:有值才进分支 let len = nick?.count ?? 0 // 链式解包 + 默认值 guard let u = user else { return } // 守卫:提前退出,减少嵌套 print(u.name) // 之后 u 已是确定非空
新手最爱用 ! 强解包

nick! 等于对编译器喊"它一定不为 nil",一旦真为空就直接崩溃(运行时陷阱)。只在你 100% 确定已赋值时用,比如 IBOutlet 或 guard let 之后的变量。能用 if let/guard let/?? 就别用 !。

值类型 struct / enum 与"无隐式共享"

Swift 默认推荐 struct(值类型)和 enum(同样是值类型)。赋值时是拷贝,不会出现"另一个地方偷偷改了我的数据"。

struct Point { var x: Int; var y: Int } // 值类型:赋值即拷贝 var a = Point(x: 1, y: 2) var b = a; b.x = 9 // a.x 还是 1,互不影响 enum NetworkState { // enum 也是值类型,且能带关联值 case idle, loading case loaded([String]) // 关联值:成功时携带数据 case failed(Error) // 失败时携带错误 }

论为什么 Swift 偏爱 struct

是什么:struct 是值类型,赋值/传参时是拷贝;class 是引用类型,传的是同一块内存的"指针"。

为什么:值类型没有隐式共享,多线程下不会出现"另一个线程偷偷改了我的数据",天然更利于并发安全。苹果官方建议:默认用 struct,需要共享/继承才用 class。

协议与面向协议编程(POP)

protocol 定义能力契约,extension 还能给协议加默认实现——这就是"面向协议编程",比继承更灵活、可组合。

protocol Greetable { var name: String { get } func greet() -> String } extension Greetable { // 协议默认实现 func greet() -> String { "hi \(name)" } } struct User: Greetable { let name: String } // 遵守即获得 greet() User(name: "MJ").greet() // "hi MJ"

论为什么 POP 比继承香

Swift 单继承。想让"既能飞又能叫"的对象复用逻辑,用继承得硬造一条不自然的父类链;用协议 + 默认实现,可以把能力像插件一样组合(struct Bird: Flyable, Singable)。而且值类型也能用协议,不像继承只服务 class。

泛型:写出类型安全又通用的代码

泛型让函数/类型"延迟确定具体类型",既通用又不被丢进 Any 丧失类型检查。

// 泛型函数:返回数组第一个元素,类型随数组走 func first<T>(_ list: [T]) -> T? { list.first } // 泛型约束:要求 T 遵守 Equatable,才能比较 func unique<T: Equatable>(_ list: [T]) -> [T] { var out: [T] = [] for item in list where !out.contains(item) { out.append(item) } return out }

现代并发:Structured Concurrency

Swift 5.5+ 引入 async/await,把回调地狱拍平成同步式写法,并引入"结构化并发"——子任务的生命周期绑定到父作用域。

func load() async throws -> Data { let (data, _) = try await URLSession.shared.data(from: url) return data } Task { // 在结构化任务里启动异步 let d = try? await load() // 作用域结束,子任务要么完成要么被取消 }

论结构化并发解决什么老问题

旧时代用回调 + DispatchQueue,任务之间的关系是隐式的,取消要靠自己一层层传 flag,极易泄漏。结构化并发让"一群子任务"被当成一个整体:父任务取消,所有子任务自动取消;任一子任务抛错,能被统一捕获。代码可读、可取消、可推理。

Actor:用隔离保护可变状态

actor 是"自带串行锁的引用类型(reference type)":它内部的属性只能由它自己顺序访问,外部调用它的方法要 await,从语言层面消灭数据竞争。(注意:actor 与 class 一样是引用类型,而非 struct/enum 那种值类型。)

actor Counter { private var n = 0 func increment() { n += 1 } // 内部串行,无竞争 func value() -> Int { n } } let c = Counter() Task { await c.increment() // 跨 actor 必须 await print(await c.value()) }
Actor 不是银弹

Actor 防止"同时改",但不防止"逻辑上依次改但结果不符合预期"。而且跨 actor 调用都要 await,把大对象塞进 actor 当全局锁用会成性能瓶颈。它替代的是手动锁,不是让你把一切状态都丢进一个巨型 actor。

错误处理:throws / Result / 断言

Swift 用 throw/throws/try 做显式错误传播,也可用 Result 把成功/失败装进一个值,适合回调场景。

enum ParseError: Error { case empty, invalid } func parse(_ s: String) throws -> Int { guard !s.isEmpty else { throw ParseError.empty } guard let n = Int(s) else { throw ParseError.invalid } return n } do { let v = try parse("42") } catch { print(error) } // 回调里用 Result 更顺 func fetch(completion: (Result<Data, Error>) -> Void) { }
2

SwiftUI 声明式 UI:界面就是状态的函数

View · @State · @Binding · @Observable · Layout

SwiftUI 是苹果 2019 年推出的声明式 UI 框架:你描述"界面在某种状态下长什么样",状态一变框架自动重绘。这一章讲清 View 协议、状态属性、布局与导航。

View 协议与 body:最小页面长这样

所有 SwiftUI 视图都遵守 View 协议,只需实现一个 body 计算属性,返回"当前状态下该长什么样"。

struct ContentView: View { @State private var count = 0 // 状态:变化时自动重绘 var body: some View { VStack(spacing: 16) { // 纵向堆叠 Text("点了 \(count) 次") Button("加一") { count += 1 } // 点按改状态→重绘 } .padding() // 修饰符链:从内到外层层包裹 } }

论some View 是什么

SwiftUI 的 body 必须返回"某个具体的 View 类型",但不想写死(因为嵌套后类型很复杂)。some View 是不透明返回类型:对外只承诺"返回一种确定的 View",编译器在内部推断真实类型。既保住静态类型检查,又不用你手写那个巨长的泛型类型。

状态属性的四种姿势(及新版 @Observable)

状态"该放哪一层"决定数据流向。下表是核心选择;Swift 5.9 又推出 @Observable 宏,比旧版 ObservableObject 更简洁。

属性包装器作用域用在哪里
@State当前视图私有按钮计数这类临时状态
@Binding子视图改父视图开关、输入框双向绑定
@ObservableObject外部可观察对象跨视图共享的 ViewModel
@EnvironmentObject全局注入用户设置、主题
// 新版 @Observable(Swift 5.9+):不用 @Published,直接标属性 import Observation @Observable final class Cart { var items: [String] = [] var total: Int { items.count } } // 视图里:@State var cart = Cart(),改 cart.items 自动刷新
新手误区:状态放错层

把本该放 @ObservableObject 的状态塞进 @State,结果父视图一重建,子视图状态就重置。状态该谁拥有,就放在谁那一层。另一个常见错:在 body 里创建 @State 对象——body 每次重绘都会 new 一遍,状态永远不持久。

布局:VStack / HStack / ZStack / Grid

SwiftUI 布局是"容器 + 子视图"的声明。堆叠方向、间距、对齐都用初始化参数表达,比 Auto Layout 的约束直观得多。

VStack(alignment: .leading, spacing: 12) { // 纵向,左对齐 Text("标题").font(.headline) HStack { Image(systemName: "star"); Text("4.5") } // 横向 } ZStack { Color.black; Text("浮层").foregroundStyle(.white) } // 层叠 LazyVGrid(columns: [GridItem(.flexible())]) { ForEach(items) { Item($0) } }

列表与导航:NavigationStack 取代老 API

列表用 List + ForEach;导航用 NavigationStack + navigationDestination,按数据驱动跳转,比老 NavigationLink(destination:) 更灵活。

NavigationStack { List(users) { user in NavigationLink(value: user) { Text(user.name) } } .navigationTitle("用户") .navigationDestination(for: User.self) { user in DetailView(user: user) // 点哪行跳到对应详情 } }

论为什么用数据驱动的导航

老写法把"目标视图"硬编码进 Link,深层跳转、动态路径、状态恢复都很难。新 navigationDestination(for:) 用"要展示的数据类型"决定去哪,导航栈是可被序列化的路径,做 deep link、撤销回退、单元测试都更顺。

修饰符链与自定义 View

SwiftUI 的 .font()、.padding() 是"修饰符"——每次调用都返回一个包了原视图的新视图,从内到外层层包裹。把常用组合抽成自定义 View 或 View 扩展,复用就清爽了。

// 自定义 View 扩展,封装一组常用样式 extension View { func cardStyle() -> some View { self.padding() .background(.thinMaterial) .clipShape(RoundedRectangle(cornerRadius: 12)) } } Text("hi").cardStyle() // 复用,一处改全局变

动画与过渡:声明式地"动起来"

SwiftUI 的动画是"描述起止状态 + 告诉它要动画",框架自己补间,不用手写帧。

@State private var big = false VStack { Circle().frame(width: big ? 200 : 100) // 状态变化驱动尺寸 Button("切换") { big.toggle() } } .animation(.spring(duration: 0.4), value: big) // 绑定到 big,自动补间
3

UIKit 与生命周期:SwiftUI 不够时的退路

UIViewController · Auto Layout · Bridge

SwiftUI 已覆盖绝大多数场景,但遇到第三方只提供 UIKit 控件、或极复杂的自定义手势/动画时,仍需 UIKit。苹果提供桥接机制让两者共存。这一章讲 UIViewController 生命周期、Auto Layout 约束、以及与 SwiftUI 的互通。

UIViewController 生命周期:视图什么时候建/卸

UIKit 是命令式的,控件要你手动创建、手动刷新。理解生命周期回调,才知道"初始化放哪、清理放哪"。

class DetailVC: UIViewController { override func loadView() { // 创建根视图(少用,多改 view) view = UIView(); view.backgroundColor = .white } override func viewDidLoad() { // 视图加载完,只一次 super.viewDidLoad(); setupUI() } override func viewWillAppear(_ a: Bool) { super.viewWillAppear(a) } // 每次即将显示 override func viewDidDisappear(_ a: Bool) { super.viewDidDisappear(a) } }

论viewDidLoad 和 viewWillAppear 的区别

viewDidLoad 整个生命周期只调一次,适合一次性的初始化(建子视图、注册通知)。viewWillAppear 每次视图要显示都调,适合随展示而变的东西(刷新数据、隐藏导航栏)。把"每次都要做"的事错放进 viewDidLoad,会导致来回切页面时不更新。

Auto Layout:用约束而非坐标摆界面

不同设备尺寸、横竖屏,不能写死 frame。Auto Layout 用"约束"(上下左右、宽高、比例)描述关系,系统求解出每个视图的位置。

let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false // 关掉旧式自动约束 view.addSubview(label) NSLayoutConstraint.activate([ label.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 16), label.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16), ])
Auto Layout 两个高频崩溃

① 忘了 translatesAutoresizingMaskIntoConstraints = false:系统会同时用新旧两套约束,冲突报错。② 约束不完整:缺一个维度(比如只有左右没高度)要么报错要么位置飘。用 UIStackView 或 NSLayoutConstraint 把约束写全。

SwiftUI 与 UIKit 混用(双向桥接)

两套可以互相嵌套:SwiftUI 里嵌 UIKit 用 UIViewRepresentable;UIKit 里嵌 SwiftUI 用 UIHostingController。迁移老项目时这是命脉。

// SwiftUI 里包一个 UIKit 原生地图视图 struct MapView: UIViewRepresentable { func makeUIView(context: Context) -> MKMapView { return MKMapView() // 创建 UIKit 视图 } func updateUIView(_ uiView: MKMapView, context: Context) {} } // UIKit 里展示一段 SwiftUI 视图 let host = UIHostingController(rootView: ContentView()) addChild(host); view.addSubview(host.view)

论选型判断:90% SwiftUI,10% 桥 UIKit

新功能优先 SwiftUI;只有当官方 SwiftUI API 缺失、或要复用庞大旧代码时才下沉到 UIKit。不要为了"纯 SwiftUI"硬造轮子——能用 UIViewRepresentable 接现成 UIKit 控件是最务实的。

手势与 delegate 模式

UIKit 大量用 delegate(委托)模式解耦:一个对象把"该谁处理某事"委托给另一个。手势识别也是 UIKit 的强项。

// 给视图加点击手势 let tap = UITapGestureRecognizer(target: self, action: #selector(onTap)) view.addGestureRecognizer(tap) @objc func onTap() { print("tapped") } // delegate:TableView 把"有多少行/每行啥样"问 dataSource func tableView(_ tv: UITableView, numberOfRowsInSection s: Int) -> Int { 10 }

Safe Area 与安全区域:别让内容钻进刘海

iPhone 有刘海/灵动岛和圆角,内容若铺满整个屏幕会钻进这些区域。UIKit 用 safeAreaLayoutGuide,SwiftUI 用 .safeAreaInset / 默认避开,确保内容落在"安全区域内"。

// UIKit:约束顶部贴到安全区上沿,而非屏幕最顶 label.topAnchor.constraint( equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 16).isActive = true // SwiftUI:默认内容已在安全区内;要延伸到边缘再显式处理 VStack { Text("hi") } .ignoresSafeArea(edges: .bottom) // 仅底部延伸到边缘
忘了安全区域

新手常把约束贴到 view.topAnchor(屏幕最顶)而非安全区,结果标题被刘海/状态栏盖住。统一用 safeAreaLayoutGuide(UIKit)或让 SwiftUI 自动处理,只有刻意要全屏背景时才 ignoresSafeArea。

4

App 架构:MVVM / Clean / VIPER 与导航

Architecture · MVVM · Coordinator · DI

能跑的小 App 把逻辑塞进 ViewController 也行。但代码一多,"VC 变成几千行的上帝对象"是 iOS 项目的经典灾难。这一章讲主流架构如何分层、解耦、可测,以及导航与依赖注入怎么落地。

MVVM:把界面逻辑从 View 里请出去

Model–View–ViewModel 是最常用的起点:View 只管展示、ViewModel 持有状态与业务逻辑、Model 是数据。SwiftUI 的 @Observable 天然适配 VM。

@Observable final class LoginVM { var username = ""; var canSubmit: Bool { !username.isEmpty } private let service: AuthService init(service: AuthService) { self.service = service } // 注入,便于测试 func login() async { try? await service.login(username) } } // 视图只绑定:@State var vm = LoginVM(service: RealAuthService())

论MVVM 解决什么

把"网络请求、校验、状态转换"从 View 里抽出来,ViewModel 不依赖任何 UI 框架,因此能脱离界面跑单元测试。View 只做"把状态画出来 + 把用户操作转成 VM 调用"。VC/View 从几千行瘦到几百行。

Clean 架构:再切一层用例

项目再大,连 VM 都开始臃肿。Clean 在 VM 之下加"用例(Use Case)"层,把"登录""拉列表"这类业务规则独立成可复用、可单测的小单元,VM 只编排它们。

// 用例层:一个用例只做一件事 struct LoginUseCase { let repo: AuthRepository func callAsFunction(_ name: String) async throws -> User { try await repo.login(name) // 纯业务,不碰 UI } } // VM 调用:let user = try await loginUseCase(username)

VIPER:最重但最解耦,适合大团队协作

VIPER 把职责切到极致:View / Interactor(业务)/ Presenter(展示逻辑)/ Entity(模型)/ Router(导航)。样板多,但每个文件极小、可测、并行开发不冲突。

角色负责
View展示、接收用户操作,不含逻辑。
Interactor核心业务规则与数据获取。
Presenter把 Interactor 的结果转成 View 能直接画的东西。
Entity纯数据模型。
Router页面跳转与依赖装配。

Coordinator 导航模式:把"跳哪"从 VC 里拿走

默认做法里 VC 互相 push 下一个 VC,导致 VC 之间紧耦合、难以复用和测试。Coordinator 专门管"导航流",VC 只发"用户想前进"的事件。

protocol LoginCoordinator { func didFinishLogin() } class AppCoordinator { func showHome() { // 集中决定登录成功去哪,VC 自己不 push navigationController.pushViewController(HomeVC(), animated: true) } }

论为什么导航要独立

VC 紧耦合跳转会让"复用这个页面到别处""写无界面的导航测试"都变难。Coordinator 把流程当成可替换的配置:同一套页面,登录后去主页还是去引导页,只改 Coordinator 一处。

依赖注入:别在内部 new 依赖

把依赖(网络层、数据库)从外部传进来,而不是在类里直接 RealService()。好处:测试时传假实现(Mock),生产换真实现,零改业务代码。

// 用协议抽象依赖,构造时注入 protocol AuthService { func login(_ n: String) async throws -> User } struct RealAuth: AuthService { func login(_ n: String) async throws -> User { /* 真网络 */ } } struct MockAuth: AuthService { func login(_ n: String) async throws -> User { User(name: n) } } // 生产用 RealAuth,测试用 MockAuth,VM 一行不改 let vm = LoginVM(service: RealAuth())
别用全局单例当依赖

到处 AuthService.shared 看起来方便,但测试时没法替换、容易形成隐式耦合网。用构造注入(或工厂),依赖关系显式可见,重构和测试都轻松。

架构选型对比:没有银弹

架构解耦程度何时选代价
无架构低(VC 上帝对象)Demo / 一次性后期难维护
MVVM中大多数 AppVM 可能变胖
Clean高中大型、业务复杂目录层级多
VIPER极高大团队、长生命周期样板极多
5

持久化:从 UserDefaults 到 SQLite

UserDefaults · Keychain · Core Data · Realm · SQLite

App 关掉再打开,数据还在——这就是持久化。iOS 方案按"量级与复杂度"分档:小配置、敏感凭证、结构化数据库、轻量关系库。这一章把选型与写法讲清。

UserDefaults 与 Keychain:小配置 vs 敏感凭证

UserDefaults 存轻量偏好(开关、上次登录名),明文、不安全。密码、token 必须进 Keychain——它是系统级加密保险箱,卸载 App 也不丢。

// UserDefaults:小配置 UserDefaults.standard.set(true, forKey: "darkMode") let dark = UserDefaults.standard.bool(forKey: "darkMode") // Keychain:敏感凭证(用 Security 框架,示意为封装后 API) Keychain.shared.set("secret-token", forKey: "authToken") let token = Keychain.shared.get("authToken")
把密码塞进 UserDefaults 是事故

UserDefaults 本质是明文 plist,越狱/备份可被读取。任何凭证(密码、token、密钥)只能放 Keychain,且必要时用 kSecAttrAccessibleWhenUnlocked 控制解锁后才可读。

Core Data / SwiftData:苹果官方结构化存储

SwiftData(iOS 17+)是 Core Data 的现代封装,用 @Model 宏 + 原生 Swift 类型,几乎零样板。适合"对象关系 + 可查询"的本地数据。

// SwiftData:声明模型 + 查询,比 Core Data 简洁太多 import SwiftData @Model final class Task { var title: String var done: Bool init(title: String, done: Bool = false) { self.title = title; self.done = done } } // 查询:@Query var tasks: [Task],或手动 fetch let todo = try modelContext.fetch(FetchDescriptor<Task>( predicate: #Predicate { !$0.done }))

论SwiftData 还是 Core Data?

新项目、iOS 17+ 直接用 SwiftData,API 现代、和 Swift 类型系统贴合。要支持 iOS 16 及以下,或要用 Core Data 的高级特性(复杂迁移、批量处理),才退回 Core Data。两者底层是同一套存储引擎。

Realm:跨平台、易上手的对象数据库

Realm 是第三方对象数据库,API 比 Core Data 友好,且 Android/iOS 通用——跨端团队重用数据层时有优势。但引入了额外依赖与迁移机制。

// Realm:模型遵守 Object,直接存对象 class Task: Object { @Persisted var title = "" @Persisted var done = false } let realm = try Realm() try realm.write { realm.add(Task(value: ["title": "买菜"])) } let todos = realm.objects(Task.self).filter("done == false")

SQLite 与 SQLite.swift:需要裸 SQL 时

当你要复杂联表、聚合、或与其他系统共用 .sqlite 文件时,直接上 SQLite。SQLite.swift 提供类型安全的 Swift 封装,比 C API 好用太多。

import SQLite let db = try Connection("path/to/app.sqlite") let tasks = Table("tasks") let title = Expression<String>("title") let done = Expression<Bool>("done") try db.run(tasks.create { t in t.column(title, primaryKey: true); t.column(done) }) for row in try db.prepare(tasks.filter(!done)) { print(row[title]) }

方案对比:按量级与复杂度选

方案适合代价
UserDefaults小配置、开关、非敏感明文、不宜存大量
Keychain密码、token、密钥API 偏底层
SwiftData结构化、可查询(iOS17+)系统版本要求
Realm跨端对象库、易上手额外依赖
SQLite.swift复杂 SQL、联表聚合要写表/列定义
6

打包与性能:证书、TestFlight 与 Instruments

Certificates · TestFlight · Instruments · Leaks

iOS 上架比安卓严格:隐私权限必须声明用途,审核会真的打开 App 逐项看。而性能上,卡顿和内存泄漏靠 Instruments 实证。这一章讲清证书体系、TestFlight、以及用 Leaks / Time Profiler 抓问题。

证书与签名体系:四件套认身份

iOS 用"证书 + 描述文件 + Bundle ID + 设备 UDID"四件套,确保只有被授权的代码能在被授权的设备上跑。先理解这张对照表,配签名才不懵。

组件干什么
Bundle IDApp 唯一身份证,和开发者账号绑定。
证书 Certificate证明"这个包确实是你签发的"(开发/分发两种)。
描述文件 Profile把证书 + Bundle ID + 设备绑在一起,真机/上架都用它。
设备 UDID开发/Ad Hoc 时限定能装的实体设备。

论为什么 iOS 签名这么烦

苹果用这套机制保证只有被你授权的代码能在被你授权的设备上跑,这是它生态封闭但 malware 极少的核心。日常用 Xcode 的"自动签名(Automatically manage signing)"能省掉八成手动配置;只有企业分发/CI 才需要手动管证书。

TestFlight:上架前先测后上

TestFlight 让你把构建版发给内部(最多 100 人,免审核)或外部测试员(需轻量审核),绕开完整 App Review 快速验证。

# 典型流程 # 1) Xcode 里 Archive → Distribute App → TestFlight # 2) 在 App Store Connect 的 TestFlight 标签页添加测试员 # 3) 测试员手机装 TestFlight App,收邀请即可安装 # 外部测试首次需苹果审核(通常几小时到一天)
TestFlight 不是绕过审核的漏洞

外部测试仍要苹果轻量审核,且构建版有 90 天有效期,过期测试员装不了。别把它当正式分发渠道——它只是"上架前快速验证"的通道。

Instruments 概览:iOS 的"听诊器"

Instruments 是 Xcode 自带的性能套件,能采样 CPU、内存、电量、网络。卡顿和泄漏都靠它定位,而不是靠"我感觉有点卡"。

模板抓什么
Time ProfilerCPU 占用与调用栈,定位耗时函数。
Leaks内存泄漏(该释放却没释放的对象)。
Allocations对象分配总量,看内存增长曲线。
Core Animation离屏渲染、帧率、掉帧。

Leaks:抓住内存泄漏

iOS 用 ARC 自动管理引用计数,但循环引用(retain cycle)会让对象永远释放不掉。典型是闭包捕获 self 没用 [weak self],或 delegate 用了强引用。

// 危险:闭包强引用 self,self 又持有闭包 → 循环 operation.completion = { self.done() } // 正确:用 [weak self],并在内部 guard 解包 operation.completion = { [weak self] in guard let self else { return } self.done() }

论为什么 ARC 还会泄漏

ARC 只在"引用计数归零"时释放。一旦 A 引用 B、B 又引用 A(或对象持有强引用自己的闭包),计数永远不为 0,就泄漏。用 Leaks 模板跑一遍,红色小旗就是泄漏点,点进去能看到是哪条引用环。

Time Profiler:定位卡顿的耗时函数

卡顿(掉帧)通常是某个函数主线程干了太久的活。Time Profiler 采样调用栈,火焰图里最宽的柱子就是热点。

// 反例:主线程同步解析大 JSON,UI 卡死 let data = try Data(contentsOf: bigFileURL) // 同步读,堵主线程 let model = try JSONDecoder().decode(Huge.self, from: data) // 正例:丢到后台解码,主线程只等结果 let model = await Task.detached(priority: .userInitiated) { let d = try Data(contentsOf: bigFileURL) return try JSONDecoder().decode(Huge.self, from: d) }.value
主线程是金贵的

任何超过 ~16ms 的同步工作都会掉帧(60fps)。网络、大文件 IO、重解码、复杂计算全要离开主线程(用 Task/Task.detached 或 DispatchQueue)。别在主线程做这些,哪怕"就一次"。

离屏渲染与卡顿优化清单

某些视觉效果(圆角+裁剪、阴影、模糊)会触发"离屏渲染"——先把内容渲染到离屏缓冲再合成,开销大。下面是常用优化清单。

优化项做法
避免不必要的圆角+mask用预处理图或 cornerRadius 而非 layer 遮罩
列表 cell 复用UITableView 自动复用,SwiftUI List 已优化
图片降采样按显示尺寸解码,别拿 4000px 图塞 100px 视图
减少视图层级扁平化层级,少嵌套 Container
结
本页要点

iOS 开发强约束、高价值:Swift 用可选值把"空"管进类型、用值类型(struct/enum)与 Actor 换取并发安全,面向协议(POP)比继承更灵活可组合,async/await + 结构化并发把回调拍平。SwiftUI 以"状态→界面"的声明式范式降低复杂度,状态属性按作用域分层(@State/@Binding/@Observable/@EnvironmentObject),布局用栈式容器、导航用数据驱动的 NavigationStack;UIKit 作为必要退路通过 UIViewRepresentable / UIHostingController 共存,且要懂 VC 生命周期与 Auto Layout 约束。App 架构按规模选 MVVM / Clean / VIPER,导航交给 Coordinator、依赖用构造注入而非全局单例。持久化按量级分档:UserDefaults 存配置、Keychain 存凭证、SwiftData/Core Data 存结构化数据、Realm 跨端、SQLite.swift 做复杂 SQL。上架需证书/描述文件/隐私清单且审核严格;TestFlight 是先测后上标准动作;性能靠 Instruments 的 Time Profiler(CPU 热点)、Leaks(循环引用泄漏)、Allocations 实证,主线程严禁重活,离屏渲染要规避。

自测 · 看你是否真懂

1.Swift 里 struct 和 class 最关键的运行时区别是什么?为什么推荐默认用 struct?

查看答案

struct 是值类型(拷贝语义),class 是引用类型(共享语义)。值类型无隐式共享,多线程下更安全、数据流向更清晰,所以苹果推荐默认 struct,需要共享/继承才用 class。

2.@State 和 @ObservableObject 分别该用在什么场景?状态放错层会怎样?

查看答案

@State 管当前视图私有的临时状态(如计数器);@ObservableObject/@Observable 管跨视图共享、来自外部的 ViewModel 状态。放错层:把共享状态塞进 @State,父视图一重建子视图状态就重置;在 body 里 new @State 对象则永远不持久。

3.ARC 自动管理内存,为什么还会发生内存泄漏?怎么抓?

查看答案

循环引用(retain cycle)会让引用计数永远不为 0:典型是闭包强捕获 self、或 delegate 用强引用。解决:闭包用 [weak self]、delegate 用 weak。用 Instruments 的 Leaks 模板跑一遍,红色小旗即泄漏点,可看引用环。

4.密码类凭证为什么不能放 UserDefaults,该放哪?

查看答案

UserDefaults 本质是明文 plist,越狱/备份可读取。密码、token、密钥必须放 Keychain——系统级加密保险箱,按 kSecAttrAccessible 控制可读时机,卸载 App 也不丢。

5.主线程做了一次"就一次"的大 JSON 解析,为什么仍会卡顿?正确做法?

查看答案

主线程任何超过约 16ms 的同步工作都会掉帧(60fps),用户直接感知卡顿。正确做法:把网络/大文件 IO/重解码/重计算丢到后台(Task.detached / DispatchQueue),主线程只等结果。

下一步往哪走

路学完 iOS 之后

① 对照 Flutter 页:理解两套移动范式如何共用"平台通道 / 原生能力"的思路,做跨端架构决策。

② 做真机 App:从"网络列表 + 本地缓存 + 上架"的最小闭环做起,把证书、隐私清单、审核流程走通一次。

③ 进阶:动画与手势深入、Combine / 异步流、Widget 与推送、自动化发布(fastlane)、以及把 Instruments 当成日常排查习惯。