告别“可移植汇编”:我已让 Swift 在 MCU 上运行七年
在苹果官方正式开启嵌入式支持之前,Andy Liu 和他的 MadMachine 团队就已经在这个领域深耕多年。他们认为,在功能日益复杂的开发场景中,Swift 的现代语言特性将展现出巨大的优势。在数年前便选择了一套与社区主流不同的理念与技术路线。 我邀请 Andy 分享他们过去几年在 Swift 嵌入式开发中的实战经历分享出来。这既是一份宝贵的历史记录,也希望能为社区提供一个不一样的思考维度。
在苹果官方正式开启嵌入式支持之前,Andy Liu 和他的 MadMachine 团队就已经在这个领域深耕多年。他们认为,在功能日益复杂的开发场景中,Swift 的现代语言特性将展现出巨大的优势。在数年前便选择了一套与社区主流不同的理念与技术路线。 我邀请 Andy 分享他们过去几年在 Swift 嵌入式开发中的实战经历分享出来。这既是一份宝贵的历史记录,也希望能为社区提供一个不一样的思考维度。
清晰的理解它们能帮你更好地管理应用状态和资源。
应用生命周期描述了 App 从启动到终止的整个过程,由UIApplicationDelegate(应用代理)来管理。
import UIKit
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
// 1. App启动完成(最核心的入口)
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
print("应用启动完成 - didFinishLaunchingWithOptions")
// 通常在这里初始化根视图控制器、配置全局设置等
return true
}
// 2. App即将进入前台(还未激活,可做界面刷新)
func applicationWillEnterForeground(_ application: UIApplication) {
print("即将进入前台 - applicationWillEnterForeground")
}
// 3. App已进入前台并激活(用户可交互)
func applicationDidBecomeActive(_ application: UIApplication) {
print("已激活 - applicationDidBecomeActive")
// 恢复定时器、重新开始播放音频、刷新数据等
}
// 4. App即将进入后台(用户按Home键/切换App)
func applicationWillResignActive(_ application: UIApplication) {
print("即将失活 - applicationWillResignActive")
// 暂停定时器、保存临时数据、暂停音频播放等
}
// 5. App已进入后台
func applicationDidEnterBackground(_ application: UIApplication) {
print("已进入后台 - applicationDidEnterBackground")
// 持久化数据、释放不必要的资源(有大约5秒时间,耗时操作需申请后台任务)
}
// 6. App即将终止(仅在后台时可能触发,如系统回收内存)
func applicationWillTerminate(_ application: UIApplication) {
print("即将终止 - applicationWillTerminate")
// 最终的资源清理、数据保存
}
}
didFinishLaunchingWithOptions → 显示界面 → 进入活跃状态。WillResignActive)→ 后台(DidEnterBackground)→ 前台(WillEnterForeground)→ 活跃(DidBecomeActive)。applicationWillTerminate(若 App 在前台,直接终止,不触发此方法)。视图控制器是管理 UIView 的核心,其生命周期围绕视图的创建、显示、隐藏、销毁展开,是 iOS 开发中最常接触的生命周期。
import UIKit
class ViewController: UIViewController {
// 1. 初始化(创建VC对象)
init?(coder: NSCoder) {
super.init(coder: coder)
print("1. 初始化 - init")
// 初始化非UI相关的属性
}
// 2. 加载视图(首次访问view属性时触发)
override func loadView() {
super.loadView()
print("2. 加载视图 - loadView")
// 手动创建view(若不重写,系统会加载storyboard/xib的view)
self.view = UIView(frame: UIScreen.main.bounds)
self.view.backgroundColor = .white
}
// 3. 视图加载完成(view已创建完成)
override func viewDidLoad() {
super.viewDidLoad()
print("3. 视图加载完成 - viewDidLoad")
// 初始化UI控件、绑定数据、添加监听(只执行一次)
}
// 4. 视图即将布局子视图(view的bounds变化时触发,如旋转屏幕)
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
print("4. 视图即将布局子视图 - viewWillLayoutSubviews")
// 调整控件布局(执行多次)
}
// 5. 视图已布局子视图
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
print("5. 视图已布局子视图 - viewDidLayoutSubviews")
// 获取控件最终的frame(执行多次)
}
// 6. 视图即将显示在屏幕上(每次显示都触发,如push/pop后重新显示)
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
print("6. 视图即将显示 - viewWillAppear")
// 刷新数据、开始动画、注册通知等
}
// 7. 视图已显示在屏幕上
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
print("7. 视图已显示 - viewDidAppear")
// 启动定时器、请求网络数据、播放视频等
}
// 8. 视图即将从屏幕上消失
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
print("8. 视图即将消失 - viewWillDisappear")
// 暂停动画、移除通知、保存数据等
}
// 9. 视图已从屏幕上消失
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
print("9. 视图已消失 - viewDidDisappear")
// 释放不必要的资源(如图片缓存)
}
// 10. 内存警告(系统内存不足时触发)
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
print("10. 内存警告 - didReceiveMemoryWarning")
// 释放缓存、非必要的视图等
}
// 11. 视图控制器销毁(deinit)
deinit {
print("11. 视图控制器销毁 - deinit")
// 最终的资源释放(如移除监听、取消网络请求)
}
}
didReceiveMemoryWarning中需主动释放非必要资源,避免 App 被系统杀死。UIView 的生命周期依附于视图控制器,核心是 “创建 - 布局 - 绘制 - 销毁”,重点关注布局和绘制相关方法。
import UIKit
class CustomView: UIView {
// 1. 初始化(创建View)
override init(frame: CGRect) {
super.init(frame: frame)
print("1. View初始化 - init(frame:)")
// 设置默认属性(如背景色、圆角)
self.backgroundColor = .lightGray
}
required init?(coder: NSCoder) {
super.init(coder: coder)
print("1. View初始化 - init(coder:)")
}
// 2. 准备布局(iOS 6+,替代autoresizingMask)
override func prepareForLayout() {
super.prepareForLayout()
print("2. 准备布局 - prepareForLayout")
// 布局前的准备工作(如设置约束优先级)
}
// 3. 布局子视图(bounds变化时触发,如frame、center修改)
override func layoutSubviews() {
super.layoutSubviews()
print("3. 布局子视图 - layoutSubviews")
// 手动调整子视图frame(若不用AutoLayout)
for subview in self.subviews {
subview.center = self.center
}
}
// 4. 绘制内容(首次显示/setNeedsDisplay()触发)
override func draw(_ rect: CGRect) {
super.draw(rect)
print("4. 绘制内容 - draw(_:)")
// 手动绘制图形(如绘制线条、文字)
let context = UIGraphicsGetCurrentContext()
context?.setStrokeColor(UIColor.red.cgColor)
context?.stroke(CGRect(x: 10, y: 10, width: 100, height: 100))
}
// 5. 即将添加到父视图
override func willMove(toSuperview newSuperview: UIView?) {
super.willMove(toSuperview: newSuperview)
print("5. 即将添加到父视图 - willMove(toSuperview:)")
// 父视图变化前的处理
}
// 6. 已添加到父视图
override func didMoveToSuperview() {
super.didMoveToSuperview()
print("6. 已添加到父视图 - didMoveToSuperview")
// 父视图变化后的处理(如根据父视图调整自身大小)
}
// 7. 即将添加到窗口
override func willMove(toWindow newWindow: UIWindow?) {
super.willMove(toWindow: newWindow)
print("7. 即将添加到窗口 - willMove(toWindow:)")
}
// 8. 已添加到窗口
override func didMoveToWindow() {
super.didMoveToWindow()
print("8. 已添加到窗口 - didMoveToWindow")
// 只有添加到window后,View才会真正显示在屏幕上
}
// 9. 销毁(deinit)
deinit {
print("9. View销毁 - deinit")
// 释放View相关资源(如移除子视图、取消动画)
}
}
setNeedsDisplay()可触发重新绘制。UIWindow(应用的主窗口)后,才会被渲染并显示在屏幕上;didMoveToWindow是 View 真正 “可见” 的标志。deinit触发,需确保子视图也被正确释放。UIApplicationDelegate的代理方法,关注前台 / 后台切换和资源保存。viewDidLoad(一次性初始化)、viewWillAppear(每次显示刷新)、deinit(资源释放),是业务逻辑的主要载体。layoutSubviews(布局)和draw(_:)(绘制),关注控件尺寸调整和视觉渲染。三者的关联:App 启动后创建根 VC → VC 创建并加载 View → View 添加到 Window 显示 → App 进入前台活跃状态;App 进入后台时,VC 的 View 会被隐藏,资源可按需释放。
![]()
摘要:在 Swift 6.2 的并发江湖中,我们迎来了两项截然不同的新功能:一项是关于极度精妙的文本侦查术(SE-0448 正则表达式向后查找断言),另一项则是关于面对应用崩溃时的从容不迫(ST-0008 退出测试)。大熊猫侯佩将与阿朱、阿紫这对姐妹花,共同演绎这冰火两重天的技术奥秘。
雁门关,数据流与现实交错的虚拟战场。
大熊猫侯佩正对着一块全息屏幕发呆,屏幕上是无数条交易记录,他正努力寻找他藏匿的竹笋基金。他用手摸了摸自己的头顶,确定了头绝对没有秃之后,才稍微心安。
他身旁站着一位温柔婉约的绿衣女子,正是阿朱。阿朱以易容术闻名江湖,擅长在纷乱的文本中寻找和伪装信息,她的心愿是天下太平,性格宽厚善良。
![]()
“侯大哥,”阿朱指着一堆交易记录说,“我想找到所有以 金币符号 $ 结算的价格,但我只想匹配出后面的数字,而不要把那个 $符号也匹配进去。我要用这些数字去结算账单,符号留着下次易容用。”
在本次大模型中,您将学到如下内容:
侯佩为难地挠了挠头:“以前的 Regex(正则表达式),要么就全部匹配进去,要么就得用复杂的捕获组再分离。要想实现‘只看前因,不取前因’,简直难如登天啊!”
![]()
阿朱的问题,正是 SE-0448 所要解决的:向后查找断言(lookbehind assertions)。
传统的正则表达式,可以轻松地实现“向前看”(Lookahead),例如 A(?=B),匹配 A,但前提是 A 后面跟着 B。
![]()
而现在,Swift 6.2 赋予了我们 “向后看” 的能力,即 (?<=A)B:匹配 B,但前提是 B 前面紧跟着 A。最关键的是,A(前置条件)不会被纳入最终的匹配结果中。
侯佩拿起代码卷轴,为阿朱演示了这招“庖丁解牛”般的绝技:
let string = "Buying a jacket costs $100, and buying shoes costs $59.99."
// (?<=\$): 向后查找断言,确认当前位置前面紧跟着一个 $ 符号。
// \d+ : 匹配至少一个数字(价格的整数部分)。
// (?:\.\d{2})?: 匹配可选的小数点和小数部分(?: 是非捕获组)。
let regex = /(?<=\$)\d+(?:\.\d{2})?/
for match in string.matches(of: regex) {
// 最终输出的 match.output 只有数字,不包含 $ 符号
print(match.output)
}
// 输出:
// 100
// 59.99
“看到了吗,阿朱姑娘?”侯佩得意洋洋,“这个 (?<=$) 就是你的易容术精髓。它帮你确认了身份(前面必须是金币),但在匹配结果中,它却完美地把自己隐藏了起来,片叶不沾身!”
![]()
阿朱喜出望外:“太妙了!这样我就可以精准地提取数据,再也不用担心多余的符号来捣乱了!”
就在侯佩和阿朱沉浸在正则表达式的精妙中时,一阵刺鼻的硫磺味突然袭来!
另一位身着紫衣的少女,阿紫,从烟雾中走了出来。阿紫的特点是心狠手辣,喜欢用毒,而且热衷于测试“极限”。
![]()
“姐姐,你在玩这么幼稚的游戏?”阿紫轻蔑一笑,“我的任务才刺激。我要测试我最新的**‘鹤顶红’代码**,确保它能让整个应用彻底崩溃并退出!”
侯佩吓得连退三步:“你要测试崩溃?阿紫姑娘,你知道这意味着什么吗?应用崩溃,测试系统也会跟着崩溃啊!这叫一锅端!”
![]()
阿紫的测试目标,正是那些会触发 precondition() 或 fatalError() 导致进程退出的代码。
struct Dice {
// 掷骰子功能
func roll(sides: Int) -> Int {
// 🚨 前提条件:骰子面数必须大于零!
// 如果 sides <= 0,程序将立即崩溃退出!
precondition(sides > 0)
return Int.random(in: 1...sides)
}
}
“以前,我们要么不能测,要么就得用各种奇技淫巧来捕获这种‘致命错误’。”侯佩擦着汗说,“但现在 Swift Testing 带来了 ST-0008:Exit Tests,让我们能优雅地‘置之死地而后生’!”
![]()
Swift 6.2 引入了 #expect(processExitsWith:),它就像是一个安全结界,允许我们在隔离的子进程中执行可能导致崩溃的代码,然后捕获并验证这个退出行为。
@Test func invalidDiceRollsFail() async throws {
let dice = Dice()
// 🛡️ 关键:使用 #expect 包裹,并等待结果
await #expect(processExitsWith: .failure) {
// 在这里,roll(sides: 0) 会导致隔离的子进程崩溃退出
let _ = dice.roll(sides: 0)
}
// 如果子进程如期以 .failure 状态退出,则测试通过。
// 如果它没有崩溃,或者崩溃状态不对,则测试失败。
}
🔍 异步执行的关键:
await注意,这里必须使用await。这是因为在幕后,测试框架必须启动一个专用的、独立的进程来执行危险代码。它会暂停当前测试,直到子进程运行完毕并返回退出状态。这才是真正的隔离测试!
![]()
阿紫满意地拍了拍手:“现在我的毒药(代码)终于可以在实验室(测试环境)里安全地爆炸了!我不仅可以测试它会死(failure),还可以测试它死得很安详(success)或其他退出状态。”
侯佩摸了摸自己的头发,确认没有被阿紫的毒气熏掉,然后问道:“阿紫姑娘,你这个毒药测试虽然厉害,但是你有没有想过一个问题?”
![]()
“什么问题?”阿紫挑了挑眉。
“如果这个 roll(sides: 0) 崩溃了,但它在崩溃前,生成了一个关键的调试日志文件,或者一个记录了现场数据的**‘遗物’**,你能不能把这个遗物附着到测试报告里?”
阿紫一愣:“不能。测试报告里只显示了‘崩溃了’这个结果,但我不知道崩溃前骰子(程序)到底在想什么!我需要那个遗物来分析我的毒药配方!”
![]()
阿朱也附和道:“是啊,侯大哥。就像我易容时,如果失败了,我希望在失败的记录旁边,能附上一张当时的照片,这样下次就知道是哪个环节出了错。”
侯佩微微一笑,从怀里掏出了一张写着 ST-0009 的秘籍:“两位姑娘,不必烦恼。下一章,Swift Testing 就能帮你们把这些日志、数据和现场文件,像附着‘随身物品’一样,直接捆绑到失败的测试报告上。这招就叫……”
![]()
(欲知后事如何,且看下回分解:Swift Testing: Attachments —— 如何将崩溃现场的证据(日志、截图、数据文件)直接附着到测试报告上,让 Bug 无所遁形。)
![]()
在赛博都市“新硅谷”(Neo-Silicon Valley)的第 1024 层地下室里,资深 iOS 赏金猎人——老李(Old Li),正盯着全息屏幕上一行行红色的报错代码发愁。他嘴里叼着一根早已熄灭的合成电子烟,眉头皱得能夹死一只纳米苍蝇。
旁边漂浮着的 AI 助手“小白”发出了机械的合成音:“警报,内存溢出测试失败。目标 App 依然像个赖皮一样活着。”
![]()
老李叹了口气:“这年头的 App,一个个都练成了‘金刚不坏之身’。我想测一下后台上传功能在**低内存(Low RAM)**情况下的表现,结果这破手机内存大得像海一样,怎么都填不满。”
“老板,直接在 App Switcher(多任务切换器)里把它划掉不就行了?”小白天真地问道。
**在本篇博文中,您将学到如下内容: **
老李冷笑一声,敲了一下小白的金属外壳:“图样图森破!手滑杀掉那是‘斩立决’,系统因内存不足杀掉那是‘自然死亡’。对于后台任务来说,这区别可大了去了。要想骗过死神,我们得用点‘阴招’。”
老李从积灰的档案袋里掏出一份绝密文档——《iOS 内存清空指南》。
![]()
最近老李接了个大活儿,要为一个 App 开发 Background Uploading(后台上传)功能。这活儿最棘手的地方在于:你得确保当系统因为 RAM constraints(内存限制)或其他不可抗力把你的 App 挂起甚至杀掉时,这上传任务还得能像“借尸还魂”一样继续跑。
要想测试这个场景,最直接的办法就是清空设备的 RAM memory。但这可不像在电脑上拔掉电源那么简单。
小白不解:“不就是上划杀进程吗?”
![]()
“错!”老李严肃地解释道,“打开 Task Switcher 然后强行关闭 App,这在系统眼里属于‘用户主动终止’。这就像是不仅杀了人,还顺手把复活点给拆了。而我们需要的是模拟 App 被系统‘挤’出内存,这才是真正的Forced out of memory。”
简而言之,我们需要制造一场完美的“意外”,让 App 以为自己只是因为太胖被系统踢了出去,而不是被用户嫌弃。
幸运的是,在 iOS 的底层代码深处,藏着一个不为人知的“秘技”。这招能像灭霸打响指一样,瞬间清空 iOS 设备的 RAM memory,让你的 App 享受到和真实内存不足时一样的“暴毙”待遇。
老李按灭了烟头,开始向小白传授这套“还我漂漂拳”:
![]()
如果你的测试机是全面屏(没有 Home 键),你得先搞个虚拟的。 “去 Settings → Accessibility → Touch → Enable Assistive Touch。”老李指挥道。
![]()
屏幕上瞬间浮现出一个半透明的小圆球。 “这就是通往内存地狱的钥匙。”
技术批注: 对于有实体 Home 键的老古董设备,这一步可以跳过。
![]()
这一步需要一点手速,就像是在玩格斗游戏搓大招。 “听好了:Volume Up(音量加),Volume Down(音量减),然后死死按住 Power Button(电源键)!”
![]()
老李的手指在机身上飞舞,直到屏幕上出现了那个熟悉的“滑动来关机”界面。
“就是现在!”老李大喝一声。
在关机界面出现后,千万别滑那个关机条。点击刚才召唤出来的 Assistive Touch 小圆球,找到里面的 Home Button(主屏幕按钮),然后——长按它。
![]()
一直按着,直到屏幕一闪,或者突然跳回输入密码的界面。
“恭喜你,”老李擦了擦额头的汗,“你刚刚成功把这台设备的 RAM memory 洗劫一空。现在,后台那些苟延残喘的 App 已经被系统无情地踢出了内存。”
![]()
小白看着屏幕上被清理得干干净净的后台,数据流终于开始正常波动了。
“这就好比演习,”老李解释道,“当我们在开发那些依赖于 Background Resuming(后台恢复)的功能时——比如后台上传、下载,或者定位服务——模拟 Out of Memory 场景简直是救命稻草。”
![]()
最让老李爽的一点是,这个操作完全脱离了 Xcode。 “以前还要连着线看 Debugger,现在我可以把手机扔给隔壁 QA 部门那个只会吃薯片的测试员,告诉他:‘按这个秘籍操作,如果上传断了,就是你们的问题,如果没断,就是我的功劳。’”
为了防止小白以后出去乱说,老李决定再深入科普一下其中的Hardcore原理。
![]()
一个被 Forced out of RAM 的 App,在用户眼里并没有完全死透。它依然会出现在 App Switcher 里,就像个植物人。更重要的是,任何已经注册的 Background Processes(后台进程,比如 NSURLSession 的后台任务)依然在系统的监管下继续运行。
![]()
所以,只有用老李刚才那招“清内存大法”,才能真实模拟用户在刷抖音、玩原神导致内存不足时,你的 App 在后台是否还能坚强地把文件传完。
测试通过,全息屏幕上显示出了令人安心的绿色 SUCCESS 字样。
![]()
老李站起身,伸了个懒腰,骨头发出噼里啪啦的响声。“行了,小白,打包发布。今晚不用加班修 Bug 了。”
他看了一眼窗外新硅谷那绚烂而又冰冷的霓虹灯。在这个充满 Bug 和 Patch 的世界里,有时候,你必须学会如何正确地“杀死”你的 App,才能让它更好地活下去。
![]()
“记住,”老李走出门口前回头对小白说,“杀进程不是目的,目的是为了验证它有没有重生的勇气。”
大门缓缓关闭,只留下那个悬浮的 Assistive Touch 按钮,在黑暗中微微闪烁,仿佛一只窥探内存深处的眼睛。
![]()
SSE(Server-Sent Events) 是一种基于 HTTP 的服务器单向推送技术。相比 WebSocket 的双向通信,SSE 更轻量、实现更简单,非常适合服务器向客户端持续推送数据的场景。ChatGPT、Claude 等 AI 产品都使用 SSE 来实现流式输出。
本文以 iOS 客户端实现为例,详细讲解 SSE 数据的接收与解析过程。
event: chunk
id: 1
data: {"content":"Hello"}
⚠️ 注意:最后有一个空行,这是事件结束的标志!
服务器发送的原始字节流:
e v e n t : c h u n k \n i d : 1 \n d a t a : { . . . } \n \n
问题:网络传输时,数据可能被分成多个块到达客户端。
假设网络把数据分成了 3 块:
| 数据块 | 内容 |
|---|---|
| 块 1 | "event: chu" |
| 块 2 | "nk\nid: 1\nda" |
| 块 3 | "ta: {\"content\":\"Hello\"}\n\n" |
"event: chu"
┌────────────────────────────────────────────────────────────┐
│ 输入: "event: chu" │
│ │
│ 处理过程: │
│ 1. 缓冲区当前为空: "" │
│ 2. 合并: "" + "event: chu" = "event: chu" │
│ 3. 扫描换行符: 没找到 \n │
│ 4. 没有完整行,全部存入缓冲区 │
│ │
│ 缓冲区: "event: chu" │
│ 输出: [] ← 空数组,没有完整行 │
└────────────────────────────────────────────────────────────┘
"nk\nid: 1\nda"
┌────────────────────────────────────────────────────────────┐
│ 输入: "nk\nid: 1\nda" │
│ │
│ 处理过程: │
│ 1. 缓冲区当前: "event: chu" │
│ 2. 合并: "event: chu" + "nk\nid: 1\nda" │
│ = "event: chunk\nid: 1\nda" │
│ 3. 扫描换行符: │
│ - 位置 12 找到 \n → 提取 "event: chunk" │
│ - 位置 18 找到 \n → 提取 "id: 1" │
│ - "da" 后面没有 \n,存入缓冲区 │
│ │
│ 缓冲区: "da" │
│ 输出: ["event: chunk", "id: 1"] │
└────────────────────────────────────────────────────────────┘
"ta: {\"content\":\"Hello\"}\n\n"
┌────────────────────────────────────────────────────────────┐
│ 输入: "ta: {\"content\":\"Hello\"}\n\n" │
│ │
│ 处理过程: │
│ 1. 缓冲区当前: "da" │
│ 2. 合并: "da" + "ta: {...}\n\n" │
│ = "data: {\"content\":\"Hello\"}\n\n" │
│ 3. 扫描换行符: │
│ - 位置 27 找到 \n → 提取 "data: {...}" │
│ - 位置 28 找到 \n → 提取 "" ← 空行! │
│ │
│ 缓冲区: "" ← 清空 │
│ 输出: ["data: {\"content\":\"Hello\"}", ""] │
└────────────────────────────────────────────────────────────┘
经过 3 次数据块处理,行解析器依次输出:
| 次序 | 输出的完整行 |
|---|---|
| 块 1 后 |
[] (无) |
| 块 2 后 | ["event: chunk", "id: 1"] |
| 块 3 后 | ["data: {...}", ""] |
合计得到 4 行: "event: chunk", "id: 1", "data: {...}", ""
SSEClient 将行解析器输出的每一行,依次传给事件解析器。
"event: chunk"
┌────────────────────────────────────────────────────────────┐
│ 输入: "event: chunk" │
│ │
│ 处理过程: │
│ 1. 行长度 > 0,不是空行 │
│ 2. 查找冒号位置: 5 │
│ 3. 字段名 = "event" │
│ 4. 字段值 = "chunk" (冒号后面,跳过空格) │
│ 5. 字段名是 "event",存储 eventType │
│ │
│ 当前状态: │
│ eventType = "chunk" ✓ │
│ eventId = "" │
│ data = "" │
│ │
│ 动作: 继续等待下一行 │
└────────────────────────────────────────────────────────────┘
"id: 1"
┌────────────────────────────────────────────────────────────┐
│ 输入: "id: 1" │
│ │
│ 处理过程: │
│ 1. 行长度 > 0,不是空行 │
│ 2. 查找冒号位置: 2 │
│ 3. 字段名 = "id" │
│ 4. 字段值 = "1" │
│ 5. 字段名是 "id",存储 eventId │
│ │
│ 当前状态: │
│ eventType = "chunk" ✓ │
│ eventId = "1" ✓ │
│ data = "" │
│ │
│ 动作: 继续等待下一行 │
└────────────────────────────────────────────────────────────┘
"data: {\"content\":\"Hello\"}"
┌────────────────────────────────────────────────────────────┐
│ 输入: "data: {\"content\":\"Hello\"}" │
│ │
│ 处理过程: │
│ 1. 行长度 > 0,不是空行 │
│ 2. 查找冒号位置: 4 │
│ 3. 字段名 = "data" │
│ 4. 字段值 = "{\"content\":\"Hello\"}" │
│ 5. 字段名是 "data",追加到 data │
│ (当前 data 为空,直接赋值) │
│ │
│ 当前状态: │
│ eventType = "chunk" ✓ │
│ eventId = "1" ✓ │
│ data = "{\"content\":\"Hello\"}" ✓ │
│ │
│ 动作: 继续等待下一行 │
└────────────────────────────────────────────────────────────┘
"" (空行) ⚡┌────────────────────────────────────────────────────────────┐
│ 输入: "" (空行) │
│ │
│ 处理过程: │
│ 1. 行长度 == 0,是空行! │
│ 2. ⚡ 空行触发事件分发! │
│ │
│ 当前状态 (即将分发): │
│ eventType = "chunk" │
│ eventId = "1" │
│ data = "{\"content\":\"Hello\"}" │
│ │
│ 执行 dispatchEvent(): │
│ 1. 调用回调: onEvent("chunk", "1", "{...}") │
│ 2. 重置状态: │
│ eventType = "" │
│ eventId = "" │
│ data = "" │
│ │
│ 动作: 🎯 触发回调!准备解析下一个事件 │
└────────────────────────────────────────────────────────────┘
┌────────────────────────────────────────────────────────────┐
│ 回调链 │
├────────────────────────────────────────────────────────────┤
│ │
│ EventParser.dispatchEvent() │
│ │ │
│ │ onEvent("chunk", "1", "{\"content\":\"Hello\"}") │
│ ↓ │
│ SSEClient.handleEvent() │
│ │ │
│ │ 判断: eventType != "connected",不更新状态 │
│ │ │
│ │ onTextChunk("chunk", "1", "{...}") │
│ ↓ │
│ MLNSSETool │
│ │ │
│ │ [onTextChunk addStringArgument:@"chunk"]; │
│ │ [onTextChunk addStringArgument:@"1"]; │
│ │ [onTextChunk addStringArgument:@"{...}"]; │
│ │ [onTextChunk callIfCan]; │
│ ↓ │
│ Lua 业务层 │
│ │ │
│ │ onEvent("chunk", "1", '{"content":"Hello"}') │
│ ↓ │
│ 业务代码处理 │
│ │ │
│ │ local json = cjson.decode(data) │
│ │ print(json.content) -- 输出: Hello │
│ │ 更新 UI 显示 │
│ ↓ │
│ ✅ 完成! │
│ │
└────────────────────────────────────────────────────────────┘
服务器发送: "event: chunk\nid: 1\ndata: {...}\n\n"
│
↓ 网络分块传输
┌─────────────────────────────────────────────────────────────┐
│ 第一步:网络层 │
├─────────────────────────────────────────────────────────────┤
│ 块1: "event: chu" │
│ 块2: "nk\nid: 1\nda" │
│ 块3: "ta: {...}\n\n" │
└─────────────────────────────────────────────────────────────┘
│
↓ NSURLSession.didReceiveData
┌─────────────────────────────────────────────────────────────┐
│ 第二步:行解析器 │
│ (UTF8LineParser) │
├─────────────────────────────────────────────────────────────┤
│ 块1 → [] │
│ 块2 → ["event: chunk", "id: 1"] │
│ 块3 → ["data: {...}", ""] │
│ │
│ 合计得到4行: `"event: chunk"`, `"id: 1"`, `"data: {...}"`, `""` │
└─────────────────────────────────────────────────────────────┘
│
↓ 逐行传递
┌─────────────────────────────────────────────────────────────┐
│ 第三步:事件解析器 │
│ (EventParser) │
├─────────────────────────────────────────────────────────────┤
│ 第1行 "event: chunk" → eventType = "chunk" │
│ 第2行 "id: 1" → eventId = "1" │
│ 第3行 "data: {...}" → data = "{...}" │
│ 第4行 "" → ⚡ 触发 dispatchEvent() │
└─────────────────────────────────────────────────────────────┘
│
↓ onEvent 回调
┌─────────────────────────────────────────────────────────────┐
│ 第四步:事件分发 │
├─────────────────────────────────────────────────────────────┤
│ SSEClient.handleEvent("chunk", "1", "{...}") │
│ ↓ │
│ MLNSSETool.onTextChunk("chunk", "1", "{...}") │
│ ↓ │
│ Lua: onEvent("chunk", "1", '{"content":"Hello"}') │
│ ↓ │
│ 业务代码: 解析 JSON,更新 UI │
└─────────────────────────────────────────────────────────────┘
│
↓
✅ 处理完成!
网络数据分块到达,一行可能被拆成多块。行解析器用缓冲区解决这个问题。
event: chunk ← 存储 eventType,不触发
id: 1 ← 存储 eventId,不触发
data: {...} ← 存储 data,不触发
← ⚡ 只有空行才触发事件分发!
空行 = 事件结束的信号
| 阶段 | 输入 | 输出 |
|---|---|---|
| 网络传输 | 字节流 | 数据块 |
| 行解析器 | 数据块 | 文本行数组 |
| 事件解析器 | 文本行 | event/id/data |
| 空行触发 | "" | dispatchEvent() |
| 回调链 | event/id/data | Lua onEvent |
上车AppStore必经之路,苹果开发者账号注册。简单盘点一下,申请苹果开发者痛点问题。
正常的个人开发账号,基本上直接使用 126、163或者QQ邮箱都可以直接使用。
对于公司开发者账号来说,最近新增了限制条件:申请的邮箱必须为公司邮箱!
这一点限制是在最近申请公司开发账号遇到的问题,对于个人账号账号目前没有影响。[这里感谢粉丝贡献的情报。]
设备问题主要是在Apple ID登录踩的坑。首当其冲的就是设备登录限制。
无解直接换新设备,不用想了。不然果子怎么卖的动新手机?
注册开发者的 Developer App,也需要更新到新版本。【有最低版本限制】不然果子怎么卖的动新手机?
![]()
在注册开发者账号的过程中,切记不要更换设备,避免遇到各种奇奇怪怪的问题。也能最大程度的保保证,在注册流程不会被账号关联,避免提交代码就夭折。
对于公司层面的账号,场景最多的问题就是:
Q: 法人用个人账号注册了开发者,那么还可以用公司身份去注册么?
A: 其实是可以的,这一点已经咨询过了苹果客服。因为对于主体而言,一个是邓白氏编码对应的账号,一个是个人身份证对应的账号。所以本质上也是2个独立的主体。
对于小部分一些人来说,可能之前注册了开发者流程,也提交的了相应信息。在最后付费环境,考虑到暂时没有产品提交又或者不知道了注册了干嘛,就把账号搁置了。
那这种情况是最头疼的,对于苹果而言信息已经被占用。如果无法使用首次注册开发者的账号,重新进行开发者验证。那么将陷入无法注册的死循环。简而言之:打苹果客服,也只能告诉你用老账户。如果忘记密码或者AppleID【也就是注册的邮箱】,那么对不起奶不回来。苹果客服没有权限获悉之前注册的任何信息。【上海端口没有这么高的权限!】
如果顺顺利利的完成了,所有前置流程,并且成功支付苹果开发者的会员费¥688.00。那么恭喜你完成了90%
但是,别高兴的太早。很多支付了费用,超过30个小时依旧没有成功获取开发者资格。
这种情况,必须要主动与苹果技术支持联系。对于个人账号大概率是需要补充身份证信息,也就是身份证正反面。
苹果会通过开发者邮箱,提供一个附件资料上传地址。上传成功之后,预计2~3个小时将会激活。
之所以遇到这种问题,是因为中国大陆区有些小区名称或者街道过于离谱。比如:
江苏南京神马路:位于南京市栖霞区,连接马群街道与仙林地区,因谐音与网络流行语 “神马都是浮云” 契合,成为网红路名。
江苏南京马桶巷:位于南京秦淮区,传说因明代此处有制作马桶的手工作坊而得名,现已更名为 “饮马巷”,但老南京人仍习惯称其旧名。
江苏苏州裤裆巷:巷子分岔呈 “Y” 形,形似裤裆,故得此名,后改名 “邾长巷”,但老苏州人仍爱调侃 “穿裤衩的路”。
四川成都肥猪市街:该地以前是卖猪的市场,所以取了这样的名字。同理还有草市街、羊市街等。
广东揭阳普宁二奶街:因上世纪 90 年代街道售卖的衣物价格昂贵,人们调侃称只有 “二奶” 才消费得起,故而得名,如今已发展成为当地有名的人气美食夜市。
![]()
遵守规则,方得长治久安,最后祝大家大吉大利,今晚过审!
# AppStore敏感词排查手册,多维度分析Guideline 2.3.1隐藏功能,轻松过审。
在 iOS 上,App 在启动 / 退出 / 响应系统事件等关键阶段如果长时间卡住,超过系统阈值就会触发保护机制,最终被 Watchdog 以 SIGKILL 强制终止。这类异常的共同特点是:不是进程内异常抛出,而是“进程外指令”直接结束进程,因此传统基于 signal/exception 的崩溃捕获往往覆盖不到,也就导致它在生产环境中经常“只见数据、不见堆栈”,长期被忽视。
为了解决这个盲区,我选择站在巨人的肩膀上:本文实践并复现字节跳动团队的文章《iOS 稳定性问题治理:卡死崩溃监控原理及最佳实践》,从 原理 → 代码实现 → 候选文件保存 → dSYM/atos/脚本符号化 + Swift demangle,把一整套卡死(ANR/Watchdog)监控链路完整跑通,并沉淀成一个可复用 Demo。
![]()
文末给出完整代码链接
线上卡死/假死常见现象:
0x00000001.... 地址:不符号化就等于没有结论
所以要解决这类问题,本质是两件事:
主线程 RunLoop 正常情况下会不断在这些状态间流转:
BeforeTimersBeforeSourcesBeforeWaitingAfterWaiting当主线程执行重任务(大解析 / 同步 IO / 复杂布局 / 锁等待等),RunLoop 会长期卡在某个阶段不动,表现为 UI 无响应。
Demo 采用的策略(参数对齐你当前实现):
hangThreshold = 8)tickInterval = 1)Thread 0)maxMainThreadSamples = 10,保留最近 10 次)这套策略的意义是:
先判断“已经卡死到足够严重”(接近 Watchdog 风险),再进入“持续采样”,避免把轻微卡顿也当成卡死去抓栈/写文件。
LagMonitorDemo/
├── LagMonitorDemo.xcodeproj
├── Sources/
│ └── Monitor/
│ ├── HMDANRMonitor.swift
│ ├── HMDLiveReportCapture.swift
│ ├── HMDANRRecord.swift
│ ├── HMDANRCandidateStore.swift
│ └── HMDDebugCacheCleaner.swift
├── Scripts/
│ └── hmd_anr_symbolicate.py
└── Samples/
├── hmd_anr_candidate.json
└── symbolicated.txt
CFRunLoopObserver,每次回调认为 RunLoop 推进:heartbeat += 1
heartbeat 是否变化
hangSeconds += 1
核心就在这一句:
_ = wakeSemaphore.wait(timeout: .now() + .seconds(config.tickInterval))
它等价于:
tickInterval = 1)作为一个观察窗口signal() → 监控线程会提前醒来
heartbeat:若 1 秒内完全没变,才算这一秒“卡住”所以这里不是“固定每秒到点触发一次”,而是:
“最多等 1 秒,但只要 RunLoop 一推进就立刻醒来重置状态”
这比 Timer 的“固定周期触发”更贴合我们想观察的对象(RunLoop 推进)。
Timer 触发本身就依赖调度与 RunLoop/线程状态,卡死时最容易抖动或延迟;信号量 + 超时是更稳定的“观察窗口”,还能被 RunLoop 推进即时唤醒。
抓栈使用 PLCrashReporter 的 Live Report 能力:
generateLiveReportAndReturnError():生成“当下全线程现场”Thread 0 作为主线程样本import CrashReporter
/// 抓一次“全线程现场报告”
static func captureAllThreadsText() -> String? {
let config = PLCrashReporterConfig(signalHandlerType: .BSD, symbolicationStrategy: .all)
guard let reporter = PLCrashReporter(configuration: config) else { return nil }
do {
let data = try reporter.generateLiveReportAndReturnError()
let report = try PLCrashReport(data: data)
return PLCrashReportTextFormatter.stringValue(for: report, with: PLCrashReportTextFormatiOS)
} catch {
print("[HMDLiveReportCapture] parse report error: \(error)")
return nil
}
}
这里的思路就是:
卡死现场抓“全线程”,用于兜底;超阈值后持续采“主线程”,用于定位稳定卡点。
Demo 保存的核心数据结构是 HMDANRRecord,通过 Codable 编码为 JSON:
public struct HMDANRRecord: Codable {
public let recordID: String
public let timestamp: Date
public var hangSeconds: Int
/// 超过阈值那一刻:全线程现场(PLCrash live report,文本)
public var allThreadsReportText: String?
/// 超阈值后:每秒采样主线程调用栈(最多保留最近 N 条)
public var mainThreadSamples: [String]
}
保存到 Caches/hmd_anr_candidate.json 后,大致字段长这样:
{
"recordID": "E2D0...-....",
"timestamp": "2026-01-05T12:34:56Z",
"hangSeconds": 9,
"allThreadsReportText": "PLCrashReporter live report text ...",
"mainThreadSamples": [
"Thread 0 ...\n0 LagMonitorDemo 0x...\n1 UIKitCore ...",
"Thread 0 ...\n0 LagMonitorDemo 0x...\n1 UIKitCore ..."
]
}
HMDANRCandidateStore卡住时把记录保存到缓存文件;一旦主线程恢复推进就立刻删除;如果进程被系统强杀来不及上报,这个文件会留到下次启动再读取导出/符号化。
Demo 里包含几种典型卡死触发方式:
stack.addArrangedSubview(makeButton("主线程 Busy 2s(轻微卡顿)") { [weak self] in self?.busy(seconds: 2) })
stack.addArrangedSubview(makeButton("主线程 Busy 20s(触发 candidate + 采样)") { [weak self] in self?.busy(seconds: 20) })
stack.addArrangedSubview(makeButton("锁竞争:子线程持锁 12s → 主线程尝试加锁") { [weak self] in self?.lockContention() })
stack.addArrangedSubview(makeButton("死锁:串行队列 sync + 主队列 sync(必卡死)") { [weak self] in self?.deadlock() })
我点击 “主线程 Busy 20s(触发 candidate + 采样)”,在第 10 秒手动杀掉 App,然后导出沙盒里的 hmd_anr_candidate.json。
你会看到类似信息:
hangSeconds = 13(很明确的主线程长时间阻塞)mainThreadSamples 有多次采样(证明卡住期间栈稳定)![]()
因此需要做:
hmd_anr_candidate.json 里解析 frame$s... → Foundation.Date.init())本项目用脚本 hmd_anr_symbolicate.py 自动完成批量符号化:
python3 hmd_anr_symbolicate.py --record hmd_anr_candidate.json --app-dsym LagMonitorDemo.app.dSYM --arch arm64 --demangle --out symbolicated.txt
符号化后的symbolicated.txt的大致内容如下:
![]()
hmd_anr_symbolicate.py在GITHUB项目的Scripts文件夹下,hmd_anr_candidate.json和symbolicated.txt在Samples文件下
从 symbolicated.txt 里抽 mainThreadSamples[0] 的关键几帧(你这次 5 次采样基本一致):
1 libsystem_c.dylib gettimeofday
3 Foundation Date.init
4 LagMonitorDemo.debug.dylib ... (ViewController.swift:52)
5 LagMonitorDemo.debug.dylib ... (ViewController.swift:33)
6 LagMonitorDemo.debug.dylib ... (ViewController.swift:44)
7 UIKitCore ...
这说明卡死期间主线程一直在跑 ViewController.swift 的某段逻辑,并且频繁调用 Date()(最终落到 gettimeofday/clock_gettime),典型特征就是忙等/死循环式等待。
对应 Demo 中的实现:
private func busy(seconds: Int) {
let end = Date().addingTimeInterval(TimeInterval(seconds))
while Date() < end {
_ = 1 + 1
}
}
这类栈顶常见现象就是:看起来“卡在 Date()”,其实根因是 while 循环让主线程一直跑。
采样刚好截在Date()这一行,于是栈顶表现为Date.init -> gettimeofday。
大厂的稳定性方案往往更深、更体系化,但很多时候只停留在文章层面:看懂了思路,却很难在项目里直接落地。本文的目标就是把它“拆开 + 跑通”:
你真正需要的不是“我们检测到了卡死”,而是:
卡死那 8~20 秒内,主线程到底在跑什么?它卡在谁身上?
当你把“采样 + 保存 + 下次启动捞取 + 自动符号化 + demangle”这条链路跑通,线上卡死排查效率会明显提升。
作为 iOS/macOS 开发者,本地数据存储是绕不开的话题。提起 Core Data,不少新手会皱眉头 —— 早期的 Core Data 配置繁琐,手动管理上下文、协调器这些组件很容易踩坑;而老开发者则清楚,自从 Apple 推出NSPersistentContainer后,Core Data 的使用体验直接 “起飞”。今天就跟大家聊聊,这个 “容器” 到底是什么、怎么用,以及它的那些优缺点。
在 iOS 10/macOS 10.12 之前,想用 Core Data 得手动搭一套 “流水线”:
NSManagedObjectModel(数据模型);NSPersistentStoreCoordinator(持久化存储协调器),指定存储类型(比如 SQLite)和路径;NSManagedObjectContext(托管对象上下文),并关联协调器;一套操作下来,代码又长又容易出错,光是初始化就能劝退一半新手。Apple 显然也发现了这个问题,于是NSPersistentContainer应运而生 —— 它把 Core Data 的核心组件全 “打包” 了,让我们不用再关心底层细节,专注于业务逻辑即可。
NSPersistentContainer本质是对 Core Data 三大核心组件的封装,相当于给我们准备了一个开箱即用的 “数据管理容器”,内部结构如下:
| 组件 | 作用 | 容器中的访问方式 |
|---|---|---|
NSManagedObjectModel |
定义数据结构(对应.xcdatamodeld 文件) | container.managedObjectModel |
NSPersistentStoreCoordinator |
管理数据存储(比如 SQLite 文件) | container.persistentStoreCoordinator |
NSManagedObjectContext |
操作数据的 “工作台”(增删改查) |
container.viewContext(主线程)/container.newBackgroundContext()(后台) |
简单说:你只需要告诉容器 “数据模型叫什么名字”,它会自动完成模型加载、协调器创建、上下文关联等所有底层工作,不用写一行冗余代码。
容器里最常用的是两个上下文,一定要分清:
光说不练假把式,我们用一个简单的 “读书笔记管理” 示例,看看怎么用容器搞定 Core Data 的增删改查。
创建 iOS 项目时勾选「Use Core Data」(Xcode 会自动生成基础的容器代码);
打开.xcdatamodeld文件,创建一个BookNote实体,添加三个属性:
bookName(String,书名);content(String,笔记内容);createTime(Date,创建时间,默认值可设为@now)。AppDelegate 中的核心代码,我们优化下错误处理(别用 fatalError,实际项目要友好):
import UIKit
import CoreData
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
// 懒加载持久化容器
lazy var persistentContainer: NSPersistentContainer = {
// 模型文件名要和.xcdatamodeld文件名称一致(比如我命名为BookNoteModel)
let container = NSPersistentContainer(name: "BookNoteModel")
// 加载持久化存储(默认是SQLite文件,存储在App沙盒中)
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
// 实际项目中替换为日志/弹窗提示,别直接崩溃
print("Core Data加载失败:(error.localizedDescription)")
}
})
return container
}()
// 封装保存上下文的方法,复用性更高
func saveContext() {
let context = persistentContainer.viewContext
guard context.hasChanges else { return } // 没有修改就不保存,减少IO消耗
do {
try context.save()
print("读书笔记保存成功✅")
} catch {
print("保存失败❌:(error.localizedDescription)")
}
}
}
import UIKit
import CoreData
class ViewController: UIViewController {
// 获取容器(实际项目建议用单例/依赖注入,别直接强转AppDelegate,这里为了简化)
private var container: NSPersistentContainer {
let appDelegate = UIApplication.shared.delegate as! AppDelegate
return appDelegate.persistentContainer
}
// 1. 添加读书笔记
@IBAction func addBookNote(_ sender: UIButton) {
let context = container.viewContext
// 创建BookNote对象
let note = BookNote(context: context)
note.bookName = "《小王子》"
note.content = "正是你为你的玫瑰花费的时光,才使你的玫瑰变得如此重要。"
note.createTime = Date() // 也可以依赖模型的默认值,这里手动赋值更直观
// 调用AppDelegate的保存方法
(UIApplication.shared.delegate as! AppDelegate).saveContext()
}
// 2. 查询所有读书笔记(可按创建时间倒序)
func fetchAllBookNotes() {
let context = container.viewContext
// 创建查询请求
let fetchRequest: NSFetchRequest<BookNote> = BookNote.fetchRequest()
// 按创建时间倒序排列,最新的笔记在前面
let sortDescriptor = NSSortDescriptor(keyPath: \BookNote.createTime, ascending: false)
fetchRequest.sortDescriptors = [sortDescriptor]
do {
let notes = try context.fetch(fetchRequest)
notes.forEach { note in
print("📚 书名:(note.bookName ?? "未知")")
print("✍️ 笔记:(note.content ?? "无内容")")
print("🕒 创建时间:(note.createTime ?? Date())\n")
}
} catch {
print("查询读书笔记失败:(error.localizedDescription)")
}
}
// 3. 删除读书笔记(示例:删除第一条《小王子》的笔记)
@IBAction func deleteBookNote(_ sender: UIButton) {
let context = container.viewContext
let fetchRequest: NSFetchRequest<BookNote> = BookNote.fetchRequest()
// 增加筛选条件:只删《小王子》的笔记
fetchRequest.predicate = NSPredicate(format: "bookName == %@", "《小王子》")
do {
if let targetNote = try context.fetch(fetchRequest).first {
context.delete(targetNote) // 删除指定笔记对象
(UIApplication.shared.delegate as! AppDelegate).saveContext()
print("《小王子》的笔记已删除")
}
} catch {
print("删除读书笔记失败:(error.localizedDescription)")
}
}
// 4. 后台批量导入读书笔记(重点:用后台上下文,不卡UI)
func batchImportBookNotes() {
// 创建后台上下文
let backgroundContext = container.newBackgroundContext()
// 设置合并策略,避免多上下文操作冲突
backgroundContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
// 在后台线程执行批量操作,不会阻塞主线程
backgroundContext.perform { [weak self] in
// 模拟批量导入3本经典书籍的笔记
let noteDatas = [
("《百年孤独》", "生命中真正重要的不是你遭遇了什么,而是你记住了哪些事,又是如何铭记的。"),
("《解忧杂货店》", "其实所有纠结做选择的人心里早就有了答案,咨询只是想得到内心所倾向的选择。"),
("《活着》", "人是为了活着本身而活着的,而不是为了活着之外的任何事物而活着。")
]
// 循环创建笔记对象
for (bookName, content) in noteDatas {
let note = BookNote(context: backgroundContext)
note.bookName = bookName
note.content = content
note.createTime = Date()
}
// 保存后台上下文的修改
do {
try backgroundContext.save()
print("批量导入读书笔记完成✅")
} catch {
print("批量导入失败❌:(error.localizedDescription)")
}
}
}
}
newBackgroundContext();perform方法会自动在对应的后台线程执行代码,不用手动写 GCD(比如DispatchQueue.global().async);BookNoteManager),把增删改查的逻辑抽离出来,ViewController 只负责调用,代码更整洁易维护。viewContext默认绑定主线程,避免了新手最容易踩的 “线程混乱” 坑;NSPersistentContainer是 Apple 为简化 Core Data 开发推出的 “利器”,封装了 Core Data 的核心组件,iOS 10 + 可直接用;viewContext处理 UI 相关操作(比如展示读书笔记)→用newBackgroundContext()处理后台耗时操作(比如批量导入)→保存上下文;Core Data 看似复杂,但有了NSPersistentContainer这个 “帮手”,新手也能快速上手。与其纠结底层原理,不如先动手写起来,遇到问题再深入研究,毕竟实践才是最好的老师~
BookNote包含bookName(书名)、content(笔记内容)、createTime(创建时间)三个核心属性;![]()
大家新年好!在过去的几年中,AI 始终占据着科技界最耀眼的 C 位。但站在 2026 年的起点回看,我发现一个显著的转折:从 2025 年末开始,人们对“万亿参数”或“榜单跑分”的狂热逐渐褪去,取而代之的是对 AI 工作流深耕细作的冷静与实战。
如果说过去两年大多数人还在尝试如何与 Chat 机器人聊天,那么现在,AI 已经通过 CLI、MCP 以及各种 Slash、Skill、SubAgent,彻底打破了对话框的限制。对于有经验的开发者来说,AI 已经不再是一个外部工具,而是像插件一样,渗透进终端、编辑器乃至整个操作系统的每一个毛细血管。
在这一点上,macOS 展示了某种“无心插柳”的天然优势。借助 AppleScript 和快捷指令这些成熟的自动化工具,即便不通过复杂的 API 开发,普通用户也能让 AI 访问自己的私有数据。这种“老树发新芽”的现象,让苹果在 AI 时代拥有了新的护城河。而如果这种能力在 iOS 上通过系统级 Agent 完全释放,硬件设备的形态或许将迎来新一轮重塑。
与此同时,某些厂商的策略则更加“激进”。字节跳动的豆包手机尝试从系统底层通过屏幕读取与模拟交互来“暴力”接管一切;华为则通过 A2A(Agent to Agent)策略,试图在后台构建一套统一的代理调度机制。无论路线如何,2026 年对于普通消费者来说都标志着一个奇点的到来:AI 不再是聊天工具,而是显式或隐式地接管了我们的数字生活。
正如那句老话:当一个技术不再被反复提及,才说明它已真正融入生活,如同血液般不可或缺。
然而,越是无感,越要警惕。当 AI 深入工作流的每一个细节,隐私将成为最昂贵的奢侈品。在追求极致自动化与效率的同时,如何选择服务商、如何平衡本地与云端模型、如何保留最后一点象征性的“隐私”,将是我们在 2026 年必须面对的命题。
2026 来了,你开始将 AI 集成到自己的工作流中了吗?
🚀 《肘子的 Swift 周报》
每周为你精选最值得关注的 Swift、SwiftUI 技术动态
- 📮 立即订阅 | weekly.fatbobman.com 获取完整内容
- 👥 加入社区 | Discord 与 2000+ 开发者交流
- 📚 深度教程 | fatbobman.com 探索 200+ 原创文章
Zipic 是我一直在高频使用的图片压缩工具,我亲眼见证了这个应用如何从一个职场工作的小需求,逐渐在作者 十里 的不断打磨下成长为一个高效、精致、专注的成功产品。独立开发者往往意味着“一人成军”,时刻在策略、设计、开发、分发与推广之间来回切换。为了挖掘这背后的故事,我邀请了十里复盘了 Zipic 从 0 到 1 的全过程。全文共分三个篇章:产品设计(本文)、不依赖 Mac App Store 的分发与售卖 以及 技术细节复盘:SwiftUI 痛点与性能瓶颈。
在开发者社区中,关于 Swift 和 Rust 性能的讨论从未停止。通常的看法是:Swift 因为自动引用计数(ARC)而相对较慢,而 Rust 则以其极致的速度和内存效率著称。但 Snow 认为,这种“快”与“慢”的简单标签往往掩盖了两者在设计哲学上的根本差异:Swift 优先开发体验和生态兼容,Rust 追求极致性能和编译时安全。
结合实际案例,文章揭示了五个真相:Rust 的所有权规则本质上是零开销的编译时工具;Swift 的真正性能包袱来自 Objective-C 兼容性而非 ARC 本身;ARC 的核心问题是性能的不可预测性;并发安全上 Swift 依赖运行时保护而 Rust 实现编译时保证;以及为何 Swift 无法“变成”Rust。
Mohammad Azam 基于多年 iOS 开发经验和真实案例,撰写了完整的 StoreKit 订阅实践教程。系列涵盖:变现模型选择(一次性购买、订阅、消耗型购买及混合策略)、付费墙策略对比(软/硬付费墙及订阅试用的权衡)、引导体验设计(从静态截图演进到 8 步交互式引导,让用户在付费前完成核心功能体验并建立情感投入)、以及完整的技术实现(App Store Connect 配置、StoreKit 集成、产品加载和购买流程的代码示例)。
在 2025 年,随着 Swift SDK for Android 在 swift.org 正式发布,Skip 通过 Skip Fuse 提供原生编译支持,解锁了数千个原生 Swift 包在 Android 上的使用。同时新增 NFC、Stripe、PostHog、Auth0、Socket.IO 等双平台框架。iOS 26 推出的 Liquid Glass 界面风格成为跨平台框架的试金石。Skip 因采用“完全原生”策略(iOS 上使用原生 SwiftUI,Android 上映射到 Jetpack Compose)而在第一天就自动支持新界面,无需重写或变通。在 2026 年 Skip 计划继续扩展集成框架、优化 Skip Fuse 工具链、提升性能和开发体验。
这是一份 Khoa Pham 在高强度使用 Claude Code 数月后整理的实战指南。核心技巧包括各种不同模式的详细应用场景,尤其是如何合理使用 Extended Thinking 模式以避免浪费 Token。另外还涵盖了关键快捷键、上下文管理技巧、MCP 集成、VS Code 和 Chrome 扩展、GitHub Actions 集成、Git Worktrees 并行工作流、插件生态以及提示词最佳实践等众多内容。内容详实、具体、有针对性,并非简单的功能介绍手册。
苹果在 WWDC 2025 中发布了 App Store Connect API Webhook,支持构建状态、App 版本状态、TestFlight 反馈等事件的实时推送。Zhong Cheng 针对打包上传后传统 Polling 方式需等待约 20 分钟(GitHub Runner 浪费 $1.24/次)的痛点,详细介绍了如何在 CI/CD 中应用该能力,实现零等待成本;GitFlow 回 master 时机可精确对齐 App 实际发布时间;开发者权限受限时也能及时收到拒审通知。
WendyOS 是一个专为嵌入式设备设计的 Linux 发行版,用 Swift 编写,旨在将 iOS 开发的便捷性带到嵌入式领域。Joannis Orlandos 在本文中提供了完整上手教程:从安装 Homebrew 和 Wendy 工具、刷写 WendyOS 到树莓派/Jetson Orin Nano 等设备、通过 USB 连接设备、配置 WiFi、创建 Swift 项目(含 wendy.json 权限配置)到使用 VSCode 扩展进行远程调试(支持断点和状态检查)。适合想将 Swift 应用到嵌入式设备或 IoT 场景的开发者作为入门教程。
这是一个由 Pedro Piñera 创建、基于 Matt Massicotte 的 Swift 并发理念整理的学习资源,用通俗易懂的方式解释 async/await、Task、Actor、Sendable 等核心概念。Pedro 通过 "Office Building(办公楼)" 这一场景,将 MainActor 比作前台、actor 比作部门办公室、await 比作敲门等待,帮助开发者建立直观的心智模型。 另外,还提供了一个适用于 AI 工具的 Skill.md 文件,方便开发者将上述并发实践直接嵌入到开发工作流的规则引擎中。
Thomas Ricouard 创建的用于 iOS/Swift 开发的 Skills 仓库,包含六个专注于实际工作流的 AI Agent Skills。涵盖 App Store Changelog 生成(从 git history 自动生成发布说明)、iOS Debugger Agent(使用 XcodeBuildMCP 构建/调试 iOS 项目)、Swift Concurrency Expert(修复 Swift 6.2 并发问题)、SwiftUI Liquid Glass(实现 iOS 26+ Liquid Glass API)、SwiftUI View Refactor(重构视图结构和依赖模式)、SwiftUI Performance Audit(审查性能瓶颈并提供优化建议)等。
由jaywcjlove开发的轻量级 StoreKit 2 封装库,专为 SwiftUI 设计,大幅简化应用内购买实现。相比直接使用 StoreKit 2 API,StoreKitHelper 减少了约 70% 的样板代码,特别适合需要快速集成应用内购买且不想处理底层复杂性的 SwiftUI 开发者。
核心特性包括:基于 @ObservableObject 的状态管理、协议驱动的类型安全产品定义、实时交易监听和自动状态更新、内置的 StoreKitHelperView 和 StoreKitHelperSelectionView UI 组件。通过 hasNotPurchased/hasPurchased 属性可以轻松控制界面显示,支持链式 API 配置购买弹窗的各种回调。
Photon 正在构建开源基础设施,帮助开发者将 AI Agent 带到人类已经熟悉的交互界面中,例如 iMessage、WhatsApp、电话通话、Discord、Signal 等。在此之上,我们还在打造以交互为核心的开源 Agent SDK,覆盖多段消息处理、消息线程处理、表情/回应(Tapbacks)等能力,让开发者和企业能够开发真正"像人一样"交流的 Agent。
职位要求
我们正在招聘 macOS 工程师,理想的候选人应具备以下条件:
薪资待遇
我们将提供具有竞争力的薪资(工作地点:美国,支持远程办公)。此外,Photon 获得多家知名投资机构的支持。
联系方式
这是朋友创业团队 Photon 的招聘。他们在做 AI Agent 在 iMessage/WhatsApp 等平台的基础设施,是个早期项目。如果你对 macOS 底层技术和早期创业机会感兴趣,可以了解一下。
如果本期周报对你有帮助,请:
🚀 拓展 Swift 视野
- 📮 邮件订阅 | weekly.fatbobman.com 获取独家技术洞察
- 👥 开发者社区 | Discord 实时交流开发经验
- 📚 原创教程 | fatbobman.com 学习 Swift/SwiftUI 最佳实践
Block是带有局部变量的匿名函数。
iOS4引入,是对c语言的扩充功能,先来理解一下局部变量和匿名函数的含义
那带有局部变量又是什么意思? 先理解一下c语言的都有哪些变量
每个变量的作用域不同。
int aa = 2; //全局变量
static int bb = 3; //静态全局变量
int main(int argc, const char * argv[]) { //argc 函数的参数
int a = 1; //auto修饰局部变量
static int b = 1; //静态局部变量
return 0;
}
| 变量 | 作用域 | 存储位置 | 生命周期 | 关键特性 |
|---|---|---|---|---|
| 全局 aa | 整个程序(文件间) | 全局数据区.data | 程序全程 | 外部链接,多文件可访问 |
| 静态全局 bb | 仅当前源文件 | 全局数据区.data | 程序全程 | 内部链接,仅本文件可见 |
| 局部 a | main 函数内 | 栈区 | main 执行期间 | 自动销毁,每次调用重新初始化 |
| 静态局部 b | main 函数内 | 全局数据区.data | 程序全程 | 仅初始化一次,保留值 |
| argc/argv | main 函数内 | 栈区 | main 执行期间 | 函数参数,栈上分配 |
所谓匿名函数,就是不带有函数名称的函数。
而在c语言中是不允许函数不带有名称的。
先理解一下c语言的函数定义:
int func(int count); //声明了名为func的函数,参数为int类型的count,返回值为int类型
调用func
int result = func(3);
使用函数指针funcPtr直接调用函数func,这种也是需要函数名才能通过函数指针调用。
int (*funPtr)(int) = &fun;
int result = (*funcPtr)(3);
官方文档:Blocks Programming Topics
//官方实例
int multiplier = 7;
int (^myBlock)(int) = ^(int num) {
return num * multiplier;
};
![]()
其中myBlock是声明的快对象,返回类型为int,myBlock 快对象有一个int类型的参数,myBlock的主体部分为 return num * multiplier;
上面表达式的特点:
^ ,插入记号,便于查找。Block的表达式: ^返回值类型(参数列表){表达式}
^int (int count){ return count +1 };
Block可以省略如返回值,参数列表,如果用不到的话
省略返回值类型
//省略返回值类型:^(参数列表){表达式};
^(int count){return count+1};
省参数列表
^int (void){ return 1};
^int {return 1};
省略返回值类型、参数列表:
^{ }; //最简洁的block
通过Block语法将Block赋值为Block类型的变量
int (^blk) (int) = ^(int count) { return count+1};
此时的表达式和c语言的指针函数表达式对比
int (*funPtr)(int) = &fun; //指针函数
block的变量声明就是把声明指针函数类型的* 变为^
在函数参数中使用 Block类型的变量
//作为函数参数的block变量
void func(int (^blk)(int)){
}
作为函数的返回值
int (^blk1(void))(int){
return ^(int count){
return count+1 ;
};
}
//作为函数返回值时,需要注意:
// 1. 调用 blk1 函数,并且是无参函数,它返回一个Block
int (^myBlock)(int) = blk1();
// 2. 调用(执行)这个返回的Block,并传入整数参数 5
int result = myBlock(5);
NSLog(@"结果是: %d", result); // 输出:结果是: 6
上述当block作为参数或返回值时,可以通过typedef声明类型,来简化,如下
typedef int ^(Blk_t)(int); //声明一个Blk_t类型的block
Blk_t blck = ^{
}
//作为函数参数和返回值就可以简化为
void func(Blk_t blck){
}
//作为函数返回值时可以简化为
Blk_t func1(){
}
void test1(void){
//默认为auto修饰局部变量
int a = 1;
void (^bck1)(void)=^(){
NSLog(@"访问量局部变量a:%d",a);
};
a = 2;
NSLog(@"访问量局部变量a1:%d",a);
bck1();
}
//访问量局部变量a1:2
//访问量局部变量a:1
block访问局部变量 auto修饰,此时block截获了变量a的当前的瞬间值,底层为值传递,所以block内部不能直接赋值修改,block外侧修改了局部变量,block内部变量值不会修改。
如果在block内部尝试修改局部变量会报错
![]()
报错信息:变量不可赋值(缺少__block类型说明符)
Variable is not assignable (missing __block type specifier)
__block说明符更准确的描述方式为“__block存储域类说明符” __block storage-class-specifier,c语言的存储域类说明符右以下几种:
__block类似于static、auto、register等说明符。用于指定将变量值设置到哪个存储域中,例如auto作为自动变量存储在栈中,static表示作为静态变量存储在数据区中。
如果非要在block内部修改局部变量,就需要再局部变量前通过__block修饰
void test1(void){
//默认为auto修饰局部变量
__block int a = 1;
void (^bck1)(void)=^(){
NSLog(@"访问量局部变量修改前a:%d",a);
a = 3;
NSLog(@"访问量局部变量修改后a:%d",a);
};
a = 2;
NSLog(@"访问量局部变量a1:%d",a);
bck1();
}
//访问量局部变量a1:2
//访问量局部变量修改前a:2
//访问量局部变量修改后a:3
block截获自动变量会报错,那截获OC对象呢,比如NSMutableArray还会报错吗?
//截获可变数组,
void test11(void){
//默认为auto修饰局部变量
NSMutableArray *array = [NSMutableArray array];
void (^bck1)(void)=^(){
[array addObject:[[NSObject alloc]init]];
NSLog(@"访问量局部变量array.count:%lu",(unsigned long)array.count);
};
NSLog(@"访问量局部变量array1.count:%lu",(unsigned long)array.count);
bck1();
}
//访问量局部变量array1.count:0
//访问量局部变量array.count:1
此时block截获的变量值是NSMutableArray类对象,及NSMutableArray类对象的结构体实例指针,因此,对变量值进行addObject操作,是没有影响的,如果在block内部,要对array进行赋值时是不行的。依然需要用__block修饰
![]()
使用c语言的数组时必须小心使用其指针,下面这个例子,看似并没有像截获的自动变量text赋值,但还是编译不通过,报错信息:Cannot refer to declaration with an array type inside block(不能引用块内数组类型的声明)
![]()
需要把text声明为指针来解决
void test111(void){
//使用c语言的数组时必须小心使用其指针,
const char *text = "hello";
void (^bck1)(void)=^(){
//截获自动变量的方法并没有实现对c语言数组的截获,此时需要用指针来解决该问题
NSLog(@"截获的局部变量:%c",text[2]);
};
bck1();
}
一开始讲了Blocks是带有局部变量的匿名函数,但是Block实质究竟是什么,类型、 变量、还是其他什么?
先说结论:Block其实是一个对象。因为它的结构体里有isa指针。
为了探究到底,我们需要通过clang 把oc转为c++源码来探个究竟
进入main.m的文件夹下,执行:clang -rewrite-objc main.m
![]()
执行后,会生成一个main.cpp文件
![]()
转换前的oc代码
//为了简化生成的c++代码,把原来oc代码的main方法传参(int argc, const char * argv[])省略。以及
int main(void) {
void (^donyBck)(void)=^(){
//这里的日志用英文,转c++后,不会转义,方便查看,中文的话,会转义,不方便查看
NSLog(@"The block prints logs internally");
};
//donyBck的调用
donyBck();
return 0;
}
转换后的C++源码
//包含block实际函数指针的结构体
struct __block_impl {
void *isa; //有isa
int Flags;
int Reserved; //今后升级所需区域大小
void *FuncPtr; //函数指针
};
//
static __NSConstantStringImpl __NSConstantStringImpl__var_folders_d__csgptyg57sqczzwhcvq7mcpr0000gn_T_main_5a37bd_mi_0 __attribute__ ((section ("__DATA, __cfstring"))) = {__CFConstantStringClassReference,0x000007c8,"The block prints logs internally",32};
//block结构体
struct __main_block_impl_0 {
struct __block_impl impl; //Block的实际指针,
struct __main_block_desc_0* Desc;
//block构造函数
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock; //isa指针 默认block创建在栈上
impl.Flags = flags; //block标志位
impl.FuncPtr = fp; //Block执行的函数指针
Desc = desc; //Block描述信息,Block大小等元信息
}
};
//Block内部函数调用
/*
void (^donyBck)(void)=^(){
//这里的日志用英文,转c++后,不会转义,方便查看,中文的话,会转义,不方便查看
NSLog(@"The block prints logs internally");
};
*/
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_d__csgptyg57sqczzwhcvq7mcpr0000gn_T_main_5a37bd_mi_0);
}
//Block底层编译后自动生成的,Block描述信息结构体变量,静态匿名结构体+变量
static struct __main_block_desc_0 {
size_t reserved; //成员1 :保留字段
size_t Block_size; //成员2:Block实例的内存大小
} __main_block_desc_0_DATA = { //初始化变量
0, //给Reserved赋值
sizeof(struct __main_block_impl_0) //给Block——size赋值
};
//main函数
int main(void) {
//1.构造block对象,并将其强制转换为无参无返回值的函数指针。
void (*donyBck)(void)=((void (*)())&__main_block_impl_0(
(void *)__main_block_func_0, //block要执行的代码逻辑(函数指针)->FuncPtr
&__main_block_desc_0_DATA //block的描述信息(版本,大小等)
));
//2.调用block的核心逻辑(donyBck的调用)
// ((void (*))(donyBck):将函数指针转回Block结构体指针
//->FuncPtr :取出Block的执行函数指针
//最后调用该函数,并传入Block自身作为参数,(block的隐式self)
((void (*)(__block_impl *))((__block_impl *)donyBck)->FuncPtr)((__block_impl *)donyBck);
return 0;
}
先看看 __main_block_impl_0 结构体
//block结构体
struct __main_block_impl_0 {
struct __block_impl impl; //Block的实际指针,
struct __main_block_desc_0* Desc;
//block构造函数
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock; //isa指针 默认block创建在栈上
impl.Flags = flags; //block标志位
impl.FuncPtr = fp; //Block执行的函数指针
Desc = desc; //Block描述信息,Block大小等元信息
}
};
从源码里可以看出,__main_block_impl_0结构体,包含三部分
成员变量 impl,是结构体__block_impl
成员变量 Desc指针,是结构体 __main_block_desc_0
__main_block_impl_0 构造函数
分别分析一下这三部分
struct __block_impl impl
//包含block实际函数指针的结构体
struct __block_impl {
void *isa; //有isa
int Flags; //标志位
int Reserved; //今后升级所需区域大小
void *FuncPtr; //函数指针
};
{ NSLog(@"The block prints logs internally");};。3.1.1里的__main_block_impl_0 里的impl 是__block_impl结构体,而__block_impl 包含了Block实际函数指针 FuncPtr。
总结:impl 主要就是包含了Block的函数指针 FuncPtr
struct __main_block_desc_0* Desc
//Block底层编译后自动生成的,Block描述信息结构体变量,静态匿名结构体+变量
static struct __main_block_desc_0 {
size_t reserved; //成员1 :保留字段
size_t Block_size; //成员2:Block实例的内存大小
} __main_block_desc_0_DATA = { //初始化变量
0, //给Reserved赋值
sizeof(struct __main_block_impl_0) //给Block——size赋值
};
总结:__main_block_desc_0是block的描述信息,也就是附加信息
__main_block_impl_0构造函数 //block构造函数
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock; //isa指针 默认block创建在栈上
impl.Flags = flags; //block标志位
impl.FuncPtr = fp; //Block执行的函数指针
Desc = desc; //Block描述信息,Block大小等元信息
}
传三个参数:
总结:构造函数主要用来初始化__block_impl的成员变量,以及把描述信息赋值给Desc
关于:__main_block_impl_0 基本概念就了解完了,那在main方法里,__main_block_impl_0 又是怎么赋值的呢
//原函数
void (^donyBck)(void)=^(){
//这里的日志用英文,转c++后,不会转义,方便查看,中文的话,会转义,不方便查看
NSLog(@"The block prints logs internally");
};
//转换c++后
//1.构造block对象,并将其强制转换为无参无返回值的函数指针。
void (*donyBck)(void)=((void (*)())&__main_block_impl_0(
(void *)__main_block_func_0, //block要执行的代码逻辑(函数指针)->FuncPtr
&__main_block_desc_0_DATA //block的描述信息(版本,大小等)
));
可以看出通过 __main_block_impl_0构造函数,生成 __main_block_impl_0结构体(Block结构体)的实例指针,并赋值给donyBck
然后对 __main_block_impl_0构造函数传了两个参数
__main_block_func_0
//Block内部函数调用
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
//对应的就是原oc NSLog(@"The block prints logs internally");
NSLog((NSString *)&__NSConstantStringImpl__var_folders_d__csgptyg57sqczzwhcvq7mcpr0000gn_T_main_5a37bd_mi_0);
}
可以看出 对应的是oc block的主体部分也就是 {NSLog(@"The block prints logs internally"); };
这里的参数 __cself就是指向Block的值的指针变量,相当于oc的self;
🎯这里画个重点:从这里可以看出,block内部,把block的^{}执行函数{}在Block定义完后,当成一个参数类型为__main_block_func_0传入到 __main_block_impl_0->__block_impl-> FuncPtr 里了。
__main_block_desc_0_DATA
__main_block_desc_0_DATA 是包含了Block的描述信息,
static struct __main_block_desc_0{
size_t reserved; //成员1 :保留字段
size_t Block_size; //成员2:Block实例的内存大小
} __main_block_desc_0_DATA = { //初始化变量
0, //给Reserved赋值
sizeof(struct __main_block_impl_0) //给Block——size赋值
};
至此Block的内部原理就浮出水面了。
Block内部是由 __main_block_impl_0结构体组成的,内部isa指针,指向所属类的结构体的实例指针,_NSConcreteStackBlock相当于Block的结构体实例,对象impl.isa = &_NSConcreteStackBlock ,将Block结构体的指针赋值给impl的成员变量isa ,相当于Block结构体成员变量保存了Block结构体的指针,和OC的对象处理方式是一致的。
所以Block的实质就是对象,和NSObject一样,都是对象。
![]()
在2.3里我们知道了Block可以截获局部变量,那背后Block是怎么截获的,为什么不能在block内部直接修改截获的局部变量呢?
先说结论:Block截获的局部变量是值传递的方式传入Block结构体中,并保存为Block的成员变量。因此当外部局部变量值发生修改后,Block内部对应的成员变量的值并没有发生改变。
为了探究到底,我们需要通过clang 把oc转为c++源码来探个究竟
进入main.m的文件夹下,执行:clang -rewrite-objc main.m
oc代码
int main(void) {
int a = 2;
void (^donyBck)(void)=^(){
NSLog(@"The block Capture local variables:%d",a);
};
a = 4;
//donyBck的调用
donyBck();
NSLog(@"The block prints local variables:%d",a);
return 0;
}
//The block Capture local variables:2
//The block prints local variables:4
转c++源代码
//block结构体
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int a; //截获的局部变量,在block内部变成了成员变量
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
//block执行函数
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int a = __cself->a; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_d__csgptyg57sqczzwhcvq7mcpr0000gn_T_main_268414_mi_1,a);
}
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
} __main_block_desc_0_DATA = {
0,
sizeof(struct __main_block_impl_0)};
int main(void) {
int a = 2;
//block定义
void (*donyBck)(void)=((void (*)())&__main_block_impl_0(
(void *)__main_block_func_0,
&__main_block_desc_0_DATA,
a));
a = 4;
//block 执行方法
((void (*)(__block_impl *))((__block_impl *)donyBck)->FuncPtr)(
(__block_impl *)donyBck
);
//打印
NSLog((NSString *)&__NSConstantStringImpl__var_folders_d__csgptyg57sqczzwhcvq7mcpr0000gn_T_main_268414_mi_2,a);
return 0;
}
__main_block_impl_0 多了一个成员变量a,这个变量就是Block捕获的局部变量__main_block_func_0 看出 int a = __cself->a在block内部访问这个变量a时,通过值传递的方式,而不是指针传递,这也就说明了a是block的内部变量,外部修改a,Block内部捕获的a是不会发生变化的。那使用__block来修饰局部变量后,就能让Block内部来修改这个变量,那背后__block 又做了什么呢?
先说结论:通过 __block修饰后,使这个局部变量在block内部通过指针传递,所以修饰后的局部变量,在block内部可以修改了。
为了探究到底,我们需要通过clang 把oc转为c++源码来探个究竟
进入main.m的文件夹下,执行:clang -rewrite-objc main.m
oc代码
int main(void) {
__block int a = 2;
void (^donyBck)(void)=^(){
a = 3;
NSLog(@"The block Capture local variables:%d",a);
};
a = 4;
//donyBck的调用
donyBck();
NSLog(@"The block prints local variables:%d",a);
return 0;
}
c++代码
//__block修饰的
struct __Block_byref_a_0 {
void *__isa; //isa指针
__Block_byref_a_0 *__forwarding; //传入变量的地址
int __flags; //标志位
int __size; //结构体大小
int a; //存放变量a的实际的值,相当与原局部变量的成员变量。
};
//block内部
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_a_0 *a; // by ref //加入__Block修饰后,这里的a是__Block_byref_a_0类型了
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_a_0 *_a, int flags=0) : a(_a->__forwarding) {
impl.isa = &_NSConcreteStackBlock; //栈block
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
//block执行函数
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_a_0 *a = __cself->a; // bound by ref
(a->__forwarding->a) = 3;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_d__csgptyg57sqczzwhcvq7mcpr0000gn_T_main_3b64fc_mi_3,(a->__forwarding->a));
}
//新增了
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
_Block_object_assign(
(void*)&dst->a,
(void*)src->a,
8/*BLOCK_FIELD_IS_BYREF*/);
}
//新增了
static void __main_block_dispose_0(struct __main_block_impl_0*src) {
_Block_object_dispose((void*)src->a,
8/*BLOCK_FIELD_IS_BYREF*/);
}
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = {
0,
sizeof(struct __main_block_impl_0),
__main_block_copy_0,
__main_block_dispose_0
};
//main函数
int main(void) {
//__block修饰的局部变量a
__attribute__((__blocks__(byref))) __Block_byref_a_0 a = {
(void*)0,
(__Block_byref_a_0 *)&a,
0,
sizeof(__Block_byref_a_0),
2};
void (*donyBck)(void)=((void (*)())&__main_block_impl_0(
(void *)__main_block_func_0,
&__main_block_desc_0_DATA,
(__Block_byref_a_0 *)&a,
570425344));
//修改局部变量a的值
(a.__forwarding->a) = 4;
//block调用
((void (*)(__block_impl *))((__block_impl *)donyBck)->FuncPtr)((__block_impl *)donyBck);
//block打印
NSLog((NSString *)&__NSConstantStringImpl__var_folders_d__csgptyg57sqczzwhcvq7mcpr0000gn_T_main_3b64fc_mi_4,(a.__forwarding->a));
return 0;
}
可以看出局部变量a加上 __block后,c++代码里新增了 __Block_byref_a_0、__main_block_copy_0、__main_block_dispose_0
__main_block_impl_0 可以看出原来oc的 被__block修饰后的局部变量a,在结构体__main_block_impl_0内部变成了 __Block_byref_a_0 *a,也就是说Block内部的结构体__main_block_impl_0实例持有指向__block变量的__Block_byref_a_0结构体实例指针。__main_block_copy_0和__main_block_dispose_0留在后面在说。这里有点绕,我们用白话文理解一下
可以把整个过程想象成寄送一个易碎品(变量a):
| 角色 | 对应代码 | 比喻说明 |
|---|---|---|
| 易碎品本身 | 局部变量 int a = 2
|
比如一个玻璃杯。 |
| 加固包装盒 |
__Block_byref_a_0结构体 |
一个专门用来固定玻璃杯的防震包装盒。 |
| 快递单/指针 |
__Block_byref_a_0 *a(Block结构体里的成员) |
一张写着包装盒地址的快递单。 |
| 整个Block |
__main_block_impl_0结构体实例 |
快递仓库。 |
__block修饰变量 a时,编译器会自动创建一个“包装盒”(__Block_byref_a_0结构体),然后把你的变量 a(玻璃杯)放进这个盒子里。__Block_byref_a_0 *a)。a的值时,它们会凭着这张“快递单”找到同一个“包装盒”,然后打开盒子修改里面的玻璃杯。因为大家访问的是同一个盒子里的东西,所以任何一方的修改,另一方都能立刻看到白话文总结:被__block修饰后的变量a,在Block内并没有直接把变量装在自己口袋里,而是记下了变量所在包装盒的地址。通过这个共享的地址,Block和外部代码就能共同修改同一个变量了。
在看看__Block_byref_a_0 结构体
//__block修饰的
struct __Block_byref_a_0 {
void *__isa; //isa指针
__Block_byref_a_0 *__forwarding; //传入变量的地址
int __flags; //标志位
int __size; //结构体大小
int a; //存放变量a的实际的值,相当与原局部变量的成员变量。
};
在看一下在main()中原oc代码为
__block int a = 2;
转为c++为
//__block修饰的局部变量a
__attribute__((__blocks__(byref))) __Block_byref_a_0 a = {
(void*)0,
(__Block_byref_a_0 *)&a,
0,
sizeof(__Block_byref_a_0),
2};
从赋值里可以看出在main()被__block修饰后的变量a,底层赋值给__Block_byref_a_0结构体时,传入的值如下
isa传入空
__forwarding 传入了局部变量a的本身地址
__flags :分配了0
sizeof:结构体的大小
a值赋值为2.
![]()
总结一下:到此知道了__forwarding就是局部变量a的本身地址,可以通过 __forwarding指针来访问局部变量。同时也能对其修改了。
//block执行函数
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_a_0 *a = __cself->a; // bound by ref
(a->__forwarding->a) = 3;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_d__csgptyg57sqczzwhcvq7mcpr0000gn_T_main_3b64fc_mi_3,(a->__forwarding->a));
}
可以看出 (a->__forwarding->a) = 3;,是通过指针取值的方式来改变局部变量的值,
从而解释了通过__block修饰后的变量,在Block内部通过指针传递的方式修改局部变量
另外__block的 __Block_byref_a_0 结构体并不在Block的 __main_block_impl_0结构体中,这样做是为了多个Block同时使用__block变量。
继续看一下OC代码
int main(void) {
//__block被多个Block使用
__block int aa = 2;
void (^donyBck)(void)=^(){
aa = 3;
NSLog(@"The block Capture local variables:%d",aa);
};
void (^donyBck1)(void)=^(){
aa = 4;
NSLog(@"The block Capture local variables1:%d",aa);
};
}
转换为c++
//__block修饰后
__Block_byref_aa_0 aa = {
0,
&aa,
0,
sizeof(__Block_byref_aa_0), 2};
//donyBck
donyBck)=&__main_block_impl_0(
__main_block_func_0,
&__main_block_desc_0_DATA,
(__Block_byref_aa_0 *)&aa,
570425344));
//donyBck1
donyBck1)=&__main_block_impl_1(
__main_block_func_1,
&__main_block_desc_1_DATA,
(__Block_byref_aa_0 *)&aa,
570425344));
可以看出donyBck和donyBck1 都是用了__Block_byref_aa_0结构体的实例aa的指针。反过来一个block中使用多个 __block也是可以的。
除了通过__block修饰局部变量外,其他变量如静态局部变量、静态全局变量、全局变量能否在block内部进行修改?
为了探究到底,我们继续需要通过clang 把oc转为c++源码来探个究竟
进入main.m的文件夹下,执行:clang -rewrite-objc main.m
oc代码
int global_a = 2;
static int static_global_a = 3;
int main(void) {
static int static_a = 4;
void (^donyBck)(void)=^(){
global_a = 1;
static_global_a = 2;
static_a = 3;
NSLog(@"The block Capture global_a:%d ,static_global_a:%d,static_a:%d",global_a,static_global_a,static_a);
};
//donyBck的调用
donyBck();
return 0;
}
C++代码
//全局变量
int global_a = 2;
//静态全局变量
static int static_global_a = 3;
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int *static_a; //静态局部变量
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_static_a, int flags=0) : static_a(_static_a) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int *static_a = __cself->static_a; // bound by copy
global_a = 1;
static_global_a = 2;
(*static_a) = 3;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_d__csgptyg57sqczzwhcvq7mcpr0000gn_T_main_6417ea_mi_3,global_a,static_global_a,(*static_a));
}
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
int main(void) {
static int static_a = 4;
void (*donyBck)(void)=((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &static_a));
((void (*)(__block_impl *))((__block_impl *)donyBck)->FuncPtr)((__block_impl *)donyBck);
return 0;
}
从__main_block_impl_0可以看出,静态局部变量static_a以指针形式添加添加为成员变量,而静态全局变量 static_global_a 和全局变量global_a并没有添加到__main_block_impl_0内部。
//全局变量
int global_a = 2;
//静态全局变量
static int static_global_a = 3;
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int *static_a; //静态局部变量
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_static_a, int flags=0) : static_a(_static_a) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
再从__main_block_func_0,可以看出全局变量global_a和全局静态变量static_global_a是在block内部访问直接访问的,而静态局部变量static_a是通过指针传递的方式进行访问和赋值的。
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int *static_a = __cself->static_a; // bound by copy
global_a = 1; //全局变量
static_global_a = 2; //全局静态变量
(*static_a) = 3; //局部静态变量
NSLog((NSString *)&__NSConstantStringImpl__var_folders_d__csgptyg57sqczzwhcvq7mcpr0000gn_T_main_6417ea_mi_3,global_a,static_global_a,(*static_a));
}
在3.2 中的Block实质源码分析里,看出impl.isa = &_NSConcreteStackBlock; //栈block,可以看出该Block存储在栈区,那Block还可以存在哪个区呢?
先说结论:Block分别可以存储在_NSConcreteGlobalBlock、_NSConcreteStackBlock、_NSConcreteMallocBlock。
| 类 | 设置对象的存储域 |
|---|---|
| _NSConcreteGlobalBlock | 数据区域(.data区) |
| _NSConcreteStackBlock | 栈 |
| _NSConcreteMallocBlock | 堆 |
![]()
为了探究到底,我们继续需要通过clang 把oc转为c++源码来探个究竟
进入main.m的文件夹下,执行:clang -rewrite-objc main.m
oc代码
//全局block
void (^donyBck)(void) =^(){
NSLog(@"global block");
};
int main(void) {
donyBck();
return 0;
}
c++源码
struct __donyBck_block_impl_0 {
struct __block_impl impl;
struct __donyBck_block_desc_0* Desc;
__donyBck_block_impl_0(void *fp, struct __donyBck_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteGlobalBlock; //全局block
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
通过源码可以看出 impl.isa = &_NSConcreteGlobalBlock,说明该Block为 _NSConcreteGlobalBlock类型。
这里需要注意使用全局block时,因为本身已经在全局区域,所以不会捕获自动变量(局部变量),存储在数据区域
除了全局block外,其他基本都存储在栈上,也就是StackBlock。
在NSConcreteStackBlock类的block,存储在栈区,如果所属的变量作用域结束,该Block就会被废弃。由于 __block变量也配置在栈上,同样的所属变量作用域结束后,该 __block变量同样也被废弃。
![]()
既然在栈上的Block在变量作用域结束后就立即被废弃,那如果不想废弃怎么办?
Block提供了 【复制copy】操作,可以将Block对象和 __block变量从栈区复制到堆区上,当Block从栈区复制到堆区后,即时变量作用域结束时,堆区上的Block和 __block还可以继续使用。
![]()
此时的在堆区的Block就是_NSConcreteMallocBlock 对象,Block结构体成员变量isa赋值为 imp.isa = &_NSConcreteMallocBlock;
而此时被__block修饰的变量用结构体成员变量 __forwarding可以实现无论__block变量配置在堆上还是在栈上,都能够正确访问__block变量
在ARC下,大多数情况下,编译器会自动进行判断,自动生成将Block从栈上复制到堆上的代码
将Block作为函数返回值返回时,会自动拷贝
向方法或函数的参数中传递Block时:(以下两种情况内部底层实现了copy操作,其他都需要手动拷贝)
NSArray类的enumerateObjectsUsingBlock方法dispatch_async函数
//在MRC下,initWithObjects后面的Block,编译器不会主动给Block添加copy操作,所以Block还存在栈上,所以会报错。
//在ARC下,编译器主动添加了copy操作,此时的block被复制到堆上了。
@implementation SDPerson
- (id) getBlockArray{
int val = 10;
void (^blk0)(void) = ^{NSLog(@"blk0:%d",val);};
void (^blk1)(void) = ^{NSLog(@"blk1:%d",val);};
NSLog(@"blk01:%@",blk0);
NSLog(@"blk02:%@",blk1);
/*
blk01:<__NSStackBlock__: 0x7ff7bfeff0d8>
blk02:<__NSStackBlock__: 0x7ff7bfeff0a8>
*/
//array 的initWithObjects 纯容器存储AIP,框架内部不会自动copy
return [[NSArray alloc]initWithObjects:blk0,blk1, nil];
}
@end
![]()
报错原因是:在执行完getBlockArray后栈上的Block被废弃,MRC 无任何自动优化,initWithObjects: 仅存栈 Block 指针 → 方法返回栈帧销毁 → 执行 Block 访问野内存 → 崩溃。此时我们需要手动复制下即可。
修改一下getBlockArray,即可
- (id) getBlockArray{
int val = 10;
void (^blk0)(void) = ^{NSLog(@"blk0:%d",val);};
void (^blk1)(void) = ^{NSLog(@"blk1:%d",val);};
blk0 = [blk0 copy];
blk1 = [blk1 copy];
NSLog(@"blk01:%@",blk0);
NSLog(@"blk02:%@",blk1);
//blk01:<__NSMallocBlock__: 0x600000c00540>
//blk02:<__NSMallocBlock__: 0x600000c00570>
return [[NSArray alloc]initWithObjects:blk0,blk1, nil];
}
所有需要让 Block「脱离原栈帧存活」的场景,都必须手动调用[block copy]
关于Block不同类的拷贝效果总结如下
| block类 | 副本源的存储域 | 复制效果 |
|---|---|---|
| _NSConcreteStackBlock | 栈区 | 从栈拷贝到堆区 |
| _NSConcreteGlobalBlock | 程序的数据区域 | 不做改变 |
| _NSConcreteMallocBlock | 堆区 | 引用计数增加 |
不管Block配置在何处,用copy复制不会引起任何问题,在不确定时,调用copy方法即可
在使用 __block变量的Block从栈复制到堆上,__block变量也受到了如下影响
| __block变量的配置存储区域 | Block从栈上复制到堆上时的影响 |
|---|---|
| 堆区 | 从栈复制到堆区,并被Block所持有 |
| 栈区 | 被Block所持有 |
和OC引用计数方式内存管理完全相同。
__block修饰的变量被Block所持有,如果Block废弃,持有的__block变量也跟着废弃在Block语法中使用局部变量array来添加元素。
理论上在变量作用域的同时,变量array被废弃,因此赋值给变量array的NSMutableArray类对象必定释放并废弃,但上述代码在main方法里的内{}外仍然可以执行,并打印日志。 这意味着array在Block的执行部分超出其变量作用域而存在。
先说结论:Block从栈复制到堆,归结为__Block_copy函数被调用,使__strong修饰的自动变量对象和__block修饰的变量,被堆上的Block所持有,所以可以超出其变量作用域而存在。
那我们转为c++代码再探个究竟
OC
typedef int (^Blk_t)(id obj);
Blk_t donyBck;
int main(void) {
{
id array = [NSMutableArray array];
donyBck = [^(id obj){
[array addObject:obj];
NSLog(@"access local variables array.count:%lu",[array count]);
} copy];
}
donyBck([[NSObject alloc]init]);
donyBck([[NSObject alloc]init]);
donyBck([[NSObject alloc]init]);
return 0;
}
/*
access local variables array.count:1
access local variables array.count:2
access local variables array.count:3
*/
C++
typedef int (*Blk_t)(id obj);
Blk_t donyBck;
//Block用的结构体
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
id array; //这里array被Block强引用 __strong
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, id _array, int flags=0) : array(_array) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
//block执行方法
static void __main_block_func_0(struct __main_block_impl_0 *__cself, id obj) {
id array = __cself->array; // bound by copy
//添加方法
((void (*)(id, SEL, ObjectType _Nonnull))(void *)objc_msgSend)((id)array, sel_registerName("addObject:"), (id)obj);
//打印日志
NSLog((NSString *)&__NSConstantStringImpl__var_folders_d__csgptyg57sqczzwhcvq7mcpr0000gn_T_main_b8c0c1_mi_8,((NSUInteger (*)(id, SEL))(void *)objc_msgSend)((id)array, sel_registerName("count")));
}
//copy
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->array, (void*)src->array, 3/*BLOCK_FIELD_IS_OBJECT*/);}
//废弃
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->array, 3/*BLOCK_FIELD_IS_OBJECT*/);}
//block描述
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};
//main方法
int main(void) {
{
id array = ((NSMutableArray * _Nonnull (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSMutableArray"), sel_registerName("array"));
donyBck = (Blk_t)((id (*)(id, SEL))(void *)objc_msgSend)((id)((void (*)(id))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, array, 570425344)), sel_registerName("copy"));
}
//Block执行
((int (*)(__block_impl *, id))((__block_impl *)donyBck)->FuncPtr)((__block_impl *)donyBck, ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init")));
((int (*)(__block_impl *, id))((__block_impl *)donyBck)->FuncPtr)((__block_impl *)donyBck, ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init")));
((int (*)(__block_impl *, id))((__block_impl *)donyBck)->FuncPtr)((__block_impl *)donyBck, ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init")));
return 0;
}
通过源码可以看出 array被Block截获,并成为__strong修饰成员变量,这里虽然没有显示__strong,默认就是强引用
//Block用的结构体
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
id array; //这里array被Block强引用 __strong
};
那Block捕获的array是在什么时候进行初始化和废弃的呢?
我们可以从__main_block_desc_0可以看出结构体新增了copy和dispose,以及对应__main_block_copy_0和__main_block_dispose_0,这两个结构体在3.3.2里__block时也遇到了。
不过__block修饰的变量和捕获的对象有一点点区别:
| 对象 | BLOCK_FIELD_IS_OBJECT |
|---|---|
__block变量 |
BLOCK_FIELD_IS_BYREF |
仅仅主要用来区分是对象还是__block变量
//block描述
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};
//copy
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
//_Block_object_assign对使Block内部对array持有
_Block_object_assign(
(void*)&dst->array,
(void*)src->array,
3/*BLOCK_FIELD_IS_OBJECT*/
);
}
//废弃
static void __main_block_dispose_0(struct __main_block_impl_0*src) {
_Block_object_dispose(
(void*)src->array,
3/*BLOCK_FIELD_IS_OBJECT*/
);
}
从__main_block_copy_0可以看出,内部通过_Block_object_assign方法对array持有,_Block_object_assign相当于retain,将对象赋值在对象类型结构体成员变量中。
__main_block_dispose_0使用_Block_object_dispose函数,相当于release,释放赋值在Block用结构体成员变量的array中的对象。
因此_Block_object_assign和__main_block_dispose_0指针赋值在__main_block_desc_0的copy和dispose中。
但是在源代码里没有看到这些函数以及指针被调用,那他们的调用时机在什么时候?
![]()
那什么时候栈上的Block会复制到堆上呢?
__strong修饰符id类型或Block类型成员变量时usingBlock的Cocoa框架方法或GCD的API中传递Block时。array默认为__strong修饰,那如果改为__weak呢?
typedef void (^Blk_t)(id obj);
Blk_t donyBck;
int main(void) {
{
id array = [NSMutableArray array];
id __weak array2 = array;
donyBck = [^(id obj){
[array2 addObject:obj];
NSLog(@"access local variables array.count:%lu",[array2 count]);
} copy];
}
donyBck([[NSObject alloc]init]);
donyBck([[NSObject alloc]init]);
donyBck([[NSObject alloc]init]);
return 0;
}
/**
access local variables array.count:0
access local variables array.count:0
access local variables array.count:0
*/
这是因为array在变量作用域结束后同时被释放、废弃。nil被赋值给__weak修饰的array2。
那如果__block和__weak同时修饰呢?
__block id __weak array2 = array;
结果一样:
access local variables array.count:0
access local variables array.count:0
access local variables array.count:0
即使被附加了__block说明符,__strong修饰符的变量array也会在变量作用域结束的同时被释放掉,nil被赋值给附有__weak的变量array2中。
另外被__unsafe_unretained修饰符的变量只不过与指针相同,所以不管在Block中使用还是附加到__block变量中,也不会想 __strong或__weak那样进行处理,使用__unsafe_unretained修饰符需要注意不能通过悬垂指针访问已被废弃的对象。
__autoreleasing修饰符也不能和__block同时使用
我们知道Block内部使用__strong修饰符的对象类型的自动变量,那当Block从栈复制到堆的时候,该对象就会被Block所持有
那么如果这个对象还同时持有Block的话,就容易发生循环引用。正所谓你中有我,我中有你
![]()
示例1:
// 文件SDPerson.m
typedef void (^blk_t)(void);
@interface SDPerson()
{
blk_t blk_;
}
@end
@implementation SDPerson
- (instancetype)init
{
self = [super init];
if (self) {
blk_ = ^{
NSLog(@"self = %@",self);
};
}
return self;
}
- (void)dealloc{
NSLog(@"dealloc");
}
//文件main.m
#import "SDPerson.h"
int main(void) {
SDPerson *person = [[SDPerson alloc]init];
NSLog(@"%@",person);
return 0;
}
最终执行结果 <SDPerson: 0x60000020d340> SDPerson的dealloc没有执行,发生了Block的循环引用
具体分析:
SDPerson内部blk_t持有了self,而self也同时持有作为成员变量的blk_t
另外编译器也会有提示
![]()
如果Block内部不使用self,还会造成循环引用吗?
示例2:
typedef void (^blk_t)(void);
@interface SDPerson()
{
blk_t blk_;
id obj_;
}
@end
@implementation SDPerson
- (instancetype)init
{
self = [super init];
if (self) {
blk_ = ^{
NSLog(@"obj_ = %@",obj_);
};
}
return self;
}
答案:会
分析一下:表面上看obj_没有使用self,但是它是self的成员变量,因此Block想持有obj_,就必须引用self,所以同样造成循环引用。
那如果这个属性使用weak修饰符呢
实例3 :
typedef void (^blk_t)(void);
@interface SDPerson()
@property (nonatomic, weak) NSArray *array;
@end
- (instancetype)init
{
self = [super init];
if (self) {
blk_ = ^{
NSLog(@"obj_ = %@",_array);
};
}
return self;
}
答案:还是会循环引用,因为循环引用是self和block之间的事情,这个被block持有的成员变量是strong或weak没有关系,即使是基本类型assign也是一样的。
那如何解决这样的循环引用呢?
为了避免循环引用,可以通过__weak修饰符,来打破互相持有
- (instancetype)init
{
self = [super init];
if (self) {
//使用__weak修饰符,使block内部为弱引用关系
id __weak tmp = self;
// id __unsafe_unretained tmp = self;
blk_ = ^{
NSLog(@"self = %@",tmp);
};
}
return self;
}
常见用 __weak typeof(self) weakSelf = self; 来进行弱引用self
__weak typeof(self) weakSelf = self;
self.myBlock = ^{
[weakSelf doSomething];
};
上述存存在一个问题,当block执行过程中,weakSelf可能被释放,导致后续操作无效
这里需要再block内部进行强化弱引用,使用__strong在局部作用域内临时强引用弱引用对象,确保在执行期间对象存活。__strong在Block内部栈上创建局部强指针,不会造成循环引用。
__weak typeof(self) weakSelf = self;
self.myBlock = ^{
__strong typeof(self) strongSelf = weakSelf;
if (strongSelf) {
[strongSelf doSomething];
[strongSelf doAnotherThing];
}
};
除了__weak typeof(self) weakSelf = self; 这里还可以参考第三方开源库ReactiveObjC这样的简洁写法 @weakify(self); 作用是一样的,都是对self进行弱引用。
@weakify(self);
self.myBlock = ^{
@strongify(self);
if (self) {
[self doSomething];
[self doAnotherThing];
}
};
除了__weak修饰符外,还可以使用__unsafe_unretained,如id __unsafe_unretained tmp = self; 效果是一样的,都是使对象为弱引用,那两者有什么区别?更推荐使用__weak
两者的区别在于当所指向的对象被释放时,如何处理指针
__weak(安全)当对象释放后,所有指向它的__weak变量会被运行时自动设置为nil,意味着后续在访问这个指针,就像nil发送消息一样,在OC中是安全的,不会导致程序崩溃 。__unsafe_unretained(不安全)当对象释放后,__unsafe_unretained指针不会自动置空,仍然保存着对象被释放前的那个内存地址,也就是变成了“悬垂指针”或“野指针”,如果此时访问了这个指针,就会发生BAD_ACCESS![]()
除了__weak外,还可以使用 __block解决block循环引用问题
typedef void (^blk_t)(void);
@interface SDPerson()
{
blk_t blk_;
}
@end
@implementation SDPerson
- (instancetype)init
{
self = [super init];
if (self) {
__block id tmp = self;
blk_ = ^{
NSLog(@"obj_ = %@",tmp);
tmp = nil;
};
}
return self;
}
//执行block
- (void)execBlock{
blk_();
}
int main(void) {
SDPerson *person = [[SDPerson alloc]init];
[person execBlock];
NSLog(@"%@",person);
return 0;
}
//执行结果
//obj_ = <SDPerson: 0x600000c08690>
//<SDPerson: 0x600000c08690>
//dealloc
如果Block不执行execBlock,依然会存在循环引用
此时-SDPerson持有Block,Block持有__block变量,__block持有SDPerson类对象。三者互相持有,导致引用循环
![]()
如何解决?
就是执行Block execBlock方法,Block内部,会把tmp置为nil。并执行block execBlock方法,因此__block持有类对象的强引用就失效了,
blk_ = ^{
NSLog(@"obj_ = %@",tmp);
tmp = nil;
};
//并执行block execBlock方法
![]()
所以__block需要执行Block来解决循环引用,基于此特点,可以通过__block控制对象的持有时间。
这里需要区分,这里利用__block解决循环引用,不是因为__block本身直接解决的,而是利用了__block的**「可写特性」+ 手动执行 tmp = nil**,并执行block的execBlock方法,使block内部tmp=nil生效,从而主动打破了循环链。
在实际开发过程中,需要具体根据实际情况,来使用__weak还是__block.
Block?Block是带有局部变量的匿名函数,本质是一个对象,内部有isa指针,内部是由结构体**__main_block_impl_0->__block_impl**组成,Block的执行函数通过在__block_impl->FuncPtr函数指针,找到封装的函数,并将block地址作为参数传给这个函数进行执行。Block捕获的变量,存入__main_block_impl_0结构体内,并通过block地址拿到捕获变量的值。
//包含block实际函数指针的结构体
struct __block_impl {
void *isa; //有isa
int Flags;
int Reserved; //今后升级所需区域大小
void *FuncPtr; //函数指针
};
//block结构体
struct __main_block_impl_0 {
struct __block_impl impl; //Block的实际指针,
struct __main_block_desc_0* Desc;
};
Block 有几种类型?分别是什么?有三种类型,分别为 _NSConcreteGlobalBlock、_NSConcreteStackBlock、_NSConcreteMallocBlock
定义在全局的block,为全局block,存储在数据区的全局区里,因为本身就是全局,所以不会访问局部变量,因此不需要捕获局部变量。
一般用到的是栈block,但是栈上的block是临时的,在它的作用域结束后就被销毁,为了延长生命周期,在arc下系统会默认会copy到堆上,来延迟生命周期,这样可以在定义它的作用域外部使用。mrc下,需要手动进行copy
Block 自动截取变量Block外部的变量,可以被block捕获到内部进行使用,这里需要注意的是变量类型
全局变量/静态全局变量 ,block不需要捕获,因为全局变量和静态全局变量数据存储在全局数据区,Block内部直接使用
局部静态变量 捕获变量地址,所以外部变量修改后,通过地址访问到变量的值,也会跟着修改。
静态变量 捕获变量的值,是通过值传递的方式捕获到block内部,并且捕获的是变量的当前瞬时值,所以外部修改了变量,block内部的变量值不会发生改变,如果需要修改,需要通过__block来修饰,然后通过指针引用传递的方式在内部使用。
不行,和局部变量的生命周期有关系,因为局部变量在出大括号后就会被释放掉,这事我们在大括号外部调用这个Block,此时局部变量已经被释放了,block内部通过变量的指针访问变量,就会抛出异常。而静态局部变量的生命周期是和整个程序的生命周期一样,也就是在整个程序运行过程中不会释放,所以可以通过指针地址访问。
这是因为静态局部变量作用域只在大括号内,出了括号,它虽然存在,但外面已经访问不了,这时通过block执行函数只能通过捕获的方式。
不会,静态局部变量是语言设计的特性,行为可预测,是一种特殊的局部变量,具有局部作用域,存储在数据区.data(初始化)或.bss(未初始化),程序在运行期间只被分配一次内存(且占内存有限),生命周期有编译器自动管理,启动时初始化,结束时销毁,不会导致运行时的内存泄漏
首先明确内存泄漏的概念:程序在运行过程中,不断分配内存而没有适当的释放,导致内存逐渐减少的情况
Block 处理循环引用如果Block内部捕获了外部的strong(强引用)类型的引用对象,那么这个对象有强引用block,就会形成循环引用,会导致内存泄漏,因为参与循环引用的对象和block无法正常释放,长期下去会导致性能问题。
这时,就需要通过__weak关键字,进行对强引用对象进行弱引用,来打破你中有我,我中有你。
都是用来弱引用self,避免循环引用,__weak typeof(self) weakSelf = self;
@weakify(self) 是宏定义,预编译阶段展开就是 __weak typeof(self) weakSelf = self;,设计初衷就是为了更简洁更优雅。
Block 的内存管理首先围绕Block的三个核心点
Block 有几种类型?分别是什么?
__block相关的使用 请查看4.6__block
__block 的解释以及在 ARC 和 MRC 下有什么不同默认情况下,Block捕获的外部自动变量(局部变量)是值捕获,在Block内部是无法修改的。
__block是修饰符,主要用于解决上述问题,block捕获的局部变量,在block内部,可以进行修改。
原理:被__block修饰后的局部变量,编译器会把这个变量包装成一个结构体对象,底层其实一个结构体__Block_byref_a_0,内部有一个__forwarding指针,和当前变量的值等成员,无论Block和__block变量本身被复制到栈上还是堆上,都可以通过这个指针访问和修改值,这样就使从值传递变成了引用传递
在ARC和MRC下的不同
被__block修饰的对象是强引用,需要注意循环引用,常用的解决方案为使用__weak弱引用。
对__block修饰对象,不会对对象进行retain,避免循环引用
Block默认创建在栈上,为了延迟生命周期,需要copy到堆上。
在arc之前,手动管理内存,为了保持block的生命周期,开发者需要手动将栈上的block复制到堆上,通过copy关键字操作,如下
@property (copy, nonatomic) void (^block)(void);
在ARC之后,虽然编译器会自动进行copy操作,把block复制到堆上,为了和MRC下保持一致,避免在不同内存管理环境下切换的混淆,在ARC之后,还是推荐用copy关键字
在ARC下可以,在MRC下不可以。
Dispatch_block_t这个有没有用过?解释一下?dispatch_block_t是GCD中的一个类型定义,代表无参数,也没有返回值的代码里。
基本定义 typedef void (^dispatch_block_t)(void);
常见的使用场景:将任务放入队列后立即返回,不阻塞当前线程
dispatch_async(queue,^{
NSLog(@"在后台执行任务");
})
大家新年好!在过去的几年中,AI 始终占据着科技界最耀眼的 C 位。但站在 2026 年的起点回看,我发现一个显著的转折:从 2025 年末开始,人们对“万亿参数”或“榜单跑分”的狂热逐渐褪去,取而代之的是对 AI 工作流深耕细作的冷静与实战。
记录一下我最近注册个人开发者账号的经历,前后历时2周,换了3个个人身份,废了两台新买的测试机。西天取经,九九八十一难,各种问题,全靠猜,联系苹果也是模棱两可,等几天最后告诉你,你的账号废了,你的设备废了,请换新的!!难度堪比提审遇到账号调查。
先说结论:
1、(非常重要)注册过程中千万不要换设备,不要换账号。遇到任何问题联系苹果解决。如果你把账号换到别的设备上尝试注册,那你这个账号和这两台设备大概率会被风控。你的这个账号和这“两台”设备就废了,无法继续用于注册了。
2、填个人信息地址时要填对,最好填身份证上的地址(这是联系苹果时,苹果告诉我的)。我猜苹果会校验的地址,比如地址是否有效,是否完整,是否精确到门牌号等。瞎填或者填的不完整是过不了的,会提示“如果要继续注册,请联系苹果”。
3、一个身份证只能注册一个个人开发者账号。即使这个身份证之前注册流程没走完,也算使用过了。无法使用新的AppleID绑定这个身份证重新注册。只能用原来的AppleID继续绑定注册。
在和苹果技术支持沟通过程中,苹果工作人员提到官方文档中明确写明了,注册过程中不能更换设备。为此,我专门去查了一下官方文档。苹果官方文档,确实有提到“你必须在整个注册流程中使用相同的设备”,无论是注册个人账号还是公司账号。
![]()
下面是我这次注册的经历,当时没想到注册个人账号这么复杂,这么多坑。下面是没有任何心眼下的小白操作。
我这边有个新项目,需要注册一个新的个人开发者账号。我顺便买了两部新测试手机(手机1、手机2)。
1、小H注册
最开始找到同事小H注册。小H重新注册了一个新的AppleID,使用手机1,下载Developer App去注册,到填街道地址那一步卡住了,提交报错,具体什么错不记得了。联系苹果,苹果说小H的身份信息以前注册过开发者账号,请使用以前的账号继续注册或登录。
![]()
由于时间太久了小H也不记得以前是否注册过了,也不记得是哪个账号了。我们又联系苹果说我们不记得账号了,能否申请用新的账号注册。苹果回信说,“不可以,请回忆之前的账号或者使用公司其他人的身份注册”。我们把小H所有有可能的AppleID通过找回密码都试了一遍,都是AppleID不存在。放弃。
![]()
2、小L注册
我又找到同事小L。小L新注册了一个AppleID,使用手机1,用Developer App去注册。到了填街道页面,小L把街道和详细地址填的很简略,提交后报错“Action not allowed”。网上查资料说设备可能被风控了,可以换个设备尝试。于是,我换了另一个新买的测试手机2,结果账号登录后,“现在注册”按钮是置灰的,无法点击。(我们没有联系苹果,自己瞎摸索后)在苹果后台找到了网页注册入口,点进去上传了身份证。第二天,苹果邮件通知我们身份信息校验通过可以继续注册了。Developer App的注册按钮恢复正常了,还是在街道页面,街道和详细地址填的很简略,提交报“Action not allowed”。
联系苹果,苹果问我们注册过程中是否换过设备,我说换过。苹果告诉我,注册条款里有明确说明,注册的时候不能换设备。我说我不知道有这个条款,能不能帮我把设备重置一下。苹果说她没有权限,帮我连线资深顾问。资深顾问说她帮我联系中国的运营团队。等了2天,收到苹果邮件:“由于一个或多个原因,你无法完成Apple Developer Program的注册。我们目前无法继续处理你的注册申请。”再次放弃。
![]()
至此,我们的两个账号、两个身份、两台设备都废了!
3、小X注册
没办法只能再换人换设备了,这次长教训了,直接用小X的私人手机注册。用小X身份新注册一个AppleID(因为用私人AppleID后续不方便),在他私人手机上下载了Developer App进行注册。前面还算顺利,直到填写街道那一步,提交后弹窗提示“请联系苹果支持”。登录苹果开发者网站 - 联系我们 - 账号注册 - 电话沟通。苹果告诉我们街道地址填的有问题,最好填身份证上的地址,这样大概率是没问题的。
Developer App上改为填小X身份证地址后果然可以到下一步了,后续就交钱了。交完钱并不代表你注册成功了。第二天收到苹果的邮件,让我们上传身份证。通过邮件链接打开苹果网站,上传身份证正面照片。
上传后收到苹果回复邮件,说两个工作日审核完毕。实际上还挺快的半天就审核完了,下午我们就收到了开发者账号注册成功的邮件。
![]()
总算是注册成功了。
划重点:
最后,祝大家注册顺利,少踩坑。
如果你近期已经升级到 3.38.1 之后的版本,包括 3.38.5 ,你就有概率发现,打包提交 iOS 的包会出现 The binary is invalid 的相关错误,简单来说,就是App Store 拒绝了某个二进制文件,因为它包含了无效的内容。
![]()
那么这个内容是怎么来的?大概率是模拟器架构的 Framework 被错误地打包进了正式发布的 App ,具体原因还要提到最新版本增加的 Native Assets 功能。
Native Assets 的目标是让在 Flutter/Dart 包中集成 C、C++、Rust 或 Go 代码,可以像集成普通 Dart 包一样简单,也就是它允许 Dart 包定义如何构建和打包原生代码,开发者不需要深入了解每个平台的底层构建系统,也是 Dart FFI 未来的重要基建。
那它怎么导致了这次这个低级问题的出现?实际上这是一个构建脚本逻辑缺陷导致的“脏构建”问题,当 Flutter 构建依赖于 Native Assets(比如 sqlite3 等库)的 Plugin 时,这些原生资源会被编译并输出到 build/native_assets/$platform 目录(例如 build/native_assets/ios)。
因为在现有的构建脚本(xcode_backend.dart)在打包时,会简单粗暴地将 build/native_assets/ios 目录下的所有框架复制到最终的 App Bundle (Runner.app/Frameworks) ,例如:
sqlite3arm64ios_sim.framework)就会被生成并留在了 build/native_assets/ios 目录flutter clean 的情况下,直接运行了 Release 构建所以说,大厂也有大厂的草台。
当然,这个问题解决起来也很简单,就是发布前 flutter clean 清理一下,当然,如果你之前打过包了,那么 Xcode 的构建缓存也需要清理下,因为可能存在即使你通过 flutter clean 删除了 Flutter 的构建产物,但是 Xcode 可能仍然认为某些中间文件(Intermediate Build Files)存在可用。
比如 DerivedData 缓存
那么这么低级的问题,修复下也很简单,所以 sqlite3 的作者也提交了一个 #179251 ,简单来说就是,针对 Native Assets :
native_assets.json 文件native_assets.json 中列出的框架,忽略目录中残留的其他无关文件(如模拟器文件)这个修复其实很简单,但是在流程上,因为目前 PR 还缺少 integration test ,所以一直卡在了等待 Review 阶段,除非有人申请豁免,不然这个 PR 的合并还会继续卡着。
只能说,一代人有一代人的草台。
GetX 的状态管理并非单一方案,而是提供了三种核心状态管理模式,兼顾简洁性和灵活性,适配不同业务场景,其核心设计围绕「轻量、无侵入、响应式」展开。
适用于简单的状态更新场景(如按钮点击刷新文本、列表局部更新),基于手动触发重建实现,无响应式依赖,性能开销极低。
Controller 继承 GetxController,在控制器中维护状态变量,并提供状态更新方法。update() 方法手动标记状态变更,通知对应的 GetBuilder 进行组件重建。GetBuilder 关联指定控制器,仅在收到 update() 通知时刷新自身布局,不影响其他组件。// 1. 定义控制器
class CountController extends GetxController {
int count = 0;
void increment() {
count++;
update(); // 手动触发状态更新
}
}
// 2. 在UI中使用
Widget build(BuildContext context) {
// 无需手动初始化控制器,GetX自动管理生命周期
return GetBuilder<CountController>(
builder: (controller) {
return Column(
children: [
Text("计数:${controller.count}"),
ElevatedButton(
onPressed: controller.increment,
child: const Text("点击增加"),
),
],
);
},
);
}
适用于复杂状态依赖场景(如网络请求结果刷新、多组件共享状态、实时数据同步),基于Dart 扩展方法+观察者模式实现,无需手动调用 update(),状态变更自动触发 UI 重建。
.obs 扩展方法转化为「可观察对象(Observable)」,GetX 会监听该对象的所有变更。Obx(或 GetX)包裹需要响应状态变更的 UI 组件,Obx 作为观察者,订阅可观察对象的状态变化。Obx 组件,触发局部重建,无需全局刷新。GetxController(可直接使用全局变量,也可结合控制器管理)。Obx 包裹的组件会重建,性能优于全局状态刷新。// 1. 定义响应式状态(两种方式:直接使用 / 结合控制器)
// 方式1:直接使用全局响应式变量
var userName = "张三".obs;
// 方式2:结合控制器管理(推荐,便于状态统一维护)
class UserController extends GetxController {
var userAge = 20.obs;
var userInfo = UserModel(name: "李四", age: 25).obs; // 自定义对象
void updateAge() {
userAge.value++; // 注意:基本类型响应式变量需通过 .value 访问/修改
userInfo.update((info) { // 自定义对象批量更新
info?.age = userAge.value;
});
}
}
// 2. 在UI中使用 Obx 监听
Widget build(BuildContext context) {
final userController = Get.put(UserController()); // 初始化控制器(单例)
return Column(
children: [
Obx(() => Text("用户名:${userName.value}")),
Obx(() => Text("用户年龄:${userController.userAge.value}")),
Obx(() => Text("自定义对象年龄:${userController.userInfo.value.age}")),
ElevatedButton(
onPressed: () {
userName.value = "王五"; // 自动触发UI刷新
userController.updateAge(); // 控制器内状态变更,自动刷新
},
child: const Text("更新状态"),
),
],
);
}
GetX 状态管理的核心支撑能力,通过内置依赖注入(DI)容器管理控制器生命周期,无需手动创建和销毁控制器,实现状态的全局共享或局部共享。
Get.put(Controller()):将控制器实例存入 GetX 的 DI 容器,默认全局单例,可指定 tag 实现多实例,或指定 permanent: false 实现自动销毁。Get.find<Controller>():从 DI 容器中获取已存入的控制器实例,无需跨组件传递。GetxController 后,可重写 onInit()、onReady()、onClose() 方法,对应组件的初始化、就绪、销毁生命周期,自动执行。InheritedWidget 包裹(对比 Provider),无组件嵌套冗余。Get.create() 或在路由中传入 binding,实现路由级别的局部状态,路由销毁时自动销毁控制器。| 特性 | GetX | Provider | Bloc |
|---|---|---|---|
| 状态管理模式 | 支持3种模式(GetBuilder/Obx/DI),兼顾简单与复杂场景 | 单一响应式模式(基于InheritedWidget + ChangeNotifier) | 单一事件驱动模式(基于Stream + Event/State分离) |
| 核心依赖 | 无额外依赖(GetX自身集成) | 依赖 provider 包(基于Flutter原生组件) |
依赖 bloc/flutter_bloc 包(基于Dart Stream) |
| 组件侵入性 | 极低(无需顶层包裹,可按需使用Obx/GetBuilder) | 较高(需顶层包裹 MultiProvider,子组件需 Consumer/Provider.of) |
较高(需 BlocProvider 包裹,子组件需 BlocBuilder/BlocConsumer) |
| 状态传递方式 | 依赖注入(Get.find),无需跨组件层层传递 | 基于InheritedWidget,自上而下跨组件传递 | 依赖注入(BlocProvider)+ Stream监听,自上而下传递 |
| 事件处理方式 | 灵活(可直接调用方法,也可自定义事件) | 简单(调用ChangeNotifier的更新方法) | 严格(Event入参 → Bloc处理 → State输出,单向数据流) |
MultiProvider/MultiBlocProvider,解决了 Provider/Bloc 中多层嵌套导致的代码可读性差的问题。Get.find() 可在任意位置获取控制器,无需传递 BuildContext,尤其在工具类、网络请求类中使用便捷。flutter_route、get_it 实现路由和DI),减少库之间的兼容性问题。permanent: false),避免 Provider/Bloc 中手动管理控制器生命周期导致的内存泄漏问题。.obs 和 Obx 的使用,即可快速上手。bloc_test(单元测试)、flutter_bloc(Flutter 适配)、bloc_concurrency(并发处理)等周边库,在大型企业级项目中应用广泛。bloc_test 轻松编写单元测试,且能通过 DevTools 追踪状态流转过程,便于问题排查。InheritedWidget 和 ChangeNotifier 实现,开发者可轻松扩展 ChangeNotifier 或自定义 InheritedWidget,实现个性化需求。BlocDevTools,可实时监控 Event 发送、State 变更、Bloc 生命周期,便于调试复杂的状态流转问题。作为资深 Flutter 架构师,我会从分层视角(原生层 → Flutter 引擎层 → Dart 运行时层 → App 业务层)为你拆解 Flutter 项目启动的完整流程,涵盖核心步骤、关键机制和底层细节,帮你全面掌握启动原理。
Flutter 是跨平台框架,最终会打包为 iOS/Android 原生应用,启动流程首先从原生平台侧开始,这是 Flutter 启动的入口。
FlutterActivity(或 FlutterFragment,对应 Fragment 嵌入场景)FlutterActivity 初始化:继承自 AppCompatActivity,启动时先执行原生 Android 的 onCreate() 生命周期方法。FlutterEngine 相关配置(如 Dart 入口路径、初始化参数),若使用预加载引擎(提前初始化优化启动速度),会直接获取预创建的 FlutterEngine 实例;若未预加载,则现场创建 FlutterEngine。FlutterView:创建用于承载 Flutter UI 的原生 View(FlutterView),并将其挂载到 Android 布局层级中,作为 Flutter 渲染内容的显示载体。FlutterNativeView 建立原生 Android 与 Flutter 引擎的通信通道,传递初始化参数(如屏幕尺寸、系统主题、原生平台信息等)。FlutterViewController(对应 iOS 的视图控制器)FlutterViewController 初始化:执行 iOS 原生的 initWithNibName:bundle: 或 init 方法,完成控制器自身初始化。FlutterEngine 实例(同样支持预加载优化),配置 Dart 执行环境参数。FlutterViewController 的视图(view 属性)本质是 FlutterView,用于渲染 Flutter UI,完成视图层级挂载。FlutterMethodChannel/FlutterEventChannel 的底层初始化,完成 iOS 原生与 Flutter 引擎的双向通信准备。Flutter 引擎(C/C++ 实现,核心是 Skia 渲染引擎、Dart 虚拟机、排版引擎等)是 Flutter 的核心运行时,原生平台初始化完成后,会触发 Flutter 引擎的启动与初始化。
Flutter 引擎初始化是多组件协同启动的过程,核心组件包括:
FlutterView 的渲染缓冲区绑定,确保 Flutter 绘制的内容能显示在原生视图上。Flutter 引擎初始化完成后,会启动 Dart 虚拟机,并执行 Dart 代码的初始化流程,这是 Flutter 业务逻辑的入口。
.so(Android)/ App.framework(iOS)格式的 AOT 编译产物,Dart 虚拟机直接加载并执行该产物,无需即时编译,启动速度更快。main() 函数(Dart 入口)main() 函数,这是 Flutter 业务代码的第一个入口方法,典型代码如下:void main() {
// 可选:初始化全局配置(如网络拦截器、日志工具、依赖注入)
WidgetsFlutterBinding.ensureInitialized(); // 关键:初始化 Flutter 核心绑定
runApp(const MyApp()); // 启动 Flutter 应用
}
WidgetsFlutterBinding.ensureInitialized() 是 Flutter 核心绑定初始化方法,若省略,runApp() 内部会自动调用,其作用是初始化 Flutter 框架的核心服务(如渲染绑定、手势绑定、生命周期绑定等)。main() 函数中调用 runApp() 后,进入 Flutter 框架初始化和 UI 首次渲染流程,这是 Flutter UI 显示的核心步骤。
WidgetsFlutterBinding 是 Flutter 框架的核心绑定类,它整合了 7 大核心绑定,确保 Flutter 框架正常工作:
GestureBinding:手势识别与事件分发绑定。ServicesBinding:平台消息通信绑定(如 MethodChannel 通信)。SchedulerBinding:任务调度与帧回调绑定(控制 UI 刷新帧率,默认 60fps)。PaintingBinding:绘制相关绑定(如图片缓存、字体加载)。SemanticsBinding:语义化绑定(支持无障碍访问)。RenderBinding:渲染管线绑定(布局、绘制、合成)。WidgetsBinding:组件框架绑定(组件构建、状态管理、路由管理)。runApp(Widget app) 核心逻辑runApp() 是 Flutter UI 启动的关键方法,核心操作如下:
MyApp)设置为 Flutter 框架的根组件(rootWidget),建立组件树的顶层节点。SchedulerBinding 向 Flutter 引擎发送「首次绘制帧」的调度请求,引擎接收到请求后,启动 UI 线程的布局与绘制流程。Flutter 首次渲染遵循「构建 → 布局 → 绘制 → 合成 → 渲染」的流水线:
MyApp 开始),执行每个 Widget 的 build() 方法,生成「元素树(Element Tree)」(Widget 是配置模板,Element 是实际渲染实例)。RenderObject)执行布局计算,确定每个组件的大小、位置(如 RenderFlex 处理 Flex 布局,RenderText 处理文本排版),生成「布局树(Layout Tree)」。FlutterView 的渲染缓冲区,最终在屏幕上显示 Flutter UI。onFirstFrame 回调当 Flutter 首次帧渲染完成后,会触发 WidgetsBinding 的 onFirstFrame 回调(可监听该回调统计启动耗时),此时用户可以看到 Flutter 应用的首屏 UI,标志着 Flutter 项目启动流程全部完成。
Flutter 调试模式(Debug)和发布模式(Release)的启动流程存在核心差异,直接影响启动速度:
| 对比维度 | Debug 模式 | Release 模式 |
|---|---|---|
| Dart 执行模式 | JIT(即时编译) | AOT(提前编译) |
| 产物加载 | 加载 Dart 快照,支持热重载 | 加载 AOT 编译产物(.so/Framework),直接执行 |
| 引擎优化 | 关闭部分渲染优化、线程优化 | 开启全量优化(线程合并、绘制优化、内存优化) |
| 启动速度 | 较慢(JIT 初始化 + 快照加载) | 较快(AOT 产物直接执行,无编译开销) |
| 额外功能 | 支持 Hot Reload、DevTools 调试 | 无调试功能,体积更小、性能更优 |
Flutter 项目启动是一个跨平台、分层级、多线程协同的复杂流程,核心步骤可概括为 4 个关键阶段:
main() 函数;runApp(),通过「构建-布局-绘制-合成-渲染」管线完成首屏显示。理解该流程有助于你优化 Flutter 项目启动速度(如预加载 Flutter 引擎、延迟初始化非核心业务、优化首屏 Widget 构建),以及排查启动阶段的跨平台兼容问题。
![]()
这是一个雨夜,霓虹灯的光晕在脏兮兮的窗玻璃上晕开,像极了那个该死的 View Hierarchy 渲染不出高斯模糊的样子。
在新上海的地下避难所里,老王(Old Wang)吐出一口合成烟雾,盯着全息屏幕上不断报错的终端。作为人类反抗军里仅存的几位「精通 Apple 软件开发」的工程师之一,他负责给 AI 霸主「智核(The Core)」生成的垃圾代码擦屁股。
![]()
门被撞开了,年轻的女黑客莉亚(Liya)气喘吁吁地冲进来,手里攥着一块存满代码的神经晶片。“老王!救命!‘智核’生成的 SwiftUI 代码在 iOS 26 上又崩了!反抗军的通讯 App 根本跑不起来!”
老王冷笑一声,掐灭了烟头。“我就知道。那些被捧上神坛的 LLM(大型语言模型),不管是 Claude、Codex 还是 Gemini,写起 Python 来是把好手,但一碰到 Swift,就像是穿着溜冰鞋走钢丝——步步惊心。”
在本篇博文中,您将学到如下内容:
他把晶片插入接口,全息投影在空中展开。“坐下,莉亚。今天我就给你上一课,让你看看所谓的‘人工智能’是如何在 Swift 的并发地狱和快速迭代中翻车的。”
![]()
老王指着屏幕上乱成一锅粥的代码说道:“这不怪它们。Swift 和 SwiftUI 的进化速度比变异病毒还快。再加上 Python 和 JavaScript 的训练数据浩如烟海,而 Swift 的高质量语料相对较少,AI 常常会产生幻觉。更别提 Swift 的 Concurrency(并发) 模型,连人类专家都头秃,更别说这些只会概率预测的傻大个了。”
![]()
“听着,莉亚,”老王严肃地说,“要想在 iOS 18 甚至更高版本的废土上生存,你必须学会识别这些‘智障操作’。我们不谈哲学,只谈生存。以下就是我从死人堆里总结出来的代码排雷指南。”
💀 AI 的烂代码: foregroundColor()
✨ 老王的修正: foregroundStyle()
“看这里,”老王指着一行代码,“AI 还在用 foregroundColor()。这就像是还在用黑火药做炸弹。虽然字数一样,但前者已经是个行将就木的Deprecated API。把它换成 foregroundStyle()!后者才是未来,它支持渐变(Gradients)等高级特性。别让你的 UI 看起来像上个世纪的产物。”
![]()
💀 AI 的烂代码: cornerRadius()
✨ 老王的修正: clipShape(.rect(cornerRadius:))
“又是一个老古董。cornerRadius() 早就该进博物馆了。现在的标准是使用 clipShape(.rect(cornerRadius:))。为什么?因为前者是傻瓜式圆角,后者能让你通过 uneven rounded rectangles(不规则圆角矩形)玩出花来。在这个看脸的世界,细节决定成败。”
💀 AI 的烂代码: onChange(of: value) { ... } (单参数版本)
✨ 老王的修正: onChange(of: value) { oldValue, newValue in ... }
老王皱起眉头:“这个 onChange 修改器,AI 经常只给一个参数闭包。这在旧版本是‘不安全’的,现在已经被标记为弃用。要么不传参,要么接受两个参数(新旧值)。别搞得不清不楚的,容易出人命。”
![]()
💀 AI 的烂代码: tabItem()
✨ 老王的修正: 新的 Tab API
“如果看到老旧的 tabItem(),立刻把它换成新的 Tab API。这不仅仅是为了所谓的‘类型安全(Type-safe)’,更是为了适配未来——比如那个传闻中的 iOS 26 搜索标签页设计。我们要领先‘智核’一步,懂吗?”
💀 AI 的烂代码: 滥用 onTapGesture()
✨ 老王的修正: 使用真正的 Button
“AI 似乎觉得万物皆可 onTapGesture()。大错特错!除非你需要知道点击的具体坐标或者点击次数,否则统统给我换成标准的 Button。这不仅是为了让 VoiceOver(旁白)用户能活下去,也是为了让 visionOS 上的眼球追踪能正常工作。别做一个对残障人士不友好的混蛋。”
💀 AI 的烂代码: ObservableObject
✨ 老王的修正: @Observable 宏
“莉亚,看着我的眼睛。除非你对 Combine 框架有什么特殊的各种癖好,否则把所有的 ObservableObject 都扔进焚化炉,换成 @Observable 宏。代码更少,速度更快,这就好比从燃油车换成了核动力战车。”
![]()
💀 AI 的烂代码: SwiftData 模型中的 @Attribute(.unique)
✨ 老王的修正: 小心使用!
“这是一个隐蔽的雷区。如果在 SwiftData 模型定义里看到 @Attribute(.unique),你要警惕——这玩意儿跟 CloudKit 八字不合。别到时候数据同步失败,你还在那儿傻乎乎地查网络连接。”
💀 AI 的烂代码: 将视图拆分为「计算属性(Computed Properties)」 ✨ 老王的修正: 拆分为独立的 SwiftUI Views
“为了图省事,AI 喜欢把大段的 UI 代码塞进计算属性里。这是尸位素餐!尤其是在使用 @Observable 时,计算属性无法享受智能视图失效(View Invalidation)的优化。把它们拆分成独立的 SwiftUI 结构体!虽然麻烦点,但为了那 60fps 的流畅度,值得。”
💀 AI 的烂代码: .font(.system(size: 14))
✨ 老王的修正: Dynamic Type (动态字体)
“有些 LLM(尤其是那个叫 Claude 的家伙)简直就是字体界的独裁者,总喜欢强行指定 .font(.system(size: ...))。给我搜出这些毒瘤,全部换成 Dynamic Type。如果是 iOS 26+,你可以用 .font(.body.scaled(by: 1.5))。记住,用户可能眼花,别让他们看瞎了。”
![]()
💀 AI 的烂代码: 列表里的内联 NavigationLink
✨ 老王的修正: navigationDestination(for:)
“在 List 里直接写 NavigationLink 的目标地址?那是原始人的做法。现在的文明人使用 navigationDestination(for:)。解耦!解耦懂不懂?别把地图画在脚底板上。”
老王喝了一口已经凉透的咖啡,继续在这堆赛博垃圾中挖掘。
💀 AI 的烂代码: 用 Label 做按钮内容
✨ 老王的修正: 内联 API Button("Title", systemImage: "plus", action: ...)
“期待看到 AI 用 Label 甚至纯 Image 来做按钮内容吧——这对 VoiceOver 用户来说简直是灾难。用新的内联 API:Button("Tap me", systemImage: "plus", action: whatever)。简单,粗暴,有效。”
💀 AI 的烂代码: ForEach(Array(x.enumerated()), ...)
✨ 老王的修正: ForEach(x.enumerated(), ...)
“看到这个 Array(x.enumerated()) 了吗?这就是脱裤子放屁。直接用 ForEach(x.enumerated(), ...) 就行了。省点内存吧,虽然现在的内存不值钱,但程序员的尊严值钱。”
![]()
💀 AI 的烂代码: 冗长的文件路径查找代码
✨ 老王的修正: URL.documentsDirectory
“那些又臭又长的查找 Document 目录的代码,统统删掉。换成 URL.documentsDirectory。一行代码能解决的事,绝不写十行。”
💀 AI 的烂代码: NavigationView
✨ 老王的修正: NavigationStack
“NavigationView 已经死了,有事烧纸。除非你要支持 iOS 15 那个上古版本,否则全部换成 NavigationStack。”
💀 AI 的烂代码: Task.sleep(nanoseconds:)
✨ 老王的修正: Task.sleep(for: .seconds(1))
“‘智核’ 似乎很喜欢纳秒,可能它觉得自己算得快。但你要用 Task.sleep(for:),配合 .seconds(1) 这种人类能读懂的单位。别再像个僵尸一样数纳秒了。”
![]()
💀 AI 的烂代码: C 风格格式化 String(format: "%.2f", ...)
✨ 老王的修正: Swift 原生格式化 .formatted()
“我知道 C 风格的字符串格式化很经典,但它不安全。把它换成 Swift 原生的 Text(abs(change), format: .number.precision(.fractionLength(2)))。虽然写起来长一点,但它像穿了防弹衣一样安全。”
💀 AI 的烂代码: 单个文件塞入大量类型 ✨ 老王的修正: 拆分文件
“AI 喜欢把几十个 struct 和 class 塞进一个文件里,这简直是编译时间毁灭者。拆开它们!除非你想在编译的时候有时间去煮个满汉全席。”
💀 AI 的烂代码: UIGraphicsImageRenderer
✨ 老王的修正: ImageRenderer
“如果你在渲染 SwiftUI 视图,别再用 UIKit 时代的 UIGraphicsImageRenderer 了。拥抱 ImageRenderer 吧,这是它的主场。”
![]()
💀 AI 的烂代码: 滥用 fontWeight()
✨ 老王的修正: 区分 bold() 和 fontWeight(.bold)
“三大 AI 巨头都喜欢滥用 fontWeight()。记住,fontWeight(.bold) 和 bold() 渲染出来的结果未必一样。这就像‘微胖’和‘壮实’的区别,微妙但重要。”
💀 AI 的烂代码: DispatchQueue.main.async
✨ 老王的修正: 现代并发模型
“一旦 AI 遇到并发问题,它就会像受惊的鸵鸟一样把头埋进 DispatchQueue.main.async 里。这是不可原谅的懒惰!那是旧时代的创可贴,现在的我们有更优雅的 Actor 模型。”
💀 AI 的烂代码: 到处加 @MainActor
✨ 老王的修正: 默认开启
“如果你在写新 App,Main Actor 隔离通常是默认开启的。不用像贴符咒一样到处贴 @MainActor。”
![]()
💀 AI 的烂代码: GeometryReader + 固定 Frame
✨ 老王的修正: visualEffect() 或 containerRelativeFrame()
“最后,也是最可怕的——GeometryReader。天哪,AI 对这玩意儿简直是真爱,还喜欢配合固定尺寸的 Frame 使用。这是布局界的核武器,一炸毁所有。试着用 visualEffect() 或者 containerRelativeFrame() 来代替。别做那个破坏布局流的罪人。”
老王敲下最后一个回车键,全息屏幕上的红色报错瞬间变成了令人愉悦的绿色构建成功提示。
// Human-verified Code
// Status: Compiling... Success.
// Fixed by: The Refiners (Old Wang & Liya)
“搞定。” 老王瘫坐在椅子上,听着窗外雨声渐大。
![]()
莉亚看着完美运行的 App,眼中闪烁着崇拜的光芒:“老王,你简直是神!既然我们能修复这些代码,为什么 AI 还是会不断地生成这种垃圾?”
老王点燃了最后一支烟,看着烟雾在霓虹灯下缭绕。“因为 AI 会产生幻觉(Hallucinations)。它们会编造出看起来很美、名字很像样,但实际上根本不存在的 API。这就像是在数字世界里见鬼了一样。”
![]()
他转过头,意味深长地看着莉亚:“对此,我也无能为力。我只能修补已知的错误,却无法预测未知的疯狂。”
“那么,”老王把目光投向了屏幕前的你——第四面墙之外的观察者,“轮到你了。在你的赛博探险中,通常会在 AI 生成的代码里发现什么‘惊喜’?”
![]()
如果你还活着,请在评论区告诉我们。毕竟,在这场人机大战中,知识是我们唯一的武器。
那么,感谢观赏,再会啦!8-)
![]()
摘要:当动态数组的随性(Array)与元组的刻板(Tuple)陷入两难,高性能的内联数组(InlineArray)横空出世。Swift 6.2 引入的 SE-0453 就像是武学中的“万剑归宗”,旨在以固定大小的内存布局,解决性能与灵活性的鱼和熊掌不可兼得之困。
琅嬛福地,天山童姥遗留的虚拟数据中心。
这里是存储着天下所有数据结构秘籍的宝库。大熊猫侯佩穿梭在巨大的全息卷轴之间,背景音乐是《天龙八部》的BGM,他一边走一边摸了摸头顶——黑毛油光锃亮,头绝对不秃,安全感十足。
他之所以来这,是因为上一回任务结束后,他仍对竹笋的存储问题耿耿于怀。
“我那四根‘镇山之宝’级竹笋,必须以最快的速度取用!”侯佩对着一卷写着 Array 的秘籍大声嚷道,“用普通的 Array,虽然方便,但那动态分配内存的方式,让我每次取笋都感觉像是在丐帮的袋子里掏东西,随性得很,但太慢了!”
“若追求极致速度,何不用 Tuple(元组)?”
![]()
一个清冷的声音传来。侯佩循声望去,只见一位容貌比数据流还精致的少女站在光影之中,她皓齿明眸,手中正拿着一本《九阴真经》的代码版本,正是王语嫣。
王语嫣,熟读天下武学秘籍,对各种数据结构了如指掌。她的爱好就是整理和分类这些代码秘籍,特点是理论知识丰富到可以开宗立派,但从未亲手实践(编写)过一行代码。
“王姑娘!”侯佩的眼睛瞬间变成了心形(花痴属性发作),“元组是快,它内存连续且固定,但您看,我如果要用下标循环遍历我的四根竹笋,myTuple.0,myTuple.1……这写法简直是望洋兴叹,既不优雅,又不支持循环!”
在本次冒险中,您将学到如下内容:
王语嫣轻轻叹了一口气:“是啊,鱼和熊掌不可兼得。内存布局(Performance)和下标访问(Usability),历来是程序员江湖的千年难题。”
![]()
就在侯佩和王语嫣陷入技术哲学的死循环时,一道新的全息卷轴从天而降,正是 SE-0453 秘籍。
“快看,这是最新的‘混血’数据结构,”侯佩激动地喊道,“它叫 InlineArray(内联数组),它把元组的‘固定大小’与数组的‘自然下标’完美地融合了!”
InlineArray 最核心的奥义在于:它将固定数量的元素直接存储在结构体内部,没有动态分配的开销,从而实现了结构体级别的内存连续性和媲美 C 语言数组的存取速度。
💡 前置条件(SE-0452): 要实现这种固定大小的泛型(Generic),Swift 6.2 还必须引入另一个重要的前提:SE-0452:Integer Generic Parameters(整数泛型参数)。这使得我们可以用一个整数值来约束泛型类型的大小,比如
InlineArray<4, String>中的4,这在以前的 Swift 版本中是无法想象的。
![]()
王语嫣作为理论大师,立刻解析了这段秘籍。
创建 InlineArray 有两种法门:
我们可以像使用泛型一样,明确告知编译器:“我要一个固定大小为 4 的 String 类型数组。”
// 明确告诉编译器:我要一个固定大小为 4 的 String 数组
var names1: InlineArray<4, String> = ["Moon", "Mercury", "Mars", "Tuxedo Mask"]
![]()
侯佩这种懒人当然更喜欢让编译器自己推断(Type Inference)大小。只要传入的元素数量和类型固定,编译器就能自动搞定。
// 编译器会根据传入的 4 个 String,自动推断出它是 InlineArray<4, String>
var names2: InlineArray = ["Moon", "Mercury", "Mars", "Tuxedo Mask"]
“太完美了!”侯佩赞叹道,“这就像是把我的四根竹笋严丝合缝地放进了四个精确尺寸的格子,一劳永逸,再也不用担心内存跳来跳去了。”
虽然它内存布局像元组,但使用起来却和数组一样,支持直观的下标读写:
// 读取:就像普通的数组一样
print(names1[0]) // 输出: Moon
// 写入:轻松修改特定位置的元素
names1[2] = "Jupiter" // 火星变木星,改写数据,毫不费力
![]()
王语嫣很快指出了这种“神功”的限制:“侯大哥,此功法虽然内力雄厚(性能卓越),但限制也多。既然是固定大小,那么它就失去了数组的动态伸缩性。”
![]()
侯佩一听,赶紧问道:“那是不是不能再多塞一根竹笋进去了?”
王语嫣点头:“正是。InlineArray 没有 append() 或 remove(at:) 方法。 它的容量在诞生之初就已是天数,无法更改。”
更让人头疼的是它的“不入流”限制:
🚨 技术哲学:
InlineArray不兼容传统的Sequence(序列)和Collection(集合)协议。
“为什么?”侯佩不解,“它不是数组吗?”
![]()
“因为它的设计目标是极致性能和编译时确定性。”王语嫣解释道,“为了避免遵循这些协议可能带来的抽象层开销,它选择‘自绝经脉’。如果你想遍历它,你必须通过它的 indices 属性,配合下标访问来实现。”
// 侯佩:虽然不方便,但为了性能,忍了!
for i in names1.indices {
// 必须通过索引 i 来访问,不能直接用 for element in names1
print("Hello, \(names1[i])!")
}
侯佩总结道:“这就像是说,虽然它是武林高手,但它拒绝参加武林大会(不遵循 Collection 协议),如果你想请教它,必须先拿到它的拜帖(indices)才行。”
![]()
“哎呀,这世道,连数据结构都得看颜值和出身。”侯佩叹了口气,把竹笋收进了虚拟的 InlineArray 容器里,感觉身轻如燕,连走路都带风了。
“不过话说回来,”侯佩看向王语嫣,“我还是觉得这种硬编码的语法有点不够‘熊猫化’(不够懒)。听说社区里有人想搞个更直观的语法?”
![]()
王语嫣提起了一件江湖轶事(SE-0483):
插曲:夭折的提案: “有一个叫做 SE-0483 的提议,想要引入类似
var names: [5 x String] = .init(repeating: "Anonymous")的简洁语法,来表示一个固定包含 5 个 String 的数组。但由于反馈意见认为它过于突兀且不够 Swift 风格,目前已被‘打回重修’。”
侯佩嘿嘿一笑:“果然,任何新秘籍的推广,都会遇到‘保守派’的阻力。不过,能用,速度快,头不秃,对我来说就够了。”
![]()
就在侯佩沉浸在高性能竹笋容器的喜悦中时,王语嫣突然脸色大变。
“侯大哥!我刚才在整理慕容复留下的数据卷轴时,发现了一个惊天的秘密!”
她指着屏幕上的一段文本,那是一篇关于“兴复大燕”的宏大计划书。
![]()
“我想用 Regex(正则表达式) 查找卷轴中所有提到他名字的地方。但是,我不想要匹配到那些他用来伪装自己身份的称呼,比如‘公冶乾’、‘包不同’这些名字后面的‘慕容复’。”王语嫣急道,“我只想匹配到那些,前面紧跟着‘我的挚爱’这四个字的‘慕容复’!”
侯佩挠了挠头:“你的意思是,你想找到一个模式,但这个模式必须满足它前面有一个特定的前置条件,而这个前置条件本身,又不被纳入匹配结果?”
![]()
“对!”王语嫣焦急万分,“我的 Regex 功夫只能‘向前看’(Lookahead),却无法完美地**‘向后看’**,我不能确定文本中这三个字前面是不是真的有‘我的挚爱’。”
侯佩望着卷轴深处那段充满秘密的代码,神秘地一笑:“王姑娘,你不用再对着旧秘籍望洋兴叹了。下一章,Swift 6.2 就要教我们一招绝顶的侦查武功:Regex lookbehind assertions(正则表达式向后查找断言)!”
![]()
(欲知后事如何,且看下回分解:Regex lookbehind assertions —— 如何在不匹配前文的情况下,精确判断前文的存在性,找到王语嫣真正的“挚爱”。)
Paul Graham 在 What to do 中探讨了一个看似简单却极具深意的问题:人的一生应该做什么?除了「帮助他人」和「爱护世界」这两个显而易见的道德责任外,他提出了第三个关键点:创造美好的新事物(Make good new things)。
读到这段话时,我马上想到的是 Make Something Wonderful 这本书。某种程度上,两者共享了同一个核心理念:「创造美好」不应只是一次性的行为,而是一种值得毕生追求的生活方式。
Steve Jobs 曾这样描述 Make Something Wonderful 这句话背后的动机:
There’s lots of ways to be as a person, and some people express their deep appreciation in different ways, but one of the ways that I believe people express their appreciation to the rest of humanity is to make something wonderful and put it out there.
And you never meet the people, you never shake their hands, you never hear their story or tell yours, but somehow, in the act of making something with a great deal of care and love, something is transmitted there.
And it’s a way of expressing to the rest of our species our deep appreciation. So, we need to be true to who we are and remember what’s really important to us. That’s what’s going to keep Apple Apple: is if we keep us us.
创造的产物不限形式,它可以是宏大的牛顿力学定律,也可以是一把精致的维京椅。文章也是一种常见的创作。在 AI 时代,「是否还有写博客的必要」成为了备受热议的话题。博客的独特价值在于其内容的多样性——它可以是一篇游记、一篇散文、一次技术折腾的记录、一本好书的读后感,甚至是稍纵即逝的灵感碎片。个体独特的经历与细腻的感受,是 AI 无法替代的。或者,也可以像 Paul Graham 或 Gwern 那样,通过写作对某一话题进行深度挖掘,以确保自己真正掌握了真理。
除了写作,还可以开发 App。AI Coding Assistants 的崛起极大地降低了编程门槛,普通人只需花时间熟悉这些助手,便能在短时间内构建出像模像样的产品。而随着各类 AI 图像生成工具(如 Nano Banana Pro 等)的出现,绘画创作也不再遥不可及。这正是 AI 时代对个体的最大赋能:曾经专属于专业人士的领域,如今已向所有人敞开大门。
但我们为什么非得「做点什么」呢?躺在沙发上刷剧岂不更舒服?的确,消费内容看起来更惬意,但「整天躺着刷剧」与「辛苦创作一天后再躺下刷剧」,这两种体验有着天壤之别。那种完成了一件作品后内心产生的通透感与充实感,是任何单纯的消费行为都无法比拟的。
从价值投资的角度看,「创作」是一项既有安全边际,又具备潜在高回报的行为。假设你花一周时间做了一个小工具,即便最后无人问津,你的安全边际依然存在:你在过程中学到了新知识、巩固了旧体系,解决了自己的痛点,收获了亲手造物的成就感。而潜在回报则是巨大的:它可能真的帮助了他人,改善了某些人的生活,甚至让你结识了同频的伙伴。
要让创作带来巨大的回报,有一个核心要点:高标准。在 AI 时代,我们面临一个残酷的现实:Average is over(平庸时代的终结)。 因为 AI 让生产 60 分的「合格品」变得几乎零成本,平庸的内容将迅速泛滥成灾。
在市场蓝海期,产品或许可以靠便宜、新奇或仅仅是「能用(Just Works)」来取胜;但一旦门槛被 AI 踏平,大量玩家涌入,最终能脱颖而出的,唯有那些超越了「平均线」、不仅能用而且好用的精品。因此,「高标准」不仅是竞争优势,更是生存线。
要达到高标准,高质量的 Input(输入) 必不可少。如果连什么是「好产品」都看不出来,就更不可能做出来。因此,我们需要花时间去多多研究优秀的 Input。当高标准成为习惯,你会发现市面上有太多产品不尽如人意。带着这把「高标准」的放大镜,你能找到无数瑕疵和痛点,而这些,就可以是创作的起点。
阻碍创作的因素通常有三:好奇心匮乏、完美主义倾向、精力被分散。其中最大的阻碍往往是好奇心的缺失。好奇心可分为两类:感知性好奇心(关注 What,如八卦新闻)和认知性好奇心(关注 How 和 Why,如探究事件背后的逻辑与影响)。Hard work is the magnitude of the vector and curiosity is the direction.(努力是矢量的长度,而好奇心是方向)。认知性好奇心,可以为创作指引方向,高标准则决定了矢量的长度。
此外,创作还有一个美好的「副作用」:它能让你更专注于当下,而不是被纷繁的新闻和社交网络裹挟。每一次创作的产出,都像是给人生这条绳索打了一个结。当你回望这些作品时,当时的记忆与点滴便会瞬间涌上心头,让你的人生有迹可循。
最后说说 AI。如果 GPT-3.5 的发布是航空史上的「莱特兄弟时刻」,那么随之而来的 AI 浪潮,则让飞机成为了大众的交通工具。它的操作逻辑与传统的地面交通截然不同,能力也更强悍。要发挥它的最大价值,你需要熟悉与它交互的最佳实践,找到属于你的那架「飞机」,然后让它载着你,飞往以前想都不敢想的地方。