独立开发者的试炼:Zipic 从 0 到 1 的产品化之路
做独立产品这件事,说起来容易,真动手了才知道水有多深。这是一个独立开发者将职场小需求变成主力产品的真实故事。我们将跟随 Zipic 作者十里的视角,一起回顾产品从 0 到 1 的全过程。本篇聚焦产品设计与决策思考。
做独立产品这件事,说起来容易,真动手了才知道水有多深。这是一个独立开发者将职场小需求变成主力产品的真实故事。我们将跟随 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 推荐的结构。众所周知Swift中Array是值类型,如果其中元素为值类型和引用类型,分别会发生什么?
检查不同层次的地址
// 1. 栈变量地址
withUnsafePointer(to: &array) {
print("\(name) 栈地址: \($0)")
}
// 2. 堆缓冲区地址
array.withUnsafeBufferPointer {
print("数组缓冲区地址: \(String(describing: $0.baseAddress))")
}
// 3. 元素地址(引用类型时比较)
if let first = array.first as? AnyObject {
print("\(name)[0] 对象地址: \(ObjectIdentifier(first))")
}
}
随便定义一个类,并创建列表1,然后直接赋值给列表2
class Person {
var name: String
init(name: String) { self.name = name }
}
var people1 = [Person(name: "Alice"),
Person(name: "Bob")]
var people2 = people1
用withUnsafePointer打印此时两个数组的栈地址(指向数组的指针)
withUnsafePointer(to: &people1) { ptr in
print("people1 地址: \(ptr)")
}
withUnsafePointer(to: &people2) { ptr in
print("people2 地址: \(ptr)")
}
// 输出结果
// people1 地址: 0x000000010df001a0
// people2 地址: 0x000000010df001a8
确实是两个不同的数组指针(废话!),但是我们再通过withUnsafeBufferPointer获取数组缓冲区地址
people1.withUnsafeBufferPointer { buffer in
if let baseAddress = buffer.baseAddress {
print("people1缓冲区地址(堆): \(baseAddress)")
}
}
people2.withUnsafeBufferPointer { buffer in
if let baseAddress = buffer.baseAddress {
print("people2缓冲区地址(堆): \(baseAddress)")
}
}
// 输出结果
// people1缓冲区地址(堆): 0x000000014d2040c0
// people2缓冲区地址(堆): 0x000000014d2040c0
会发现指向的是同一块缓冲区
如果我们更改people2中元素的name,指针地址和缓冲区地址都没有任何变化(这里就不贴代码和打印结果了),但是如果新增元素
people2.append(Person(name: "newPerson"))
withUnsafePointer(to: &people2) { ptr in
print("people2 地址: \(ptr)")
}
people2.withUnsafeBufferPointer { buffer in
if let baseAddress = buffer.baseAddress {
print("people2缓冲区地址(堆):\(baseAddress)")
}
}
// 输出结果:
// people2 地址: 0x000000010df001a8
// people2缓冲区地址(堆): 0x000000014f404b10
指针地址没变,但是缓冲区地址变了!证明Swift中的数组是写时复制,新开辟了缓冲区。(删除同理)
但是缓冲区里存的是什么?打印下数组中的元素看看
/* people1
people1 元素对象地址:
[0]: 0x122b04570
[1]: 0x122b04590
people2 元素对象地址:
[0]: 0x122b04570
[1]: 0x122b04590
[2]: 0x122b05ea0
*/
得出结论。虽然缓冲区确实开了新的,但是引用类型的元素还是不会被复制,相当于只是开了一块新地址存引用类型元素的指针而已。
结论:
如果真的能读到值类型,相信也能看懂直接用代码解释了
var array1 = ["AAA", "BBB", "CCC"]
var array2 = array1
// 输出结果:
// 栈地址验证,不同
// array1 栈地址: 0x00000001101d0058
// array2 栈地址: 0x00000001101d0060
// 缓冲区 暂时相同
// array1 缓冲区地址: 0x0000000129b04440
// array2 缓冲区地址: 0x0000000129b04440
此时修改元素再查看,array2已经开辟新的缓冲区,就不重复贴新增和删除的代码了,结果也是如此。
array2[0] = "new AAA"
// 输出结果:
// array1 缓冲区地址: 0x0000000129b04440
// array2 缓冲区地址: 0x0000000129b0d950
但是!修改了array2并没有像array1那样影响到同一个元素,现在用下面的方法验证下数组中的元素,打印修改后的结果
array1.withUnsafeBufferPointer { buffer in
if let baseAddress = buffer.baseAddress {
for i in 0..<buffer.count {
let elementAddress = baseAddress + i
print("array[\(i)] 地址: \(elementAddress), 值: \(elementAddress.pointee)")
}
}
}
array2.withUnsafeBufferPointer { buffer in
if let baseAddress = buffer.baseAddress {
for i in 0..<buffer.count {
let elementAddress = baseAddress + i
print("array[\(i)] 地址: \(elementAddress), 值: \(elementAddress.pointee)")
}
}
}
/* 输出结果:
array[0] 地址: 0x0000000127504170, 值: AAA
array[1] 地址: 0x0000000127504180, 值: BBB
array[2] 地址: 0x0000000127504190, 值: CCC
array[0] 地址: 0x000000012750ba70, 值: newAAA
array[1] 地址: 0x000000012750ba80, 值: BBB
array[2] 地址: 0x000000012750ba90, 值: CCC
*/
元素地址不同,值也不同
| 元素类型 | 值类型 | 引用类型 |
|---|---|---|
| 赋值 | 逻辑复制 | 逻辑复制 |
| 缓冲区共享 | 初始共享 | 初始共享 |
| 元素独立性 | 完全独立 | 共享对象 |
| 写时复制触发 | 修改时 | 修改结构时候(增删) |
| 内存影响 | 元素复制 | 只复制指针 |
在学习 SwiftUI 的过程中,很多人第一次看到 compositingGroup() 都会被官方文档这句话绕晕:
Use compositingGroup() to apply effects to a parent view before applying effects to this view.
“让父 View 的效果先于子 View 的效果生效” —— 这句话如果按字面理解,几乎一定会误解。
本文将从 渲染顺序、效果作用范围、实际示例 三个角度,彻底讲清楚 compositingGroup() 到底解决了什么问题。
compositingGroup()会创建一个“合成边界”:
- 没有它:父 View 的合成效果会被「拆分」并逐个作用到子 View
- 有了它:子 View 会先整体合成,再统一应用父 View 的合成效果
⚠️ 它改变的不是 modifier 的书写顺序,而是“效果的作用范围”。
先看一个最简单的例子:
VStack {
Text("A")
Text("B")
}
.opacity(0.5)
但 SwiftUI 实际做的是:
Text("A") -> opacity 0.5
Text("B") -> opacity 0.5
再进行叠加
也就是说:
opacity 并没有作为一个“整体效果”存在这就是很多「透明度叠加变脏」「blur 看起来不对劲」的根源。
加上 compositingGroup():
VStack {
Text("A")
Text("B")
}
.compositingGroup()
.opacity(0.5)
SwiftUI 的渲染流程会变成:
VStack
├─ Text("A")
└─ Text("B")
↓
先合成为一张离屏图像
↓
对这张图像应用 opacity 0.5
父 View 的合成类效果不再下发到子 View。
这句话并不是时间顺序,而是:
父 View 的合成效果不会参与子 View 的内部计算。
换句话说:
而不是交叉、叠加、重复计算。
ZStack {
Text("Hello")
Text("Hello")
.blur(radius: 5)
}
.opacity(0.5)
实际效果:
ZStack {
Text("Hello")
Text("Hello")
.blur(radius: 5)
}
.compositingGroup()
.opacity(0.5)
渲染流程变为:
📌 blur 不再被“二次污染”
VStack {
...
}
.compositingGroup()
.opacity(0.8)
ZStack {
...
}
.compositingGroup()
.blendMode(.multiply)
.content
.compositingGroup()
.transition(.opacity)
可显著减少闪烁、重影问题。
| 对比项 | compositingGroup | drawingGroup |
|---|---|---|
| 是否离屏渲染 | 是 | 是 |
| 是否使用 Metal | 否 | 是 |
| 主要目的 | 控制合成效果作用范围 | 性能 / 特效加速 |
| 常见问题 | 解决视觉叠加 | 解决复杂绘制性能 |
📌 compositingGroup 关注“视觉正确性”,drawingGroup 更偏向“性能”。
要“整体效果”,用 compositingGroup;
不想被子 View 叠加污染,也用 compositingGroup。
compositingGroup() 并不会改变 modifier 的书写顺序如果你在 SwiftUI 中遇到:
👉 第一时间就该想到 compositingGroup()
希望这篇文章能帮你真正理解 SwiftUI 背后的渲染逻辑。
从对象到类、从结构体到元类、从 C++ 到内存分布区、到手机硬件内存的完整知识线
引用计数 = 记录"有多少个地方在使用这个对象"的数字
想象一下图书馆的书:
一本书(对象):
- 被借出时:借书人数 = 1
- 又有人借:借书人数 = 2
- 有人还书:借书人数 = 1
- 所有人还完:借书人数 = 0 → 书可以放回仓库(对象被释放)
OC 对象也是一样:
"引用计数加1" = 又多了一个地方在使用这个对象
// 步骤 1:创建对象
NSObject *obj = [[NSObject alloc] init];
// 此时:obj 指向的对象,引用计数 = 1
// 意思:有 1 个地方在使用这个对象(就是 obj 这个变量)
// 步骤 2:另一个变量也指向这个对象
NSObject *obj2 = obj; // 强引用赋值
// 此时:obj 指向的对象,引用计数 = 2
// 意思:有 2 个地方在使用这个对象(obj 和 obj2)
// 步骤 3:obj 不再指向这个对象
obj = nil;
// 此时:obj 指向的对象,引用计数 = 1
// 意思:还有 1 个地方在使用(obj2 还在用)
// 步骤 4:obj2 也不再指向
obj2 = nil;
// 此时:引用计数 = 0
// 意思:没有地方在使用这个对象了 → 对象被释放!
"self 引用计数加1" = 又多了一个地方在强引用 self 这个对象
@interface ViewController : UIViewController
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// self 的引用计数 = 1(假设只有系统在引用它)
// 创建一个强引用
ViewController *anotherRef = self; // 强引用赋值
// 此时:self 的引用计数 = 2
// 意思:有 2 个地方在强引用 self(系统 + anotherRef)
// anotherRef 不再引用
anotherRef = nil;
// 此时:self 的引用计数 = 1(恢复)
}
@end
- (void)viewDidLoad {
[super viewDidLoad];
// self 的引用计数 = 1
// ❌ 情况 A:block 直接捕获 self
self.block = ^{
[self doSomething]; // block 强引用 self
};
// 此时:self 的引用计数 = 2
// 原因:self 强引用 block,block 强引用 self
// 形成循环:self → block → self(循环引用!)
// ✅ 情况 B:block 捕获 weakSelf
__weak typeof(self) weakSelf = self;
self.block = ^{
[weakSelf doSomething]; // block 弱引用 self(不增加引用计数)
};
// 此时:self 的引用计数 = 1(没有增加!)
// 原因:weakSelf 是弱引用,不会让引用计数 +1
}
- (void)viewDidLoad {
[super viewDidLoad];
// 初始状态:self 引用计数 = 1
__weak typeof(self) weakSelf = self;
// 此时:self 引用计数 = 1(weakSelf 不增加引用计数)
self.block = ^{
// block 被创建,捕获了 weakSelf(弱引用)
// 此时:self 引用计数 = 1(仍然没有增加)
// block 执行时:
__strong typeof(weakSelf) strongSelf = weakSelf;
// 此时:self 引用计数 = 2(strongSelf 强引用,+1)
// 意思:又多了一个地方在强引用 self(就是 strongSelf)
[strongSelf doSomething];
// block 执行完,strongSelf 作用域结束
// 此时:self 引用计数 = 1(strongSelf 释放,-1)
// 意思:strongSelf 不再引用 self,引用计数恢复
};
// 最终:self 引用计数 = 1(block 只弱引用 self,不增加引用计数)
}
// 伪代码:引用计数的实现
struct NSObject {
int retainCount; // 引用计数(实际可能不在对象里,在 side table)
};
// retain(加1)
- (id)retain {
retainCount++; // 引用计数 +1
return self;
}
// release(减1)
- (void)release {
retainCount--; // 引用计数 -1
if (retainCount == 0) {
[self dealloc]; // 引用计数为 0,释放对象
}
}
// 你写的代码
NSObject *obj = [[NSObject alloc] init];
NSObject *obj2 = obj;
// 编译器实际生成的代码(伪代码)
NSObject *obj = [[NSObject alloc] init]; // retainCount = 1
NSObject *obj2 = [obj retain]; // retainCount = 2(自动插入 retain)
// ... 使用 ...
[obj release]; // retainCount = 1(自动插入 release)
[obj2 release]; // retainCount = 0,对象释放
NSObject *obj = [[NSObject alloc] init];
// obj 这个指针变量本身不占引用计数
// 引用计数是对象自己的属性,不是指针的属性
// 多个指针指向同一个对象
NSObject *obj1 = [[NSObject alloc] init]; // 对象引用计数 = 1
NSObject *obj2 = obj1; // 对象引用计数 = 2(不是 obj2 的引用计数)
NSObject *obj3 = obj1; // 对象引用计数 = 3(不是 obj3 的引用计数)
// 所有指针都指向同一个对象,所以这个对象的引用计数 = 3
NSObject *obj = [[NSObject alloc] init]; // 引用计数 = 1
__weak NSObject *weakObj = obj; // 引用计数 = 1(没有增加!)
__strong NSObject *strongObj = obj; // 引用计数 = 2(增加了!)
// weak 引用不会让引用计数 +1
// 只有 strong 引用才会让引用计数 +1
// ❌ 错误理解:引用计数 = 对象的数量
NSObject *obj1 = [[NSObject alloc] init]; // 1 个对象,引用计数 = 1
NSObject *obj2 = [[NSObject alloc] init]; // 2 个对象,引用计数 = 1(每个对象都是 1)
// ✅ 正确理解:引用计数 = 指向这个对象的强引用数量
NSObject *obj = [[NSObject alloc] init]; // 1 个对象
NSObject *ref1 = obj; // 对象引用计数 = 2(2 个强引用指向它)
NSObject *ref2 = obj; // 对象引用计数 = 3(3 个强引用指向它)
"引用计数加1" = 又多了一个强引用指向这个对象,对象的引用计数数值 +1
关键点:
ARC 不是运行时技术,而是编译时技术!
编译器会在编译阶段自动插入 retain、release、autorelease 调用。
MRC 时代(手动):
// MRC 代码
- (void)example {
NSObject *obj = [[NSObject alloc] init]; // 引用计数 = 1
[obj retain]; // 引用计数 = 2
[obj release]; // 引用计数 = 1
[obj release]; // 引用计数 = 0,对象被释放
}
ARC 时代(自动):
// ARC 代码(你写的)
- (void)example {
NSObject *obj = [[NSObject alloc] init];
// 编译器自动在方法结束前插入 [obj release];
}
编译器转换后的伪代码:
// 编译器实际生成的代码
- (void)example {
NSObject *obj = [[NSObject alloc] init]; // 引用计数 = 1
// ... 你的代码 ...
[obj release]; // ← 编译器自动插入!
}
NSObject *obj1 = [[NSObject alloc] init]; // 引用计数 = 1
NSObject *obj2 = obj1; // obj2 强引用,引用计数 = 2
// 编译器自动插入:obj2 = [obj1 retain];
- (void)example {
NSObject *obj = [[NSObject alloc] init]; // 引用计数 = 1
// ... 使用 obj ...
// 编译器在方法结束前自动插入:[obj release];
}
@property (strong, nonatomic) NSObject *obj;
- (void)setObj:(NSObject *)obj {
if (_obj != obj) {
[_obj release]; // 编译器自动插入:释放旧值
_obj = [obj retain]; // 编译器自动插入:持有新值
}
}
想象两个好朋友互相借钱:
小明 和 小红:
小明说:"我借了小红 100 元,小红必须还我,我才能还别人"
小红说:"我借了小明 100 元,小明必须还我,我才能还别人"
结果:两个人互相等待对方还钱,永远还不完!
这就是"互相引用"的问题。
在代码中:
对象 A 说:"我强引用了对象 B,B 必须存在,我才能存在"
对象 B 说:"我强引用了对象 A,A 必须存在,我才能存在"
结果:两个对象互相等待对方释放,永远释放不了!
这就是"循环引用"。
正常情况(没有循环):
对象 A(引用计数 = 1)
↑
│ 强引用
│
变量 a
对象 B(引用计数 = 1)
↑
│ 强引用
│
变量 b
结果:a = nil 时,A 被释放;b = nil 时,B 被释放 ✅
循环引用情况:
对象 A(引用计数 = 2)
↑ ↑
│ │
│ 强引用 │ 强引用(来自 B)
│ │
变量 a 对象 B(引用计数 = 2)
↑ ↑
│ │
│ 强引用 │ 强引用(来自 A)
│ │
变量 b 对象 A(引用计数 = 2)
↑
│
│(形成循环!)
│
对象 B(引用计数 = 2)
问题:
a = nil 和 b = nil,A 和 B 的引用计数都还是 1(因为互相引用)// 定义两个类
@interface PersonA : NSObject
@property (strong, nonatomic) PersonB *personB; // A 强引用 B
@end
@interface PersonB : NSObject
@property (strong, nonatomic) PersonA *personA; // B 强引用 A
@end
// 使用
PersonA *a = [[PersonA alloc] init]; // A 引用计数 = 1
PersonB *b = [[PersonB alloc] init]; // B 引用计数 = 1
a.personB = b; // B 引用计数 = 2(A 强引用 B)
b.personA = a; // A 引用计数 = 2(B 强引用 A)
// 此时:
// A 引用计数 = 2(变量 a + B.personA)
// B 引用计数 = 2(变量 b + A.personB)
a = nil; // A 引用计数 = 1(还有 B.personA 在引用)
b = nil; // B 引用计数 = 1(还有 A.personB 在引用)
// 问题:A 和 B 的引用计数都是 1,永远不会变成 0
// 结果:A 和 B 永远不会被释放 → 内存泄漏!
图示:
初始:
变量 a → PersonA(引用计数 = 1)
变量 b → PersonB(引用计数 = 1)
互相引用后:
变量 a → PersonA(引用计数 = 2)← PersonB.personA
↓ PersonA.personB
变量 b → PersonB(引用计数 = 2)← PersonA.personB
↑ PersonB.personA
│
└───────────┘(形成循环!)
a = nil, b = nil 后:
PersonA(引用计数 = 1)← PersonB.personA
↓ PersonA.personB
PersonB(引用计数 = 1)← PersonA.personB
↑ PersonB.personA
│
└───────────┘(循环还在,无法释放!)
方法:把其中一个强引用改成弱引用
// ✅ 正确:B 弱引用 A
@interface PersonA : NSObject
@property (strong, nonatomic) PersonB *personB; // A 强引用 B
@end
@interface PersonB : NSObject
@property (weak, nonatomic) PersonA *personA; // B 弱引用 A(关键!)
@end
// 使用
PersonA *a = [[PersonA alloc] init]; // A 引用计数 = 1
PersonB *b = [[PersonB alloc] init]; // B 引用计数 = 1
a.personB = b; // B 引用计数 = 2(A 强引用 B)
b.personA = a; // A 引用计数 = 1(B 弱引用 A,不增加引用计数)
// 此时:
// A 引用计数 = 1(只有变量 a)
// B 引用计数 = 2(变量 b + A.personB)
a = nil; // A 引用计数 = 0 → A 被释放!
// B.personA 自动变成 nil(weak 的特性)
b = nil; // B 引用计数 = 1(还有 A.personB?不对,A 已经释放了)
// 实际上,A 释放时,A.personB 也被释放
// 所以 B 引用计数 = 0 → B 被释放!
// 结果:两个对象都能正常释放 ✅
图示(打破循环后):
变量 a → PersonA(引用计数 = 1)
↓ PersonA.personB(强引用)
变量 b → PersonB(引用计数 = 2)
↑ PersonB.personA(弱引用,不增加引用计数)
a = nil 后:
PersonA(引用计数 = 0)→ 被释放!
↓ PersonA.personB 也被释放
PersonB(引用计数 = 1)← 只有变量 b
↑ PersonB.personA = nil(自动置 nil)
b = nil 后:
PersonB(引用计数 = 0)→ 被释放!✅
循环引用 = 两个或多个对象互相强引用,形成闭环,导致都无法释放
关键点:
解决方法:
最直接的后果:对象永远不会被释放,占用内存越来越多
@interface ViewController : UIViewController
@property (copy, nonatomic) void (^block)(void);
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// ❌ 循环引用
self.block = ^{
[self doSomething]; // block 强引用 self
};
// self 强引用 block,block 强引用 self → 循环引用
}
@end
// 使用场景:
ViewController *vc = [[ViewController alloc] init];
[self.navigationController pushViewController:vc animated:YES];
// 用户返回上一页
[self.navigationController popViewControllerAnimated:YES];
// 问题:
// vc 应该被释放,但因为循环引用,vc 无法释放
// 内存泄漏!vc 占用的内存永远不会回收
影响:
dealloc 方法不会被调用,清理代码不会执行
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// ❌ 循环引用
self.block = ^{
[self doSomething];
};
}
- (void)dealloc {
NSLog(@"ViewController 被释放"); // ❌ 永远不会打印!
// 清理代码不会执行
[[NSNotificationCenter defaultCenter] removeObserver:self];
[self.timer invalidate];
// 这些清理代码都不会执行!
}
@end
影响:
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 添加通知观察者
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(handleNotification:)
name:@"SomeNotification"
object:nil];
// ❌ 循环引用
self.block = ^{
[self doSomething];
};
}
- (void)dealloc {
// ❌ 永远不会执行!
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
// 问题:
// ViewController 无法释放
// 通知观察者无法移除
// 即使 ViewController 已经不在屏幕上,仍然会接收通知
// 可能导致崩溃或逻辑错误
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// ❌ 循环引用
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0
target:self // timer 强引用 self
selector:@selector(timerAction)
userInfo:nil
repeats:YES];
// self 强引用 timer,timer 强引用 self → 循环引用
}
- (void)dealloc {
// ❌ 永远不会执行!
[self.timer invalidate]; // 定时器无法停止
self.timer = nil;
}
// 问题:
// ViewController 无法释放
// 定时器继续运行,即使 ViewController 已经不在屏幕上
// 定时器回调可能访问已销毁的视图,导致崩溃
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// ❌ 循环引用
[NetworkManager requestWithCompletion:^(NSData *data) {
[self handleResponse:data]; // block 强引用 self
}];
// 如果 NetworkManager 也强引用这个 block,可能形成循环引用
}
- (void)dealloc {
// ❌ 永远不会执行!
// 清理代码不会执行
}
// 问题:
// ViewController 无法释放
// 网络请求完成后,回调可能访问已销毁的视图
// 可能导致崩溃或逻辑错误
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self.model addObserver:self
forKeyPath:@"value"
options:NSKeyValueObservingOptionNew
context:nil];
// ❌ 循环引用
self.block = ^{
[self doSomething];
};
}
- (void)dealloc {
// ❌ 永远不会执行!
[self.model removeObserver:self forKeyPath:@"value"];
}
// 问题:
// ViewController 无法释放
// KVO 观察者无法移除
// 如果 model 被释放,可能导致崩溃
| 影响 | 说明 | 严重程度 |
|---|---|---|
| 内存泄漏 | 对象无法释放,内存持续增长 | ⚠️⚠️⚠️ 严重 |
| dealloc 不执行 | 清理代码不会执行 | ⚠️⚠️⚠️ 严重 |
| 通知无法移除 | 继续接收通知,可能导致崩溃 | ⚠️⚠️ 中等 |
| 定时器无法停止 | 定时器继续运行,可能访问已销毁对象 | ⚠️⚠️ 中等 |
| 网络回调继续执行 | 回调可能访问已销毁对象 | ⚠️⚠️ 中等 |
| KVO 无法移除 | 可能导致崩溃 | ⚠️⚠️ 中等 |
- (void)dealloc {
NSLog(@"✅ ViewController 被释放"); // 如果没打印,说明有循环引用
}
自动检测内存泄漏,在开发阶段就能发现问题。
// ❌ 错误:循环引用
@interface ViewController ()
@property (nonatomic, copy) void (^block)(void);
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// self 强引用 block
self.block = ^{
// block 强引用 self(捕获了 self)
[self doSomething]; // ← 形成循环引用!
};
}
@end
// ✅ 正确:使用 weak-strong dance
- (void)viewDidLoad {
[super viewDidLoad];
__weak typeof(self) weakSelf = self;
self.block = ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
if (!strongSelf) return;
[strongSelf doSomething];
};
}
🔍 Weak-Strong Dance 详细解释:
__weak typeof(self) weakSelf = self;
__weak typeof(self) weakSelf = self;
作用:
weak 指针指向 self
self 的引用计数self 被释放,weakSelf 会自动变成 nil
内存状态:
self 的引用计数 = 1(假设只有这里引用)
weakSelf → 指向 self(但不增加引用计数)
为什么需要 weak?
self,block 会强引用 self
self → block → self(循环引用!)weakSelf 后,block 只弱引用 self,打破循环__strong typeof(weakSelf) strongSelf = weakSelf;
self.block = ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
// ...
};
作用:
weakSelf 转成 strongSelf(强引用)weakSelf 是 nil,strongSelf 也是 nil
weakSelf 不是 nil,strongSelf 会增加引用计数,保证执行期间 self 不会被释放内存状态变化:
情况 A:block 执行时,self 还存在
执行前:
self 引用计数 = 1
weakSelf → self(弱引用)
执行时(进入 block):
strongSelf = weakSelf; // strongSelf 强引用 self
self 引用计数 = 2 ← 增加了!
执行中:
[self doSomething]; // 安全!self 不会被释放
执行后(block 结束):
strongSelf 作用域结束,自动 release
self 引用计数 = 1 ← 恢复
情况 B:block 执行时,self 已经被释放
执行前:
self 引用计数 = 0,已被释放
weakSelf = nil(自动置 nil)
执行时(进入 block):
strongSelf = weakSelf; // strongSelf = nil
if (!strongSelf) return; // 直接返回,不执行后续代码
if (!strongSelf) return;
if (!strongSelf) return;
作用:
self 已经被释放,weakSelf 是 nil,strongSelf 也是 nil
为什么需要这个检查?
nil 对象在 OC 中是安全的(不会崩溃),但逻辑上不应该执行strongSelf 而不是 weakSelf
[strongSelf doSomething]; // ✅ 正确
// [weakSelf doSomething]; // ⚠️ 理论上可以,但不推荐
为什么用 strongSelf?
关键原因:防止执行中途被释放
// ❌ 危险:只用 weakSelf
self.block = ^{
__weak typeof(self) weakSelf = self;
if (!weakSelf) return;
// 假设 doSomething 执行时间很长
[weakSelf doSomething]; // 执行到一半...
// 如果此时 self 被释放了(其他强引用都断了)
// weakSelf 变成 nil,但代码还在执行!
[weakSelf doAnotherThing]; // 可能访问 nil
};
// ✅ 安全:使用 strongSelf
self.block = ^{
__weak typeof(self) weakSelf = self;
__strong typeof(weakSelf) strongSelf = weakSelf;
if (!strongSelf) return;
// strongSelf 强引用 self,保证整个 block 执行期间 self 不会被释放
[strongSelf doSomething]; // self 引用计数 = 2,安全
[strongSelf doAnotherThing]; // self 引用计数 = 2,安全
// block 结束,strongSelf 释放,self 引用计数 = 1
};
@interface ViewController ()
@property (nonatomic, copy) void (^block)(void);
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 步骤 1:创建 weak 引用
__weak typeof(self) weakSelf = self;
// 此时:self 引用计数 = 1,weakSelf → self(弱引用)
// 步骤 2:创建 block(捕获 weakSelf,不是 self)
self.block = ^{
// 步骤 3:block 执行时,转为 strong 引用
__strong typeof(weakSelf) strongSelf = weakSelf;
// 步骤 4:安全检查
if (!strongSelf) {
NSLog(@"self 已被释放,不执行");
return;
}
// 步骤 5:使用 strongSelf(保证执行期间 self 不会被释放)
NSLog(@"执行任务,self 引用计数 = %lu", [strongSelf retainCount]);
[strongSelf doSomething];
// 步骤 6:block 结束,strongSelf 自动释放
// self 引用计数恢复
};
// 步骤 7:viewDidLoad 结束,但 block 还在(被 self.block 持有)
}
- (void)doSomething {
NSLog(@"执行任务");
}
- (void)dealloc {
NSLog(@"ViewController 被释放");
// 如果 block 还在,这里不会被调用(因为循环引用)
// 如果用了 weak-strong dance,这里会被调用
}
@end
Q1:为什么不能直接用 weakSelf?
// ❌ 不推荐
self.block = ^{
__weak typeof(self) weakSelf = self;
[weakSelf doSomething]; // 执行中途 self 可能被释放
};
答案: 虽然不会崩溃(OC 对 nil 消息安全),但执行中途 self 可能被释放,导致逻辑错误。
Q2:strongSelf 会不会又造成循环引用?为什么 block 里用了 strong 修饰,不也是强引用 self 吗?
答案:不会! 这是最关键的理解点!
重要:block 捕获的是 weakSelf(弱引用),不是 strongSelf!
__weak typeof(self) weakSelf = self; // 步骤 1:创建 weak 引用
self.block = ^{
// 步骤 2:block 捕获的是 weakSelf(弱引用)
// block 内部结构(伪代码):
// struct Block {
// __weak typeof(self) weakSelf; // ← block 捕获的是这个!
// void (*invoke)(...);
// };
// 步骤 3:block 执行时,才创建 strongSelf(局部变量)
__strong typeof(weakSelf) strongSelf = weakSelf;
// strongSelf 是 block 执行时才创建的,不是 block 捕获的!
};
情况 A:如果 block 直接捕获 self(会形成循环引用)
// ❌ 错误:block 捕获 self(强引用)
self.block = ^{
[self doSomething]; // block 捕获 self(强引用)
};
// 内存关系:
// self → block(强引用)
// block → self(强引用,因为捕获了 self)
// 形成循环:self → block → self ❌
情况 B:block 捕获 weakSelf,执行时创建 strongSelf(不会形成循环引用)
// ✅ 正确:block 捕获 weakSelf(弱引用)
__weak typeof(self) weakSelf = self;
self.block = ^{
// block 捕获的是 weakSelf(弱引用),不是 self!
// 所以:block → weakSelf(弱引用,不增加引用计数)
// strongSelf 是 block 执行时才创建的局部变量
__strong typeof(weakSelf) strongSelf = weakSelf;
// strongSelf 不是 block 捕获的,是执行时的临时变量
};
// 内存关系:
// self → block(强引用)
// block → weakSelf(弱引用,不增加引用计数)✅
// block 执行时:strongSelf → self(临时强引用,执行完就释放)✅
// 没有循环!✅
错误情况(会循环引用):
self ──→ block ──→ self(强引用)
↑ │
└──────────────────┘(循环!)
正确情况(不会循环引用):
self ──→ block ──→ weakSelf ──→ self(弱引用,不形成循环)
↑
└──────────────────────────────┘(没有循环!)
block 执行时:
self ──→ block ──→ weakSelf ──→ self(弱引用)
↑ ↑
│ │
└──────────────────────────────┘
│
strongSelf(临时强引用,执行完就释放)
block 捕获的是什么?
weakSelf(弱引用),不是 strongSelf
self,不会形成循环strongSelf 是什么?
strongSelf 是 block 执行时才创建的局部变量
strongSelf 就释放了为什么不会形成循环?
weakSelf(弱引用),所以 block 不强引用 selfstrongSelf 只是执行时的临时强引用,不会形成持久的循环创建阶段:
self(引用计数 = 1)
↓ 强引用
block(捕获 weakSelf,弱引用 self)
↓ 弱引用(不增加引用计数)
weakSelf → self(引用计数 = 1,没有增加)
执行阶段(block 被调用):
self(引用计数 = 1)
↓ 强引用
block
↓ 弱引用
weakSelf → self(引用计数 = 1)
↓
strongSelf(局部变量,强引用 self)
↓ 强引用(临时)
self(引用计数 = 2,临时增加)
执行结束:
strongSelf 释放 → self(引用计数 = 1,恢复)
block 仍然存在,但只弱引用 self(不形成循环)
答案:不会! 因为:
weakSelf(弱引用),不是 strongSelfstrongSelf 是局部变量,只在 block 执行期间存在strongSelf 自动释放Q3:什么时候 weakSelf 会变成 nil?
答案: 当 self 的所有强引用都断开时:
// 场景:ViewController 被 pop 或 dismiss
[self.navigationController popViewControllerAnimated:YES];
// 此时如果 self 没有其他强引用,会被释放
// weakSelf 自动变成 nil
Q4:可以简化成这样吗?
// ⚠️ 简化版(不推荐,但某些场景可用)
__weak typeof(self) weakSelf = self;
self.block = ^{
[weakSelf doSomething]; // 直接使用 weakSelf
};
答案:
doSomething 执行很快,且不涉及多步操作strongSelf 保证执行期间对象不被释放Weak-Strong Dance 的作用:
weakSelf:打破循环引用,让 block 不强持有 self
strongSelf:在 block 执行期间强持有 self,防止执行中途被释放if (!strongSelf) return:安全检查,如果 self 已释放则提前返回核心思想: 用弱引用打破循环,用临时强引用保证执行安全。
// ❌ 错误:循环引用
@interface ViewController ()
@property (nonatomic, strong) NSTimer *timer;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// self 强引用 timer
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0
target:self // ← timer 强引用 self
selector:@selector(timerAction)
userInfo:nil
repeats:YES];
// 形成循环:self → timer → self
}
// ✅ 正确:使用中间对象或 block-based API
- (void)viewDidLoad {
[super viewDidLoad];
__weak typeof(self) weakSelf = self;
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0
repeats:YES
block:^(NSTimer * _Nonnull timer) {
__strong typeof(weakSelf) strongSelf = weakSelf;
if (!strongSelf) return;
[strongSelf timerAction];
}];
}
- (void)dealloc {
[self.timer invalidate]; // 必须手动停止
self.timer = nil;
}
// ⚠️ iOS 9+ 后通知中心会弱引用观察者,但业务代码仍需注意
- (void)viewDidLoad {
[super viewDidLoad];
// 如果 self 强引用通知,通知回调里又用 self,可能形成循环
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(handleNotification:)
name:@"SomeNotification"
object:nil];
}
- (void)dealloc {
// 必须移除观察者
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
Weak 表 = 一张全局的哈希表,记录所有 weak 指针
// 伪代码:Weak 表的结构
struct WeakTable {
// key: 对象的地址
// value: 所有指向这个对象的 weak 指针数组
HashMap<对象地址, Array<weak指针地址>>;
};
NSObject *obj = [[NSObject alloc] init]; // 引用计数 = 1
__weak NSObject *weakObj = obj; // 引用计数仍 = 1
// 步骤 1:weakObj 被注册到 Weak 表
// Weak 表记录:obj 的地址 → [weakObj 的地址]
obj = nil; // 引用计数 = 0,对象即将被释放
// 步骤 2:对象释放时,系统遍历 Weak 表
// 找到所有指向这个对象的 weak 指针
// 步骤 3:把所有 weak 指针置为 nil
// weakObj 现在 = nil(安全!)
答案要点:
内存对齐 = 数据在内存中的起始地址必须是某个数的倍数
基本类型对齐:
char:1 字节对齐short:2 字节对齐int:4 字节对齐long / 指针:8 字节对齐double:8 字节对齐结构体对齐:
struct Example {
char a; // 1 字节,偏移 0
// 填充 3 字节(padding)
int b; // 4 字节,偏移 4(必须是 4 的倍数)
char c; // 1 字节,偏移 8
// 填充 7 字节(padding)
double d; // 8 字节,偏移 16(必须是 8 的倍数)
};
// 总大小 = 24 字节(必须是 8 的倍数)
// 验证
NSLog(@"Size: %lu", sizeof(struct Example)); // 输出:24
@interface Person : NSObject {
@public
char _name; // 1 字节
int _age; // 4 字节
double _height; // 8 字节
}
@end
// 内存布局(64 位系统):
// [isa 指针: 8 字节] [padding: 0]
// [_name: 1 字节] [padding: 3 字节]
// [_age: 4 字节]
// [padding: 4 字节](为了 double 对齐)
// [_height: 8 字节]
// 总大小 = 8 + 4 + 4 + 8 = 24 字节(必须是 8 的倍数)
Person *p = [[Person alloc] init];
// 方法 1:实例大小(对齐后)
size_t instanceSize = class_getInstanceSize([Person class]);
NSLog(@"Instance size: %zu", instanceSize); // 输出:24
// 方法 2:实际分配大小(系统可能分配更多)
size_t mallocSize = malloc_size((__bridge const void *)p);
NSLog(@"Malloc size: %zu", mallocSize); // 可能输出:32(系统额外分配)
@interface Example : NSObject {
char a; // 偏移 8(isa 后),大小 1
// 编译器插入 padding: 3 字节
int b; // 偏移 12,大小 4
char c; // 偏移 16,大小 1
// 编译器插入 padding: 7 字节(为了 double 对齐)
double d; // 偏移 24,大小 8
}
@end
// 编译器优化:调整成员顺序可以减少 padding
@interface OptimizedExample : NSObject {
double d; // 偏移 8,大小 8(最大对齐值)
int b; // 偏移 16,大小 4
char a; // 偏移 20,大小 1
char c; // 偏移 21,大小 1
// padding: 6 字节(为了整体 8 字节对齐)
}
@end
// 优化后总大小可能更小!
Q:为什么需要内存对齐?
答案要点:
Tagged Pointer = 把小数据直接编码进指针里,不占用堆内存
// 传统方式(64 位系统)
NSNumber *num1 = @(42);
// 内存布局:
// 指针变量(栈上,8 字节)→ 指向堆上的 NSNumber 对象(至少 16 字节)
// 总占用:8 + 16 = 24 字节
// Tagged Pointer 方式
NSNumber *num2 = @(42);
// 内存布局:
// 指针变量(栈上,8 字节),但指针里直接存了 42 的值!
// 总占用:8 字节(节省 16 字节!)
NSNumber *num1 = @(42);
NSNumber *num2 = @(1000000); // 大数字
// 判断是否是 Tagged Pointer
NSLog(@"num1 is Tagged: %d", _objc_isTaggedPointer((__bridge void *)num1));
// 输出:1(是 Tagged Pointer)
NSLog(@"num2 is Tagged: %d", _objc_isTaggedPointer((__bridge void *)num2));
// 输出:0(不是,因为数字太大)
NSString *str1 = @"abc"; // Tagged Pointer
NSString *str2 = @"abcdefghijkl"; // 普通对象(堆上)
// 验证
NSLog(@"str1 pointer: %p", str1); // 指针值看起来很奇怪(有 tag 位)
NSLog(@"str2 pointer: %p", str2); // 正常的堆地址
// 查看实际内容
NSLog(@"str1: %@", str1); // 正常输出
NSLog(@"str2: %@", str2); // 正常输出
Q:Tagged Pointer 如何工作?
答案要点:
Mach-O = macOS/iOS 的可执行文件格式
类似于:
.exe(PE 格式).app(Mach-O 格式)Mach-O 文件
├── Header(文件头)
│ ├── 魔数(标识文件类型)
│ ├── CPU 架构(arm64/x86_64)
│ └── 加载命令数量
│
├── Load Commands(加载命令)
│ ├── 代码段位置
│ ├── 数据段位置
│ └── 动态库依赖
│
└── Data(数据区)
├── __TEXT(代码段)
│ ├── 可执行代码
│ └── 常量字符串
│
└── __DATA(数据段)
├── 全局变量
├── 静态变量
└── 类元数据
特点:只读(Read-Only)、可执行(Executable)
// 这些内容在 __TEXT 段:
// 1. 可执行代码
- (void)example {
NSLog(@"Hello"); // 这行代码编译后的机器指令在 __TEXT 段
}
// 2. 常量字符串
NSString *str = @"Hello"; // @"Hello" 在 __TEXT 段
// 3. 常量数据
const int kValue = 100; // 在 __TEXT 段
特点:可读写(Read-Write)
// 这些内容在 __DATA 段:
// 1. 全局变量
int globalVar = 10; // 在 __DATA 段
// 2. 静态变量
static int staticVar = 20; // 在 __DATA 段
// 3. 类元数据(运行时注册)
@interface MyClass : NSObject
@end
// MyClass 的类对象信息在 __DATA 段
@interface Person : NSObject
@end
// 编译后,Person 类的信息存储在:
// 1. __TEXT 段:方法实现(机器码)
// 2. __DATA 段:类对象结构
// - isa 指针
// - superclass 指针
// - 方法列表指针
// - 属性列表指针
// - 协议列表指针
// 静态库的代码被直接链接进主可执行文件
// 内存映射:
// 主可执行文件的 __TEXT 段包含静态库的代码
// 主可执行文件的 __DATA 段包含静态库的数据
// 动态库由 dyld(动态链接器)在运行时加载
// 内存映射:
// 1. dyld 读取动态库的 Mach-O 文件
// 2. 将 __TEXT 段映射到内存(只读、可执行)
// 3. 将 __DATA 段映射到内存(可读写)
// 4. 每个进程共享同一份 __TEXT 段(节省内存)
// 5. 每个进程有独立的 __DATA 段副本
Q:类对象在哪里?
答案要点:
AutoreleasePool = 延迟释放池,让对象"晚一点"释放
// 传统 release(立即释放)
NSObject *obj = [[NSObject alloc] init];
[obj release]; // 立即释放,引用计数 = 0
// Autorelease(延迟释放)
NSObject *obj = [[NSObject alloc] init];
[obj autorelease]; // 加入自动释放池,等池子结束时才 release
// AutoreleasePool 是一个栈结构
@autoreleasepool {
// Pool 1(外层)
@autoreleasepool {
// Pool 2(内层)
NSObject *obj = [[NSObject alloc] init];
// obj 被加入 Pool 2
}
// Pool 2 结束,obj 被释放
}
// Pool 1 结束
// 主线程的 RunLoop 结构(简化)
void mainRunLoop() {
while (appIsRunning) {
@autoreleasepool { // ← 系统自动创建
// 处理事件
handleEvents();
// 处理定时器
handleTimers();
// 处理 Source
handleSources();
}
// 池子结束,释放所有 autorelease 的对象
}
}
关键点:
@autoreleasepool
// ❌ 错误:子线程大量创建对象
dispatch_async(dispatch_get_global_queue(0, 0), ^{
for (int i = 0; i < 10000; i++) {
NSObject *obj = [[NSObject alloc] init];
// obj 被 autorelease,但没有池子,会积压!
}
});
// ✅ 正确:手动创建 AutoreleasePool
dispatch_async(dispatch_get_global_queue(0, 0), ^{
@autoreleasepool {
for (int i = 0; i < 10000; i++) {
NSObject *obj = [[NSObject alloc] init];
// obj 在池子结束时释放
}
}
// 或者更细粒度:
for (int i = 0; i < 10000; i++) {
@autoreleasepool {
NSObject *obj = [[NSObject alloc] init];
// 每次循环结束就释放
}
}
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
@autoreleasepool {
// 大量临时对象
for (int i = 0; i < 100000; i++) {
NSString *str = [NSString stringWithFormat:@"%d", i];
// 使用 str...
}
}
// 池子结束,所有临时对象立即释放,降低峰值内存
});
// ❌ 不好:所有临时对象积压到外层池子
for (int i = 0; i < 10000; i++) {
NSMutableArray *arr = [NSMutableArray array]; // autorelease
// 使用 arr...
}
// ✅ 好:每次循环结束就释放
for (int i = 0; i < 10000; i++) {
@autoreleasepool {
NSMutableArray *arr = [NSMutableArray array];
// 使用 arr...
}
// arr 立即释放
}
Q:为什么子线程需要手动创建 AutoreleasePool?
答案要点:
@autoreleasepool,及时释放临时对象iOS 使用 jemalloc 或类似的分配器管理堆内存。
堆内存分配器
├── Tiny 区(< 16 字节)
│ └── 快速分配,固定大小块
│
├── Small 区(16 字节 ~ 几 KB)
│ └── 按大小分类的块池
│
└── Large 区(> 几 KB)
└── 直接 mmap 分配
// 场景:频繁分配和释放不同大小的对象
// 1. 分配 100 字节
void *p1 = malloc(100);
// 2. 分配 200 字节
void *p2 = malloc(200);
// 3. 释放 p1(100 字节的空洞)
free(p1);
// 4. 现在想分配 150 字节
void *p3 = malloc(150);
// 问题:p1 的空洞只有 100 字节,不够!
// 只能从其他地方分配,导致碎片
策略 1:对象池(Object Pool)
// 复用对象,而不是频繁创建和销毁
@interface ObjectPool : NSObject
+ (instancetype)sharedPool;
- (id)getObject;
- (void)returnObject:(id)obj;
@end
// 使用
ObjectPool *pool = [ObjectPool sharedPool];
MyObject *obj = [pool getObject];
// 使用 obj...
[pool returnObject:obj]; // 归还,而不是释放
策略 2:批量分配
// 一次性分配大块内存,自己管理
void *buffer = malloc(1024 * 1024); // 1MB
// 自己在这 1MB 里分配小对象
// 减少系统 malloc 调用次数
Q:如何优化内存分配性能?
答案要点:
栈 = 函数调用的"工作区"
void functionA() {
int a = 10; // 在栈上
functionB();
}
void functionB() {
int b = 20; // 在栈上
functionC();
}
void functionC() {
int c = 30; // 在栈上
}
// 调用栈(从下往上):
// [functionA 的栈帧: a = 10]
// [functionB 的栈帧: b = 20]
// [functionC 的栈帧: c = 30] ← 栈顶
// 主线程栈大小:通常 1MB
// 子线程栈大小:通常 512KB(可配置)
// 创建自定义栈大小的线程
NSThread *thread = [[NSThread alloc] initWithTarget:self
selector:@selector(threadMain)
object:nil];
thread.stackSize = 1024 * 1024; // 1MB
[thread start];
// ❌ 错误:无限递归
- (void)recursive {
int localVar[1000]; // 大局部变量
[self recursive]; // 无限递归,栈帧不断增长
// 最终:栈溢出(Stack Overflow)
}
// ✅ 正确:有终止条件
- (void)recursiveWithDepth:(int)depth {
if (depth <= 0) return; // 终止条件
int localVar[1000];
[self recursiveWithDepth:depth - 1];
}
// ❌ 危险:大数组在栈上
- (void)example {
int hugeArray[1000000]; // 4MB 在栈上!
// 可能栈溢出
}
// ✅ 安全:大数组在堆上
- (void)example {
int *hugeArray = malloc(1000000 * sizeof(int)); // 堆上
// 使用...
free(hugeArray);
}
Q:栈溢出如何避免?
答案要点:
malloc 或对象pthread_attr_setstacksize(不推荐,治标不治本)// 调用:[obj methodName]
// 步骤 1:通过 isa 找到类对象
Class cls = object_getClass(obj); // obj->isa
// 步骤 2:在类对象的方法列表中查找
Method method = class_getInstanceMethod(cls, @selector(methodName));
// 步骤 3:如果没找到,沿 superclass 链向上查找
while (cls && !method) {
cls = class_getSuperclass(cls);
method = class_getInstanceMethod(cls, @selector(methodName));
}
// 步骤 4:如果找到,调用 method->imp(函数指针)
为什么需要缓存?
方法查找需要遍历类的方法列表,如果每次都查找,性能很差。
缓存机制:
// 伪代码:方法缓存结构
struct MethodCache {
// 哈希表:selector → IMP
HashMap<Selector, IMP> cache;
};
// 查找流程(带缓存):
IMP imp = cache.get(selector);
if (imp) {
return imp; // 缓存命中,直接返回
} else {
// 缓存未命中,查找方法列表
imp = findMethodInClass(selector);
cache.set(selector, imp); // 加入缓存
return imp;
}
@interface Person : NSObject
- (void)instanceMethod; // 实例方法
+ (void)classMethod; // 类方法
@end
// 调用实例方法
Person *p = [[Person alloc] init];
[p instanceMethod];
// 查找路径:p->isa(Person 类)→ 查找实例方法列表
// 调用类方法
[Person classMethod];
// 查找路径:Person 类对象->isa(Person 元类)→ 查找类方法列表
// 元类链(简化)
Person 实例
└─ isa → Person 类对象
├─ isa → Person 元类
│ ├─ isa → NSObject 元类
│ │ └─ isa → NSObject 元类(根元类指向自己)
│ └─ superclass → NSObject 元类
└─ superclass → NSObject 类对象
└─ isa → NSObject 元类
Q:方法查找的完整流程?
答案要点:
forwardingTargetForSelector: 等)// OC 对象总是在堆上
NSObject *obj = [[NSObject alloc] init];
// obj 是指针(栈上),指向堆上的对象
// C++ 对象可以在栈上
class MyClass {
public:
int value;
};
void example() {
MyClass obj; // 栈上对象
obj.value = 10;
} // obj 自动析构
// 也可以在堆上
MyClass *obj = new MyClass(); // 堆上对象
delete obj; // 手动释放
NSObject *obj1 = [[NSObject alloc] init]; // 引用计数 = 1
NSObject *obj2 = obj1; // 引用计数 = 2
obj1 = nil; // 引用计数 = 1
obj2 = nil; // 引用计数 = 0,对象释放
class MyClass {
public:
MyClass() { /* 构造 */ }
~MyClass() { /* 析构,自动调用 */ }
};
void example() {
MyClass obj; // 构造
// 使用 obj...
} // 自动析构(栈上对象)
// 堆上对象需要手动管理
MyClass *obj = new MyClass();
delete obj; // 手动析构
@interface Animal : NSObject
- (void)speak;
@end
@interface Dog : Animal
- (void)speak; // 重写
@end
Animal *animal = [[Dog alloc] init];
[animal speak]; // 运行时查找,调用 Dog 的 speak
// 通过 isa 指针找到实际类型
class Animal {
public:
virtual void speak() { /* 基类实现 */ }
// 有虚函数,对象有 vptr(虚函数表指针)
};
class Dog : public Animal {
public:
void speak() override { /* 派生类实现 */ }
};
Animal *animal = new Dog();
animal->speak(); // 通过 vptr 找到虚函数表,调用 Dog::speak
// Objective-C++ 文件(.mm)
// OC 对象
NSObject *obj = [[NSObject alloc] init];
// C++ 对象
std::vector<int> vec;
vec.push_back(1);
// ⚠️ 注意:C++ 异常不能穿越 OC 代码
// 如果 C++ 代码抛异常,必须在 C++ 代码里捕获
Q:OC 和 C++ 的内存管理有什么区别?
答案要点:
虚拟内存 = 进程看到的"假地址空间"
进程视角(虚拟地址):
0x00000000 ──────────┐
│
0x10000000 ──────────┤ 代码段
│
0x20000000 ──────────┤ 数据段
│
0x30000000 ──────────┤ 堆
│
0x40000000 ──────────┤ 栈
│
0x7FFFFFFF ──────────┘
实际物理内存:
[物理地址 0x1000] ← 可能映射到虚拟地址 0x10000000
[物理地址 0x2000] ← 可能映射到虚拟地址 0x20000000
...
页 = 内存管理的最小单位(通常 4KB 或 16KB)
// 虚拟地址空间被分成页
虚拟地址:0x10000000 - 0x10000FFF → 页 1
虚拟地址:0x10001000 - 0x10001FFF → 页 2
虚拟地址:0x10002000 - 0x10002FFF → 页 3
// 每页可以独立映射到物理内存
页 1 → 物理页 A
页 2 → 物理页 B
页 3 → 未映射(访问会触发缺页异常)
页表 = 虚拟地址到物理地址的映射表
虚拟地址:0x10000000
↓
页表查找
↓
物理地址:0x50000000
// 场景:fork 进程或复制大对象
// 1. 父进程有数据
NSMutableArray *arr = [NSMutableArray arrayWithObjects:@1, @2, nil];
// 2. 子进程 fork(或复制)
// 此时:父子进程共享同一份物理内存(只读)
// 3. 子进程修改数据
[arr addObject:@3];
// 4. 触发写时复制
// 系统复制物理页,子进程有自己的副本
// 现在:父子进程有独立的物理内存
// 多个进程运行同一个 App
进程 A:加载 MyApp
进程 B:加载 MyApp
// 代码段(__TEXT)的物理页被共享
// 节省物理内存!
// 没有 ASLR(固定地址)
代码段起始:0x10000000(固定)
// 有 ASLR(随机地址)
进程 1 代码段起始:0x10001234(随机)
进程 2 代码段起始:0x10005678(随机)
// 目的:防止攻击者预测地址
Q:虚拟内存的作用?
答案要点:
// 伪代码:Weak 表结构
struct WeakTable {
// 全局哈希表
// key: 对象的地址(作为弱引用目标)
// value: 指向这个对象的所有 weak 指针的数组
HashMap<void *, Array<void **>> weakReferences;
};
// 示例:
NSObject *obj = [[NSObject alloc] init];
__weak NSObject *weak1 = obj;
__weak NSObject *weak2 = obj;
// Weak 表记录:
// obj 的地址 → [weak1 的地址, weak2 的地址]
NSObject *obj = [[NSObject alloc] init]; // 对象创建
__weak NSObject *weakObj = obj; // weak 指针赋值
// 系统内部操作(伪代码):
void weak_assign(id *location, id newObj) {
// 1. 如果之前有 weak 指针,先移除
if (*location) {
removeWeakReference(*location, location);
}
// 2. 设置新的 weak 指针
*location = newObj;
// 3. 如果新对象不为 nil,注册到 Weak 表
if (newObj) {
addWeakReference(newObj, location);
}
}
// 对象释放流程(伪代码)
void object_release(id obj) {
// 1. 引用计数减 1
if (retainCount(obj) > 1) {
retainCount(obj)--;
return;
}
// 2. 引用计数为 0,准备释放
// 3. 查找 Weak 表,找到所有指向这个对象的 weak 指针
Array<void **> weakRefs = getWeakReferences(obj);
// 4. 把所有 weak 指针置为 nil
for (void **weakPtr in weakRefs) {
*weakPtr = nil;
}
// 5. 从 Weak 表中移除记录
removeWeakTableEntry(obj);
// 6. 释放对象内存
free(obj);
}
// 场景:大量 weak 指针指向同一个对象
NSObject *obj = [[NSObject alloc] init];
for (int i = 0; i < 10000; i++) {
__weak NSObject *weak = obj; // 每个 weak 都注册到 Weak 表
}
// 对象释放时,需要清理 10000 个 weak 指针
// 虽然还是 O(n),但 n 可能很大
Q:Weak 表如何实现?性能如何?
答案要点:
1. 代码层面
└─ OC 对象(Person *p = [[Person alloc] init])
├─ isa 指针 → 类对象
├─ 成员变量(内存对齐)
└─ 引用计数管理
2. 运行时层面
└─ 类对象 / 元类
├─ 方法列表
├─ 属性列表
└─ 方法缓存
3. 内存布局层面
└─ 虚拟地址空间
├─ 代码段(__TEXT):类的方法实现
├─ 数据段(__DATA):类对象、全局变量
├─ 堆:对象实例
└─ 栈:局部变量、函数调用
4. 系统层面
└─ 虚拟内存 → 物理内存映射
├─ 页表映射
├─ ASLR 随机化
└─ 写时复制
5. 硬件层面
└─ 物理内存(RAM)
└─ CPU 缓存(L1/L2/L3)
祝你面试顺利! 🚀
@ViewBuilder 全面解析在 SwiftUI 的世界里,@ViewBuilder 是一个你每天都在用,却可能从未认真了解过的核心机制。
很多 SwiftUI 看起来“像写 DSL 一样优雅”的代码,其实都离不开它。
本文将从为什么需要它、它解决了什么问题、如何使用、常见坑点几个维度,系统性地介绍 @ViewBuilder,适合 SwiftUI 初学者到中级开发者 阅读。
在 Swift 中,函数或计算属性只能返回一个值。
但在 SwiftUI 中,我们却经常写出这样的代码:
var body: some View {
Text("Hello")
Image(systemName: "star")
Button("Tap") { }
}
表面看起来像是“返回了多个 View”,这在普通 Swift 函数里是不可能的。
那 SwiftUI 是怎么做到的?
答案就是: @ViewBuilder。
@ViewBuilder 是什么@ViewBuilder 是 Swift 的一种 Result Builder(结果构建器) 。
它的核心职责只有一个:
把多行 View 表达式,组合成一个 View 返回。
你写的代码是这样:
Text("A")
Text("B")
Text("C")
编译器在背后会帮你组合成类似:
TupleView<(Text, Text, Text)>
但这些具体类型对开发者是隐藏的,你只需要关心:
可以像写布局一样写 View,而不是手动拼装结构。
@ViewBuilder
因为 SwiftUI 已经帮你加好了。
例如:
struct ContentView: View {
var body: some View {
Text("Hello")
Text("World")
}
}
实际上等价于:
struct ContentView: View {
@ViewBuilder
var body: some View {
Text("Hello")
Text("World")
}
}
👉 body 天生就支持多 View 与条件语法。
@ViewBuilder 支持哪些能力@ViewBuilder
var content: some View {
Text("Title")
Text("Subtitle")
}
if / else 条件渲染(非常重要)没有 @ViewBuilder,下面代码是非法的:
func makeView(flag: Bool) -> some View {
if flag {
Text("Yes")
} else {
Text("No")
}
}
使用 @ViewBuilder 后:
@ViewBuilder
func makeView(flag: Bool) -> some View {
if flag {
Text("Yes")
} else {
Text("No")
}
}
👉 这正是 SwiftUI 条件 UI 渲染的基础能力。
if(没有 else)@ViewBuilder
var body: some View {
Text("Always Visible")
if isLogin {
Text("Welcome")
}
}
当条件不成立时,SwiftUI 会自动插入一个 EmptyView。
switch
@ViewBuilder
func stateView(_ state: LoadState) -> some View {
switch state {
case .loading:
ProgressView()
case .success:
Text("Success")
case .error:
Text("Error")
}
}
struct Card<Content: View>: View {
let content: Content
init(@ViewBuilder content: () -> Content) {
self.content = content()
}
var body: some View {
VStack(spacing: 8) {
content
}
.padding()
.background(.gray.opacity(0.2))
.cornerRadius(12)
}
}
使用时:
Card {
Text("Title")
Text("Subtitle")
}
👉 这正是 SwiftUI 组件化体验优秀的原因之一。
.sheet / .toolbar)func customOverlay<Content: View>(
@ViewBuilder content: () -> Content
) -> some View {
overlay {
content()
}
}
@ViewBuilder
var body: some View {
let count = 10 // ❌ 编译错误
Text("(count)")
}
原因是:
@ViewBuilder只接受 生成 View 的表达式。
✅ 正确方式:
var count: Int { 10 }
@ViewBuilder
var body: some View {
Text("(count)")
}
for 循环@ViewBuilder
var body: some View {
for i in 0..<3 { // ❌
Text("(i)")
}
}
✅ 正确方式:
ForEach(0..<3, id: .self) { i in
Text("(i)")
}
@ViewBuilder
当你遇到以下情况时,就该考虑它:
if / else / switch
简单判断法则:
“这个 API 是否应该像 SwiftUI 一样写 UI?”
如果答案是「是」,那基本就需要 @ViewBuilder。
@ViewBuilder 是 SwiftUI 的核心基础设施@ViewBuilder 能极大提升 API 体验一句话总结:
没有
@ViewBuilder,就没有今天的 SwiftUI。
如果你觉得这篇文章有帮助,欢迎点赞 / 收藏 / 交流 🙌
后续也可以深入聊:
ViewBuilder 源码实现@ViewBuilder 与 @ToolbarContentBuilder 的区别@Observable / @Bindable)下的最佳实践“她说明年就结婚,转头就把我拉黑了!”2024 年 9 月,山东鱼台县居民王某攥着手机账单冲进警局,声音颤抖。这位常年打工攒下 5 万积蓄的单身汉,从未想过自己在 “念梦”“冬梦” 两款交友 App 上邂逅的 “化妆品店老板娘”,竟是一场精心设计的骗局。
三个月里,这位昵称 “为你而来” 的 “女神” 温柔体贴,频频描绘二人未来的家,却以 “解锁视频聊天”“线下见面需充值刷亲密度” 为由,分三次榨干了他的全部积蓄。当王某停止充值后,昔日热情的恋人瞬间蒸发,只留下 27177 元、9592 元、13794 元三笔冰冷的充值记录。他不知道的是,自己只是这场 3.6 亿诈骗大案中,上千名受害者之一。
山东济宁公安破获特大网络交友诈骗案,40余款App全是陷阱。王某的报警,像一把钥匙打开了潘多拉魔盒。警方顺着涉诈 App 的线索深挖,一个隐藏在合法公司外壳下的犯罪集团逐渐浮出水面。团伙头目王某某是正规大学毕业生,曾因运营 “来遇” App 涉诈被查处,却在 2023 年卷土重来,注册多家空壳公司,一口气推出 40 余款交友 App,形成 “换汤不换药” 的马甲矩阵。
这个诈骗团伙的运作模式堪称 “产业化”:运营部负责招募培训 5000 余名女聊手,定制从 “初遇暧昧” 到 “诱导充值” 的全套话术;客服部专门安抚投诉用户,用 “系统维护”“亲密度未达标” 等借口掩盖骗局;甚至设立法务部,钻法律空子规避监管。女聊手们则按照统一剧本,虚构 “单身富婆”“温柔贤妻” 等人设,精准瞄准三、四线城市的大龄单身男性,用暧昧言语和虚假承诺编织情感牢笼。
更令人咋舌的是平台设计的 “吸血机制”:文字消息 10-100 金币 / 条,视频通话 100-2000 金币 / 分钟,充值 1 元仅能兑换 100 金币。女聊手与公司按 4:6 分成,为了多赚钱,她们会用平台发放的免费金币给用户刷礼物,制造 “双向奔赴” 的假象,引诱受害者不断充值。警方后续查获的聊天记录显示,团伙内部流传着 “养鱼玩法拉高点,大哥刷一你刷两” 的黑暗话术。
“这不是零散诈骗,是有组织、有预谋的犯罪网络。” 济宁市公安局迅速成立 “10.14” 专案组,抽调百余名警力攻坚。面对团伙设置的多层数据加密、定期删除证据、核心骨干分散办公等障碍,民警自主编写分析程序,从 8T 容量、超 62 亿条聊天记录和资金明细中抽丝剥茧。
立足当前行业大环境,存量社交产品必须将合规化置于开发工作的核心首位。
若不存在关键性的功能迭代需求,建议尽量减少版本更新频次,甚至暂停更新,以此规避审核环节可能出现的风险,避免给产品运营增添不必要的阻碍。
当前国内市场的恶性竞争态势,必然会导致社交类产品在App Store平台面临更严峻的监管压力与发展困境。因此,尽早布局出海业务、开拓海外新市场,已成这类产品突破发展瓶颈的关键方向。
合规化的价值懂的无需多言,不懂得多说无益。
遵守规则,方得长治久安,最后祝大家大吉大利,今晚过审!
# AppStore敏感词排查手册,多维度分析Guideline 2.3.1隐藏功能,轻松过审。