Xcode 26 Debug view hierarchy 不显示隐藏视图问题
![]()
![]()
![]()
![]()
C 端与 B 端的差异
Agent 看上去效果很好, 但是要抽卡, C 端声量高,但企业侧落地率低
大模型解决的核心问题
![]()
企业 AI 价值价值是什么?![]()
从下到上, AI 对企业的价值越高; 从上到下, 标准化程度越高
![]()
中国开源模型发展迅速,许多企业开始自己部署开源模型(如文心一言、千问等)
商汤的 AI 原生产品策略
合作伙伴策略
![]()
AI 时代的特点
产品整体图![]()
![]()
给跟大模型离的不是那么近的用户群体做推广
![]()
为什么不选择 ChatBI/Text2SQL:
对于企业用户推广:
C 端到 B 端的转化路径
突破传统模式
技术精度突破
企业问题解决
金融公司财务部门推广失败:
传统 BI 的问题
![]()
二次编辑能力
用户体验优化, 相比精度提升 1-2 个点,快速编辑功能用户感知更强, 显著提高用户黏性
![]()
![]()
![]()
解决的问题:
![]()
![]()
时间优化, 从最早的 3 个月缩短到最快 2 个月, 但私有化还是很难
![]()
客户沟通重点
客户选择标准
市场推广节奏
![]()
在过去的几天里,我回顾了这一年来 Swift、SwiftUI 以及 SwiftData 的演进。总的感觉是:惊喜虽不算多,但“成熟感”却在不经意间扑面而来。
毋庸置疑,Swift 今年的重头戏在于改善并发编程的体验。尽管新增的选项和关键字在短期内又给开发者带来了不小的困扰,但经过这几个月的讨论与实践,社区已经显现出逐渐总结出新范式实践路径的趋势。我不认为新范式被确立且广泛接受会是一个简单、迅速的过程,但或许再过一两年,开发者对 Swift 的讨论重心将从并发转向跨平台,届时 Swift 也将迈入全新的发展阶段。
今年 SwiftUI 的更新重心大多集中在 Liquid Glass 的适配上。受限于系统初期的实现,显示效果起初并不尽如人意,但在 iOS 26.2 版本发布后,性能与稳定性都有了显著改善。坦率地说,对于今年 SwiftUI 没有引入更多革命性的新功能,我个人是挺高兴的。这让框架团队和开发者都能获得一点喘息之机,去进一步消化这个框架。在现阶段,解决遗留问题、优化性能与稳定性,远比一味堆砌新特性更有意义。
“变化较小”在 SwiftData 身上体现得尤为明显。但我认为 SwiftData 今年的表现尤为值得肯定,特别是许多改进与新功能都向下适配到了更早的系统版本。真希望它在三年前初次发布时,就能具备现在的状态。尽管 SwiftData 目前仍缺失一些关键功能,但对于相当比例的项目而言,它已经足以胜任。有了这个稳固的基础,其未来几年在性能与功能上的提高非常值得期待。
对于 2025 年 Swift 三件套的交出的答卷,我个人是满意的,不知你的感受如何?
这是本年度的最后一期周报,由衷感谢各位一年的陪伴与厚爱。
祝大家新年快乐,Happy Coding!
🚀 《肘子的 Swift 周报》
每周为你精选最值得关注的 Swift、SwiftUI 技术动态
- 📮 立即订阅 | weekly.fatbobman.com 获取完整内容
- 👥 加入社区 | Discord 与 2000+ 开发者交流
- 📚 深度教程 | fatbobman.com 探索 200+ 原创文章
下个月(1 月 21 日 - 23 日),iOS Conf SG 将在新加坡举行。我也将前往现场,并作为嘉宾进行主题为 “Using SwiftUI as a Language” 的演讲——不仅关于代码,更是关于思维方式的转换。
如果你也在附近,或者计划前往,欢迎来现场打招呼!组委会专门为我的读者提供了优惠:Fatbobman 读者专属九折优惠链接
我一直认为,所谓的苹果生态是由很多的硬件、软件、服务、人文、气质等综合构建起来的。在这其中,CloudKit 无疑是非常重要的一环。而且对于开发者来说,用好 CloudKit 不仅可以给用户更好的体验,也能低成本的为自己的应用带来创新。
IceCream 作者 Cai Yue 分享他与 CloudKit 八年的开发历程:从 2017 年开源 IceCream 并获得 Apple 官方认可,到将 CloudKit 应用于 Music Mate 和 Setlists 等商业项目的实战经验。文章深入探讨了 CloudKit 的核心优势、关键局限以及进阶玩法。
这是一篇面向 Swift 社区的年度收官综述文章,由 Tim Sneath 和 Dave Lester 撰写,系统回顾了 2025 年 Swift 生态在语言特性、平台覆盖与社区建设方面的关键进展。
文章不仅总结了 Swift 6.2 在并发模型上通过更温和的默认策略降低使用门槛,同时继续推进 C++ 互操作与内存安全能力;更重要的是,从 Android、WASM、Windows、BSD、嵌入式到 AWS 等方向的持续投入,反复强化了一个清晰信号——Swift 已不再只是围绕 Apple 平台展开的语言。
或许你未必会认同其中的每一项变化,但在迈入第二个十年后的第一个年头里,Swift 依然交出了一份相当扎实的答卷。
几天前无意间在 Reddit 上看到的帖子,作者对 PM 轻易选择 SwiftUI 有所抱怨,认为其无法胜任他们一个七年前开发的应用转换。对于这个观点我不置可否,但评论区的走向却出乎意料——绝大多数参与者都坚定地站在了 SwiftUI 的一边。
大量开发者认为:
这个帖子展现了一个出乎我预料的现实:SwiftUI 在实际生产环境中的采用率比我们想象的高得多;开发者社区对 SwiftUI 的信心已经建立。在 2025 年底,“SwiftUI 难堪大任”的论调或许已经站不住脚了。
作为 SwiftUI 框架的推崇者,我既喜欢该框架,也很清楚它仍有很长的路要走。如果你仍在犹豫是否应该在 SwiftUI 上下功夫,或许可以看一下我在去年写的《几个常见的关于 SwiftUI 的误解》——这篇文章讨论的很多误解,恰好在这次 Reddit 讨论中得到了印证。
随着 Swift 6 时代的到来,开发者逐渐养成了一种惯性:要么让类型符合 Sendable,要么给它套上 @MainActor 或 actor。在这篇文章中,Matt Massicotte 提出了一个极具启发性的哲学:“非 Sendable 优先设计”。
这一思路的关键在于对“隔离(Isolation)”的重新认识:隔离本身是一种约束。当一个类型被标记为 @MainActor,它实际上就失去了在非 UI 环境下进行同步调用的自由度。相比之下,一个非隔离、非 Sendable 的普通类型反而具有更高的通用性——它可以被任意 Actor 持有,并在其内部安全地进行同步访问,同时也更容易遵循 Equatable 等基础协议,而无需处理跨隔离域带来的复杂性。
随着 Swift 引入 NonisolatedNonsendingByDefault,这种“非 Sendable 优先”的设计路径不再像过去那样笨重或别扭,反而逐渐显现出其优势:以更少的隔离、换取更清晰的语义与更低的架构负担。这或许并非适用于所有场景,但在 Swift 6 之后,它已经成为一种值得认真考虑的、符合语言直觉的“减法”方案。
传统的 SPM 依赖解析是基于 Git URL 的,Xcode 需要克隆整个 Git 仓库来获取版本信息和代码,这在依赖较多(如 Firebase)时非常耗时。而 Registry 是苹果定义的另一种规范:通过包的标识符(ID)直接下载特定版本的归档文件,跳过了繁重的 Git 操作。Tuist 最近宣布将其 Swift Package Registry 功能向所有开发者开放,最大的变化是现在无需登录或创建 Tuist 账号即可使用。
Lee Young-jun 实测发现,使用 Registry 后,依赖解析(Installation)时间缩短至原来的约 35%;但项目生成与构建阶段并未获得同等收益,甚至略有回退。在 GitHub Actions 中配合缓存使用时,二次构建的依赖安装时间则从 53s 降至 11s,优势主要体现在 CI 场景。
总体来看,Tuist Registry 并非“全流程加速器”,而是一个专注于依赖解析与缓存友好性的优化点。如果你的项目依赖数量庞大、CI 成本较高,它值得优先尝试。
很多开发者在处理 DispatchSourceTimer 时,最头疼的就是它那“易碎”的状态:调用顺序稍有不对便会引发闪退。ZhgChgLi 在本文中针对这种极其敏感的状态管理提出了工程化的解决方案。文章详尽列举了导致崩溃的五大常见场景(如重复 resume、suspend 状态下直接释放等),并分享了如何利用有限状态机 (FSM) 封装操作,从逻辑层屏蔽非法调用,同时配合私有串行队列确保多线程环境下的调用安全。
这是一篇引导读者从“写代码”转向“做设计”的实战案例。它不仅讲清了 GCD 定时器的正确使用方式,更展示了如何借助设计模式,将一个“危险”的底层 API,封装为语义清晰、使用安全、可长期维护的工业级组件。在 Swift Concurrency 日益成为主流的今天,理解并优雅地封装这些底层 GCD 工具,依然是高级 iOS 开发者的重要基本功。
苹果在上周开源了 SHARP (Sharp Monocular View Synthesis),一个能在不到 1 秒内将单张 2D 照片转换为 3D 场景的 AI 模型(模型大小 2.8 GB)。相比之前的最佳模型,视觉质量提升 25-34%,速度提升 1000 倍。
社区普遍认为 SHARP 可能用于未来版本的空间照片功能。目前 iOS 26 的 Spatial Scenes 使用 Neural Engine 进行深度重建,而 SHARP 采用更先进的 3D Gaussian Splatting 技术,质量显著提升。
模型支持 CPU/CUDA/MPS 运行,已有开发者在 M1/M2/M3 Mac 上成功运行。输出的 .ply 文件兼容各种 3DGS 查看器,Vision Pro 用户可通过 Metal Splatter 直接查看效果。
尽管苹果在通用语言大模型上不如竞争对手惊艳,但在垂直场景的 AI 模型上,凭借硬件深度整合与明确的应用导向,依然展现出强大的竞争力。
Oskar Groth (Sensei 作者)开源了 MaterialView,一个能够突破 NSVisualEffectView 限制的高度可定制毛玻璃视图库。通过逆向 Control Center 的实现,Oskar 实现了对模糊半径、饱和度、亮度和色调的完全控制,并撰写了详细的技术文章讲解实现原理。
与系统原生材质只能“选类型”不同,MaterialView 将模糊效果彻底参数化,允许开发者精确控制模糊半径、饱和度、亮度、tint 颜色与混合模式,并支持 active / inactive / emphasized / accessibility 等状态配置。这使得它非常适合用于侧边栏、浮层面板、工具窗口等对视觉一致性要求极高的场景。
该库同时支持 SwiftUI 与 AppKit,并提供了一个可实时调参的 Demo App,方便快速探索不同材质组合的效果。
需要注意的是,它依赖部分未公开的 Core Animation 能力(如
CABackdropLayer、CAFilter等)。尽管这些 API 多年来相当稳定,但仍存在未来系统版本变动的潜在风险。
![]()
如果本期周报对你有帮助,请:
🚀 拓展 Swift 视野
- 📮 邮件订阅 | weekly.fatbobman.com 获取独家技术洞察
- 👥 开发者社区 | Discord 实时交流开发经验
- 📚 原创教程 | fatbobman.com 学习 Swift/SwiftUI 最佳实践
Lua 5.5.0 已经正式发布。所以,skynet 的 Lua 版本也随之升级。
skynet 维护了一份修改版的 Lua ,允许在多个虚拟机之间共享函数原型。这可以节省初始化 Lua 服务的时间,减少内存占用。
跨虚拟机共享函数原型最困难的部分是函数原型会引用常量字符串,而 Lua 在处理短字符串时,需要在虚拟机内部做 interning 。所以 skynet 的这个 patch 主要解决的是正确处理被 interning 的短字符串和从外部导入的函数原型中包含的字符串共存的问题。具体方法记录在这篇 blog 中。
这个 patch 的副产品是允许在多个 Lua VM 间共享常量表。打了这个 patch 后,就可以使用 skynet.sharetable 这个库共享只读常量表了。
这次 Lua 5.5 的更新引入了 external strings 这个特性,已经大幅度减少了 Lua 加载字节码的速度。我比较倾向于在未来不再依赖额外的 patch 减少维护成本。所以建议新项目避免再使用共享常量表,减少对 patch 过的 Lua 版本的依赖。
Lua 5.5 基本上兼容 Lua 5.4 ,我认为绝大多数 skynet 项目都不需要特别改动。但在升级后,还是建议充分测试。注意:更新仓库后,需要用 make cleanall 清除 lua 的编译中间文件,强制 Lua 重新编译。直接 make clean 并不清理它们。
Lua 5.5 有几处更新我认为值得升级:
增加了 global 关键字。对减少拼写错误引起的 bug 很有帮助。skynet 自身代码暂时还没有使用,但后续会逐步添加。
分代 GC 的主流程改为步进式进行。过去版本如果采用分代模式,对于内存占用较大的服务,容易造成停顿。所以这类服务往往需要切换为步进模式。升级到 Lua 5.5 后,应该就不需要了。
新的不定长参数语法 ...args 可以用 table 形式访问不定长参数列表。以后可以简化一部分 skynet 中 Lua 代码的实现。
![]()
摘要:在并发编程的江湖里,当一个位高权重的任务被迫等待一个无名小卒时,会发生什么?Swift 6.2 带来的
Task Priority Escalation APIs就像是香香公主那惊心动魄的美貌,能让原本慵懒的后台任务瞬间“鸡犬升天”。本文将借大熊猫侯佩与香香公主的沙漠奇遇,为您解析 SE-0462 的奥秘。
回疆,赛里木湖畔的数字荒原。
这里是系统资源的边缘地带,网络带宽如同细细的涓流。大熊猫侯佩正蹲在一块虚拟的岩石上,第 10086 次尝试刷新他的“高德地图导航”。
“这该死的路痴属性……”侯佩焦躁地拍了拍自己圆润的脑袋,顺手摸了一把头顶那倔强的黑毛,“还好,发际线依然坚挺,绝对没有秃。只是这下载速度,比蜗牛爬还慢。”
在他的视野里,代表下载任务的进度条(Task)是一个穿着破烂羊皮袄的老头,正赶着一辆破破烂烂的牛车,在 background(后台)优先级的泥潭里慢悠悠地挪动。
![]()
突然,天地变色。远处的数据流卷起狂沙,一支装备精良、杀气腾腾的皇家骑兵队(高优先级任务)呼啸而来,却被这辆破牛车死死挡在了单行道上。
骑兵队的为首者刚要发怒,却见那破牛车旁,不知何时站了一位白衣少女。她鬓边插着一朵天山雪莲,肌肤胜雪,虽然只是静静站着,却让周围狂暴的 CPU 周期瞬间变得温柔起来。
她是香香公主。
![]()
当那位皇家骑兵统领(Main Actor)看到香香公主竟然也在等待这辆牛车时,他立刻下令:“传令下去!给这破车换上法拉利的引擎!全军护送!谁敢让公主多等一秒,提头来见!”
刹那间,那辆原本属于 background 优先级的牛车,瞬间获得了 high 优先级的加持,快得连影子都看不清。
在本次穿越大冒险中,您将学到如下内容:
侯佩目瞪口呆,嘴里的竹笋掉在了地上:“这就叫……一人得道,鸡犬升天?这难道就是传说中的 Priority Escalation(优先级提升)?”;)
![]()
在 Swift 的并发世界里,这叫 “优先级反转(Priority Inversion)”的自动消解。
香香公主(高优先级任务)需要等待那个破老头(低优先级任务)的结果(比如 Data Race 里的锁,或者是 await 一个结果)。如果系统不干预,高贵的公主就要在这个“低贱”的队列里无限期等待,这显然不符合皇家(UI 响应性)的体面。
于是,Swift 运行时会自动把那个老头的优先级提升,让他暂时拥有和公主一样的地位,直到他把事情做完。
SE-0462 赋予了我们监控这种“飞升”现象的能力,甚至允许我们手动干预。
![]()
“虽然飞升很爽,但那个赶车的老头得知道自己被‘提拔’了啊,不然他还以为自己在逛花园呢。”侯佩捡起竹笋,若有所思。
Swift 6.2 引入了 withTaskPriorityEscalationHandler,让任务能够感知自己是否“被动”变强了。
侯佩看着香香公主正在试图从一个慢速服务器获取最新的食谱(她最近想学做竹笋炒肉喂侯佩),于是写下了这段代码:
// 创建一个中等优先级 (medium) 的任务
let newsFetcher = Task(priority: .medium) {
// 🛡️ 使用处理程序包裹你的业务逻辑
try await withTaskPriorityEscalationHandler {
// 这里是任务原本要做的苦力活
// 比如去下载一个 JSON 数据
let url = URL(string: "https://hws.dev/messages.json")!
let (data, _) = try await URLSession.shared.data(from: url)
return data
} onPriorityEscalated: { oldPriority, newPriority in
// 🚨 这里的闭包会在优先级发生变化时被调用
print("天哪!公主在等我!我的优先级从 \(oldPriority) 飞升到了 \(newPriority)!")
print("兄弟们,抄家伙,开足马力干活了!")
}
}
香香公主眨着那双清澈如水的眼睛,好奇地问:“侯大哥,这意思是,一旦有人催这个任务,它自己就会知道?”
![]()
“没错。”侯佩解释道,顺便摆了一个自以为很帅的 Pose,“这就好比我在睡觉,如果只是普通人叫我,我理都不理;但如果是你叫我,我脑子里的这个 onPriorityEscalated 就会立刻触发,瞬间从‘死猪模式’切换到‘舔狗模式’……啊不,是‘战斗模式’。”
![]()
通常情况下,优先级提升是自动发生的(比如高优先级任务 await 了低优先级任务)。但有时候,我们作为架构师,需要扮演“陈家洛”的角色,手动去推一把。
Swift 6.2 允许我们使用 escalatePriority(to:) 来手动提升某个任务的优先级。
// 侯佩看着下载进度条太慢,实在忍不住了
// 他决定动用特权,手动把优先级拉满
newsFetcher.escalatePriority(to: .high)
香香公主有些担忧:“可是,如果我们把它提升到了 high,后来又觉得不重要了,能把它降回去吗?”
![]()
侯佩摇了摇头,神色变得严肃起来(虽然脸上还粘着竹笋渣):“妹子,江湖路是一条不归路。在 Swift 的任务调度里,优先级只能升,不能降。”
💡 技术要点: 你的
onPriorityEscalated回调可能会被触发多次。比如从low升到medium,再从medium升到high。但这就像武功境界,一旦突破,就回不到从前了。这是为了防止系统调度的震荡。
香香公主看着那些在数据流中奔跑的任务,问道:“那我们是不是应该把所有任务都手动设为最高级?这样大家都很开心呀。”
侯佩叹了口气,语重心长地说:“傻丫头,如果人人都是 VIP,那就没有 VIP 了。如果所有任务都是 high,那 CPU 就会像陷入‘红花会’内乱一样,谁也抢不到资源。”
![]()
官方建议(Note): 任务优先级提升通常是自动发生的,而且 Swift 做得很棒。虽然这个 API 给了我们手动的权力,但在绝大多数情况下,还是应该顺其自然,无为而治。除非你真的遇到了特殊的性能瓶颈。
就像香香公主的美,不需要刻意修饰,自然就能引得千军万马为之驻足。
夕阳西下,赛里木湖波光粼粼。
经过优先级的调整,数据终于下载完成了。侯佩看着手里高清的地图,终于确认了自己的位置——好吧,他离目的地还有三千公里,果然又走反了。
![]()
就在这时,系统中突然窜出一个黑影!
那是一个失控的后台任务(Rogue Task),它像是个疯子一样在内存里乱窜,消耗着宝贵的电量,却又不干正事。
“站住!”侯佩大喝一声,想要通过代码杀掉这个进程,“你是哪个部门的?叫什么名字?”
然而,那个任务只是留下一串乱码,继续狂奔。侯佩尴尬地发现,他创建这个任务的时候,忘记给它起名字了。在调试器里,它只是一个冷冰冰的内存地址。
![]()
“这就尴尬了,”侯佩挠了挠头,看着香香公主投来的疑惑目光,“我想教训它,却连它叫‘阿猫’还是‘阿狗’都不知道。”
香香公主轻轻一笑,指着下一章的预告说:“侯大哥,别急,听说下一招能给它们每人发一张身份证。”
(欲知后事如何,且看下回分解:Task Naming —— 也就是给任务起个响当当的绰号,好让你在它闯祸时能指名道姓地骂它。)
![]()
做独立产品这件事,说起来容易,真动手了才知道水有多深。这是一个独立开发者将职场小需求变成主力产品的真实故事。我们将跟随 Zipic 作者十里的视角,一起回顾产品从 0 到 1 的全过程。本篇聚焦产品设计与决策思考。
Mac App Store 固然使用简单,但可能并不适合所有的产品。本文中,我们将跟随 Zipic 作者十里的视角,来解决一款 macOS 独立应用的分发与售卖问题。
图片压缩软件还有什么技术难点?本文充满了硬核、实用的 macOS 开发经验,从 SwiftUI 的组件适配到 Core Graphics 的底层应用,从 Raycast 扩展的集成到 PDF 压缩的实现,不仅解决了性能瓶颈,更让原生体验达到了极致。
近期AI赛道异常“内卷”,硅谷甚至出现了“996”乃至“007”的新闻。AI在编码(如Cursor、Anthropic)和解决复杂问题(如ACM竞赛夺冠、IMO金牌水平)上的表现,似乎已超越大部分程序员。
这引发了一个普遍的焦虑:AI coding + AI debug 是否将形成一个完美闭环,从而替代程序员?
然而,在快手这样拥有亿级日活(DAU)的复杂业务场景中,我们的实践表明,需要冷静看待这一议题。AI并非替代者,而是团队产出的放大器。今天的分享,将围绕快手在性能稳定性领域如何利用AI进行智能诊断与实践,揭示AI在真实工业场景中扮演的角色。![]()
快手移动端稳定性建设经历了四个清晰的阶段:![]()
每个阶段都基于上一阶段的成果进行迭代,这与移动互联网发展的节奏同步。
尽管硬件性能(如iPhone)已提升百倍,软件架构(如GMPC)演进多年,但大前端的性能稳定性问题远未解决。复杂性体现在多个维度:![]()
从算法复杂度视角看,我们解决问题的“算法”本质未变,但“输入”却因业务增长和技术栈扩张(如新增鸿蒙)而急剧增加,导致问题规模(年报警事件超150起,必解问题超2000个)庞大。
![]()
我们观察到团队中一个普遍的困境:
AI的机遇正在于此——它有望成为打破这一循环的放大器,将专家经验沉淀和复制,赋能整个团队。
AI x 稳定性:整体策略与架构设计![]()
![]()
稳定性体系覆盖研发生命周期多个环节(开发、测试、监控、排障、应急处置)。我们选择从 “问题处置” 切入,因为这里是消耗研发时间最多的“重灾区”。问题处置又可细分为:
我们判断,AI在工程领域的落地形态将是 “Agent(智能体)” 。因此,我们从一开始就以可扩展的Agent框架为基础进行架构设计。
我们的性能稳定性Agent架构分为四层:![]()
AI辅助根因排障——从“破案”到“自动修复”
![]()
一个典型的NPE(空指针)崩溃,堆栈全是系统代码,无业务逻辑。它仅在特定活动场景下偶发,现场信息缺失,线下难以复现。直接将此堆栈扔给ChatGPT,它能解决吗? 实践表明,非常困难。
调研数据显示,96%的研发认为日常排障有痛点,其中69%认为现场信息太少,50%认为日志太多。行业数据也指出,开发者35-50%的时间花在调试验证上。这印证了我们的新范式:“Code is cheap, show me the (bug-free) fix.”![]()
排障本质上是逆向推理的认知活动,与侦探破案高度相似:![]()
AI的能力在此链条上并非均匀:
我们自研了Holmes排障工具,核心思路是动静结合:![]()
特别是Holmes UI视图,它能在崩溃时捕获:![]()
面对Holmes采集的海量、复杂信息,我们通过Agent编排来让AI消化:![]()
![]()
提升准确率的关键在于“上下文工程”,目标是达到“适定问题”状态:
AI x 根因排障:效果展示![]()
![]()
性能分析的火焰图数据量巨大(十几秒可能产生60MB数据),分析门槛高、效率低、易遗漏。![]()
我们的方案是:![]()
AI加速故障应急处置——与时间赛跑
![]()
以iOS 26升级导致大量历史版本App崩溃为例。传统手段各有局限:![]()
应急处置的核心在于时效性,必须与故障扩散赛跑。
我们自研了Ekko安全气垫系统,其核心思想是:在崩溃发生后、应用闪退前,动态修改程序执行流,让其“跳回”安全状态继续执行,实现类似游戏“R技能”的时光倒流效果。![]()
Ekko 崩溃阻断:覆盖所有崩溃类型![]()
![]()
Ekko是 “售后方案” ,只在崩溃发生时触发,避免了无异常用户端的性能损耗,保证了安全性。
即使有了Ekko,配置和使用它依然复杂(需指定跳转地址、恢复上下文等),在紧急状态下人工操作易出错、易遗漏。![]()
我们引入故障应急处置Agent,实现:
![]()
![]()
在“黑天鹅”事件中(如某次误操作导致千万级崩溃),AI冷静、全面的分析能力,能有效避免人在高压下的决策失误。![]()
![]()
回到最初的焦虑,Linus Torvalds的观点值得深思:“代码的审查和维护本身就充满挑战。” AI不会改变这一本质,而是帮助我们更好地应对它。
我们的结论是:
在快手亿级DAU的复杂战场上,AI × 性能稳定性的探索刚刚启航。未来将是人机协同(Human in/on the Loop) 的深度结合。我们应积极拥抱AI,将其作为强大的杠杆,释放工程师的创造力,共同应对大前端领域越发复杂的稳定性挑战,奔赴星辰大海。![]()
在过去的几天里,我回顾了这一年来 Swift、SwiftUI 以及 SwiftData 的演进。总的感觉是:惊喜虽不算多,但“成熟感”却在不经意间扑面而来。
自2017年Google提出Transformer后,AI在各领域实现突破。
2023年起,大语言模型商业化加速,年增速达30倍以上。
AICoding 领域是发展最快的学科之一,因为反馈机制明确(“对就是对,错就是错”)。
![]()
![]()
| 阶段 | 描述 | 人机角色 | 典型能力 |
|---|---|---|---|
| L1 | 人类主导,Agent实时辅助 | 人主导,AI辅助 | 代码提示(如GitHub Copilot) |
| L2 | 人类布置任务,Agent生成代码 | 人布置单一任务 | 单一任务代码生成 |
| L3 | 人类设定范围,Agent推进多环节流程 | 人设定范围,AI推进流程 | 生成方案 + 生成代码 |
| L4 | 人类输入PRD,Agent端到端交付 | 人输入PRD,AI端到端交付 | 需求解析 + 架构设计 + 编码 |
| L5 | 人定义目标,多Agent分工协作 | 人定义目标,多AI协作 | 多Agent模拟完整软件团队 |
![]()
![]()
“前端开发像是在标准化、开放的乐高环境中工作;客户端则像是在碎片化、半封闭的复杂系统中进行精密工程。”
![]()
![]()
![]()
科学评测体系的建立:从SWE-bench到Mobile-SWE-bench
**SWE-bench**:由普林斯顿与芝加哥大学推出,基于真实GitHub Issue,要求AI生成PR来修复问题,以单元测试通过率为评测标准。
局限性:侧重于Bug修复而非功能实现,项目多集中后端,缺少移动端特有考量(如UI还原、多模态输入)。
移动端评测 Mobile-SWE-bench:
![]()
把整个需求的测评级分成三类, 可以看到哪怕是业界比较火的一些模型放在测试集中表现也
一般, 30%已经算是很高了.
为什么这些 Code Agent 都表现不佳?![]()
PRD 是 “产品需求文档”(Product Requirements Document) 的缩写. 在传统的软件和产品开发流程中,PRD 是一个核心文档。它由产品经理(或业务分析师)撰写,详细描述了一个产品、功能或项目应该做什么、为谁而做以及要达到什么目标。
一个典型的 PRD 通常包含:
这里探讨的是一种前沿的、由AI驱动的开发范式。在这个范式中,PRD 的角色发生了根本性的转变:
![]()
![]()
一个实际的业务场景和需求分析, 用户登录页面,包含手机号输入框、密码框、登录按钮、忘记密码链接及成功/失败反馈。
流程:
![]()
端到端提升:定制化Code Agent在Easy/Medium/Hard需求集上,比通用Agent(如GPT-5、Claude)提升约10%。
![]()
客户端实现PRD到代码的完全直出目前尚不可能,但可通过“评测驱动子能力提升”路径逐步推进。
应关注四个关键课题:
1. 如何构建科学的端到端评测体系?
2. PRD该如何拆解、拆解到什么粒度?
3. 如何保证UI高还原度出码?
4. 如何实现组件的智能召回与闭环迭代?
![]()
Swift 与 Objective-C 最大的区别之一,就是 Optional(可选值)机制。
它从语言层面解决了“空指针崩溃”的问题,但如果使用不当,也可能引入新的 Crash。
在日常开发中,我们经常看到下面这些写法:
var name: String?
var age: Int!
let title = text ?? "默认标题"
imageView.image = UIImage(named: imgName ?? "")
本文将系统讲解 ?、!、?? 的含义、区别、适用场景与工程级最佳实践,帮助你在 Swift 项目中写出更安全、更专业的代码。
在 Swift 中,Optional 表示一个变量「可能有值,也可能为 nil」。
var name: String? = "Hello World"
name = nil
等价理解为:
「这个变量可以为空,编译器强制你在使用前处理好为空的情况」
这与 OC 中的 id、NSString * 完全不同,是 编译器层面的安全保障。
从语言层面来看:
let value: Int?
本质上相当于一个枚举:
enum Optional<Int> {
case some(Int)
case none
}
也正是因为这样,Swift 不允许你直接使用 Optional 的值。
? —— 可选类型(Optional)var username: String?
表示:
nil
let len = username.count ❌ 编译错误
你必须 先解包(unwrap) ,才能使用。
if let
if let name = username {
print(name.count)
} else {
print("username 为 nil")
}
guard let
func printName(_ username: String?) {
guard let name = username else {
print("name 为 nil,提前返回")
return
}
print(name.count)
}
! —— 强制解包(Force Unwrap)var age: Int! = 18
表示:
“我确信这个变量在使用时一定不为 nil”
swift
print(age + 1) // 看起来像非 Optional
风险点
age = nil
print(age + 1) // 运行时崩溃
IBOutlet
生命周期受控变量
@IBOutlet weak var titleLabel: UILabel!
原因:
某些依赖注入后一定存在的对象
var userName: String!
print(userName.count) // 非常危险 ❌
总结一句话:
!是写给“你未来的自己看的承诺”,一旦违背就会 Crash
?? —— 空值合并运算符(Nil-Coalescing)let displayName = username ?? "匿名用户"
含义:
如果
username不为 nil,使用它
否则使用"匿名用户"
titleLabel.text = model.title ?? "暂无标题"
func loadData(page: Int?) {
let currentPage = page ?? 1
print(currentPage)
}
let data = Data(base64Encoded: base64Str ?? "")
? + ?. —— 可选链(Optional Chaining)let length = username?.count
返回值类型:
Int?
如果 username == nil:
.count
let city = user?.profile?.address?.city
struct User {
let name: String?
let age: Int?
}
func showUser(_ user: User?) {
guard let user else {
print("user 不存在")
return
}
let name = user.name ?? "未知"
let age = user.age ?? 0
print("(name),(age) 岁")
}
滥用 !
user!.name!.count ❌
嵌套 if let 过深
if let a = a {
if let b = b {
if let c = c {
...
}
}
}
更优写法:
guard let a, let b, let c else { return }
Swift 的 Optional 不是语法糖,而是 逼着你在代码层面提前思考风险。
如有说错的地方,满发指正相互学习,谢谢~
这边文章是 Qcon 上海站 2025 来自支付宝的KMP分享总结, 主题为”AI Agent编码助手实战:面向KMP原生跨端实现研发提效”
文章参考: 支付宝 MYKMP 原生跨平台解决方案
文章参考 : AI Agent 编码助手实战:面向 KMP 原生跨端实现研发提效
本次分享首先对相关核心技术术语进行说明:
| 术语名称 | 术语介绍 |
|---|---|
| KMP(Kotlin Multiplatform) | JetBrains 基于 Kotlin 推出的一套跨端框架,允许开发者使用 Kotlin 语言编写一次业务逻辑代码,然后将其编译成适用于多个平台的原生应用、Web 应用或服务端应用。 |
| CMP(Compose Multiplatform) | JetBrains 提供的一套基于 Compose 基础库的声明式 UI 跨端框架,支持在 Android、iOS、桌面和 Web 开发共享 UI。 |
| 支付宝 KMP 原生跨端 | 在 “Kotlin + Compose Multiplatform” 的基础上,为支付宝终端开发者提供一整套完善的跨端框架能力。 |
| AntUI 组件库 | 基于 Compose 编写的支付宝 UI 组件库,包含丰富且风格统一的 UI 组件。 |
| OHOS、Harmony | OHOS 是鸿蒙项目的开源操作系统基底,而 HarmonyOS 是基于 OHOS 打造的商用智能终端操作系统。 |
KMP原生跨端的核心优势在于显著减少为不同平台重复开发的工作量,同时能保持各平台原生的最佳用户体验。
支付宝在基础KMP架构上进行了深度扩展,构建了增强型跨端框架,其分层架构如下:![]()
目前,该KMP跨端架构已在支付宝多个核心业务场景(如“我的”、理财、直播、消息页,以及出行服务、健康管家等独立APP)中落地,覆盖安卓、iOS、鸿蒙三大平台,均实现了与原生开发对标的高性能体验。整体已支撑亿级PV,成为支付宝内重点发展的主流原生跨端技术栈。
尽管KMP技术带来效率提升,但其研发全流程仍存在若干痛点:![]()
针对上述痛点,我们对现有AI编码工具进行了调研,结论是:目前缺乏一款能与客户端基础框架深度结合、支持KMP技术栈、并适配支付宝终端研发工程体系的专用编码助手。![]()
具体对比如下:
因此,我们期望打造一款具备跨端特色的AI编程伙伴,以解决实际研发问题,提升效率。
构建了KMP的编码助手,其核心目标是运用AI技术为KMP开发带来“二次加速”。以下从方案构思到核心功能实现进行剖析。![]()
![]()
项目初期,我们从四个维度评估了可行性:
整体架构分为三层:![]()
为帮助开发者快速上手Compose UI,我们提供了两种生码方案:
![]()
启动链路:通过Node服务连接Sketch应用、IDE插件和Webview。
设计稿转IR:将设计稿元素转换为中间表示(IR),包括类型、参数、样式及视图层级信息。
IR转Compose:依据规则将IR映射为Compose组件与修饰符。
优化与输出:通过人工规则与模型二次优化,对生成的代码进行组件化、数据驱动等重构,输出高质量的生产级代码。
再来整体对比下,从原始设计稿,到原始 Compose UI,再到模型二次优化的界面效果。这里能感受到模型二次优化后,基本上能够还原设计稿组件,但是代码更加直接可用。![]()
我们自然而然的会想有更加简便,且支持高阶 UI 组件库的方案,就是图生码。
数据构造, 构建自动化流程,通过大模型生成随机Compose代码→渲染截图→生成精确的图文数据对,解决了训练数据匮乏问题。
模型训练, 采用LoRA(低秩适应)等参数高效微调技术,对多模态大模型进行SFT(监督微调)和强化学习,使其获得精准的UI页面解析能力,能识别AntUI高阶组件。
后处理增强, 针对模型幻觉导致的位置、颜色、布局偏差,结合传统图像算法进行校准,提升输出IR的精确度。
为帮助开发者快速上手KMP逻辑开发与解决线上问题,我们构建了基于RAG和MCP的智能助手。
背景
开发者常咨询这三类问题:
RAG 检索问答基本流程:![]()
针对开发者不熟悉多端尤其是鸿蒙平台的痛点,我们通过定制Agent工作流解决问题:
KMP 模块在三端平台构建失败,无法定位原因
KMP 核心产物需要同时三端构建,一旦出现构建失败问题,传统排查方式效率比较低下,花费的时间从几分钟到一小时不等。![]()
这里我们通过 Agent 工作流的方式,帮助开发者主动触发构建,利用 KMP 日志分析脚本,提取关键日志,再结合现有构建知识库进行召回,最终由模型整理组织答案。从而加快构建失败问题的排查速度。![]()
开发者可以直接将闪退日志输入给 Agent ,Agent 会触发闪退分析的工作流,先用 KMP 堆栈反解工具提取关键内容并解析,再将解析结果返回给 Agent,由 Agent 结合当前的项目代码上下文,给出原因和解决方案。![]()
如何将众多工具(堆栈分析、模板生成、文件操作等)整合到大Agent中?我们采用了本地MCP(Model Context Protocol)路由机制。
KMP编码助手将持续优化与创新,重点方向包括:![]()
最后再来看一下AI Agent面向软件开发整个的生命周期,你可以发现 agent正在以一个非常非常快的速度改变我们的工作方式. 从构思到开发到落地, agent在每一个环节都会驱动我们来进行一些创新.
比如
简而言之, AIAgent正在引领一场软件开发的全新的变革, 这将会深深地改变我们之后的一个工作方式, 那在这里呢也也祝愿大家能够在AI人工智能席卷而来的浪潮里面抓住机遇勇于创新, 说不定会有意想不到的惊喜和收获.
dart
// 定义 Mixin
mixin LoggerMixin {
String tag = 'Logger';
void log(String message) {
print('[$tag] $message');
}
void debug(String message) {
print('[$tag] DEBUG: $message');
}
}
mixin ValidatorMixin {
bool validateEmail(String email) {
return RegExp(r'^[^@]+@[^@]+.[^@]+').hasMatch(email);
}
bool validatePhone(String phone) {
return RegExp(r'^[0-9]{10,11}$').hasMatch(phone);
}
}
// 使用 Mixin
class UserService with LoggerMixin, ValidatorMixin {
void registerUser(String email, String phone) {
if (validateEmail(email) && validatePhone(phone)) {
log('用户注册成功: $email');
} else {
debug('注册信息验证失败');
}
}
}
void main() {
final service = UserService();
service.registerUser('test@example.com', '13800138000');
}
dart
mixin AuthenticationMixin {
// 抽象方法 - 强制混入类实现
Future<String> fetchToken();
// 具体方法 - 可以使用抽象方法
Future<Map<String, dynamic>> getProfile() async {
final token = await fetchToken();
log('使用 token: $token 获取用户资料');
return {'name': '张三', 'token': token};
}
void log(String message) {
print('[Auth] $message');
}
}
class ApiService with AuthenticationMixin {
@override
Future<String> fetchToken() async {
// 实现抽象方法
await Future.delayed(Duration(milliseconds: 100));
return 'jwt_token_123456';
}
}
void main() async {
final api = ApiService();
final profile = await api.getProfile();
print('用户资料: $profile');
}
on 关键字限制 Mixin 范围dart
// 基类
abstract class Animal {
String name;
Animal(this.name);
void eat() {
print('$name 正在吃东西');
}
}
// 只能用于 Animal 及其子类的 Mixin
mixin WalkerMixin on Animal {
void walk() {
print('$name 正在行走');
eat(); // 可以访问宿主类的方法
}
}
mixin SwimmerMixin on Animal {
void swim() {
print('$name 正在游泳');
}
}
// 正确使用
class Dog extends Animal with WalkerMixin {
Dog(String name) : super(name);
void bark() {
print('$name: 汪汪!');
}
}
// 错误使用(编译错误):
// class Robot with WalkerMixin {} // 错误:WalkerMixin 只能用于 Animal
void main() {
final dog = Dog('小黑');
dog.walk(); // 小黑 正在行走
dog.bark(); // 小黑: 汪汪!
dog.eat(); // 小黑 正在吃东西
}
dart
// 功能模块化 Mixin
mixin ApiClientMixin {
Future<Map<String, dynamic>> get(String url) async {
print('GET 请求: $url');
await Future.delayed(Duration(milliseconds: 100));
return {'status': 200, 'data': '响应数据'};
}
}
mixin CacheMixin {
final Map<String, dynamic> _cache = {};
void cacheData(String key, dynamic data) {
_cache[key] = data;
}
dynamic getCache(String key) => _cache[key];
}
mixin LoggingMixin {
void logRequest(String method, String url) {
print('[${DateTime.now()}] $method $url');
}
}
// 组合多个 Mixin
class NetworkService with ApiClientMixin, CacheMixin, LoggingMixin {
Future<Map<String, dynamic>> fetchWithCache(String url) async {
final cached = getCache(url);
if (cached != null) {
print('使用缓存数据');
return cached;
}
logRequest('GET', url);
final response = await get(url);
cacheData(url, response);
return response;
}
}
void main() async {
final service = NetworkService();
final result1 = await service.fetchWithCache('/api/user');
final result2 = await service.fetchWithCache('/api/user'); // 第二次使用缓存
}
dart
mixin A {
String message = '来自A';
void show() {
print('A.show(): $message');
}
void methodA() {
print('A.methodA()');
}
}
mixin B {
String message = '来自B';
void show() {
print('B.show(): $message');
}
void methodB() {
print('B.methodB()');
}
}
mixin C {
String message = '来自C';
void show() {
print('C.show(): $message');
}
}
// 父类
class Base {
String message = '来自Base';
void show() {
print('Base.show(): $message');
}
}
// 混入顺序:Base -> A -> B -> C(最后混入的优先级最高)
class MyClass extends Base with A, B, C {
// 可以通过super调用线性化链中的方法
@override
void show() {
super.show(); // 调用C的show方法
print('MyClass.show() 完成');
}
}
// 线性化顺序验证
class AnotherClass with C, B, A {
// 顺序:Object -> C -> B -> A
void test() {
show(); // 调用A的show(最后混入)
print(message); // 输出:来自A
}
}
void main() {
print('=== MyClass 测试 ===');
final obj1 = MyClass();
obj1.show(); // 调用C.show(),因为C最后混入
print(obj1.message); // 输出:来自C
print('\n=== AnotherClass 测试 ===');
final obj2 = AnotherClass();
obj2.test();
print('\n=== 方法调用链 ===');
obj1.methodA(); // 可以调用
obj1.methodB(); // 可以调用
// 验证类型
print('\n=== 类型检查 ===');
print(obj1 is Base); // true
print(obj1 is A); // true
print(obj1 is B); // true
print(obj1 is C); // true
}
dart
class Base {
void execute() => print('Base.execute()');
}
mixin Mixin1 {
void execute() {
print('Mixin1.execute() - 开始');
super.execute();
print('Mixin1.execute() - 结束');
}
}
mixin Mixin2 {
void execute() {
print('Mixin2.execute() - 开始');
super.execute();
print('Mixin2.execute() - 结束');
}
}
mixin Mixin3 {
void execute() {
print('Mixin3.execute() - 开始');
super.execute();
print('Mixin3.execute() - 结束');
}
}
class MyService extends Base with Mixin1, Mixin2, Mixin3 {
@override
void execute() {
print('MyService.execute() - 开始');
super.execute(); // 调用链:Mixin3 -> Mixin2 -> Mixin1 -> Base
print('MyService.execute() - 结束');
}
}
void main() {
final service = MyService();
service.execute();
// 输出顺序:
// MyService.execute() - 开始
// Mixin3.execute() - 开始
// Mixin2.execute() - 开始
// Mixin1.execute() - 开始
// Base.execute()
// Mixin1.execute() - 结束
// Mixin2.execute() - 结束
// Mixin3.execute() - 结束
// MyService.execute() - 结束
}
dart
// 可序列化接口
abstract class Serializable {
Map<String, dynamic> toJson();
}
// Mixin 提供序列化功能
mixin JsonSerializableMixin implements Serializable {
@override
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
// 使用反射获取所有字段(实际项目中可能需要 dart:mirrors 或代码生成)
// 这里简化处理
for (final field in _getFields()) {
json[field] = _getFieldValue(field);
}
return json;
}
List<String> _getFields() {
// 实际实现应使用反射
return [];
}
dynamic _getFieldValue(String field) {
// 实际实现应使用反射
return null;
}
}
// 使用 Mixin 增强类的功能
class User with JsonSerializableMixin {
final String name;
final int age;
User(this.name, this.age);
@override
List<String> _getFields() => ['name', 'age'];
@override
dynamic _getFieldValue(String field) {
switch (field) {
case 'name': return name;
case 'age': return age;
default: return null;
}
}
}
void main() {
final user = User('张三', 25);
print(user.toJson()); // {name: 张三, age: 25}
}
dart
// 服务定位器 Mixin
mixin ServiceLocatorMixin {
final Map<Type, Object> _services = {};
void registerService<T>(T service) {
_services[T] = service;
}
T getService<T>() {
final service = _services[T];
if (service == null) {
throw StateError('未找到服务: $T');
}
return service as T;
}
}
// 网络服务
class NetworkService {
Future<String> fetchData() async {
await Future.delayed(Duration(milliseconds: 100));
return '网络数据';
}
}
// 数据库服务
class DatabaseService {
Future<String> queryData() async {
await Future.delayed(Duration(milliseconds: 50));
return '数据库数据';
}
}
// 使用 Mixin 的应用类
class MyApp with ServiceLocatorMixin {
MyApp() {
// 注册服务
registerService(NetworkService());
registerService(DatabaseService());
}
Future<void> run() async {
final network = getService<NetworkService>();
final database = getService<DatabaseService>();
final results = await Future.wait([
network.fetchData(),
database.queryData(),
]);
print('结果: $results');
}
}
void main() async {
final app = MyApp();
await app.run();
}
dart
// 1. 单一职责的 Mixin
mixin EquatableMixin<T> {
bool equals(T other);
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is T && equals(other);
@override
int get hashCode => toString().hashCode;
}
mixin CloneableMixin<T> {
T clone();
}
// 2. 带生命周期的 Mixin
mixin LifecycleMixin {
bool _isInitialized = false;
void initialize() {
if (!_isInitialized) {
_onInit();
_isInitialized = true;
}
}
void dispose() {
if (_isInitialized) {
_onDispose();
_isInitialized = false;
}
}
// 钩子方法
void _onInit() {}
void _onDispose() {}
}
// 3. 可观察的 Mixin
mixin ObservableMixin {
final List<Function()> _listeners = [];
void addListener(Function() listener) {
_listeners.add(listener);
}
void removeListener(Function() listener) {
_listeners.remove(listener);
}
void notifyListeners() {
for (final listener in _listeners) {
listener();
}
}
}
// 使用多个 Mixin 的模型类
class UserModel with EquatableMixin<UserModel>, CloneableMixin<UserModel>, ObservableMixin {
String name;
int age;
UserModel(this.name, this.age);
@override
bool equals(UserModel other) =>
name == other.name && age == other.age;
@override
UserModel clone() => UserModel(name, age);
void updateName(String newName) {
name = newName;
notifyListeners(); // 通知观察者
}
@override
String toString() => 'User(name: $name, age: $age)';
}
void main() {
final user1 = UserModel('Alice', 30);
final user2 = UserModel('Alice', 30);
final user3 = user1.clone();
print('user1 == user2: ${user1 == user2}'); // true
print('user1 == user3: ${user1 == user3}'); // true
// 添加监听器
user1.addListener(() {
print('用户数据已更新!');
});
user1.updateName('Bob'); // 触发监听器
}
| 特性 | 说明 |
|---|---|
| 定义方式 | 使用 mixin 关键字定义 |
| 使用方式 | 使用 with 关键字混入到类中 |
| 继承限制 | 每个类只能继承一个父类,但可以混入多个 Mixin |
| 实例化 | Mixin 不能被实例化,只能被混入 |
| 构造函数 | Mixin 不能声明构造函数(无参构造函数除外) |
| 抽象方法 | 可以包含抽象方法,强制宿主类实现 |
| 范围限制 | 可以使用 on 关键字限制 Mixin 只能用于特定类 |
| 线性化顺序 | 混入顺序决定方法调用优先级(最后混入的优先级最高) |
| 类型系统 | Mixin 在类型系统中是透明的,宿主类拥有 Mixin 的所有接口 |
横切关注点(Cross-cutting Concerns)
功能组合(Feature Composition)
接口增强(Interface Enhancement)
代码复用(Code Reuse)
Mixin 后缀,如 LoggerMixin
| 概念 | 与 Mixin 的区别 |
|---|---|
| 抽象类 | 可以有构造函数、可以有状态;Mixin 不能有构造函数 |
| 接口 | 只定义契约,不提供实现;Mixin 可以提供实现 |
| 扩展方法 | 在类外部添加方法;Mixin 在类内部添加 |
| 继承 | 单继承,强调 "is-a" 关系;Mixin 强调 "has-a" 或 "can-do" 关系 |
Mixin 是 Dart 语言中非常强大的特性,合理使用可以让代码更加模块化、可复用和可维护。
精准回答:
"Mixin 是 Dart 中一种代码复用机制,它允许一个类通过 with 关键字混入一个或多个独立的功能模块。Mixin 的主要作用是解决 Dart 单继承的限制,实现类似多继承的效果,让代码更加模块化和可复用。"
加分点:
精准回答(表格对比):
| 特性 | Mixin | 继承 | 接口 |
|---|---|---|---|
| 关系 | "具有" 功能 (has-a) | "是一个" (is-a) | "能做什么" (can-do) |
| 数量 | 可多个 | 单继承 | 可实现多个 |
| 实现 | 可包含具体实现 | 可包含具体实现 | 只定义契约 |
| 构造函数 | 不能有(除无参) | 可以有 | 不能有 |
| 关键字 | with |
extends |
implements |
详细补充:
"Mixin 强调的是功能组合,让类获得某些能力;继承强调的是父子关系;接口强调的是契约实现。Mixin 提供了比接口更灵活的实现复用,又避免了传统多继承的复杂性。"
精准回答:
"Mixin 的线性化顺序遵循以下规则:
with 关键字后 Mixin 的声明顺序,从左到右处理线性化算法: 深度优先,从左到右,不重复。"
示例说明:
dart
class A {}
mixin B {}
mixin C {}
class D extends A with B, C {}
// 线性化顺序:A → B → C → D
// 方法查找顺序:D → C → B → A → Object
精准回答:
"可以。Mixin 中包含抽象方法的主要作用是:
示例:
dart
mixin ValidatorMixin {
bool validate(String input); // 抽象方法
void validateAndProcess(String input) {
if (validate(input)) {
// 处理逻辑
}
}
}
on 关键字在 Mixin 中有什么作用?精准回答:
"on 关键字用于限制 Mixin 的使用范围,确保 Mixin 只能用于特定类型或其子类。主要有两个作用:
示例:
dart
mixin Walker on Animal {
void walk() {
move(); // 可以安全调用 Animal 的方法
}
}
// 只能用于 Animal 及其子类
精准回答:
"Dart 通过线性化顺序解决同名方法冲突:
super:调用线性化链中下一个实现这是编译时确定的,不会产生运行时歧义。"
冲突解决示例:
dart
class MyClass with A, B {
@override
void conflictMethod() {
// 调用特定 Mixin 的方法
super.conflictMethod(); // 调用 B 的实现
}
}
精准回答:
"Mixin 不能声明有参数的构造函数,只能有默认的无参构造函数。这是因为:
如果需要初始化逻辑,可以使用初始化方法配合调用。"
精准回答(结合实际经验):
"在实际项目中,我主要将 Mixin 用于:
横切关注点(Cross-cutting Concerns)
UI 组件功能组合
dart
class Button with HoverEffect, RippleEffect, TooltipMixin {}
服务层功能增强
dart
class ApiService with CacheMixin, RetryMixin, LoggingMixin {}
设计模式实现
精准回答:
优点:
缺点:
精准回答:
"应该使用 Mixin 的情况:
不应该使用 Mixin 的情况:
精准回答:
"两者都用于扩展类型功能,但适用场景不同:
| 方面 | Mixin | 扩展方法 |
|---|---|---|
| 作用域 | 类内部 | 类外部 |
| 访问权限 | 可访问私有成员 | 只能访问公开成员 |
| 适用性 | 需要状态时 | 纯函数操作时 |
| 使用方式 |
with 关键字 |
extension 关键字 |
扩展方法适合为现有类添加静态工具方法,Mixin 适合为类添加有状态的复杂功能。"
精准回答:
"处理 Mixin 依赖关系的几种策略:
on 限制:确保 Mixin 只在合适的上下文中使用最佳实践: 保持 Mixin 尽可能独立,依赖通过抽象定义。"
当被问到复杂问题时,展示对底层机制的理解:
示例回答:
"Mixin 的线性化机制实际上是编译时进行的,Dart 编译器会生成一个线性的类层次结构。从实现角度看,Mixin 会被编译为普通的类,然后通过代理模式将方法调用转发到正确的实现。"
"在我之前的电商项目中,我们使用 Mixin 实现了购物车的各种行为:
WithCacheMixin:缓存商品信息WithValidationMixin:验证库存和价格WithAnalyticsMixin:记录用户行为"在设计 Mixin 时,我遵循 SOLID 原则:
问题: "多个类混入同一个 Mixin 会共享状态吗?"
回答: "不会。每个实例都有自己的 Mixin 状态副本。Mixin 中的字段在编译时会复制到宿主类中,每个实例独立。"
问题: "如果多个 Mixin 都需要初始化怎么办?"
回答: "使用初始化方法模式:
dart
mixin Initializable {
void initialize() {
// 初始化逻辑
}
}
class MyClass with A, B {
void init() {
// 按需调用初始化
(this as A).initialize();
(this as B).initialize();
}
}
代码写得再好,没有自动化的流水线,就像法拉利引擎装在牛车上!!!
什么是持续集成与部署?简单说就是:
今天我们一起来搭建这条"代码流水线",让你的开发效率大幅提升!
先看看传统开发流程的痛点:
// 传统发布流程(手动版)
1. 本地运行测试(); // 某些测试可能忘记运行
2. 手动打包Android(); // 配置证书、签名、版本号...
3. 手动打包iOS(); // 证书、描述文件、上架截图...
4. 上传到测试平台(); // 找测试妹子要手机号
5. 收集反馈修复bug(); // 来回沟通,效率低下
6. 重复步骤1-5(); // 无限循环...
再看自动化流水线:
# 自动化发布流程(CI/CD版)
流程:
1. 推送代码到GitHub/Gitlab → 自动触发
2. 运行所有测试 → 失败自动通知
3. 打包所有平台 → 同时进行
4. 分发到测试环境 → 自动分发给测试人员
5. 发布到应用商店 → 条件触发
很多新手觉得CI/CD是"大公司才需要的东西",其实完全错了!它解决的是这些痛点:
问题1:环境不一致
本地环境: Flutter 3.10, Dart 2.18, Mac M1
测试环境: Flutter 3.7, Dart 2.17, Windows
生产环境: ???
问题2:手动操作容易出错 之前遇到过同事把debug包发给了用户,因为打包时选错了构建变体。
问题3:反馈周期太长 代码提交 → 手动打包 → 发给测试 → 发现问题 → 已经过了半天
graph LR
A[代码提交] --> B[持续集成 CI]
B --> C[持续交付 CD]
C --> D[持续部署 CD]
B --> E[自动构建]
B --> F[自动测试]
C --> G[自动打包]
C --> H[自动发布到测试]
D --> I[自动发布到生产]
style A fill:#e3f2fd
style B fill:#f3e5f5
style C fill:#e8f5e8
style D fill:#fff3e0
持续集成(CI):频繁集成代码到主干,每次集成都通过自动化测试
持续交付(CD):自动将代码打包成可部署的产物
持续部署(CD):自动将产物部署到生产环境
注意:两个CD虽然缩写一样,但含义不同。Continuous Delivery(持续交付)和 Continuous Deployment(持续部署)
我们以github为例,当然各公司有单独部署的gitlab,大同小异这里不在赘述。。。
GitHub Actions不是魔法,而是GitHub提供的自动化执行环境。想象一下:
graph LR
A[你的代码仓库] --> B[事件推送/PR]
B --> C[GitHub Actions服务器]
C --> D[分配虚拟机]
D --> E[你的工作流]
E --> F[运行你的脚本]
style A fill:#f9f,stroke:#333,stroke-width:1px
style C fill:#9f9,stroke:#333,stroke-width:1px
style E fill:#99f,stroke:#333,stroke-width:1px
核心组件解析:
# 工作流组件关系图
工作流文件 (.github/workflows/ci.yml)
├── 触发器: 什么情况下运行 (push, pull_request)
├── 任务: 在什么环境下运行 (ubuntu-latest)
└── 步骤: 具体执行什么 (安装Flutter、运行测试)
别被吓到,其实创建一个基础的CI流程只需要5分钟:
mkdir -p .github/workflows
# .github/workflows/flutter-ci.yml
name: Flutter CI # 工作流名称
# 触发条件:当有代码推送到main分支,或者有PR时
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
# 设置权限
permissions:
contents: read # 只读权限,保证安全
# 工作流中的任务
jobs:
# 任务1:运行测试
test:
# 运行在Ubuntu最新版
runs-on: ubuntu-latest
# 任务步骤
steps:
# 步骤1:检出代码
- name: Checkout code
uses: actions/checkout@v3
# 步骤2:安装Flutter
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: '3.10.x' # 指定Flutter版本
channel: 'stable' # 稳定版
# 步骤3:获取依赖
- name: Get dependencies
run: flutter pub get
# 步骤4:运行测试
- name: Run tests
run: flutter test
# 步骤5:检查代码格式
- name: Check formatting
run: flutter format --set-exit-if-changed .
# 步骤6:静态分析
- name: Analyze code
run: flutter analyze
git add .github/workflows/flutter-ci.yml
git commit -m "添加CI工作流"
git push origin main
推送到GitHub后,打开你的仓库页面,点击"Actions"标签,你会看到一个工作流正在运行!
graph TB
subgraph "GitHub Actions架构"
A[你的代码仓库] --> B[触发事件]
B --> C[GitHub Actions Runner]
subgraph "Runner执行环境"
C --> D[创建虚拟机]
D --> E[执行工作流]
subgraph "工作流步骤"
E --> F[检出代码]
F --> G[环境配置]
G --> H[执行脚本]
H --> I[产出物]
end
end
I --> J[结果反馈]
J --> K[GitHub UI显示]
J --> L[邮件/通知]
end
style A fill:#e3f2fd
style C fill:#f3e5f5
style E fill:#e8f5e8
style I fill:#fff3e0
核心概念解释:
功能上线前,全部功能手动测试耗时长,易出bug。加入自动化测试,有效减少bug率。
测试金字塔理论:
/\
/ \ E2E测试(少量)
/____\
/ \ 集成测试(适中)
/________\
/ \ 单元测试(大量)
/____________\
对于Flutter,测试分为三层:
单元测试是最基础的,测试单个函数或类:
# .github/workflows/unit-tests.yml
name: Unit Tests
on: [push, pull_request]
jobs:
unit-tests:
runs-on: ubuntu-latest
strategy:
matrix:
# 在不同版本的Flutter上运行测试
flutter: ['3.7.x', '3.10.x']
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Setup Flutter ${{ matrix.flutter }}
uses: subosito/flutter-action@v2
with:
flutter-version: ${{ matrix.flutter }}
- name: Get dependencies
run: flutter pub get
- name: Run unit tests
run: |
# 运行所有单元测试
flutter test
# 生成测试覆盖率报告
flutter test --coverage
# 上传覆盖率报告
bash <(curl -s https://codecov.io/bash)
单元测试:
// test/calculator_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:my_app/utils/calculator.dart';
void main() {
group('以Calculator测试为例', () {
late Calculator calculator;
// 准备工作
setUp(() {
calculator = Calculator();
});
test('两个正数相加', () {
expect(calculator.add(2, 3), 5);
});
test('正数与负数相加', () {
expect(calculator.add(5, -3), 2);
});
test('除以零应该抛出异常', () {
expect(() => calculator.divide(10, 0), throwsA(isA<ArgumentError>()));
});
});
}
集成测试测试多个组件的交互:
# 集成测试工作流
jobs:
integration-tests:
runs-on: macos-latest # iOS集成测试需要macOS
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Setup Flutter
uses: subosito/flutter-action@v2
- name: Get dependencies
run: flutter pub get
- name: Run integration tests
run: |
# 启动模拟器
# flutter emulators --launch flutter_emulator
# 运行集成测试
flutter test integration_test/
# 如果集成测试失败,上传截图辅助调试
- name: Upload screenshots on failure
if: failure()
uses: actions/upload-artifact@v3
with:
name: integration-test-screenshots
path: screenshots/
Widget测试测试UI组件:
jobs:
widget-tests:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Setup Flutter
uses: subosito/flutter-action@v2
- name: Install dependencies
run: |
flutter pub get
- name: Run widget tests
run: |
# 运行所有widget测试
flutter test test/widget_test.dart
# 或者运行特定目录
flutter test test/widgets/
sequenceDiagram
participant D as 开发者
participant G as Git仓库
participant CI as CI服务器
participant UT as 单元测试服务
participant WT as Widget测试服务
participant IT as 集成测试服务
participant R as 报告服务
participant N as 通知服务
D->>G: 推送代码
G->>CI: 触发Webhook
CI->>CI: 解析工作流配置
CI->>CI: 分配测试资源
par 并行执行
CI->>UT: 启动单元测试
UT->>UT: 准备环境
UT->>UT: 执行测试
UT->>UT: 分析覆盖率
UT-->>CI: 返回结果
and
CI->>WT: 启动Widget测试
WT->>WT: 准备UI环境
WT->>WT: 执行测试
WT->>WT: 截图对比
WT-->>CI: 返回结果
and
CI->>IT: 启动集成测试
IT->>IT: 准备设备
IT->>IT: 执行测试
IT->>IT: 端到端验证
IT-->>CI: 返回结果
end
CI->>CI: 收集所有结果
alt 所有测试通过
CI->>R: 请求生成报告
R->>R: 生成详细报告
R-->>CI: 返回报告
CI->>N: 发送成功通知
N-->>D: 通知开发者
else 有测试失败
CI->>R: 请求生成错误报告
R->>R: 生成错误报告
R-->>CI: 返回报告
CI->>N: 发送失败通知
N-->>D: 警报开发者
end
Android打包相对简单,但要注意签名问题:
# .github/workflows/android-build.yml
name: Android Build
on:
push:
tags:
- 'v*' # 只有打tag时才触发打包
jobs:
build-android:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Setup Java
uses: actions/setup-java@v3
with:
distribution: 'temurin'
java-version: '17'
- name: Setup Flutter
uses: subosito/flutter-action@v2
- name: Get dependencies
run: flutter pub get
- name: Setup keystore
# 从GitHub Secrets读取签名密钥
run: |
echo "${{ secrets.ANDROID_KEYSTORE }}" > android/app/key.jks.base64
base64 -d android/app/key.jks.base64 > android/app/key.jks
- name: Build APK
run: |
# 构建Release版APK
flutter build apk --release \
--dart-define=APP_VERSION=${{ github.ref_name }} \
--dart-define=BUILD_NUMBER=${{ github.run_number }}
- name: Build App Bundle
run: |
# 构建App Bundle
flutter build appbundle --release
- name: Upload artifacts
uses: actions/upload-artifact@v3
with:
name: android-build-${{ github.run_number }}
path: |
build/app/outputs/flutter-apk/app-release.apk
build/app/outputs/bundle/release/app-release.aab
iOS打包相对复杂,需要苹果开发者账号:
# .github/workflows/ios-build.yml
name: iOS Build
on:
push:
tags:
- 'v*'
jobs:
build-ios:
runs-on: macos-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Setup Flutter
uses: subosito/flutter-action@v2
- name: Install CocoaPods
run: |
cd ios
pod install
- name: Setup Xcode
run: |
# 设置Xcode版本
sudo xcode-select -s /Applications/Xcode_14.2.app
- name: Setup provisioning profiles
# 配置证书和描述文件
env:
BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE }}
P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
BUILD_PROVISION_PROFILE_BASE64: ${{ secrets.BUILD_PROVISION_PROFILE }}
run: |
# 导入证书
echo $BUILD_CERTIFICATE_BASE64 | base64 --decode > certificate.p12
# 创建钥匙链
security create-keychain -p "" build.keychain
security default-keychain -s build.keychain
security unlock-keychain -p "" build.keychain
# 导入证书到钥匙链
security import certificate.p12 -k build.keychain \
-P $P12_PASSWORD -T /usr/bin/codesign
# 导入描述文件
echo $BUILD_PROVISION_PROFILE_BASE64 | base64 --decode > profile.mobileprovision
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
cp profile.mobileprovision ~/Library/MobileDevice/Provisioning\ Profiles/
- name: Build iOS
run: |
# 构建iOS应用
flutter build ipa --release \
--export-options-plist=ios/ExportOptions.plist \
--dart-define=APP_VERSION=${{ github.ref_name }} \
--dart-define=BUILD_NUMBER=${{ github.run_number }}
- name: Upload IPA
uses: actions/upload-artifact@v3
with:
name: ios-build-${{ github.run_number }}
path: build/ios/ipa/*.ipa
真实的项目通常有多个环境:
# 多环境构建配置
env:
# 根据分支选择环境
APP_ENV: ${{ github.ref == 'refs/heads/main' && 'production' || 'staging' }}
APP_NAME: ${{ github.ref == 'refs/heads/main' && '生产' || '测试' }}
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
# 同时构建多个Flavor
flavor: [development, staging, production]
platform: [android, ios]
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Setup Flutter
uses: subosito/flutter-action@v2
- name: Build ${{ matrix.platform }} for ${{ matrix.flavor }}
run: |
if [ "${{ matrix.platform }}" = "android" ]; then
flutter build apk --flavor ${{ matrix.flavor }} --release
else
flutter build ipa --flavor ${{ matrix.flavor }} --release
fi
- name: Upload ${{ matrix.flavor }} build
uses: actions/upload-artifact@v3
with:
name: ${{ matrix.platform }}-${{ matrix.flavor }}
path: |
build/app/outputs/flutter-apk/app-${{ matrix.flavor }}-release.apk
build/ios/ipa/*.ipa
构建完成后,自动分发给测试人员:
# 分发到测试平台
jobs:
distribute:
runs-on: ubuntu-latest
needs: [build] # 依赖build任务
steps:
- name: Download artifacts
uses: actions/download-artifact@v3
with:
path: artifacts/
- name: Upload to Firebase App Distribution
# 分发到Firebase
run: |
# 安装Firebase CLI
curl -sL https://firebase.tools | bash
# 登录Firebase
echo "${{ secrets.FIREBASE_TOKEN }}" > firebase_token.json
# 分发Android APK
firebase appdistribution:distribute artifacts/android-production/app-release.apk \
--app ${{ secrets.FIREBASE_ANDROID_APP_ID }} \
--groups "testers" \
--release-notes-file CHANGELOG.md
- name: Upload to TestFlight
# iOS上传到TestFlight
if: matrix.platform == 'ios'
run: |
# 使用altool上传到App Store Connect
xcrun altool --upload-app \
-f artifacts/ios-production/*.ipa \
-t ios \
--apiKey ${{ secrets.APPSTORE_API_KEY }} \
--apiIssuer ${{ secrets.APPSTORE_API_ISSUER }}
- name: Notify testers
# 通知测试人员
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
fields: repo,message,commit,author,action,eventName,ref,workflow,job,took
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
gantt
title Flutter打包发布流水线
dateFormat HH:mm
axisFormat %H:%M
section 触发与准备
代码提交检测 :00:00, 2m
环境初始化 :00:02, 3m
依赖安装 :00:05, 4m
section Android构建
Android环境准备 :00:05, 2m
Android代码编译 :00:07, 6m
Android代码签名 :00:13, 3m
Android打包 :00:16, 2m
section iOS构建
iOS环境准备 :00:05, 3m
iOS代码编译 :00:08, 8m
iOS证书配置 :00:16, 4m
iOS打包 :00:20, 3m
section 测试分发
上传到测试平台 :00:23, 5m
测试人员通知 :00:28, 2m
测试执行周期 :00:30, 30m
section 生产发布
测试结果评估 :01:00, 3m
生产环境准备 :01:03, 5m
提交到应用商店 :01:08, 10m
商店审核等待 :01:18, 30m
发布完成通知 :01:48, 2m
section 环境配置管理
密钥加载 :00:02, 3m
环境变量设置 :00:05, 2m
配置文件解析 :00:07, 3m
版本号处理 :00:10, 2m
先看一个反面教材:我们项目早期,不同环境的API地址是硬编码的:
// 不推荐:硬编码配置
class ApiConfig {
static const String baseUrl = 'https://api.production.com';
// 测试时需要手动改成:'https://api.staging.com'
// 很容易忘记改回来!
}
结果就是:测试时调用了生产接口,把测试数据插到了生产数据库!💥
方案一:基于Flavor的配置
// lib/config/flavors.dart
enum AppFlavor {
development,
staging,
production,
}
class AppConfig {
final AppFlavor flavor;
final String appName;
final String apiBaseUrl;
final bool enableAnalytics;
AppConfig({
required this.flavor,
required this.appName,
required this.apiBaseUrl,
required this.enableAnalytics,
});
// 根据Flavor创建配置
factory AppConfig.fromFlavor(AppFlavor flavor) {
switch (flavor) {
case AppFlavor.development:
return AppConfig(
flavor: flavor,
appName: 'MyApp Dev',
apiBaseUrl: 'https://api.dev.xxxx.com',
enableAnalytics: false,
);
case AppFlavor.staging:
return AppConfig(
flavor: flavor,
appName: 'MyApp Staging',
apiBaseUrl: 'https://api.staging.xxxx.com',
enableAnalytics: true,
);
case AppFlavor.production:
return AppConfig(
flavor: flavor,
appName: 'MyApp',
apiBaseUrl: 'https://api.xxxx.com',
enableAnalytics: true,
);
}
}
}
方案二:使用dart-define传入配置
# CI配置中传入环境变量
- name: Build with environment variables
run: |
flutter build apk --release \
--dart-define=APP_FLAVOR=production \
--dart-define=API_BASE_URL=https://api.xxxx.com \
--dart-define=ENABLE_ANALYTICS=true
// 在代码中读取环境变量
class EnvConfig {
static const String flavor = String.fromEnvironment('APP_FLAVOR');
static const String apiBaseUrl = String.fromEnvironment('API_BASE_URL');
static const bool enableAnalytics = bool.fromEnvironment('ENABLE_ANALYTICS');
}
敏感信息绝不能写在代码里!
# 使用GitHub Secrets
steps:
- name: Use secrets
env:
# 从Secrets读取
API_KEY: ${{ secrets.API_KEY }}
DATABASE_URL: ${{ secrets.DATABASE_URL }}
SIGNING_KEY: ${{ secrets.ANDROID_SIGNING_KEY }}
run: |
# 在脚本中使用
echo "API Key: $API_KEY"
# 写入到配置文件
echo "{ \"apiKey\": \"$API_KEY\" }" > config.json
如何设置Secrets:
推荐以下分层配置策略:
config/
├── .env.example # 示例文件,不含真实值
├── .env.development # 开发环境配置
├── .env.staging # 测试环境配置
├── .env.production # 生产环境配置
└── config_loader.dart # 配置加载器
// config/config_loader.dart
import 'package:flutter_dotenv/flutter_dotenv.dart';
class ConfigLoader {
static Future<void> load(String env) async {
// 根据环境加载对应的配置文件
await dotenv.load(fileName: '.env.$env');
}
static String get apiBaseUrl => dotenv.get('API_BASE_URL');
static String get apiKey => dotenv.get('API_KEY');
static bool get isDebug => dotenv.get('DEBUG') == 'true';
}
// main.dart
void main() async {
// 根据编译模式选择环境
const flavor = String.fromEnvironment('FLAVOR', defaultValue: 'development');
await ConfigLoader.load(flavor);
runApp(MyApp());
}
graph TB
subgraph "环境配置管理架构"
A[配置来源] --> B[优先级]
subgraph "B[优先级]"
B1[1. 运行时环境变量] --> B2[最高优先级]
B3[2. 配置文件] --> B4[中等优先级]
B5[3. 默认值] --> B6[最低优先级]
end
A --> C[敏感信息处理]
subgraph "C[敏感信息处理]"
C1[密钥/密码] --> C2[GitHub Secrets]
C3[API令牌] --> C4[环境变量注入]
C5[数据库连接] --> C6[运行时获取]
end
A --> D[环境类型]
subgraph "D[环境类型]"
D1[开发环境] --> D2[本地调试]
D3[测试环境] --> D4[CI/CD测试]
D5[预发环境] --> D6[生产前验证]
D7[生产环境] --> D8[线上用户]
end
B --> E[配置合并]
C --> E
D --> E
E --> F[最终配置]
F --> G[应用启动]
F --> H[API调用]
F --> I[功能开关]
end
subgraph "安全实践"
J[永远不要提交] --> K[.env文件到Git]
L[使用.gitignore] --> M[忽略敏感文件]
N[定期轮换] --> O[密钥和令牌]
P[最小权限原则] --> Q[仅授予必要权限]
end
style A fill:#e3f2fd
style C fill:#f3e5f5
style D fill:#e8f5e8
style J fill:#fff3e0
Flutter项目依赖下载很慢,使用缓存可以大幅提速:
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Cache Flutter dependencies
uses: actions/cache@v3
with:
path: |
/opt/hostedtoolcache/flutter
${{ github.workspace }}/.pub-cache
${{ github.workspace }}/build
key: ${{ runner.os }}-flutter-${{ hashFiles('pubspec.lock') }}
restore-keys: |
${{ runner.os }}-flutter-
- name: Cache Android dependencies
uses: actions/cache@v3
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
同时测试多个配置组合:
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
# 定义
os: [ubuntu-latest, macos-latest]
flutter-version: ['3.7.x', '3.10.x']
exclude:
- os: macos-latest
flutter-version: '3.7.x'
# 包含特定组合
include:
- os: windows-latest
flutter-version: '3.10.x'
channel: 'beta'
steps:
- name: Test on ${{ matrix.os }} with Flutter ${{ matrix.flutter-version }}
run: echo "Running tests..."
jobs:
deploy:
# 只有特定分支才执行
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- name: Check changed files
# 只有特定文件改动才执行
uses: dorny/paths-filter@v2
id: changes
with:
filters: |
src:
- 'src/**'
configs:
- 'config/**'
- name: Run if src changed
if: steps.changes.outputs.src == 'true'
run: echo "Source code changed"
- name: Skip if only docs changed
if: github.event_name == 'pull_request' && contains(github.event.pull_request.title, '[skip-ci]')
run: |
echo "Skipping CI due to [skip-ci] in PR title"
exit 0
当通用Actions不够用时,可以自定义:
# .github/actions/flutter-setup/action.yml
name: 'Flutter Setup with Custom Options'
description: 'Setup Flutter environment with custom configurations'
inputs:
flutter-version:
description: 'Flutter version'
required: true
default: 'stable'
channel:
description: 'Flutter channel'
required: false
default: 'stable'
enable-web:
description: 'Enable web support'
required: false
default: 'false'
runs:
using: "composite"
steps:
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: ${{ inputs.flutter-version }}
channel: ${{ inputs.channel }}
- name: Enable web if needed
if: ${{ inputs.enable-web == 'true' }}
shell: bash
run: flutter config --enable-web
- name: Install licenses
shell: bash
run: flutter doctor --android-licenses
如果我们有一个现成的Flutter应用,需要添加CI/CD:
项目结构:
my_flutter_app/
├── lib/
├── test/
├── android/
├── ios/
└── pubspec.yaml
当前问题:
第一阶段:实现基础CI
第二阶段:自动化构建
第三阶段:自动化发布
# .github/workflows/ecommerce-ci.yml
name: E-commerce App CI/CD
on:
push:
branches: [develop]
pull_request:
branches: [main, develop]
schedule:
# 每天凌晨2点跑一遍测试
- cron: '0 2 * * *'
jobs:
# 代码质量
quality-gate:
runs-on: ubuntu-latest
outputs:
passed: ${{ steps.quality-check.outputs.passed }}
steps:
- uses: actions/checkout@v3
- name: Quality Check
id: quality-check
run: |
# 代码规范检查
flutter analyze . || echo "::warning::Code analysis failed"
# 检查测试覆盖率
flutter test --coverage
PERCENTAGE=$(lcov --summary coverage/lcov.info | grep lines | awk '{print $4}' | sed 's/%//')
if (( $(echo "$PERCENTAGE < 80" | bc -l) )); then
echo "::error::Test coverage $PERCENTAGE% is below 80% threshold"
echo "passed=false" >> $GITHUB_OUTPUT
else
echo "passed=true" >> $GITHUB_OUTPUT
fi
# 集成测试
integration-test:
needs: quality-gate
if: needs.quality-gate.outputs.passed == 'true'
runs-on: macos-latest
services:
# 启动测试数据库
postgres:
image: postgres:14
env:
POSTGRES_PASSWORD: postgres
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v3
- name: Setup Flutter
uses: subosito/flutter-action@v2
- name: Run integration tests with database
env:
DATABASE_URL: postgres://postgres:postgres@postgres:5432/test_db
run: |
flutter test integration_test/ --dart-define=DATABASE_URL=$DATABASE_URL
# 性能测试
performance-test:
needs: integration-test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Run performance benchmarks
run: |
# 运行性能测试
flutter drive --target=test_driver/app_perf.dart
# 分析性能数据
dart analyze_performance.dart perf_data.json
- name: Upload performance report
uses: actions/upload-artifact@v3
with:
name: performance-report
path: perf_report.json
# 安全扫描
security-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Run security scan
uses: snyk/actions/dart@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
- name: Check for secrets in code
uses: trufflesecurity/trufflehog@main
with:
path: ./
# 报告
report:
needs: [quality-gate, integration-test, performance-test, security-scan]
runs-on: ubuntu-latest
if: always()
steps:
- name: Generate CI/CD Report
run: |
echo "# CI/CD Run Report" > report.md
echo "## Run: ${{ github.run_id }}" >> report.md
echo "## Status: ${{ job.status }}" >> report.md
echo "## Jobs:" >> report.md
echo "- Quality Gate: ${{ needs.quality-gate.result }}" >> report.md
echo "- Integration Test: ${{ needs.integration-test.result }}" >> report.md
echo "- Performance Test: ${{ needs.performance-test.result }}" >> report.md
echo "- Security Scan: ${{ needs.security-scan.result }}" >> report.md
- name: Upload report
uses: actions/upload-artifact@v3
with:
name: ci-cd-report
path: report.md
CI/CD不是一次性的,需要持续优化:
# 监控CI/CD性能
name: CI/CD Performance Monitoring
on:
workflow_run:
workflows: ["E-commerce App CI/CD"]
types: [completed]
jobs:
analyze-performance:
runs-on: ubuntu-latest
steps:
- name: Download workflow artifacts
uses: actions/github-script@v6
with:
script: |
const { data: artifacts } = await github.rest.actions.listWorkflowRunArtifacts({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: context.payload.workflow_run.id,
});
// 分析执行时间
const runDuration = new Date(context.payload.workflow_run.updated_at) -
new Date(context.payload.workflow_run.run_started_at);
console.log(`Workflow took ${runDuration / 1000} seconds`);
// 发送到监控系统
// ...
- name: Send to monitoring
run: |
# 发送指标到Prometheus/Grafana
echo "ci_duration_seconds $DURATION" | \
curl -X POST -H "Content-Type: text/plain" \
--data-binary @- http://monitoring.xxxx.com/metrics
Q:工作流运行太慢怎么办?
A:优化手段:
# 1. 使用缓存
- uses: actions/cache@v3
with:
path: ~/.pub-cache
key: ${{ runner.os }}-pub-${{ hashFiles('pubspec.lock') }}
# 2. 并行执行独立任务
jobs:
test-android:
runs-on: ubuntu-latest
test-ios:
runs-on: macos-latest
# 两个任务会并行执行
# 3. 项目大可以考虑使用自托管Runner
runs-on: [self-hosted, linux, x64]
Q:iOS构建失败,证书问题?
A:iOS证书配置流程:
# 1. 导出开发证书
openssl pkcs12 -in certificate.p12 -out certificate.pem -nodes
# 2. 在GitHub Secrets中存储
# 使用base64编码
base64 -i certificate.p12 > certificate.txt
# 3. 在CI中还原
echo "${{ secrets.IOS_CERTIFICATE }}" | base64 --decode > certificate.p12
security import certificate.p12 -k build.keychain -P "${{ secrets.CERT_PASSWORD }}"
Q:如何调试失败的CI?
A:调试技巧:
# 1. 启用调试日志
run: |
# 显示详细日志
flutter build apk --verbose
# 或使用环境变量
env:
FLUTTER_VERBOSE: true
# 2. 上传构建日志
- name: Upload build logs
if: failure()
uses: actions/upload-artifact@v3
with:
name: build-logs
path: |
~/flutter/bin/cache/
build/
# 3. 使用tmate进行SSH调试
- name: Setup tmate session
uses: mxschmitt/action-tmate@v3
if: failure() && github.ref == 'refs/heads/main'
Q:不同版本兼容性?
A:版本管理策略:
# 使用版本测试兼容性
strategy:
matrix:
flutter-version: ['3.7.x', '3.10.x', 'stable']
# 在代码中检查版本
void checkFlutterVersion() {
const minVersion = '3.7.0';
final currentVersion = FlutterVersion.instance.version;
if (Version.parse(currentVersion) < Version.parse(minVersion)) {
throw Exception('Flutter version $minVersion or higher required');
}
}
Q:Web构建失败?
A:Web构建配置:
# 确保启用Web支持
- name: Enable web
run: flutter config --enable-web
# 构建Web版本
- name: Build for web
run: |
flutter build web \
--web-renderer canvaskit \
--release \
--dart-define=FLUTTER_WEB_USE_SKIA=true
# 处理Web特定问题
- name: Fix web issues
run: |
# 清理缓存
flutter clean
# 更新Web引擎
flutter precache --web
Q:如何管理敏感信息?
A:安全实践:
# 1. 使用环境级别的Secrets
env:
SUPER_SECRET_KEY: ${{ secrets.PRODUCTION_KEY }}
# 2. 最小权限原则
permissions:
contents: read
packages: write # 只有需要时才写
# 3. 使用临时凭证
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-east-1
# 4. 定期轮换密钥
# 设置提醒每月更新一次Secrets
通过这篇教程我们掌握了Flutter CI/CD的核心知识,一个完美的流水线是一次次迭代出来的,需要不断优化。如果觉得文章对你有帮助,别忘了一键三连,支持一下
有任何问题或想法,欢迎在评论区交流讨论。
Xcode 26之前不需要多窗口的很多app没有适配SceneDelegate,升级到Xcode 26后运行没有问题,但是控制台有以下输出:
`UIScene` lifecycle will soon be required. Failure to adopt will result in an assert in the future.
UIApplicationDelegate 中的相关生命周期函数也有弃用标记:
/// Tells the delegate that the application has become active
/// - Note: This method is not called if `UIScene` lifecycle has been adopted.
- (void)applicationDidBecomeActive:(UIApplication *)application API_DEPRECATED("Use UIScene lifecycle and sceneDidBecomeActive(_:) from UISceneDelegate or the UIApplication.didBecomeActiveNotification instead.", ios(2.0, 26.0), tvos(9.0, 26.0), visionos(1.0, 26.0)) API_UNAVAILABLE(watchos);
建议尽早适配
以下是我的适配方案,供大家参考
Delegate Class Name和Configuration Name 可自定义
![]()
![]()
- (UISceneConfiguration *)application:(UIApplication *)application configurationForConnectingSceneSession:(UISceneSession *)connectingSceneSession options:(UISceneConnectionOptions *)options API_AVAILABLE(ios(13.0)){
// name要和Info.plist中配置一致
return [[UISceneConfiguration alloc] initWithName:@"Default Configuration" sessionRole:connectingSceneSession.role];
}
- (void)application:(UIApplication *)application didDiscardSceneSessions:(NSSet<UISceneSession *> *)sceneSessions API_AVAILABLE(ios(13.0)){
// 释放资源,单窗口app不用关注
}
scene: willConnectToSession: options: 方法中创建Window,之前仍然在 didFinishLaunchingWithOptions:
AppDelegate:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[AppLifecycleHelper sharedInstance].launchOptions = launchOptions;
// ... 自定义逻辑
if (@available(iOS 13, *)) {
} else {
[[AppLifecycleHelper sharedInstance] createKeyWindow];
}
}
SceneDelgate:
URL冷启动APP时不调用openURLContexts方法,这里保存URL在DidBecomeActive处理
- (void)scene:(UIScene *)scene willConnectToSession:(UISceneSession *)session options:(UISceneConnectionOptions *)connectionOptions API_AVAILABLE(ios(13.0)){
[[AppLifecycleHelper sharedInstance] createKeyWindowWithScene:(UIWindowScene *)scene];
// 通过url冷启动app,一般只有一个url
for (UIOpenURLContext *context **in** connectionOptions.URLContexts) {
NSURL *URL = context.URL;
if (URL && URL.absoluteString.length > 0) {
self.launchUrl = URL;
}
}
}
AppLifecycleHelper:
- (void)createKeyWindow {
UIWindow *window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
[self setupMainWindow:window];
}
- (void)createKeyWindowWithScene:(UIWindowScene *)scene API_AVAILABLE(ios(13.0)) {
UIWindow *window = [[UIWindow alloc] initWithWindowScene:scene];
[self setupMainWindow:window];
}
- (void)setupMainWindow:(UIWindow *)window {
}
在AppLifecycleHelper中实现,共享给两个DelegateClass
- (void)applicationDidBecomeActive:(UIApplication *)application {
[[AppLifecycleHelper sharedInstance] appDidBecomeActive];
}
- (void)applicationWillResignActive:(UIApplication *)application {
}
- (void)applicationDidEnterBackground:(UIApplication *)application {
}
- (void)applicationWillEnterForeground:(UIApplication *)application {
}
/// URL Scheme
- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey, **id**> *)options {
}
/// 接力用户活动
- (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray<**id**<UIUserActivityRestoring>> * _Nullable))restorationHandler {
}
/// 快捷方式点击
- (void)application:(UIApplication *)application performActionForShortcutItem:(UIApplicationShortcutItem *)shortcutItem completionHandler:(void (^)(BOOL))completionHandler API_AVAILABLE(ios(9.0)) {
}
SceneDelegate部分代码示例:
- (void)sceneDidBecomeActive:(UIScene *)scene API_AVAILABLE(ios(13.0)){
[[AppLifecycleHelper sharedInstance] appDidBecomeActiveWithLaunchUrl:self.launchUrl];
// 清空冷启动时的url
self.launchUrl = nil;
}
这个方法总结下来就是求同存异,由Helper提供SceneDelegate与AppDelegate相同或类似的方法,适合单窗口、且支持iOS 13以下的app;
另外注意URL Scheme冷启动app不会执行openURL需要记录URL,在合适的时机(一般是DidBecomeActive)处理。
在 iOS 13 之前,iOS 应用通常只有一个主窗口(UIWindow)。但随着 iPadOS 的推出和多任务处理需求的增加,Apple 引入了 UIWindowScene 架构,让单个应用可以同时管理多个窗口,每个窗口都有自己的场景(Scene)。本文将深入探讨 UIWindowScene 的核心概念和使用方法。
UIWindowScene 是 iOS 13+ 中引入的新架构,它代表了应用程序用户界面的一个实例。每个场景都有自己的窗口、视图控制器层级和生命周期管理。
UISceneSession → UIWindowScene → UIWindow → UIViewController
↓
UISceneConfiguration
首先需要在 Info.plist 中启用多场景支持:
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<true/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneConfigurationName</key>
<string>Default Configuration</string>
<key>UISceneDelegateClassName</key>
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
<key>UISceneStoryboardFile</key>
<string>Main</string>
</dict>
</array>
</dict>
</dict>
import UIKit
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene,
willConnectTo session: UISceneSession,
options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
window = UIWindow(windowScene: windowScene)
window?.rootViewController = YourRootViewController()
window?.makeKeyAndVisible()
// 处理深度链接
if let userActivity = connectionOptions.userActivities.first {
self.scene(scene, continue: userActivity)
}
}
func sceneDidDisconnect(_ scene: UIScene) {
// 场景被系统释放时调用
}
func sceneDidBecomeActive(_ scene: UIScene) {
// 场景变为活动状态时调用
}
func sceneWillResignActive(_ scene: UIScene) {
// 场景即将变为非活动状态时调用
}
func sceneWillEnterForeground(_ scene: UIScene) {
// 场景即将进入前台
}
func sceneDidEnterBackground(_ scene: UIScene) {
// 场景进入后台
}
}
class SceneManager {
static func createNewScene(with userInfo: [String: Any]? = nil) {
let activity = NSUserActivity(activityType: "com.yourapp.newWindow")
activity.userInfo = userInfo
activity.targetContentIdentifier = "newWindow"
let options = UIScene.ActivationRequestOptions()
options.requestingScene = UIApplication.shared.connectedScenes.first as? UIWindowScene
UIApplication.shared.requestSceneSessionActivation(
nil,
userActivity: activity,
options: options,
errorHandler: { error in
print("Failed to create new scene: \(error)")
}
)
}
}
// 自定义场景配置
class CustomSceneDelegate: UIResponder, UIWindowSceneDelegate {
static let configurationName = "CustomSceneConfiguration"
func scene(_ scene: UIScene,
willConnectTo session: UISceneSession,
options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = scene as? UIWindowScene else { return }
// 根据场景角色自定义配置
if session.role == .windowApplication {
configureApplicationWindow(scene: windowScene,
session: session,
options: connectionOptions)
} else if session.role == .windowExternalDisplay {
configureExternalDisplayWindow(scene: windowScene)
}
}
private func configureApplicationWindow(scene: UIWindowScene,
session: UISceneSession,
options: UIScene.ConnectionOptions) {
// 主窗口配置
let window = UIWindow(windowScene: scene)
// 根据用户活动恢复状态
if let userActivity = options.userActivities.first {
window.rootViewController = restoreViewController(from: userActivity)
} else {
window.rootViewController = UIViewController()
}
window.makeKeyAndVisible()
self.window = window
}
}
class DocumentViewController: UIViewController {
var document: Document?
func openInNewWindow() {
guard let document = document else { return }
let userActivity = NSUserActivity(activityType: "com.yourapp.editDocument")
userActivity.title = "Editing \(document.title)"
userActivity.userInfo = ["documentId": document.id]
userActivity.targetContentIdentifier = document.id
let options = UIScene.ActivationRequestOptions()
UIApplication.shared.requestSceneSessionActivation(
nil,
userActivity: userActivity,
options: options,
errorHandler: nil
)
}
}
// 在 SceneDelegate 中处理
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
guard let windowScene = scene as? UIWindowScene,
let documentId = userActivity.userInfo?["documentId"] as? String else {
return
}
let document = fetchDocument(by: documentId)
let editorVC = DocumentEditorViewController(document: document)
windowScene.windows.first?.rootViewController = editorVC
}
extension Notification.Name {
static let documentDidChange = Notification.Name("documentDidChange")
static let sceneDidBecomeActive = Notification.Name("sceneDidBecomeActive")
}
class DocumentManager {
static let shared = DocumentManager()
private init() {}
func updateDocument(_ document: Document) {
// 更新数据
NotificationCenter.default.post(
name: .documentDidChange,
object: nil,
userInfo: ["document": document]
)
}
}
class ExternalDisplayManager {
static func setupExternalDisplay() {
// 监听外部显示器连接
NotificationCenter.default.addObserver(
self,
selector: #selector(handleScreenConnect),
name: UIScreen.didConnectNotification,
object: nil
)
}
@objc private static func handleScreenConnect(notification: Notification) {
guard let newScreen = notification.object as? UIScreen,
newScreen != UIScreen.main else { return }
let options = UIScene.ActivationRequestOptions()
options.requestingScene = UIApplication.shared.connectedScenes.first as? UIWindowScene
let activity = NSUserActivity(activityType: "externalDisplay")
UIApplication.shared.requestSceneSessionActivation(
nil,
userActivity: activity,
options: options,
errorHandler: nil
)
}
}
// 在 SceneDelegate 中配置外部显示器场景
func configureExternalDisplayWindow(scene: UIWindowScene) {
let window = UIWindow(windowScene: scene)
window.screen = UIScreen.screens.last // 使用外部显示器
window.rootViewController = ExternalDisplayViewController()
window.makeKeyAndVisible()
}
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
func stateRestorationActivity(for scene: UIScene) -> NSUserActivity? {
// 返回用于恢复场景状态的 activity
let activity = NSUserActivity(activityType: "restoration")
if let rootVC = window?.rootViewController as? Restorable {
activity.addUserInfoEntries(from: rootVC.restorationInfo)
}
return activity
}
func scene(_ scene: UIScene,
willConnectTo session: UISceneSession,
options connectionOptions: UIScene.ConnectionOptions) {
// 检查是否有保存的状态
if let restorationActivity = session.stateRestorationActivity {
restoreState(from: restorationActivity)
}
}
}
class MemoryAwareSceneDelegate: UIResponder, UIWindowSceneDelegate {
func sceneDidEnterBackground(_ scene: UIScene) {
// 释放不必要的资源
if let vc = window?.rootViewController as? MemoryManageable {
vc.releaseUnnecessaryResources()
}
}
func sceneWillEnterForeground(_ scene: UIScene) {
// 恢复必要的资源
if let vc = window?.rootViewController as? MemoryManageable {
vc.restoreResources()
}
}
}
enum SceneError: Error {
case sceneCreationFailed
case invalidConfiguration
case resourceUnavailable
}
class RobustSceneManager {
static func createSceneSafely(configuration: UISceneConfiguration,
completion: @escaping (Result<UIWindowScene, SceneError>) -> Void) {
let options = UIScene.ActivationRequestOptions()
UIApplication.shared.requestSceneSessionActivation(
nil,
userActivity: nil,
options: options
) { error in
if let error = error {
completion(.failure(.sceneCreationFailed))
} else {
// 监控新场景创建
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
if let newScene = UIApplication.shared.connectedScenes
.compactMap({ $0 as? UIWindowScene })
.last {
completion(.success(newScene))
} else {
completion(.failure(.sceneCreationFailed))
}
}
}
}
}
}
extension UIWindowScene {
func logSceneInfo() {
print("""
Scene Information:
- Session: \(session)
- Role: \(session.role)
- Windows: \(windows.count)
- Screen: \(screen)
- Activation State: \(activationState)
""")
}
}
// 在 AppDelegate 中监控所有场景
func application(_ application: UIApplication,
configurationForConnecting connectingSceneSession: UISceneSession,
options: UIScene.ConnectionOptions) -> UISceneConfiguration {
print("Connecting scene: \(connectingSceneSession)")
return UISceneConfiguration(
name: "Default Configuration",
sessionRole: connectingSceneSession.role
)
}
class SceneLeakDetector {
static var activeScenes: [String: WeakReference<UIWindowScene>] = [:]
static func trackScene(_ scene: UIWindowScene) {
let identifier = "\(ObjectIdentifier(scene).hashValue)"
activeScenes[identifier] = WeakReference(object: scene)
// 定期检查泄漏
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
self.checkForLeaks()
}
}
private static func checkForLeaks() {
activeScenes = activeScenes.filter { $0.value.object != nil }
print("Active scenes: \(activeScenes.count)")
}
}
class WeakReference<T: AnyObject> {
weak var object: T?
init(object: T) {
self.object = object
}
}
@available(iOS 13.0, *)
class ModernSceneDelegate: UIResponder, UIWindowSceneDelegate {
// iOS 13+ 实现
}
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
if #available(iOS 13.0, *) {
// 使用场景架构
} else {
// 传统 UIWindow 设置
window = UIWindow(frame: UIScreen.main.bounds)
window?.rootViewController = UIViewController()
window?.makeKeyAndVisible()
}
return true
}
}
UIWindowScene 架构为 iOS 应用带来了强大的多窗口支持,特别适合 iPadOS 和需要复杂多任务处理的应用。通过合理使用场景管理,可以:
虽然学习曲线较陡,但掌握 UIWindowScene 将显著提升应用的现代化水平和用户体验。
示例项目: 完整的示例代码可以在 GitHub 仓库 找到。
进一步阅读:
在swift中,泛型T是一个非常强大的特性,它允许我们编写灵活且可复用的代码。而当我们在 协议(Protocol) 中需要使用泛型时,associatedtype 就派上了用场。
在 Swift 的协议中,我们无法直接使用泛型 <T>,但可以使用 associatedtype 关键字来声明一个占位类型,让协议在不确定具体类型的情况下仍然能够正常使用。
protocol SomeProtocol {
associatedtype SomeType // 声明一个占位类型 SomeType,但不指定具体类型。
func doSomething(with value: SomeType)
}
// Int类型
protocol SomeProtocol {
associatedtype Item
mutating func doSomething(with value: Item)
func getItem(at index: Int) -> Item
}
struct ContainerDemo: SomeProtocol {
typealias Item = Int // 指定Item为Int类型
private var items: [Int] = []
mutating func doSomething(with value: Int) {
items.append(value)
print(value)
}
func getItem(at index: Int) -> Int {
return items[index]
}
}
// String类型
struct StringContainer: SomeProtocol {
typealias Item = String
private var items: [String] = []
mutating func doSomething(with value: String) {
items.append(value)
}
func getItem(at index: Int) -> String {
return items[index]
}
}
protocol StackProtocol {
associatedtype Element
mutating func push(_ item: Element)
mutating func pop() -> Element?
}
struct IntStack: StackProtocol {
typealias Element = Int
private var stacks: [Int] = []
mutating func push(_ item: Int) {
stacks.append(item)
}
mutating func pop() -> Int? {
return stacks.popLast()
}
}
有时候希望assocaitedtype只能是某种类型的子类或实现了某个协议。可以使用where关键字进行类型约束
protocol Summable {
associatedtype Number: Numeric // 限定Number必须是Numeric协议的子类型( Int、Double)
func sum(a: Number,b: Number) -> Number
}
struct myIntergerAddr: Summable {
func sum(a: Int, b: Int) -> Int {
return a + b
}
}
// 使用泛型结构体遵循协议
struct myGenericSatck<T>: StackProtocol {
private var elements: [T] = []
var isEmpty: Bool {return elements.isEmpty}
var count: Int {return elements.count}
mutating func push(_ item: T) {
elements.append(item)
}
mutating func pop() -> T? {
return elements.popLast()
}
}
| 比较项 | associatedtype (协议中的泛型) | 普通泛型 |
|---|---|---|
| 适用范围 | 只能用于 协议 | 可用于 类、结构体、函数 |
| 作用 | 让协议支持不确定的类型,由实现者决定具体类型 | 让类型/函数支持泛型 |
| 例子 | protocol Container { associatedtype Item } | struct Stack {} |
| 限制 | 只能用于协议,不能直接实例化 | 适用于所有类型 |
是一种访问集合、列表或序列中元素成员的快捷方式。它允许你通过下标语法(使用方括号 [])来访问实例中的数据,而不需要调用方法。
使用Subscript可以给任意类型(枚举、结构体、类)增加下标功能。
subscript的语法类似于实例方法,计算属性,本质就是方法
// demo1
struct TimesTable {
let multiplier: Int
subscript(index: Int) -> Int {
return multiplier * index
}
}
let threeTimesTable = TimesTable(multiplier: 3)
print(threeTimesTable[6]) // 输出: 18
// demo2
class MyPoint {
var x = 0.0
var y = 0.0
subscript(index: Int) ->Double {
set {
if index == 0 {
x = newValue
} else if index == 1 {
y = newValue
}
}
get {
if index == 0 {
return x
} else if (index == 1) {
return y
}
return 0
}
}
}
var mmpoint = MyPoint()
mmpoint[0] = 11.1
mmpoint[1] = 22.2
print(mmpoint.x)
print(mmpoint.y)
print(mmpoint[0])
print(mmpoint[1])
// dem3
struct Container {
var items: [Int] = []
// 单个整数下标
subscript(index: Int) -> Int {
return items[index]
}
// 范围下标
subscript(range: Range<Int>) -> [Int] {
return Array(items[range])
}
// 可变参数下标
subscript(indices: Int...) -> [Int] {
return indices.map { items[$0] }
}
}
1、subscript中定义的返回值类型决定了
2、get方法的返回值类型 set方法中的newvalue的类型3、subscript可以接受多个参数,并且类型任意
4、subscript可以没有set方法,但是必须要有get方法,如果只有get方法,可以省略get关键字
5、可以设置参数标签
6、下标可以是类型方法
在swift中,Sequence是一个协议,表示可以被逐一遍历的有序集合。一个符合Sequence协议的类型可以使用for-in循环迭代其所有元素。
Sequence是swift集合类型(Array,Dictionary、set等)的基础协议,许多高级功能如:map、filter、 reduce都依赖于它
许多 Swift 标准库类型都符合 Sequence 协议,例如:
Array:一个有序的集合。
Set:一个无序、唯一的集合。
Dictionary:键值对集合。
Range:连续的整数范围。
String:一个字符序列。
/// Sequence的核心定义
public protocol Sequence {
/// 表示序列中元素的类型。
associatedtype Element
associatedtype Iterator: IteratorProtocol where Iterator.Element == Element
/// 返回一个迭代器对象,该对象遵循 IteratorProtocol 协议,并提供逐一访问元素的功能。
func makeIterator() -> Iterator
}
public protocol IteratorProtocol {
associatedtype Element
/// 每次调用时返回序列的下一个元素;如果没有更多元素可用,则返回 nil。
mutating func next() -> Element?
}
总结:
1.Sequence只承诺“能生成迭代器”,不能保证反复便利,也不保证有count
2.迭代器几乎总是是struct:值语义保证“复制一份就从头开始”,不会意外共享状态
3.单趟序列完全合法;第二次makeIterator()可以返回空迭代器
// 可以创建自己的类型并使符合Sequence协议,只需要实现makeIterator()方法,并返回一个符合IteratorProtocol的迭代器
// 自定义一个从n倒数到0的序列
struct myCountDownDemo: Sequence {
let start: Int
func makeIterator() -> Iterator {
Iterator(current: start)
}
struct Iterator: IteratorProtocol {
var current: Int
mutating func nex() -> Int? {
guard current >= 0 else {return nil}
defer {current -= 1}
return current
}
}
}
// 调用了myArr.makeIterator()拿到一个迭代器 反复调用iterator.next() 返回的可选值解包后赋值给item
for n in myCountDownDemo(start: 3) {
print(n)
}
let myArr = [1,5,6,8]
for item in myArr {
print(item)
}
// for in 实际执行的是
var iterator = myArr.makeIterator()
while let element = iterator.next() {
print(element)
}
// collection可以额外保证:多次遍历且顺序稳定,提供count、endIndex、下标访问,支持切片、前缀、后缀等默认实现
// 三种安全写法
// 方法一
todoItems.removeAll{$0 == "B"}
// 方法二 先记下索引,后删除
let indexsToRemove = todoItems.indices.filter{todoItems[$0] == "B"}
for i in indexsToRemove.reversed() {
todoItems.remove(at: i)
}
// 方法三
todoItems = todoItems.filter{$0 != "B"}
//map
var numbersArr = [3,6,8]
let squares = numbersArr.map{$0 * $0}
print(squares) // 输出 [9,36,64]
// filter过滤列表中的元素
let eventNumbers = numbersArr.filter{ $0 % 2 == 0}
print(eventNumbers) // 输出[6,8]
// reduce将列表中所有元素组合成一个值
let sum = numbersArr.reduce(0 , +)
print(sum) // 输出17
// forEach对列表中的每个元素执行操作
numbersArr.forEach{print($0)}
而 Sequence 更适合:
选择依据:如果你的数据源是异步的或会产生延迟,使用 AsyncSequence;如果数据是同步可用的,使用 Sequence。
// demo1
import Foundation
// 自定义异步序列
struct AsyncCountdown: AsyncSequence {
typealias Element = Int
let count: Int
// 必须实现 makeAsyncIterator()
func makeAsyncIterator() -> AsyncIterator {
AsyncIterator(count: count)
}
// 异步迭代器
struct AsyncIterator: AsyncIteratorProtocol {
var count: Int
// 注意:next() 是异步的!
mutating func next() async -> Int? {
guard count > 0 else { return nil }
// 模拟异步等待
await Task.sleep(1_000_000_000) // 等待1秒
let value = count
count -= 1
return value
}
}
}
// demo2
// 模拟从网络获取分页数据
struct PaginatedAPISequence: AsyncSequence {
typealias Element = [String]
let totalPages: Int
let delay: UInt64
func makeAsyncIterator() -> AsyncIterator {
AsyncIterator(totalPages: totalPages, delay: delay)
}
struct AsyncIterator: AsyncIteratorProtocol {
let totalPages: Int
let delay: UInt64
var currentPage = 0
mutating func next() async throws -> [String]? {
guard currentPage < totalPages else { return nil }
// 模拟网络延迟
await Task.sleep(delay)
// 模拟获取数据
let items = (0..<10).map { "Item \(currentPage * 10 + $0)" }
currentPage += 1
return items
}
}
}
// 使用
func fetchPaginatedData() async throws {
let pageSize = 10
let apiSequence = PaginatedAPISequence(totalPages: 5, delay: 500_000_000)
for try await page in apiSequence {
print("收到页面数据: \(page.count) 条")
// 处理数据...
}
}
下面内容只关注 GetxController / GetBuilder / Obx / 局部状态组件这些部分。
本文介绍在项目中如何使用 GetxController、GetBuilder、Obx / GetX 等组件来组织业务逻辑和控制 UI 更新。
GetxController 用来承载页面或模块的业务状态与逻辑,通常搭配 StatelessWidget 使用,无需再写 StatefulWidget。
推荐习惯:
initState / dispose 里面的逻辑迁移到 Controller 的 onInit / onClose 中,UI 层尽量保持“傻瓜视图”。GetX 内置两类状态管理方式:简单状态管理(GetBuilder)与响应式状态管理(Obx / GetX)。
适用场景:不想使用 Rx 类型(.obs),希望显式控制刷新时机。
写法示例:
class CounterController extends GetxController {
int count = 0;
void increment() {
count++;
update(); // 手动触发使用该 controller 的 GetBuilder 重建
}
}
class CounterPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final controller = Get.put(CounterController());
return Scaffold(
body: Center(
child: GetBuilder<CounterController>(
builder: (c) => Text('Count: ${c.count}'),
),
),
floatingActionButton: FloatingActionButton(
onPressed: controller.increment,
),
);
}
}
特点:
.obs,状态是普通字段。update() 的时候,使用该 Controller 的 GetBuilder 才会重建。适用场景:已经在使用 .obs,或希望局部 UI 随状态变化自动刷新。
写法示例:
class CounterController extends GetxController {
var count = 0.obs;
void increment() => count++;
}
class CounterPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final controller = Get.put(CounterController());
return Scaffold(
body: Center(
child: Obx(() => Text('Count: ${controller.count}')),
// 或
// child: GetX<CounterController>(
// builder: (c) => Text('Count: ${c.count}'),
// ),
),
floatingActionButton: FloatingActionButton(
onPressed: controller.increment,
),
);
}
}
特点:
.obs 变为 Rx 类型(如 RxInt、RxString)。update()。在同一个项目、同一个 Controller 中,可以同时使用:
GetBuilder。.obs + Obx / GetX。经验规则:
GetBuilder 本质上是一个 StatefulWidget,内部有完整的 State 生命周期,对外通过参数暴露部分回调:[1]
initState:对应 State.initState,适合调用 Controller 方法、发请求等。didChangeDependencies:父级依赖变化时触发,用得不多。didUpdateWidget:父组件重建、参数改变时触发。dispose:组件销毁时触发,适合释放本地资源。示例:
GetBuilder<CounterController>(
initState: (_) {
// widget 创建时执行
},
dispose: (_) {
// widget 销毁时执行
},
builder: (c) => Text('Count: ${c.count}'),
);
建议:
initState / dispose。对于“只在一个小部件内部使用”的临时状态,可以使用局部状态组件,而不必放入 Controller:
ValueBuilder(简单本地状态):dart ValueBuilder<bool>( initialValue: false, builder: (value, update) => Switch( value: value, onChanged: update, // update(newValue) ), );
ObxValue(本地 Rx 状态):
ObxValue<RxBool>(
(data) => Switch(
value: data.value,
onChanged: data, // 相当于 (v) => data.value = v
),
false.obs,
);
使用建议:
| 需求场景 | 状态写法 | UI 组件 | 刷新方式 |
|---|---|---|---|
| 不想用 Rx,页面级 / 大块区域 | 普通字段 | GetBuilder | 手动 update()
|
已使用 .obs,局部自动刷新 |
.obs(RxXX 类型) |
Obx / GetX | 值变化自动刷新 |
| 单个小 widget 内部的临时简单状态 | 普通字段 | ValueBuilder | 调用 updateFn
|
| 单个小 widget 内部的临时响应式状态 | .obs |
ObxValue | 值变化自动刷新 |
在这种“页面加载时请求 API”的需求里,推荐把请求放在 GetxController 的生命周期 里做,而不是放在页面 Widget 里。
适合“只要创建了这个 Controller(进入页面)就立刻请求”的场景。
class ArticleController extends GetxController {
int pageCount = 0; // 简单状态
var likeCount = 0.obs; // 响应式状态
var isFavorite = false.obs;
var loading = false.obs; // 加载状态
var article = Rxn<Article>(); // 文章详情
@override
void onInit() {
super.onInit();
increasePageCount(); // 原来的逻辑
fetchArticle(); // 页面加载时请求 API
}
Future<void> fetchArticle() async {
loading.value = true;
try {
final data = await api.getArticleDetail(); // 这里调用你的 repository / service
article.value = data;
// article 是 Rx,使用 Obx 的地方会自动刷新
// 如果你有依赖简单状态的 GetBuilder,需要的话再调用 update()
// update();
} finally {
loading.value = false;
}
}
void increasePageCount() {
pageCount++;
update(); // 刷新 GetBuilder
}
void like() => likeCount++;
void toggleFavorite() => isFavorite.toggle();
}
页面里依然混用 GetBuilder + Obx:
class ArticlePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final controller = Get.put(ArticleController());
return Scaffold(
appBar: AppBar(title: const Text('Article Detail')),
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 顶部浏览次数(简单状态)
GetBuilder<ArticleController>(
builder: (c) => Text('页面浏览次数:${c.pageCount}'),
),
const SizedBox(height: 16),
// 中间部分:加载中 / 内容(响应式状态)
Obx(() {
if (controller.loading.value) {
return const CircularProgressIndicator();
}
final article = controller.article.value;
if (article == null) {
return const Text('暂无数据');
}
return Text(article.title); // 文章标题
}),
const SizedBox(height: 16),
// 点赞 + 收藏(响应式状态)
Obx(
() => Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('点赞:${controller.likeCount}'),
const SizedBox(width: 16),
Icon(
controller.isFavorite.value
? Icons.favorite
: Icons.favorite_border,
color: controller.isFavorite.value ? Colors.red : null,
),
],
),
),
const SizedBox(height: 24),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: controller.increasePageCount,
child: const Text('增加浏览次数 (GetBuilder)'),
),
const SizedBox(width: 16),
ElevatedButton(
onPressed: controller.like,
child: const Text('点赞 (Obx)'),
),
const SizedBox(width: 16),
ElevatedButton(
onPressed: controller.toggleFavorite,
child: const Text('收藏切换 (Obx)'),
),
],
),
],
),
);
}
}
如果你的 API 请求需要在“首帧 UI 出来之后”再做,比如要先弹一个对话框提示用户,将请求放在 onReady:
@override
void onReady() {
super.onReady();
fetchArticle(); // 首帧渲染完成后请求
}
initState 里请求,而是优先放到 GetxController.onInit / onReady,这样视图层更干净,也更符合 GetX 推荐的结构。