
本课目标入门和进阶前4课解决了功能与架构。本课解决一个更隐蔽的问题为什么界面卡、内存涨、启动慢。掌握 Xcode Instruments 的 SwiftUI、Time Profiler、Allocations、Leaks 模板理解视图刷新机制、Equatable 优化、图片解码、List 性能、启动优化并能用数据而不是感觉定位问题。一、核心概念· InstrumentsXcode 自带的性能分析工具按模板采集数据。· SwiftUI 模板查看视图刷新次数和耗时定位频繁刷新。· Time Profiler采样 CPU找出耗时函数。· Allocations跟踪内存分配找出大对象和增长点。· Leaks检测内存泄漏。· Launch分析启动各阶段耗时。· 视图刷新机制body 在依赖状态变化时重新计算频繁刷新会卡。· Equatable让 SwiftUI 能判断视图是否需要更新减少无效刷新。· id 稳定性ForEach 的 id 变化会导致重建破坏性能和动画。· 图片解码大图在主线程解码会卡需降采样。· LazyVStack / List按需创建视图避免一次性渲染全部。· 启动优化延迟初始化、减少动态库、减少 main 前的重活。二、用 SwiftUI 模板定位刷新问题打开 InstrumentsXcode 菜单Product → Profile或 Cmd I。选择 SwiftUI 模板点击录制。查看视图刷新在 SwiftUI 轨道中可以看到每个视图的 body 被调用的次数和耗时。常见问题· 某个 body 被调用几百次说明状态更新过于频繁。· 父视图刷新导致所有子视图跟着刷新。· ForEach 中每行都重新计算。常见优化手段// 问题整个列表都在父视图 body 中构建任何状态变化都全量刷新structBadListView:View{StateprivatevarsearchTextStateprivatevarbooks:[Book][]varbody:someView{VStack{TextField(搜索,text:$searchText)// 输入会刷新整个 bodyList(books){bookinHeavyRow(book:book)// 每行都很重}}}}// 优化把重行拆成独立视图并让它遵循 EquatablestructHeavyRow:View,Equatable{letbook:Book// 只有 book 变化时才刷新staticfunc(lhs:HeavyRow,rhs:HeavyRow)-Bool{lhs.book.idrhs.book.idlhs.book.titlerhs.book.title}varbody:someView{// 复杂布局HStack{Text(book.title)Text(book.author)}}}// 使用 EquatableView 或直接让视图遵循 EquatableList(books){bookinHeavyRow(book:book).equatable()// 告诉 SwiftUI 按 Equatable 判断是否刷新}解读Equatable .equatable() 能让 SwiftUI 跳过内容未变的行显著减少刷新。三、Time Profiler找出耗时函数使用Product → Profile → Time Profiler录制后操作 App停止。在调用树中勾选 Invert Call Tree 和 Hide System Libraries就能看到自己的耗时函数。常见耗时点· 在 body 中做复杂计算或格式化。· 在主线程做 JSON 解码、图片解码、文件读写。· 频繁创建 DateFormatter、NumberFormatter。// 问题每次 body 都创建 DateFormatterstructBadDateView:View{letdate:Datevarbody:someView{Text(DateFormatter.localizedString(from:date,dateStyle:.medium,timeStyle:.none))}}// 优化复用静态 formatterstructGoodDateView:View{letdate:Dateprivatestaticletformatter:DateFormatter{letfDateFormatter()f.dateStyle.medium f.timeStyle.nonereturnf}()varbody:someView{Text(Self.formatter.string(from:date))}}解读DateFormatter 创建成本高应复用body 中避免重计算抽成计算属性或提前算好。四、Allocations 和 Leaks内存问题AllocationsProduct → Profile → Allocations查看内存增长。常见问题· 图片未降采样一张原图占几十 MB。· 缓存无限增长。· 闭包捕获大对象。// 问题直接解码原图内存暴涨ifletdatabook.coverData,letimageUIImage(data:data){Image(uiImage:image)// 原图可能 4000×3000}// 优化降采样到显示尺寸funcdownsample(data:Data,to pointSize:CGSize,scale:CGFloatUIScreen.main.scale)-UIImage?{letoptions:[CFString:Any][kCGImageSourceShouldCache:false]guardletsourceCGImageSourceCreateWithData(dataasCFData,optionsasCFDictionary)else{returnnil}letmaxDimensionmax(pointSize.width,pointSize.height)*scaleletdownsampleOptions:[CFString:Any][kCGImageSourceCreateThumbnailFromImageAlways:true,kCGImageSourceShouldCacheImmediately:true,kCGImageSourceCreateThumbnailWithTransform:true,kCGImageSourceThumbnailMaxPixelSize:maxDimension]guardletcgImageCGImageSourceCreateThumbnailAtIndex(source,0,downsampleOptionsasCFDictionary)else{returnnil}returnUIImage(cgImage:cgImage)}解读降采样只解码到显示所需尺寸内存占用可降低几十倍。LeaksProduct → Profile → Leaks录制后查看泄漏点。常见原因· 循环引用闭包捕获 selfself 又持有闭包。· Timer 未 invalidate。· NotificationCenter 观察者未移除。// 问题循环引用classBadViewModel{varonUpdate:(()-Void)?funcsetup(){onUpdate{self.doSomething()// 捕获 self形成循环}}funcdoSomething(){}}// 优化弱引用classGoodViewModel{varonUpdate:(()-Void)?funcsetup(){onUpdate{[weakself]inself?.doSomething()}}funcdoSomething(){}}解读闭包捕获 self 时用 [weak self]避免循环引用。五、List 性能优化// 问题ForEach 用索引做 id删除时错位且重建ForEach(books.indices,id:\.self){indexinBookRow(book:books[index])}// 优化用稳定 idForEach(books){bookinBookRow(book:book)}// 问题行内有复杂计算structBadRow:View{letbook:Bookvarbody:someView{letscorecalculateScore(book)// 每次刷新都算Text(\(score))}funccalculateScore(_book:Book)-Int{// 复杂计算book.title.count*10}}// 优化提前算好或缓存structGoodRow:View{letbook:Bookvarbody:someView{Text(\(book.title.count*10))}}其他要点· 行高固定时用 .listRowInsets 减少布局计算。· 大量数据分页加载不要一次全查。· 图片异步加载不要阻塞行渲染。· 用 LazyVStack 替代 VStack 放在 ScrollView 中。六、启动优化Product → Profile → App Launch查看启动各阶段。常见优化· 减少 main 之前的全局初始化。· 延迟创建数据库容器和网络客户端。· 减少动态库和启动时注册。· 首屏只加载必要数据其余异步加载。// 问题启动时同步做重活mainstructMyApp:App{letcontainertry!ModelContainer(for:Book.self)// 启动时同步创建varbody:someScene{WindowGroup{ContentView()}}}// 优化延迟到需要时创建或用后台任务预热mainstructMyApp:App{Stateprivatevarcontainer:ModelContainer?varbody:someScene{WindowGroup{ifletcontainer{ContentView().modelContainer(container)}else{ProgressView().task{// 异步创建containertry?awaitTask.detached{tryModelContainer(for:Book.self)}.value}}}}}解读启动时只做必要工作重活放到后台首屏更快出现。七、习题、答案与解读选择题题1题目查看 SwiftUI 视图刷新次数使用哪个 Instruments 模板A. SwiftUI B. Time Profiler C. Allocations D. Leaks要求选出正确选项。答案A解读SwiftUI 模板专门显示视图 body 的刷新次数和耗时。题2题目找出 CPU 耗时函数使用哪个模板A. Time Profiler B. SwiftUI C. Leaks D. Launch要求选出正确选项。答案A解读Time Profiler 采样 CPU展示调用树和耗时。题3题目让 SwiftUI 跳过内容未变的行使用A. Equatable .equatable() B. State C. Binding D. Query要求选出正确选项。答案A解读遵循 Equatable 并调用 .equatable()SwiftUI 会跳过未变化的视图。题4题目大图直接解码会导致什么问题A. 内存暴涨 B. 动画变快 C. 启动变快 D. 无影响要求选出正确选项。答案A解读原图分辨率高解码后占用大量内存应降采样。题5题目ForEach 应该使用什么作为 idA. 稳定且唯一的 id B. 数组索引 C. 随机数 D. 时间戳要求选出正确选项。答案A解读索引用在删除时会错位应使用 Identifiable 的稳定 id。判断题题1题目在 body 中做复杂计算会影响性能。要求判断对错。答案对解读body 可能被频繁调用重计算会拖慢界面。题2题目DateFormatter 可以每次使用时创建没有性能问题。要求判断对错。答案错解读DateFormatter 创建成本高应复用。题3题目闭包捕获 self 一定造成循环引用。要求判断对错。答案错解读只有 self 又持有闭包时才形成循环用 [weak self] 可避免。题4题目LazyVStack 会一次性创建所有子视图。要求判断对错。答案错解读LazyVStack 按需创建滚动到可见区域才创建。题5题目启动时同步创建数据库容器会拖慢首屏。要求判断对错。答案对解读重活应延迟或异步执行。填空题题1题目查看视图刷新使用 ______ 模板。要求填入空白处。答案SwiftUI解读SwiftUI 模板显示刷新次数。题2题目找出耗时函数使用 ______ 模板。要求填入空白处。答案Time Profiler解读Time Profiler 采样 CPU。题3题目跳过未变化视图使用 .______()。要求填入空白处。答案equatable解读配合 Equatable 使用。题4题目大图应做 ______ 处理。要求填入空白处。答案降采样解读CGImageSourceCreateThumbnailAtIndex。题5题目检测内存泄漏使用 ______ 模板。要求填入空白处。答案Leaks解读Leaks 显示泄漏对象和调用栈。改错题题1题目ForEach(books.indices,id:\.self){indexinBookRow(book:books[index])}要求找出潜在问题并改正。答案ForEach(books){bookinBookRow(book:book)}解读索引作为 id 在删除时错位应使用稳定 id。题2题目varbody:someView{letformatterDateFormatter()formatter.dateStyle.mediumreturnText(formatter.string(from:date))}要求找出性能问题并改正。答案privatestaticletformatter:DateFormatter{letfDateFormatter()f.dateStyle.mediumreturnf}()varbody:someView{Text(Self.formatter.string(from:date))}解读body 中每次创建 DateFormatter 开销大应复用静态实例。题3题目classViewModel{varonUpdate:(()-Void)?funcsetup(){onUpdate{self.doSomething()}}}要求找出循环引用并改正。答案funcsetup(){onUpdate{[weakself]inself?.doSomething()}}解读闭包强引用 selfself 又持有闭包形成循环。代码预测题题目structRow:View,Equatable{letbook:Bookstaticfunc(lhs:Row,rhs:Row)-Bool{lhs.book.idrhs.book.id}varbody:someView{Text(book.title)}}List(books){bookinRow(book:book).equatable()}要求如果 book.title 变了但 id 没变Row 会刷新吗答案不会。解读 只比较 idtitle 变化不会被检测到所以不会刷新。 应比较所有影响显示的属性。编程题题1题目优化一个频繁刷新的列表行。要求让行遵循 Equatable并只比较影响显示的属性。参考代码structBookRow:View,Equatable{letbook:Bookstaticfunc(lhs:BookRow,rhs:BookRow)-Bool{lhs.book.idrhs.book.idlhs.book.titlerhs.book.titlelhs.book.authorrhs.book.authorlhs.book.ratingrhs.book.rating}varbody:someView{VStack(alignment:.leading){Text(book.title).font(.headline)Text(book.author).foregroundStyle(.secondary)}}}List(books){bookinBookRow(book:book).equatable()}解读比较所有影响显示的属性确保变化时能正确刷新。题2题目写一个图片降采样函数。要求按显示尺寸缩放避免内存暴涨。参考代码funcdownsample(data:Data,to pointSize:CGSize,scale:CGFloatUIScreen.main.scale)-UIImage?{letoptions:[CFString:Any][kCGImageSourceShouldCache:false]guardletsourceCGImageSourceCreateWithData(dataasCFData,optionsasCFDictionary)else{returnnil}letmaxDimensionmax(pointSize.width,pointSize.height)*scaleletdownsampleOptions:[CFString:Any][kCGImageSourceCreateThumbnailFromImageAlways:true,kCGImageSourceShouldCacheImmediately:true,kCGImageSourceCreateThumbnailWithTransform:true,kCGImageSourceThumbnailMaxPixelSize:maxDimension]guardletcgImageCGImageSourceCreateThumbnailAtIndex(source,0,downsampleOptionsasCFDictionary)else{returnnil}returnUIImage(cgImage:cgImage)}解读只解码到显示所需尺寸内存占用大幅降低。题3题目挑战——把启动时的数据库创建改为异步。要求首屏先显示加载数据库就绪后显示内容。参考代码mainstructMyApp:App{Stateprivatevarcontainer:ModelContainer?varbody:someScene{WindowGroup{ifletcontainer{ContentView().modelContainer(container)}else{ProgressView(启动中...).task{containertry?awaitTask.detached{tryModelContainer(for:Book.self)}.value}}}}}解读数据库创建放到后台任务首屏立即出现减少启动等待。开放思考题题1题目为什么不能只凭感觉优化性能要求简要回答。参考感觉不可靠可能优化了不重要的地方真正的瓶颈在别处。解读用 Instruments 采集数据按数据优化。题2题目Equatable 优化有什么风险要求简要回答。参考如果 漏掉影响显示的属性界面不会刷新出现数据不一致。解读 必须覆盖所有影响显示的属性。题3题目启动优化的核心原则是什么要求简要回答。参考启动时只做必要工作重活延迟或异步执行。解读首屏越快出现用户感知越好。八、全课知识点总结· Instruments 模板SwiftUI、Time Profiler、Allocations、Leaks、Launch。· SwiftUI 模板查看视图刷新次数和耗时。· Time Profiler采样 CPU找出耗时函数。· Allocations跟踪内存分配。· Leaks检测循环引用和泄漏。· Launch分析启动各阶段。· Equatable .equatable()跳过未变化的视图刷新。· 必须比较所有影响显示的属性。· 稳定 idForEach 使用 Identifiable 的 id不用索引。· 复用 DateFormatter、NumberFormatter 等重对象。· 图片降采样CGImageSourceCreateThumbnailAtIndex。· LazyVStack 和 List 按需创建视图。· 闭包捕获 self 用 [weak self] 避免循环引用。· 启动优化延迟初始化、异步创建、减少动态库。· 性能优化原则先测量再优化后验证。本课达标标准能用 Instruments 的 SwiftUI、Time Profiler、Allocations、Leaks、Launch 模板定位问题能用 Equatable 减少无效刷新能用稳定 id能复用重对象能对图片降采样能避免循环引用能把重活从启动路径移走并形成“测量—优化—验证”的性能调优习惯。