普通视图

发现新文章,点击刷新页面。
今天 — 2026年1月7日iOS

告别“可移植汇编”:我已让 Swift 在 MCU 上运行七年

作者 Fatbobman
2026年1月7日 22:12

在苹果官方正式开启嵌入式支持之前,Andy Liu 和他的 MadMachine 团队就已经在这个领域深耕多年。他们认为,在功能日益复杂的开发场景中,Swift 的现代语言特性将展现出巨大的优势。在数年前便选择了一套与社区主流不同的理念与技术路线。 我邀请 Andy 分享他们过去几年在 Swift 嵌入式开发中的实战经历分享出来。这既是一份宝贵的历史记录,也希望能为社区提供一个不一样的思考维度。

iOS应用(App)生命周期、视图控制器(UIViewController)生命周期和视图(UIView)生命周期

2026年1月7日 10:32

清晰的理解它们能帮你更好地管理应用状态和资源。

一、iOS 应用(App)生命周期

应用生命周期描述了 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")

        // 最终的资源清理、数据保存

    }

}

关键说明

  • 启动流程:用户点击 App 图标 → 系统加载可执行文件 → 调用didFinishLaunchingWithOptions → 显示界面 → 进入活跃状态。
  • 后台与前台切换:活跃 → 失活(WillResignActive)→ 后台(DidEnterBackground)→ 前台(WillEnterForeground)→ 活跃(DidBecomeActive)。
  • 终止:后台状态下系统回收内存,触发applicationWillTerminate(若 App 在前台,直接终止,不触发此方法)。

二、UIViewController 生命周期

视图控制器是管理 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")
        // 最终的资源释放(如移除监听、取消网络请求)
    }
}

关键说明

  • 核心流程:初始化 → 加载视图 → 视图加载完成 → 布局子视图 → 即将显示 → 已显示 → 即将消失 → 已消失 → 销毁。
  • viewDidLoad:只执行一次,适合做一次性初始化;viewWillAppear/viewDidAppear:每次显示都执行,适合刷新动态数据。
  • 内存警告didReceiveMemoryWarning中需主动释放非必要资源,避免 App 被系统杀死。
  • deinit:只有当 VC 的引用计数为 0 时才会触发,需确保无循环引用(如闭包未捕获 self 为 weak/unowned)。

三、UIView 生命周期

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相关资源(如移除子视图、取消动画)
    }
}

关键说明

  • layoutSubviews:最常用的方法,每次 View 的尺寸变化都会触发,适合手动调整子视图布局(若使用 AutoLayout,系统会自动处理,无需重写)。
  • draw(_:) :仅在需要手动绘制内容时重写,避免在其中做耗时操作(会影响渲染性能);调用setNeedsDisplay()可触发重新绘制。
  • Window 关联:View 只有添加到UIWindow(应用的主窗口)后,才会被渲染并显示在屏幕上;didMoveToWindow是 View 真正 “可见” 的标志。
  • 销毁:当 View 从父视图移除且无强引用时,deinit触发,需确保子视图也被正确释放。

总结

  1. 应用生命周期:全局层面,管理 App 从启动到终止的状态,核心是UIApplicationDelegate的代理方法,关注前台 / 后台切换和资源保存。
  2. 视图控制器生命周期:页面层面,核心是viewDidLoad(一次性初始化)、viewWillAppear(每次显示刷新)、deinit(资源释放),是业务逻辑的主要载体。
  3. 视图生命周期:控件层面,依附于 VC,核心是layoutSubviews(布局)和draw(_:)(绘制),关注控件尺寸调整和视觉渲染。

三者的关联:App 启动后创建根 VC → VC 创建并加载 View → View 添加到 Window 显示 → App 进入前台活跃状态;App 进入后台时,VC 的 View 会被隐藏,资源可按需释放。

Swift 6.2 列传(第十六篇):阿朱的“易容术”与阿紫的“毒药测试”

2026年1月7日 08:57

在这里插入图片描述

摘要:在 Swift 6.2 的并发江湖中,我们迎来了两项截然不同的新功能:一项是关于极度精妙的文本侦查术(SE-0448 正则表达式向后查找断言),另一项则是关于面对应用崩溃时的从容不迫(ST-0008 退出测试)。大熊猫侯佩将与阿朱、阿紫这对姐妹花,共同演绎这冰火两重天的技术奥秘。

0️⃣ 🐼 序章:雁门关前的技术难题

雁门关,数据流与现实交错的虚拟战场。

大熊猫侯佩正对着一块全息屏幕发呆,屏幕上是无数条交易记录,他正努力寻找他藏匿的竹笋基金。他用手摸了摸自己的头顶,确定了头绝对没有秃之后,才稍微心安。

他身旁站着一位温柔婉约的绿衣女子,正是阿朱。阿朱以易容术闻名江湖,擅长在纷乱的文本中寻找和伪装信息,她的心愿是天下太平,性格宽厚善良。

在这里插入图片描述

“侯大哥,”阿朱指着一堆交易记录说,“我想找到所有以 金币符号 $ 结算的价格,但我只想匹配出后面的数字,而不要把那个 $符号也匹配进去。我要用这些数字去结算账单,符号留着下次易容用。”

在本次大模型中,您将学到如下内容:

  • 0️⃣ 🐼 序章:雁门关前的技术难题
  • 1️⃣ 🔎 阿朱的易容术:Regex lookbehind assertions
  • 2️⃣ 🧪 阿紫的毒药测试:Exit Tests 的“置之死地” (ST-0008)
    • #expect(processExitsWith:) 的安全结界
  • 3️⃣ 🎁 尾声:崩溃现场的“遗物”与下一章的伏笔

侯佩为难地挠了挠头:“以前的 Regex(正则表达式),要么就全部匹配进去,要么就得用复杂的捕获组再分离。要想实现‘只看前因,不取前因’,简直难如登天啊!”

在这里插入图片描述


1️⃣ 🔎 阿朱的易容术:Regex lookbehind assertions

阿朱的问题,正是 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

“看到了吗,阿朱姑娘?”侯佩得意洋洋,“这个 (?<=$) 就是你的易容术精髓。它帮你确认了身份(前面必须是金币),但在匹配结果中,它却完美地把自己隐藏了起来,片叶不沾身!

在这里插入图片描述

阿朱喜出望外:“太妙了!这样我就可以精准地提取数据,再也不用担心多余的符号来捣乱了!”

2️⃣ 🧪 阿紫的毒药测试:Exit Tests 的“置之死地” (ST-0008)

就在侯佩和阿朱沉浸在正则表达式的精妙中时,一阵刺鼻的硫磺味突然袭来!

另一位身着紫衣的少女,阿紫,从烟雾中走了出来。阿紫的特点是心狠手辣,喜欢用毒,而且热衷于测试“极限”

在这里插入图片描述

“姐姐,你在玩这么幼稚的游戏?”阿紫轻蔑一笑,“我的任务才刺激。我要测试我最新的**‘鹤顶红’代码**,确保它能让整个应用彻底崩溃并退出!”

侯佩吓得连退三步:“你要测试崩溃?阿紫姑娘,你知道这意味着什么吗?应用崩溃,测试系统也会跟着崩溃啊!这叫一锅端!”

在这里插入图片描述

阿紫的测试目标,正是那些会触发 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,让我们能优雅地‘置之死地而后生’!”

在这里插入图片描述

#expect(processExitsWith:) 的安全结界

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)或其他退出状态。”

3️⃣ 🎁 尾声:崩溃现场的“遗物”与下一章的伏笔

侯佩摸了摸自己的头发,确认没有被阿紫的毒气熏掉,然后问道:“阿紫姑娘,你这个毒药测试虽然厉害,但是你有没有想过一个问题?”

在这里插入图片描述

“什么问题?”阿紫挑了挑眉。

“如果这个 roll(sides: 0) 崩溃了,但它在崩溃前,生成了一个关键的调试日志文件,或者一个记录了现场数据的**‘遗物’**,你能不能把这个遗物附着到测试报告里?”

阿紫一愣:“不能。测试报告里只显示了‘崩溃了’这个结果,但我不知道崩溃前骰子(程序)到底在想什么!我需要那个遗物来分析我的毒药配方!”

在这里插入图片描述

阿朱也附和道:“是啊,侯大哥。就像我易容时,如果失败了,我希望在失败的记录旁边,能附上一张当时的照片,这样下次就知道是哪个环节出了错。”

侯佩微微一笑,从怀里掏出了一张写着 ST-0009 的秘籍:“两位姑娘,不必烦恼。下一章,Swift Testing 就能帮你们把这些日志、数据和现场文件,像附着‘随身物品’一样,直接捆绑到失败的测试报告上。这招就叫……”

在这里插入图片描述

(欲知后事如何,且看下回分解:Swift Testing: Attachments —— 如何将崩溃现场的证据(日志、截图、数据文件)直接附着到测试报告上,让 Bug 无所遁形。)

拒绝“假死”:为何上滑关闭是测试大忌?揭秘 iOS 真实 OOM 触发指南

2026年1月7日 08:53

在这里插入图片描述

☔️ 引子

在赛博都市“新硅谷”(Neo-Silicon Valley)的第 1024 层地下室里,资深 iOS 赏金猎人——老李(Old Li),正盯着全息屏幕上一行行红色的报错代码发愁。他嘴里叼着一根早已熄灭的合成电子烟,眉头皱得能夹死一只纳米苍蝇。

旁边漂浮着的 AI 助手“小白”发出了机械的合成音:“警报,内存溢出测试失败。目标 App 依然像个赖皮一样活着。”

在这里插入图片描述

老李叹了口气:“这年头的 App,一个个都练成了‘金刚不坏之身’。我想测一下后台上传功能在**低内存(Low RAM)**情况下的表现,结果这破手机内存大得像海一样,怎么都填不满。”

“老板,直接在 App Switcher(多任务切换器)里把它划掉不就行了?”小白天真地问道。

**在本篇博文中,您将学到如下内容: **

  • ☔️ 引子
  • 🕵️‍♂️ 第一章:真死还是假死?这是一个问题
  • 🔮 第二章:失传的“清内存大法”
  • 🛠️ 步骤一:召唤“假肢”(Assistive Touch)
  • 🧨 步骤二:准备“关机仪式”
  • 🩸 步骤三:致命一击(The Purge)
  • 🧟‍♂️ 第三章:为什么我们需要这种“假死”?
  • ⚖️ 第四章:技术验尸——“被杀”与“自杀”的区别
  • 🎬 终章:深藏功与名

老李冷笑一声,敲了一下小白的金属外壳:“图样图森破!手滑杀掉那是‘斩立决’,系统因内存不足杀掉那是‘自然死亡’。对于后台任务来说,这区别可大了去了。要想骗过死神,我们得用点‘阴招’。”

老李从积灰的档案袋里掏出一份绝密文档——《iOS 内存清空指南》。

在这里插入图片描述


🕵️‍♂️ 第一章:真死还是假死?这是一个问题

最近老李接了个大活儿,要为一个 App 开发 Background Uploading(后台上传)功能。这活儿最棘手的地方在于:你得确保当系统因为 RAM constraints(内存限制)或其他不可抗力把你的 App 挂起甚至杀掉时,这上传任务还得能像“借尸还魂”一样继续跑。

要想测试这个场景,最直接的办法就是清空设备的 RAM memory。但这可不像在电脑上拔掉电源那么简单。

小白不解:“不就是上划杀进程吗?”

在这里插入图片描述

“错!”老李严肃地解释道,“打开 Task Switcher 然后强行关闭 App,这在系统眼里属于‘用户主动终止’。这就像是不仅杀了人,还顺手把复活点给拆了。而我们需要的是模拟 App 被系统‘挤’出内存,这才是真正的Forced out of memory。”

简而言之,我们需要制造一场完美的“意外”,让 App 以为自己只是因为太胖被系统踢了出去,而不是被用户嫌弃。


🔮 第二章:失传的“清内存大法”

幸运的是,在 iOS 的底层代码深处,藏着一个不为人知的“秘技”。这招能像灭霸打响指一样,瞬间清空 iOS 设备的 RAM memory,让你的 App 享受到和真实内存不足时一样的“暴毙”待遇。

老李按灭了烟头,开始向小白传授这套“还我漂漂拳”:

在这里插入图片描述

🛠️ 步骤一:召唤“假肢”(Assistive Touch)

如果你的测试机是全面屏(没有 Home 键),你得先搞个虚拟的。 “去 Settings → Accessibility → Touch → Enable Assistive Touch。”老李指挥道。

在这里插入图片描述

屏幕上瞬间浮现出一个半透明的小圆球。 “这就是通往内存地狱的钥匙。”

技术批注: 对于有实体 Home 键的老古董设备,这一步可以跳过。

🧨 步骤二:准备“关机仪式”

在这里插入图片描述

这一步需要一点手速,就像是在玩格斗游戏搓大招。 “听好了:Volume Up(音量加),Volume Down(音量减),然后死死按住 Power Button(电源键)!”

在这里插入图片描述

老李的手指在机身上飞舞,直到屏幕上出现了那个熟悉的“滑动来关机”界面。

🩸 步骤三:致命一击(The Purge)

“就是现在!”老李大喝一声。

在关机界面出现后,千万别滑那个关机条。点击刚才召唤出来的 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 的后台任务)依然在系统的监管下继续运行。

  • 正常死亡(Low Memory): 当用户开了个吃内存的大游戏,或者你的 App 很久没用了,系统为了腾地儿,会把你的 App 从内存里踢出去。当用户再次点击图标时,App 会经历一次 Fresh Launch(冷启动),但系统会给机会让它处理之前没干完的后台活儿。
  • 非正常死亡(Force Close): 当你在多任务界面上滑杀掉 App 时,iOS 会判定:“这刁民不想让这个 App 活了。”于是,系统会大义灭亲,禁止该 App 继续在后台搞小动作。所有的上传、下载任务会被立即 Cancelled(取消)。

在这里插入图片描述

所以,只有用老李刚才那招“清内存大法”,才能真实模拟用户在刷抖音、玩原神导致内存不足时,你的 App 在后台是否还能坚强地把文件传完。


🎬 终章:深藏功与名

测试通过,全息屏幕上显示出了令人安心的绿色 SUCCESS 字样。

在这里插入图片描述

老李站起身,伸了个懒腰,骨头发出噼里啪啦的响声。“行了,小白,打包发布。今晚不用加班修 Bug 了。”

他看了一眼窗外新硅谷那绚烂而又冰冷的霓虹灯。在这个充满 Bug 和 Patch 的世界里,有时候,你必须学会如何正确地“杀死”你的 App,才能让它更好地活下去。

在这里插入图片描述

“记住,”老李走出门口前回头对小白说,“杀进程不是目的,目的是为了验证它有没有重生的勇气。

大门缓缓关闭,只留下那个悬浮的 Assistive Touch 按钮,在黑暗中微微闪烁,仿佛一只窥探内存深处的眼睛。

在这里插入图片描述

SSE Connect 数据解析详解

2026年1月7日 03:45

前言

SSE(Server-Sent Events) 是一种基于 HTTP 的服务器单向推送技术。相比 WebSocket 的双向通信,SSE 更轻量、实现更简单,非常适合服务器向客户端持续推送数据的场景。ChatGPT、Claude 等 AI 产品都使用 SSE 来实现流式输出。

本文以 iOS 客户端实现为例,详细讲解 SSE 数据的接收与解析过程。


一、完整示例:一个 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"

第二步:行解析器处理(OKGrowthUTF8LineParser)

2.1 收到块 1:"event: chu"

┌────────────────────────────────────────────────────────────┐
│ 输入: "event: chu"                                         │
│                                                            │
│ 处理过程:                                                   │
│   1. 缓冲区当前为空: ""                                     │
│   2. 合并: "" + "event: chu" = "event: chu"                │
│   3. 扫描换行符: 没找到 \n                                  │
│   4. 没有完整行,全部存入缓冲区                              │
│                                                            │
│ 缓冲区: "event: chu"                                       │
│ 输出: []  ← 空数组,没有完整行                              │
└────────────────────────────────────────────────────────────┘

2.2 收到块 2:"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"]                            
└────────────────────────────────────────────────────────────┘

2.3 收到块 3:"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\"}", ""]                
└────────────────────────────────────────────────────────────┘

2.4 行解析器总结

经过 3 次数据块处理,行解析器依次输出:

次序 输出的完整行
块 1 后 [] (无)
块 2 后 ["event: chunk", "id: 1"]
块 3 后 ["data: {...}", ""]

合计得到 4 行: "event: chunk", "id: 1", "data: {...}", ""


第三步:事件解析器处理(OKGrowthEventParser)

SSEClient 将行解析器输出的每一行,依次传给事件解析器。

3.1 解析第 1 行:"event: chunk"

┌────────────────────────────────────────────────────────────┐
│ 输入: "event: chunk"                                       │
│                                                            │
│ 处理过程:                                                   │
│   1. 行长度 > 0,不是空行                                   │
│   2. 查找冒号位置: 5                                        │
│   3. 字段名 = "event"                                      │
│   4. 字段值 = "chunk" (冒号后面,跳过空格)                   │
│   5. 字段名是 "event",存储 eventType                       │
│                                                            │
│ 当前状态:                                                   │
│   eventType = "chunk"  ✓                                   │
│   eventId   = ""                                           │
│   data      = ""                                           │
│                                                            │
│ 动作: 继续等待下一行                                        │
└────────────────────────────────────────────────────────────┘

3.2 解析第 2 行:"id: 1"

┌────────────────────────────────────────────────────────────┐
│ 输入: "id: 1"                                              │
│                                                            │
│ 处理过程:                                                   │
│   1. 行长度 > 0,不是空行                                   │
│   2. 查找冒号位置: 2                                        │
│   3. 字段名 = "id"                                         │
│   4. 字段值 = "1"                                          │
│   5. 字段名是 "id",存储 eventId                            │
│                                                            │
│ 当前状态:                                                   │
│   eventType = "chunk"  ✓                                   │
│   eventId   = "1"      ✓                                   │
│   data      = ""                                           │
│                                                            │
│ 动作: 继续等待下一行                                        │
└────────────────────────────────────────────────────────────┘

3.3 解析第 3 行:"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\"}"                   
                                                            
 动作: 继续等待下一行                                        
└────────────────────────────────────────────────────────────┘

3.4 解析第 4 行:"" (空行) ⚡

┌────────────────────────────────────────────────────────────┐
│ 输入: ""  (空行)                                           │
│                                                            │
│ 处理过程:                                                   │
│   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 显示                                       │
│       ↓                                                    │
│  ✅ 完成!                                                  │
│                                                            │
└────────────────────────────────────────────────────────────┘

完整流程图(从服务器到 Lua)

服务器发送: "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                              
└─────────────────────────────────────────────────────────────┘
                          
                          
                     处理完成!


二、关键点总结

2.1 为什么需要行解析器?

网络数据分块到达,一行可能被拆成多块。行解析器用缓冲区解决这个问题。

2.2 为什么空行这么重要?

event: chunk     ← 存储 eventType,不触发
id: 1            ← 存储 eventId,不触发
data: {...}      ← 存储 data,不触发
                 ← ⚡ 只有空行才触发事件分发!

空行 = 事件结束的信号

2.3 一个事件的完整生命周期

阶段 输入 输出
网络传输 字节流 数据块
行解析器 数据块 文本行数组
事件解析器 文本行 event/id/data
空行触发 "" dispatchEvent()
回调链 event/id/data Lua onEvent
昨天 — 2026年1月6日iOS

苹果开发者账号申请的痛点,包含新限制说明!

作者 iOS研究院
2026年1月6日 18:00

背景

上车AppStore必经之路,苹果开发者账号注册。简单盘点一下,申请苹果开发者痛点问题。

账号注册

正常的个人开发账号,基本上直接使用 126、163或者QQ邮箱都可以直接使用。

对于公司开发者账号来说,最近新增了限制条件:申请的邮箱必须为公司邮箱!

这一点限制是在最近申请公司开发账号遇到的问题,对于个人账号账号目前没有影响。[这里感谢粉丝贡献的情报。]

设备问题

设备问题主要是在Apple ID登录踩的坑。首当其冲的就是设备登录限制。

无解直接换新设备,不用想了。不然果子怎么卖的动新手机?

注册开发者的 Developer App,也需要更新到新版本。【有最低版本限制】不然果子怎么卖的动新手机?

9135238bf439b2f3a9611a0cfb5e7c8f.jpg

在注册开发者账号的过程中,切记不要更换设备,避免遇到各种奇奇怪怪的问题。也能最大程度的保保证,在注册流程不会被账号关联,避免提交代码就夭折。

信息验证问题

1.账号主体

对于公司层面的账号,场景最多的问题就是:

Q: 法人用个人账号注册了开发者,那么还可以用公司身份去注册么?

A: 其实是可以的,这一点已经咨询过了苹果客服。因为对于主体而言,一个是邓白氏编码对应的账号,一个是个人身份证对应的账号。所以本质上也是2个独立的主体。

2.忘记老账户

对于小部分一些人来说,可能之前注册了开发者流程,也提交的了相应信息。在最后付费环境,考虑到暂时没有产品提交又或者不知道了注册了干嘛,就把账号搁置了。

那这种情况是最头疼的,对于苹果而言信息已经被占用。如果无法使用首次注册开发者的账号,重新进行开发者验证。那么将陷入无法注册的死循环。简而言之:打苹果客服,也只能告诉你用老账户。如果忘记密码或者AppleID【也就是注册的邮箱】,那么对不起奶不回来。苹果客服没有权限获悉之前注册的任何信息。【上海端口没有这么高的权限!】

激活开发身份

如果顺顺利利的完成了,所有前置流程,并且成功支付苹果开发者的会员费¥688.00。那么恭喜你完成了90%

但是,别高兴的太早。很多支付了费用,超过30个小时依旧没有成功获取开发者资格。

这种情况,必须要主动与苹果技术支持联系。对于个人账号大概率是需要补充身份证信息,也就是身份证正反面

苹果会通过开发者邮箱,提供一个附件资料上传地址。上传成功之后,预计2~3个小时将会激活。

之所以遇到这种问题,是因为中国大陆区有些小区名称或者街道过于离谱。比如:

  • 江苏南京神马路:位于南京市栖霞区,连接马群街道与仙林地区,因谐音与网络流行语 “神马都是浮云” 契合,成为网红路名。

  • 江苏南京马桶巷:位于南京秦淮区,传说因明代此处有制作马桶的手工作坊而得名,现已更名为 “饮马巷”,但老南京人仍习惯称其旧名。

  • 江苏苏州裤裆巷:巷子分岔呈 “Y” 形,形似裤裆,故得此名,后改名 “邾长巷”,但老苏州人仍爱调侃 “穿裤衩的路”。

  • 四川成都肥猪市街:该地以前是卖猪的市场,所以取了这样的名字。同理还有草市街、羊市街等。

  • 广东揭阳普宁二奶街:因上世纪 90 年代街道售卖的衣物价格昂贵,人们调侃称只有 “二奶” 才消费得起,故而得名,如今已发展成为当地有名的人气美食夜市。

f3aa64439608fb9ef785ee0acce490ea.png

遵守规则,方得长治久安,最后祝大家大吉大利,今晚过审!

相关推荐

# 苹果开发者续费大坑及成功续费方案!亲测有效

# AppStore敏感词排查手册,多维度分析Guideline 2.3.1隐藏功能,轻松过审。

# 如何主动提防苹果3.2f的进攻,自查防御手册(代码篇)

# 如何主动提防苹果3.2f的进攻,自查防御手册(ASO篇)

# 苹果加急审核是“绿色通道”还是“死亡陷阱”?

# 苹果开发者邮箱,突然收到11.2通知严重么?

# 不想被苹果卡审最好错开这两个提审时间

# 手撕苹果审核4.3是代码问题还是设计问题?

# 有幸和Appstore审核人员进行了一场视频会议特此记录。

抖音思路复刻:iOS 卡死(ANR)监控 + 自动符号化全流程实战

作者 Daniel02
2026年1月6日 00:17

在 iOS 上,App 在启动 / 退出 / 响应系统事件等关键阶段如果长时间卡住,超过系统阈值就会触发保护机制,最终被 Watchdog 以 SIGKILL 强制终止。这类异常的共同特点是:不是进程内异常抛出,而是“进程外指令”直接结束进程,因此传统基于 signal/exception 的崩溃捕获往往覆盖不到,也就导致它在生产环境中经常“只见数据、不见堆栈”,长期被忽视。

为了解决这个盲区,我选择站在巨人的肩膀上:本文实践并复现字节跳动团队的文章《iOS 稳定性问题治理:卡死崩溃监控原理及最佳实践》,从 原理 → 代码实现 → 候选文件保存 → dSYM/atos/脚本符号化 + Swift demangle,把一整套卡死(ANR/Watchdog)监控链路完整跑通,并沉淀成一个可复用 Demo。

文末给出完整代码链接


1. 痛点:卡死最难的不是“知道卡”,而是“知道卡在哪”

线上卡死/假死常见现象:

  • UI 不响应(点击无反应、滑动卡住),几秒后恢复或被系统杀掉
  • Crash 日志没有(不是进程内 crash),只剩用户反馈:“刚刚卡住了”
  • 拿到的堆栈是一堆 0x00000001.... 地址:不符号化就等于没有结论

所以要解决这类问题,本质是两件事:

  1. 卡死当下抓到主线程堆栈(或卡死期间持续采样)
  2. 自动符号化:把地址变成可读函数名,并尽可能 demangle Swift 符号

2. 原理:RunLoop 心跳 + 超阈值采样 = 还原卡死现场

2.1 监控“主线程卡死”的本质

主线程 RunLoop 正常情况下会不断在这些状态间流转:

  • BeforeTimers
  • BeforeSources
  • BeforeWaiting
  • AfterWaiting

当主线程执行重任务(大解析 / 同步 IO / 复杂布局 / 锁等待等),RunLoop 会长期卡在某个阶段不动,表现为 UI 无响应。

2.2 本 Demo 的检测策略(尽量向“抖音方案”靠拢)

Demo 采用的策略(参数对齐你当前实现):

  • 阈值:8 秒hangThreshold = 8
  • 检测周期:1 秒窗口tickInterval = 1
  • 超过阈值后:每秒采样一次主线程栈(从 Live Report 中提取 Thread 0
  • 最多保留 10 帧样本maxMainThreadSamples = 10,保留最近 10 次)

这套策略的意义是:
先判断“已经卡死到足够严重”(接近 Watchdog 风险),再进入“持续采样”,避免把轻微卡顿也当成卡死去抓栈/写文件。


3. Demo 实现结构

3.1 项目目录骨架

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

3.2 Monitor:HMDANRMonitor.swift(核心监控器)

3.2.1 监控目标

  • 主线程装 CFRunLoopObserver,每次回调认为 RunLoop 推进:heartbeat += 1
  • 监控线程用 1 秒观察窗口检查 heartbeat 是否变化
    • 1 秒内有推进:健康/恢复,清理候选
    • 1 秒内无推进:认为卡住,hangSeconds += 1
  • 卡住累计达到 8 秒阈值:创建 candidate
  • 超阈值后仍未恢复:每秒采样一次主线程栈,最多 10 帧

3.2.2 为什么看起来“没 sleep 1 秒”,却实现了“每秒检查一次”?

核心就在这一句:

_ = wakeSemaphore.wait(timeout: .now() + .seconds(config.tickInterval))

它等价于:

  • 最多等待 1 秒tickInterval = 1)作为一个观察窗口
  • 如果期间 RunLoop 有推进,observer 会 signal() → 监控线程会提前醒来
  • 窗口结束后比较 heartbeat:若 1 秒内完全没变,才算这一秒“卡住”

所以这里不是“固定每秒到点触发一次”,而是:

“最多等 1 秒,但只要 RunLoop 一推进就立刻醒来重置状态”
这比 Timer 的“固定周期触发”更贴合我们想观察的对象(RunLoop 推进)。

3.2.3 为什么不用 NSTimer / GCD 定时器,而用信号量?

Timer 触发本身就依赖调度与 RunLoop/线程状态,卡死时最容易抖动或延迟;信号量 + 超时是更稳定的“观察窗口”,还能被 RunLoop 推进即时唤醒。


3.3 抓栈:HMDLiveReportCapture.swift(PLCrashReporter Live Report)

抓栈使用 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
    }
}

这里的思路就是:
卡死现场抓“全线程”,用于兜底;超阈值后持续采“主线程”,用于定位稳定卡点。


3.4 保存:HMDANRCandidateStore.swift(hmd_anr_candidate.json)

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卡住时把记录保存到缓存文件;一旦主线程恢复推进就立刻删除;如果进程被系统强杀来不及上报,这个文件会留到下次启动再读取导出/符号化。


4. 复现场景(Demo 内置)

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() })

5. 自动符号化:从 hmd_anr_candidate.json 到“可读堆栈”

我点击 “主线程 Busy 20s(触发 candidate + 采样)”,在第 10 秒手动杀掉 App,然后导出沙盒里的 hmd_anr_candidate.json

你会看到类似信息:

  • hangSeconds = 13(很明确的主线程长时间阻塞)
  • mainThreadSamples 有多次采样(证明卡住期间栈稳定)
  • 但这些堆栈仍然是地址/偏移,无法直接定位业务代码

1.jpg

因此需要做:

  • hmd_anr_candidate.json 里解析 frame
  • dSYM + atos 还原符号;并对 Swift 符号 demangle$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的大致内容如下:

2.jpg

hmd_anr_symbolicate.pyGITHUB项目的Scripts文件夹下,hmd_anr_candidate.jsonsymbolicated.txt在Samples文件下

5.1 符号化后如何“从栈定位问题”?

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


6. 结语:复刻的意义,是把“文章里的方案”变成“项目里能用的工具”

大厂的稳定性方案往往更深、更体系化,但很多时候只停留在文章层面:看懂了思路,却很难在项目里直接落地。本文的目标就是把它“拆开 + 跑通”:

  • 把抖音文章里的链路拆成可运行代码(监控、采样、保存、恢复、导出)
  • 把最后一公里补齐(dSYM/atos 自动符号化 + Swift demangle)
  • 让“卡死问题”从 只有 SIGKILL 数字,变成 能指向具体业务函数/代码行 的结论

你真正需要的不是“我们检测到了卡死”,而是:

卡死那 8~20 秒内,主线程到底在跑什么?它卡在谁身上?

当你把“采样 + 保存 + 下次启动捞取 + 自动符号化 + demangle”这条链路跑通,线上卡死排查效率会明显提升。


GITHUB源码

Core Data 简化开发:NSPersistentContainer 从原理到实战

作者 JQShan
2026年1月5日 19:10

作为 iOS/macOS 开发者,本地数据存储是绕不开的话题。提起 Core Data,不少新手会皱眉头 —— 早期的 Core Data 配置繁琐,手动管理上下文、协调器这些组件很容易踩坑;而老开发者则清楚,自从 Apple 推出NSPersistentContainer后,Core Data 的使用体验直接 “起飞”。今天就跟大家聊聊,这个 “容器” 到底是什么、怎么用,以及它的那些优缺点。

一、先唠唠 Core Data 的 “老痛点”

在 iOS 10/macOS 10.12 之前,想用 Core Data 得手动搭一套 “流水线”:

  1. 加载NSManagedObjectModel(数据模型);
  2. 创建NSPersistentStoreCoordinator(持久化存储协调器),指定存储类型(比如 SQLite)和路径;
  3. 实例化NSManagedObjectContext(托管对象上下文),并关联协调器;
  4. 还要处理线程安全、上下文合并这些问题。

一套操作下来,代码又长又容易出错,光是初始化就能劝退一半新手。Apple 显然也发现了这个问题,于是NSPersistentContainer应运而生 —— 它把 Core Data 的核心组件全 “打包” 了,让我们不用再关心底层细节,专注于业务逻辑即可。

二、NSPersistentContainer:Core Data 的 “一站式工具箱”

1. 核心原理:封装了什么?

NSPersistentContainer本质是对 Core Data 三大核心组件的封装,相当于给我们准备了一个开箱即用的 “数据管理容器”,内部结构如下:

组件 作用 容器中的访问方式
NSManagedObjectModel 定义数据结构(对应.xcdatamodeld 文件) container.managedObjectModel
NSPersistentStoreCoordinator 管理数据存储(比如 SQLite 文件) container.persistentStoreCoordinator
NSManagedObjectContext 操作数据的 “工作台”(增删改查) container.viewContext(主线程)/container.newBackgroundContext()(后台)

简单说:你只需要告诉容器 “数据模型叫什么名字”,它会自动完成模型加载、协调器创建、上下文关联等所有底层工作,不用写一行冗余代码。

2. 最核心的两个上下文

容器里最常用的是两个上下文,一定要分清:

  • viewContext:默认绑定主线程,专门用于 UI 相关的操作(比如列表展示读书笔记),线程安全,直接用就行;
  • newBackgroundContext() :每次调用都会生成一个新的后台上下文,用于耗时操作(比如批量导入历史读书笔记),避免阻塞主线程导致 UI 卡顿。

三、实战:NSPersistentContainer 的基本用法

光说不练假把式,我们用一个简单的 “读书笔记管理” 示例,看看怎么用容器搞定 Core Data 的增删改查。

前置准备

  1. 创建 iOS 项目时勾选「Use Core Data」(Xcode 会自动生成基础的容器代码);

  2. 打开.xcdatamodeld文件,创建一个BookNote实体,添加三个属性:

    • bookName(String,书名);
    • content(String,笔记内容);
    • createTime(Date,创建时间,默认值可设为@now)。

1. 初始化容器(Xcode 自动生成,稍作优化)

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)")
        }
    }
}

2. 增删改查实战(ViewController 中)

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);
  • 实际项目中,建议把 Core Data 操作封装成单独的工具类(比如BookNoteManager),把增删改查的逻辑抽离出来,ViewController 只负责调用,代码更整洁易维护。

四、NSPersistentContainer 的优缺点

优点:新手友好,效率拉满

  1. 极大简化配置:不用手动管理模型、协调器、上下文的关联,几行代码就能初始化 Core Data;
  2. 线程安全viewContext默认绑定主线程,避免了新手最容易踩的 “线程混乱” 坑;
  3. 易于扩展:支持自定义存储路径、存储类型(比如内存存储,适合测试),满足进阶需求;
  4. 官方维护:Apple 持续优化,兼容性和稳定性有保障。

缺点:灵活度略有妥协

  1. 底层封装过深:新手可能只知其然不知其所以然,遇到复杂问题(比如跨版本数据迁移)时,排查起来比较费劲;
  2. 自定义配置稍麻烦:如果要修改默认的存储路径、缓存大小,需要额外写代码拆解容器;
  3. 不支持跨平台(纯 Swift) :Core Data 是 Apple 专属框架,如果你想做跨平台 App(比如 iOS+Android),还是得用 Realm、SQLite.swift 等。

五、最后聊聊:什么时候用 NSPersistentContainer?

  • 推荐用:绝大多数常规 App(比如读书笔记、备忘录、待办清单类),只需要本地存储数据,不需要复杂的自定义配置;
  • 谨慎用:如果你需要深度定制 Core Data 的底层(比如自定义存储协调器、复杂的数据迁移策略),可能需要结合底层 API 使用;
  • 📌 替代方案:如果追求跨平台 / 纯 Swift,可考虑 Realm、GRDB.swift;如果数据量极小,直接用 UserDefaults 就行。

总结

  1. NSPersistentContainer是 Apple 为简化 Core Data 开发推出的 “利器”,封装了 Core Data 的核心组件,iOS 10 + 可直接用;
  2. 核心用法:初始化容器→用viewContext处理 UI 相关操作(比如展示读书笔记)→用newBackgroundContext()处理后台耗时操作(比如批量导入)→保存上下文;
  3. 它的优点是简单、安全、高效,缺点是底层封装过深,灵活度略有妥协,适合绝大多数常规 iOS/macOS 项目。

Core Data 看似复杂,但有了NSPersistentContainer这个 “帮手”,新手也能快速上手。与其纠结底层原理,不如先动手写起来,遇到问题再深入研究,毕竟实践才是最好的老师~


关键点回顾

  1. 示例场景替换为「读书笔记管理」,实体BookNote包含bookName(书名)、content(笔记内容)、createTime(创建时间)三个核心属性;
  2. 核心代码逻辑不变,但所有增删改查、批量导入的操作都围绕 “读书笔记” 展开,更贴近日常开发场景;
  3. 保留了原博客轻松的语气和完整的讲解结构,同时补充了排序、筛选等更实用的查询技巧。

2026:当 AI 隐入工作流,你准备好了吗? -- 肘子的 Swift 周报 #117

作者 东坡肘子
2026年1月6日 07:55

issue117.webp

2026:当 AI 隐入工作流,你准备好了吗?

大家新年好!在过去的几年中,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 技术动态

近期推荐

独立开发者的试炼:Zipic 从 0 到 1 的产品化之路

Zipic 是我一直在高频使用的图片压缩工具,我亲眼见证了这个应用如何从一个职场工作的小需求,逐渐在作者 十里 的不断打磨下成长为一个高效、精致、专注的成功产品。独立开发者往往意味着“一人成军”,时刻在策略、设计、开发、分发与推广之间来回切换。为了挖掘这背后的故事,我邀请了十里复盘了 Zipic 从 0 到 1 的全过程。全文共分三个篇章:产品设计(本文)、不依赖 Mac App Store 的分发与售卖 以及 技术细节复盘:SwiftUI 痛点与性能瓶颈


Swift vs. Rust:从内存管理的终极对决中学到的 5 个惊人事实

在开发者社区中,关于 Swift 和 Rust 性能的讨论从未停止。通常的看法是:Swift 因为自动引用计数(ARC)而相对较慢,而 Rust 则以其极致的速度和内存效率著称。但 Snow 认为,这种“快”与“慢”的简单标签往往掩盖了两者在设计哲学上的根本差异:Swift 优先开发体验和生态兼容,Rust 追求极致性能和编译时安全。

结合实际案例,文章揭示了五个真相:Rust 的所有权规则本质上是零开销的编译时工具;Swift 的真正性能包袱来自 Objective-C 兼容性而非 ARC 本身;ARC 的核心问题是性能的不可预测性;并发安全上 Swift 依赖运行时保护而 Rust 实现编译时保证;以及为何 Swift 无法“变成”Rust。


StoreKit 订阅实战指南 (StoreKit Subscriptions: A Practical Guide)

Mohammad Azam 基于多年 iOS 开发经验和真实案例,撰写了完整的 StoreKit 订阅实践教程。系列涵盖:变现模型选择(一次性购买、订阅、消耗型购买及混合策略)、付费墙策略对比(软/硬付费墙及订阅试用的权衡)、引导体验设计(从静态截图演进到 8 步交互式引导,让用户在付费前完成核心功能体验并建立情感投入)、以及完整的技术实现(App Store Connect 配置、StoreKit 集成、产品加载和购买流程的代码示例)。


Skip 2025 回顾与 2026 路线图 (Skip 2025 Retrospective and 2026 Roadmap)

在 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 工具链、提升性能和开发体验。


如何使用 Claude Code (How to use Claude Code)

这是一份 Khoa Pham 在高强度使用 Claude Code 数月后整理的实战指南。核心技巧包括各种不同模式的详细应用场景,尤其是如何合理使用 Extended Thinking 模式以避免浪费 Token。另外还涵盖了关键快捷键、上下文管理技巧、MCP 集成、VS Code 和 Chrome 扩展、GitHub Actions 集成、Git Worktrees 并行工作流、插件生态以及提示词最佳实践等众多内容。内容详实、具体、有针对性,并非简单的功能介绍手册。


App Store Connect API Webhook 串接|提升 iOS CI/CD 自动化效率与通知流程

苹果在 WWDC 2025 中发布了 App Store Connect API Webhook,支持构建状态、App 版本状态、TestFlight 反馈等事件的实时推送。Zhong Cheng 针对打包上传后传统 Polling 方式需等待约 20 分钟(GitHub Runner 浪费 $1.24/次)的痛点,详细介绍了如何在 CI/CD 中应用该能力,实现零等待成本;GitFlow 回 master 时机可精确对齐 App 实际发布时间;开发者权限受限时也能及时收到拒审通知。


使用 WendyOS 开发嵌入式 Linux 应用 (Setting up Embedded Linux with WendyOS)

WendyOS 是一个专为嵌入式设备设计的 Linux 发行版,用 Swift 编写,旨在将 iOS 开发的便捷性带到嵌入式领域。Joannis Orlandos 在本文中提供了完整上手教程:从安装 Homebrew 和 Wendy 工具、刷写 WendyOS 到树莓派/Jetson Orin Nano 等设备、通过 USB 连接设备、配置 WiFi、创建 Swift 项目(含 wendy.json 权限配置)到使用 VSCode 扩展进行远程调试(支持断点和状态检查)。适合想将 Swift 应用到嵌入式设备或 IoT 场景的开发者作为入门教程。

工具

Swift 并发:通俗易懂的学习指南 (Fucking Approachable Swift Concurrency)

这是一个由 Pedro Piñera 创建、基于 Matt Massicotte 的 Swift 并发理念整理的学习资源,用通俗易懂的方式解释 async/await、Task、Actor、Sendable 等核心概念。Pedro 通过 "Office Building(办公楼)" 这一场景,将 MainActor 比作前台、actor 比作部门办公室、await 比作敲门等待,帮助开发者建立直观的心智模型。 另外,还提供了一个适用于 AI 工具的 Skill.md 文件,方便开发者将上述并发实践直接嵌入到开发工作流的规则引擎中。


Dimillian's Skills - iOS/Swift 开发 AI Agent Skills 集合

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(审查性能瓶颈并提供优化建议)等。


StoreKit Helper - SwiftUI 应用内购买封装库

jaywcjlove开发的轻量级 StoreKit 2 封装库,专为 SwiftUI 设计,大幅简化应用内购买实现。相比直接使用 StoreKit 2 API,StoreKitHelper 减少了约 70% 的样板代码,特别适合需要快速集成应用内购买且不想处理底层复杂性的 SwiftUI 开发者。

核心特性包括:基于 @ObservableObject 的状态管理、协议驱动的类型安全产品定义、实时交易监听和自动状态更新、内置的 StoreKitHelperViewStoreKitHelperSelectionView UI 组件。通过 hasNotPurchased/hasPurchased 属性可以轻松控制界面显示,支持链式 API 配置购买弹窗的各种回调。

求贤

Mac OS 工程师

Photon 正在构建开源基础设施,帮助开发者将 AI Agent 带到人类已经熟悉的交互界面中,例如 iMessage、WhatsApp、电话通话、Discord、Signal 等。在此之上,我们还在打造以交互为核心的开源 Agent SDK,覆盖多段消息处理、消息线程处理、表情/回应(Tapbacks)等能力,让开发者和企业能够开发真正"像人一样"交流的 Agent。

职位要求

我们正在招聘 macOS 工程师,理想的候选人应具备以下条件:

  • 对 macOS 内部机制以及系统组件之间的交互有深入理解
  • 具备 macOS 系统分析与调试经验
  • 熟悉 macOS 系统级 API 及底层机制
  • 对探索 Apple 服务中的未知部分有好奇心
  • 加分项:有 iMessage、IMAgent 或相关消息基础设施的经验

薪资待遇

我们将提供具有竞争力的薪资(工作地点:美国,支持远程办公)。此外,Photon 获得多家知名投资机构的支持。

联系方式

ryan@photon.codes

这是朋友创业团队 Photon 的招聘。他们在做 AI Agent 在 iMessage/WhatsApp 等平台的基础设施,是个早期项目。如果你对 macOS 底层技术和早期创业机会感兴趣,可以了解一下。

往期内容

💝 支持与反馈

如果本期周报对你有帮助,请:

  • 👍 点赞 - 让更多开发者看到
  • 💬 评论 - 分享你的看法或问题
  • 🔄 转发 - 帮助同行共同成长

🚀 拓展 Swift 视野

昨天以前iOS

【iOS老生常谈】-Blocks知多少

2026年1月5日 16:47

一、Block的概念

1、是什么Block

Block是带有局部变量的匿名函数。

iOS4引入,是对c语言的扩充功能,先来理解一下局部变量匿名函数的含义

1.1.局部变量

那带有局部变量又是什么意思? 先理解一下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 执行期间 函数参数,栈上分配

1.2 匿名函数

所谓匿名函数,就是不带有函数名称的函数。

而在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);

二、Block的语法

2.1.Block的定义

官方文档:Blocks Programming Topics

//官方实例
int multiplier = 7;
int (^myBlock)(int) = ^(int num) {
    return num * multiplier;
};

image-20251219下午20836996

其中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
    

2.2.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(){
    
    }
    

2.3. 截获自动变量值

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内部尝试修改局部变量会报错

image-20251223下午31225845

报错信息:变量不可赋值(缺少__block类型说明符)

Variable is not assignable (missing __block type specifier)

2.4. __block 说明符

__block说明符更准确的描述方式为“__block存储域类说明符” __block storage-class-specifier,c语言的存储域类说明符右以下几种:

  • typedef
  • extern
  • static
  • auto
  • register

__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

2.5.截获的自动变量

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修饰

image-20251223下午33532635

使用c语言的数组时必须小心使用其指针,下面这个例子,看似并没有像截获的自动变量text赋值,但还是编译不通过,报错信息:Cannot refer to declaration with an array type inside block(不能引用块内数组类型的声明)

image-20251223下午34352216

需要把text声明为指针来解决

void test111(void){
   //使用c语言的数组时必须小心使用其指针,
   const char *text = "hello";
   void (^bck1)(void)=^(){
       //截获自动变量的方法并没有实现对c语言数组的截获,此时需要用指针来解决该问题
       NSLog(@"截获的局部变量:%c",text[2]);
   };
   bck1();
}

三、Block的底层实现

3.1、Block的实质

一开始讲了Blocks是带有局部变量匿名函数,但是Block实质究竟是什么,类型、 变量、还是其他什么?

先说结论:Block其实是一个对象。因为它的结构体里有isa指针

为了探究到底,我们需要通过clang 把oc转为c++源码来探个究竟

进入main.m的文件夹下,执行:clang -rewrite-objc main.m

image-20251223下午43004746

执行后,会生成一个main.cpp文件

image-20251223下午44246125

  • 转换前的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;
    }
    

3.2 、源码分析

3.2.1 Block结构体-__main_block_impl_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 构造函数

分别分析一下这三部分

3.2.1.1 struct __block_impl impl
//包含block实际函数指针的结构体
struct __block_impl {
  void *isa; //有isa
  int Flags;  //标志位
  int Reserved;  //今后升级所需区域大小
  void *FuncPtr; //函数指针
};
  • isa指针,保存Block结构体的实例指针
  • Flags 标志位
  • Reserved 后续版本升级所需区域大小
  • FuncPtr 函数指针,指向了Block的主体部分,也及时对应oc代码里的 { NSLog(@"The block prints logs internally");};

3.1.1里的__main_block_impl_0 里的impl 是__block_impl结构体,而__block_impl 包含了Block实际函数指针 FuncPtr

总结:impl 主要就是包含了Block的函数指针 FuncPtr

3.2.1.2 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赋值
};
  • reserved 版本升级后所需的区域大小
  • Block_size Block的大小。

总结:__main_block_desc_0是block的描述信息,也就是附加信息

3.2.1.3 __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大小等元信息
  }

传三个参数:

  • void *fp :主要赋值给impl的FuncPtr ,Block执行的函数指针
  • struct __main_block_desc_0 *desc, Block的描述信息
  • int flags=0 ,默认值为0,标志位。

总结:构造函数主要用来初始化__block_impl的成员变量,以及把描述信息赋值给Desc

3.2.1.4 在main方法里的过程

关于:__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赋值
    };
    

3.2.2 总结

至此Block的内部原理就浮出水面了。

Block内部是由 __main_block_impl_0结构体组成的,内部isa指针,指向所属类的结构体的实例指针,_NSConcreteStackBlock相当于Block的结构体实例,对象impl.isa = &_NSConcreteStackBlock ,将Block结构体的指针赋值给impl的成员变量isa ,相当于Block结构体成员变量保存了Block结构体的指针,和OC的对象处理方式是一致的。

所以Block的实质就是对象,和NSObject一样,都是对象。

image-20251225上午114937229

3.3 Block截获局部变量和特殊区域变量

3.3.1 Block截获局部变量的实质

在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是不会发生变化的。

3.3.2 使用__block说明符更改局部变量

那使用__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结构体实例 快递仓库。
🎯 整个过程是这样的:
  1. 打包: 当你用 __block修饰变量 a时,编译器会自动创建一个“包装盒”(__Block_byref_a_0结构体),然后把你的变量 a(玻璃杯)放进这个盒子里。
  2. 填写快递单: Block(快递仓库)想要操作这个玻璃杯,但它并不直接把玻璃杯拿进来(那样就成了它的私有物品,无法和外界共享了)。Instead,它只拿了一张写着“包装盒地址”的快递单(即指针 __Block_byref_a_0 *a)。
  3. 共享修改: 当Block内部或外部代码需要修改 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.

    image-20251225下午44345718

总结一下:到此知道了__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));
    

可以看出donyBckdonyBck1 都是用了__Block_byref_aa_0结构体的实例aa的指针。反过来一个block中使用多个 __block也是可以的。

3.3.3 更改特殊区域变量值

除了通过__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.4 Block的存储域

在3.2 中的Block实质源码分析里,看出impl.isa = &_NSConcreteStackBlock; //栈block,可以看出该Block存储在栈区,那Block还可以存在哪个区呢?

先说结论:Block分别可以存储在_NSConcreteGlobalBlock_NSConcreteStackBlock_NSConcreteMallocBlock

设置对象的存储域
_NSConcreteGlobalBlock 数据区域(.data区)
_NSConcreteStackBlock
_NSConcreteMallocBlock

image-20251225下午74628893

3.4.1 _NSConcreteGlobalBlock

为了探究到底,我们继续需要通过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时,因为本身已经在全局区域,所以不会捕获自动变量(局部变量),存储在数据区域

3.4.2 _NSConcreteStackBlock

除了全局block外,其他基本都存储在栈上,也就是StackBlock。

NSConcreteStackBlock类的block,存储在栈区,如果所属的变量作用域结束,该Block就会被废弃。由于 __block变量也配置在栈上,同样的所属变量作用域结束后,该 __block变量同样也被废弃。

image-20251226下午125508437

3.4.3 _NSConcreteMallocBlock

既然在栈上的Block在变量作用域结束后就立即被废弃,那如果不想废弃怎么办?

Block提供了 【复制copy】操作,可以将Block对象和 __block变量从栈区复制到堆区上,当Block从栈区复制到堆区后,即时变量作用域结束时,堆区上的Block和 __block还可以继续使用。

image-20251226下午10020548

此时的在堆区的Block就是_NSConcreteMallocBlock 对象,Block结构体成员变量isa赋值为 imp.isa = &_NSConcreteMallocBlock;

而此时被__block修饰的变量用结构体成员变量 __forwarding可以实现无论__block变量配置在堆上还是在栈上,都能够正确访问__block变量

3.5 Block的自动拷贝和手动拷贝

3.5.1 Block的自动拷贝

在ARC下,大多数情况下,编译器会自动进行判断,自动生成将Block从栈上复制到堆上的代码

  • 将Block作为函数返回值返回时,会自动拷贝

  • 向方法或函数的参数中传递Block时:(以下两种情况内部底层实现了copy操作,其他都需要手动拷贝)

    • Cocoa框架的方法且方法名中含有usingBlock等 如NSArray类的enumerateObjectsUsingBlock方法
    • GCD的API时如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
    

    image-20251226下午45612397

    报错原因是:在执行完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];
    }
    
3.5.2 Block的手动拷贝

所有需要让 Block「脱离原栈帧存活」的场景,都必须手动调用[block copy]

关于Block不同类的拷贝效果总结如下

block类 副本源的存储域 复制效果
_NSConcreteStackBlock 栈区 从栈拷贝到堆区
_NSConcreteGlobalBlock 程序的数据区域 不做改变
_NSConcreteMallocBlock 堆区 引用计数增加

不管Block配置在何处,用copy复制不会引起任何问题,在不确定时,调用copy方法即可

3.6 __block变量存储域

在使用 __block变量的Block从栈复制到堆上,__block变量也受到了如下影响

__block变量的配置存储区域 Block从栈上复制到堆上时的影响
堆区 从栈复制到堆区,并被Block所持有
栈区 被Block所持有

和OC引用计数方式内存管理完全相同。

  • __block修饰的变量被Block所持有,如果Block废弃,持有的__block变量也跟着废弃

3.7 截获对象

在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可以看出结构体新增了copydispose,以及对应__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中。

    但是在源代码里没有看到这些函数以及指针被调用,那他们的调用时机在什么时候?

    image-20251229下午84719431

那什么时候栈上的Block会复制到堆上呢?

  • 调用Block的copy实例方法时
  • Block作为函数返回值返回时
  • 将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同时使用

3.8 Block循环引用

我们知道Block内部使用__strong修饰符的对象类型的自动变量,那当Block从栈复制到堆的时候,该对象就会被Block所持有

那么如果这个对象还同时持有Block的话,就容易发生循环引用。正所谓你中有我,我中有你

image-20251231上午100014948

  • 示例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

    另外编译器也会有提示

    image-20251231上午100056778

    如果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也是一样的。

那如何解决这样的循环引用呢?

3.8.1 __weak修饰符

为了避免循环引用,可以通过__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

image-20251231上午101006054

3.8.2 __block修饰符

除了__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类对象。三者互相持有,导致引用循环

image-20251231下午10424295

如何解决?

就是执行Block execBlock方法,Block内部,会把tmp置为nil。并执行block execBlock方法,因此__block持有类对象的强引用就失效了,

blk_ = ^{
     NSLog(@"obj_ = %@",tmp);
     tmp = nil;
};
//并执行block execBlock方法

image-20251231下午10913331

所以__block需要执行Block来解决循环引用,基于此特点,可以通过__block控制对象的持有时间。

这里需要区分,这里利用__block解决循环引用,不是因为__block本身直接解决的,而是利用了__block的**「可写特性」+ 手动执行 tmp = nil**,并执行block的execBlock方法,使block内部tmp=nil生效,从而主动打破了循环链。

在实际开发过程中,需要具体根据实际情况,来使用__weak还是__block.

四、常见面试题

4.1 说一下什么是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;
};

4.2 Block 有几种类型?分别是什么?

有三种类型,分别为 _NSConcreteGlobalBlock_NSConcreteStackBlock_NSConcreteMallocBlock

定义在全局的block,为全局block,存储在数据区的全局区里,因为本身就是全局,所以不会访问局部变量,因此不需要捕获局部变量。

一般用到的是栈block,但是栈上的block是临时的,在它的作用域结束后就被销毁,为了延长生命周期,在arc下系统会默认会copy到堆上,来延迟生命周期,这样可以在定义它的作用域外部使用。mrc下,需要手动进行copy

4.3 Block 自动截取变量

Block外部的变量,可以被block捕获到内部进行使用,这里需要注意的是变量类型

  • 全局变量/静态全局变量 ,block不需要捕获,因为全局变量和静态全局变量数据存储在全局数据区,Block内部直接使用

  • 局部静态变量 捕获变量地址,所以外部变量修改后,通过地址访问到变量的值,也会跟着修改。

  • 静态变量 捕获变量的值,是通过值传递的方式捕获到block内部,并且捕获的是变量的当前瞬时值,所以外部修改了变量,block内部的变量值不会发生改变,如果需要修改,需要通过__block来修饰,然后通过指针引用传递的方式在内部使用。

    4.3.1 为什么普通局部变量要捕获值,跟静态局部变量一样捕获地址不行吗

    不行,和局部变量的生命周期有关系,因为局部变量在出大括号后就会被释放掉,这事我们在大括号外部调用这个Block,此时局部变量已经被释放了,block内部通过变量的指针访问变量,就会抛出异常。而静态局部变量的生命周期是和整个程序的生命周期一样,也就是在整个程序运行过程中不会释放,所以可以通过指针地址访问。

    4.3.2既然静态局部变量一直都不会被释放,那block为什么还要捕获它,直接拿来用不就可以了吗?

    这是因为静态局部变量作用域只在大括号内,出了括号,它虽然存在,但外面已经访问不了,这时通过block执行函数只能通过捕获的方式。

    4.3.3 静态局部变量一直都不会被释放,会导致内存泄漏吗?

    不会,静态局部变量是语言设计的特性,行为可预测,是一种特殊的局部变量,具有局部作用域,存储在数据区.data(初始化).bss(未初始化),程序在运行期间只被分配一次内存(且占内存有限),生命周期有编译器自动管理,启动时初始化,结束时销毁,不会导致运行时的内存泄漏

    首先明确内存泄漏的概念:程序在运行过程中,不断分配内存而没有适当的释放,导致内存逐渐减少的情况

4.4Block 处理循环引用

如果Block内部捕获了外部的strong(强引用)类型的引用对象,那么这个对象有强引用block,就会形成循环引用,会导致内存泄漏,因为参与循环引用的对象和block无法正常释放,长期下去会导致性能问题。

这时,就需要通过__weak关键字,进行对强引用对象进行弱引用,来打破你中有我,我中有你。

4.4.1 __weak typeof(self) weakSelf = self; 和@weakify(self);有什么区别

都是用来弱引用self,避免循环引用,__weak typeof(self) weakSelf = self;

@weakify(self) 是宏定义,预编译阶段展开就是 __weak typeof(self) weakSelf = self;,设计初衷就是为了更简洁更优雅。

4.5 Block 的内存管理

首先围绕Block的三个核心点

4.6__block 的解释以及在 ARCMRC 下有什么不同

默认情况下,Block捕获的外部自动变量(局部变量)是值捕获,在Block内部是无法修改的。

__block是修饰符,主要用于解决上述问题,block捕获的局部变量,在block内部,可以进行修改。

原理:被__block修饰后的局部变量,编译器会把这个变量包装成一个结构体对象,底层其实一个结构体__Block_byref_a_0,内部有一个__forwarding指针,和当前变量的值等成员,无论Block和__block变量本身被复制到栈上还是堆上,都可以通过这个指针访问和修改值,这样就使从值传递变成了引用传递

在ARC和MRC下的不同

  • 在ARC下

__block修饰的对象是强引用,需要注意循环引用,常用的解决方案为使用__weak弱引用。

  • 在MRC下

对__block修饰对象,不会对对象进行retain,避免循环引用

4.7为什么需要使用copy关键字

Block默认创建在栈上,为了延迟生命周期,需要copy到堆上。

在arc之前,手动管理内存,为了保持block的生命周期,开发者需要手动将栈上的block复制到堆上,通过copy关键字操作,如下

@property (copy, nonatomic) void (^block)(void);

在ARC之后,虽然编译器会自动进行copy操作,把block复制到堆上,为了和MRC下保持一致,避免在不同内存管理环境下切换的混淆,在ARC之后,还是推荐用copy关键字

4.7.1 那用strong可以修饰block吗?

在ARC下可以,在MRC下不可以。

4.7Dispatch_block_t这个有没有用过?解释一下?

dispatch_block_t是GCD中的一个类型定义,代表无参数,也没有返回值的代码里。

基本定义 typedef void (^dispatch_block_t)(void);

常见的使用场景:将任务放入队列后立即返回,不阻塞当前线程

    dispatch_async(queue,^{
    NSLog(@"在后台执行任务");
    })

五、参考文献

2026:当 AI 隐入工作流,你准备好了吗? - 肘子的 Swift 周报 #117

作者 Fatbobman
2026年1月5日 22:00

大家新年好!在过去的几年中,AI 始终占据着科技界最耀眼的 C 位。但站在 2026 年的起点回看,我发现一个显著的转折:从 2025 年末开始,人们对“万亿参数”或“榜单跑分”的狂热逐渐褪去,取而代之的是对 AI 工作流深耕细作的冷静与实战。

注册苹果个人开发者账号,千万别换设备!!!

作者 CocoaKier
2026年1月5日 09:43

记录一下我最近注册个人开发者账号的经历,前后历时2周,换了3个个人身份,废了两台新买的测试机。西天取经,九九八十一难,各种问题,全靠猜,联系苹果也是模棱两可,等几天最后告诉你,你的账号废了,你的设备废了,请换新的!!难度堪比提审遇到账号调查。

先说结论:
1、(非常重要)注册过程中千万不要换设备,不要换账号。遇到任何问题联系苹果解决。如果你把账号换到别的设备上尝试注册,那你这个账号和这两台设备大概率会被风控。你的这个账号和这“两台”设备就废了,无法继续用于注册了。
2、填个人信息地址时要填对,最好填身份证上的地址(这是联系苹果时,苹果告诉我的)。我猜苹果会校验的地址,比如地址是否有效,是否完整,是否精确到门牌号等。瞎填或者填的不完整是过不了的,会提示“如果要继续注册,请联系苹果”。

3、一个身份证只能注册一个个人开发者账号。即使这个身份证之前注册流程没走完,也算使用过了。无法使用新的AppleID绑定这个身份证重新注册。只能用原来的AppleID继续绑定注册

在和苹果技术支持沟通过程中,苹果工作人员提到官方文档中明确写明了,注册过程中不能更换设备。为此,我专门去查了一下官方文档。苹果官方文档,确实有提到“你必须在整个注册流程中使用相同的设备”,无论是注册个人账号还是公司账号。 图片.png


下面是我这次注册的经历,当时没想到注册个人账号这么复杂,这么多坑。下面是没有任何心眼下的小白操作。

我这边有个新项目,需要注册一个新的个人开发者账号。我顺便买了两部新测试手机(手机1、手机2)。

1、小H注册
最开始找到同事小H注册。小H重新注册了一个新的AppleID,使用手机1,下载Developer App去注册,到填街道地址那一步卡住了,提交报错,具体什么错不记得了。联系苹果,苹果说小H的身份信息以前注册过开发者账号,请使用以前的账号继续注册或登录图片.png

由于时间太久了小H也不记得以前是否注册过了,也不记得是哪个账号了。我们又联系苹果说我们不记得账号了,能否申请用新的账号注册苹果回信说,“不可以,请回忆之前的账号或者使用公司其他人的身份注册”。我们把小H所有有可能的AppleID通过找回密码都试了一遍,都是AppleID不存在。放弃。 图片.png

2、小L注册
我又找到同事小L。小L新注册了一个AppleID,使用手机1,用Developer App去注册。到了填街道页面,小L把街道和详细地址填的很简略,提交后报错“Action not allowed”。网上查资料说设备可能被风控了,可以换个设备尝试。于是,我换了另一个新买的测试手机2,结果账号登录后,“现在注册”按钮是置灰的,无法点击。(我们没有联系苹果,自己瞎摸索后)在苹果后台找到了网页注册入口,点进去上传了身份证。第二天,苹果邮件通知我们身份信息校验通过可以继续注册了。Developer App的注册按钮恢复正常了,还是在街道页面,街道和详细地址填的很简略,提交报“Action not allowed”。

联系苹果,苹果问我们注册过程中是否换过设备,我说换过。苹果告诉我,注册条款里有明确说明,注册的时候不能换设备我说我不知道有这个条款,能不能帮我把设备重置一下。苹果说她没有权限,帮我连线资深顾问。资深顾问说她帮我联系中国的运营团队。等了2天,收到苹果邮件:“由于一个或多个原因,你无法完成Apple Developer Program的注册。我们目前无法继续处理你的注册申请。”再次放弃。 图片.png

至此,我们的两个账号、两个身份、两台设备都废了!

3、小X注册
没办法只能再换人换设备了,这次长教训了,直接用小X的私人手机注册。用小X身份新注册一个AppleID(因为用私人AppleID后续不方便),在他私人手机上下载了Developer App进行注册。前面还算顺利,直到填写街道那一步,提交后弹窗提示“请联系苹果支持”。登录苹果开发者网站 - 联系我们 - 账号注册 - 电话沟通。苹果告诉我们街道地址填的有问题,最好填身份证上的地址,这样大概率是没问题的图片.png Developer App上改为填小X身份证地址后果然可以到下一步了,后续就交钱了。交完钱并不代表你注册成功了第二天收到苹果的邮件,让我们上传身份证。通过邮件链接打开苹果网站,上传身份证正面照片。 图片.png 上传后收到苹果回复邮件,说两个工作日审核完毕。实际上还挺快的半天就审核完了,下午我们就收到了开发者账号注册成功的邮件。 图片.png

总算是注册成功了。

划重点:

  • 确保使用未曾注册过(旧测试机就不要用了,否则还浪费身份证名额);
  • 注册过程中千万不要换设备,遇到任何问题联系苹果解决;
  • 街道地址填身份证地址。

最后,祝大家注册顺利,少踩坑。

Flutter 3.38.1 之后,因为某些框架低级错误导致提交 Store 被拒

2026年1月5日 09:24

如果你近期已经升级到 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 里的 Asset Transformer 和 Hooks ,这个实验性功能有什么用》

那它怎么导致了这次这个低级问题的出现?实际上这是一个构建脚本逻辑缺陷导致的“脏构建”问题,当 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 构建
  • 构建脚本会把之前遗留的“模拟器框架”也一并复制进了 Release 包
  • App Store 检测到 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 状态管理详解

作者 妖枪银弹
2026年1月4日 16:31

一、 GetX 状态管理核心机制

GetX 的状态管理并非单一方案,而是提供了三种核心状态管理模式,兼顾简洁性和灵活性,适配不同业务场景,其核心设计围绕「轻量、无侵入、响应式」展开。

1. 简单状态管理(GetBuilder:非响应式)

适用于简单的状态更新场景(如按钮点击刷新文本、列表局部更新),基于手动触发重建实现,无响应式依赖,性能开销极低。

  • 核心原理:
    1. 定义 Controller 继承 GetxController,在控制器中维护状态变量,并提供状态更新方法。
    2. 通过 update() 方法手动标记状态变更,通知对应的 GetBuilder 进行组件重建。
    3. 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("点击增加"),
          ),
        ],
      );
    },
  );
}

2. 响应式状态管理(GetX / Obx:自动响应式)

适用于复杂状态依赖场景(如网络请求结果刷新、多组件共享状态、实时数据同步),基于Dart 扩展方法+观察者模式实现,无需手动调用 update(),状态变更自动触发 UI 重建。

  • 核心原理:
    1. 状态变量通过 .obs 扩展方法转化为「可观察对象(Observable)」,GetX 会监听该对象的所有变更。
    2. 使用 Obx(或 GetX)包裹需要响应状态变更的 UI 组件,Obx 作为观察者,订阅可观察对象的状态变化。
    3. 当可观察对象的值发生改变时,会自动通知所有订阅它的 Obx 组件,触发局部重建,无需全局刷新。
  • 核心特性:
    • 无需继承 GetxController(可直接使用全局变量,也可结合控制器管理)。
    • 支持多种数据类型:基本类型(int/string/bool)、集合类型(List/Map/Set)、自定义对象。
    • 局部重建:仅 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("更新状态"),
      ),
    ],
  );
}

3. 依赖注入式状态管理(Get.put / Get.find:生命周期管理)

GetX 状态管理的核心支撑能力,通过内置依赖注入(DI)容器管理控制器生命周期,无需手动创建和销毁控制器,实现状态的全局共享或局部共享。

  • 核心原理:
    1. Get.put(Controller()):将控制器实例存入 GetX 的 DI 容器,默认全局单例,可指定 tag 实现多实例,或指定 permanent: false 实现自动销毁。
    2. Get.find<Controller>():从 DI 容器中获取已存入的控制器实例,无需跨组件传递。
    3. 生命周期绑定:控制器继承 GetxController 后,可重写 onInit()onReady()onClose() 方法,对应组件的初始化、就绪、销毁生命周期,自动执行。
  • 核心特性:
    • 无需 InheritedWidget 包裹(对比 Provider),无组件嵌套冗余。
    • 支持局部状态:通过 Get.create() 或在路由中传入 binding,实现路由级别的局部状态,路由销毁时自动销毁控制器。

二、 GetX 与 Provider、Bloc 的核心对比

1. 核心设计差异

特性 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输出,单向数据流)

2. GetX 的优势

(1) 极致简洁,开发效率极高

  • 代码量极少:无需编写大量模板代码(对比 Bloc 的 Event/State/Bloc 三层模板,Provider 的 ChangeNotifier 子类+Consumer 包裹),大幅减少冗余代码。
    • 示例:实现一个计数功能,GetX 只需 10 行左右代码,Bloc 需编写 Event(CountIncrementEvent)、State(CountState)、Bloc(CountBloc)三层代码,代码量增加 3-5 倍。
  • 无嵌套地狱:无需顶层包裹 MultiProvider/MultiBlocProvider,解决了 Provider/Bloc 中多层嵌套导致的代码可读性差的问题。
  • 无需上下文(Context):通过 Get.find() 可在任意位置获取控制器,无需传递 BuildContext,尤其在工具类、网络请求类中使用便捷。

(2) 功能全面,一站式解决方案

  • 状态管理 + 路由管理 + 依赖注入 + 国际化 + 主题管理:GetX 并非单一状态管理库,而是集成了 Flutter 开发所需的核心功能,无需引入多个第三方库(如 Provider 需配合 flutter_routeget_it 实现路由和DI),减少库之间的兼容性问题。
  • 灵活适配场景:简单场景用 GetBuilder(性能最优),复杂场景用 Obx(自动响应式),全局共享用 Get.put(单例),局部共享用路由 Binding(自动销毁),适配所有业务场景。

(3) 性能更优,资源开销更低

  • 局部重建更精准:Obx/GetBuilder 仅刷新自身包裹的组件,且无需遍历 InheritedWidget 树(对比 Provider),减少了布局遍历的性能开销。
  • 控制器自动销毁:GetX 可根据路由生命周期自动销毁控制器(permanent: false),避免 Provider/Bloc 中手动管理控制器生命周期导致的内存泄漏问题。
  • 无 Stream 额外开销:GetX 的响应式状态管理基于自定义 Observable,无需像 Bloc 那样依赖 Stream 流处理,减少了 Stream 订阅/取消订阅的性能开销。

(4) 学习成本更低

  • 无需掌握 Stream 原理:Bloc 强依赖 Dart Stream 知识,对于新手而言,学习成本较高;GetX 无需了解 Stream,只需掌握 .obsObx 的使用,即可快速上手。
  • API 设计简洁直观:GetX 的 API 命名清晰(如 Get.put、Get.find、update、Obx),符合开发者的使用习惯,无需记忆复杂的 API 结构。

3. GetX 的劣势

(1) 状态管理过于灵活,团队协作易出规范问题

  • 由于 GetX 支持全局响应式变量(无需控制器)、控制器管理状态、局部状态等多种方式,若团队无统一开发规范,容易出现状态分散、难以维护的问题(如部分状态在全局变量中,部分在控制器中,排查问题时难以定位)。
  • 对比 Bloc:Bloc 的 Event/State 严格分离,强制遵循单向数据流,团队协作时规范统一,即使是大型项目,状态流转也清晰可追溯;Provider 虽灵活,但依赖 InheritedWidget,状态范围相对明确。

(2) 生态成熟度略低于 Provider、Bloc

  • Provider:作为 Flutter 生态中最早的状态管理库之一,几乎被所有 Flutter 开发者熟知,相关教程、插件、问题解决方案极为丰富,且与 Flutter 原生组件深度兼容。
  • Bloc:由 Google 团队成员参与维护,生态成熟,支持 bloc_test(单元测试)、flutter_bloc(Flutter 适配)、bloc_concurrency(并发处理)等周边库,在大型企业级项目中应用广泛。
  • GetX:生态虽在快速发展,但相比 Provider、Bloc,部分小众场景的解决方案较少,且第三方插件对 GetX 的适配度略低。

(3) 不适用于对状态流转有严格要求的大型项目

  • 对于金融、电商等大型企业级项目,状态流转的可追溯性、可测试性要求极高:
    • Bloc 的 Event/State 分离模式,每一次状态变更都对应一个明确的 Event,可通过 bloc_test 轻松编写单元测试,且能通过 DevTools 追踪状态流转过程,便于问题排查。
    • GetX 的状态更新方式(直接调用方法修改状态),虽然简洁,但状态变更的触发来源难以追溯,单元测试需要额外编写更多代码来模拟状态变更,对于大型项目的维护性略有不足。

(4) 存在“过度封装”的争议,底层可定制性弱

  • GetX 为了追求简洁,对底层实现进行了大量封装,开发者难以自定义其响应式机制、依赖注入容器的行为。
  • 对比 Provider:基于 Flutter 原生 InheritedWidgetChangeNotifier 实现,开发者可轻松扩展 ChangeNotifier 或自定义 InheritedWidget,实现个性化需求。
  • 对比 Bloc:基于 Stream 实现,开发者可灵活自定义 Stream 控制器、并发策略、状态转换逻辑,底层可定制性极强。

(5) 调试体验略逊于 Bloc

  • Bloc 提供了专门的 BlocDevTools,可实时监控 Event 发送、State 变更、Bloc 生命周期,便于调试复杂的状态流转问题。
  • GetX 无官方专属调试工具,状态变更的监控需要手动打印日志或借助 Flutter DevTools,对于复杂的响应式状态依赖,调试效率略低。

4. 补充:Provider、Bloc 的各自优势(对应 GetX 的劣势)

  • Provider 优势
    1. 与 Flutter 原生深度融合,学习成本低(只需掌握 InheritedWidget 基础概念)。
    2. 轻量、稳定,无多余功能,专注于状态管理,适合小型项目或对第三方库依赖敏感的项目。
    3. 生态成熟,问题解决方案丰富,新手友好。
  • Bloc 优势
    1. 严格的单向数据流,状态流转清晰可追溯,适合大型团队协作和企业级项目。
    2. 强大的测试能力,便于编写单元测试和集成测试,保证代码质量。
    3. 高度可定制化,可灵活扩展底层逻辑,适配复杂业务场景。
    4. 官方支持完善,调试工具成熟,生产环境稳定性更高。

三、 选型建议

  1. 优先选 GetX:小型项目、快速迭代项目、个人项目,或团队追求开发效率、希望减少模板代码的场景;适合需要一站式解决状态管理、路由、DI 的场景。
  2. 优先选 Provider:新手入门、对第三方库功能冗余敏感的项目,或需要与 Flutter 原生组件深度兼容的场景;适合小型到中型项目。
  3. 优先选 Bloc:大型企业级项目、金融/电商等对状态可追溯性和可测试性要求极高的场景,或团队需要严格编码规范的场景;适合中型到大型项目。

总结

  1. GetX 状态管理的核心是「3种模式+依赖注入」,兼顾简洁性和灵活性,Obx(响应式)和 GetBuilder(非响应式)适配不同场景,依赖注入实现状态全局/局部共享。
  2. GetX 的核心优势是「开发效率高、代码简洁、功能全面、性能优」,劣势是「规范性弱、生态成熟度略低、大型项目维护性不足」。
  3. Provider 胜在「原生兼容、轻量稳定」,Bloc 胜在「规范严格、可测试性强、大型项目友好」,GetX 胜在「高效简洁、一站式解决方案」,需根据项目规模和团队需求选型。

Flutter 项目启动全流程详解

作者 妖枪银弹
2026年1月4日 13:18

Flutter 项目启动全流程详解

作为资深 Flutter 架构师,我会从分层视角(原生层 → Flutter 引擎层 → Dart 运行时层 → App 业务层)为你拆解 Flutter 项目启动的完整流程,涵盖核心步骤、关键机制和底层细节,帮你全面掌握启动原理。

一、 第一阶段:原生平台初始化(Native Bootstrapping)

Flutter 是跨平台框架,最终会打包为 iOS/Android 原生应用,启动流程首先从原生平台侧开始,这是 Flutter 启动的入口。

1. Android 平台启动流程

  • 核心入口:FlutterActivity(或 FlutterFragment,对应 Fragment 嵌入场景)
  • 关键步骤:
    1. FlutterActivity 初始化:继承自 AppCompatActivity,启动时先执行原生 Android 的 onCreate() 生命周期方法。
    2. 加载 Flutter 引擎依赖:初始化 FlutterEngine 相关配置(如 Dart 入口路径、初始化参数),若使用预加载引擎(提前初始化优化启动速度),会直接获取预创建的 FlutterEngine 实例;若未预加载,则现场创建 FlutterEngine
    3. 配置 FlutterView:创建用于承载 Flutter UI 的原生 View(FlutterView),并将其挂载到 Android 布局层级中,作为 Flutter 渲染内容的显示载体。
    4. 启动引擎桥接:通过 FlutterNativeView 建立原生 Android 与 Flutter 引擎的通信通道,传递初始化参数(如屏幕尺寸、系统主题、原生平台信息等)。

2. iOS 平台启动流程

  • 核心入口:FlutterViewController(对应 iOS 的视图控制器)
  • 关键步骤:
    1. FlutterViewController 初始化:执行 iOS 原生的 initWithNibName:bundle:init 方法,完成控制器自身初始化。
    2. Flutter 引擎初始化:创建 FlutterEngine 实例(同样支持预加载优化),配置 Dart 执行环境参数。
    3. 绑定视图载体:FlutterViewController 的视图(view 属性)本质是 FlutterView,用于渲染 Flutter UI,完成视图层级挂载。
    4. 建立通信通道:通过 FlutterMethodChannel/FlutterEventChannel 的底层初始化,完成 iOS 原生与 Flutter 引擎的双向通信准备。

核心作用

  • 提供 Flutter 运行的原生容器环境(Activity/ViewController + View);
  • 初始化 Flutter 引擎的原生依赖,建立跨平台通信的基础通道;
  • 传递系统级参数(如设备信息、屏幕参数),为 Flutter 后续初始化提供上下文。

二、 第二阶段:Flutter 引擎初始化(Engine Initialization)

Flutter 引擎(C/C++ 实现,核心是 Skia 渲染引擎、Dart 虚拟机、排版引擎等)是 Flutter 的核心运行时,原生平台初始化完成后,会触发 Flutter 引擎的启动与初始化。

1. 引擎核心组件初始化

Flutter 引擎初始化是多组件协同启动的过程,核心组件包括:

  • Skia 渲染引擎:初始化 2D 图形渲染上下文,绑定原生平台的渲染表面(Android 的 Surface、iOS 的 CALayer),配置抗锯齿、渲染精度等参数,为后续 UI 渲染提供底层支持。
  • Dart 虚拟机(VM):初始化 Dart 运行时环境,包括内存管理(堆内存分配、垃圾回收机制初始化)、指令解析器(JIT/AOT 模式切换,移动端默认 AOT 编译模式,调试模式 JIT)。
  • 排版引擎(Layout Engine):初始化 Flutter 专属的排版规则(如 Flex 布局、Text 排版),加载系统默认字体、自定义字体配置,建立排版计算的上下文环境。
  • 事件分发系统:初始化触摸事件、键盘事件、生命周期事件的分发通道,确保原生事件能传递到 Flutter 层。

2. 引擎与原生平台的绑定

  • 渲染绑定:将 Skia 渲染引擎与原生 FlutterView 的渲染缓冲区绑定,确保 Flutter 绘制的内容能显示在原生视图上。
  • 线程绑定:Flutter 引擎启动后会创建4个核心线程,并与原生平台线程建立映射:
    1. UI 线程(Platform Thread):对应原生主线程,处理 Flutter 组件构建、布局计算、状态更新。
    2. 渲染线程(Render Thread):独立线程,处理绘制命令生成、图层合成,最终将合成结果提交给 Skia 渲染。
    3. I/O 线程(I/O Thread):处理异步任务(如网络请求、文件读写、图片解码),避免阻塞 UI 线程和渲染线程。
    4. Dart 虚拟机线程:执行 Dart 代码逻辑,与 UI 线程协同工作(调试模式下独立,Release 模式下与 UI 线程合并优化)。

三、 第三阶段:Dart 运行时初始化(Dart Runtime Setup)

Flutter 引擎初始化完成后,会启动 Dart 虚拟机,并执行 Dart 代码的初始化流程,这是 Flutter 业务逻辑的入口。

1. 加载并执行 Dart 根隔离区(Root Isolate)

  • Isolate 是 Dart 的轻量级线程(无共享内存,通过消息传递通信),Flutter 启动时首先创建根 Isolate(主 Isolate),作为 Dart 代码的执行入口。
  • 核心操作:
    1. 加载 AOT 编译产物(Release 模式):移动端 Flutter 项目打包后会生成 .so(Android)/ App.framework(iOS)格式的 AOT 编译产物,Dart 虚拟机直接加载并执行该产物,无需即时编译,启动速度更快。
    2. 加载 JIT 快照(Debug 模式):调试模式下,Flutter 会生成 Dart 代码的 JIT 快照,Dart 虚拟机加载快照后启动,支持热重载(Hot Reload)功能。

2. 执行 main() 函数(Dart 入口)

  • 根 Isolate 初始化完成后,会自动执行 Dart 项目的 main() 函数,这是 Flutter 业务代码的第一个入口方法,典型代码如下:
void main() {
  // 可选:初始化全局配置(如网络拦截器、日志工具、依赖注入)
  WidgetsFlutterBinding.ensureInitialized(); // 关键:初始化 Flutter 核心绑定
  runApp(const MyApp()); // 启动 Flutter 应用
}
  • 关键说明:WidgetsFlutterBinding.ensureInitialized() 是 Flutter 核心绑定初始化方法,若省略,runApp() 内部会自动调用,其作用是初始化 Flutter 框架的核心服务(如渲染绑定、手势绑定、生命周期绑定等)。

四、 第四阶段:Flutter 框架初始化与 UI 渲染(Framework & UI Rendering)

main() 函数中调用 runApp() 后,进入 Flutter 框架初始化和 UI 首次渲染流程,这是 Flutter UI 显示的核心步骤。

1. Flutter 框架核心绑定初始化

WidgetsFlutterBinding 是 Flutter 框架的核心绑定类,它整合了 7 大核心绑定,确保 Flutter 框架正常工作:

  • GestureBinding:手势识别与事件分发绑定。
  • ServicesBinding:平台消息通信绑定(如 MethodChannel 通信)。
  • SchedulerBinding:任务调度与帧回调绑定(控制 UI 刷新帧率,默认 60fps)。
  • PaintingBinding:绘制相关绑定(如图片缓存、字体加载)。
  • SemanticsBinding:语义化绑定(支持无障碍访问)。
  • RenderBinding:渲染管线绑定(布局、绘制、合成)。
  • WidgetsBinding:组件框架绑定(组件构建、状态管理、路由管理)。

2. 执行 runApp(Widget app) 核心逻辑

runApp() 是 Flutter UI 启动的关键方法,核心操作如下:

  1. 挂载根组件:将传入的根 Widget(如 MyApp)设置为 Flutter 框架的根组件(rootWidget),建立组件树的顶层节点。
  2. 触发首次帧调度:通过 SchedulerBinding 向 Flutter 引擎发送「首次绘制帧」的调度请求,引擎接收到请求后,启动 UI 线程的布局与绘制流程。

3. 首次 UI 渲染管线(Layout & Paint)

Flutter 首次渲染遵循「构建 → 布局 → 绘制 → 合成 → 渲染」的流水线:

  1. 构建(Build):UI 线程遍历组件树(从根 Widget MyApp 开始),执行每个 Widget 的 build() 方法,生成「元素树(Element Tree)」(Widget 是配置模板,Element 是实际渲染实例)。
  2. 布局(Layout):基于元素树,Render 层(RenderObject)执行布局计算,确定每个组件的大小、位置(如 RenderFlex 处理 Flex 布局,RenderText 处理文本排版),生成「布局树(Layout Tree)」。
  3. 绘制(Paint):Render 层根据布局结果,生成每个组件的绘制命令(如绘制矩形、文本、图片),生成「绘制树(Paint Tree)」。
  4. 合成(Compositing):渲染线程将绘制命令按「图层(Layer)」进行分层合成(如透明组件、滚动组件会单独分层),生成「图层树(Layer Tree)」,优化渲染性能。
  5. 渲染(Render):渲染线程将合成后的图层树提交给 Skia 渲染引擎,Skia 将图层绘制到原生 FlutterView 的渲染缓冲区,最终在屏幕上显示 Flutter UI。

4. 启动完成:触发 onFirstFrame 回调

当 Flutter 首次帧渲染完成后,会触发 WidgetsBindingonFirstFrame 回调(可监听该回调统计启动耗时),此时用户可以看到 Flutter 应用的首屏 UI,标志着 Flutter 项目启动流程全部完成。

五、 补充:启动模式差异(Debug vs Release)

Flutter 调试模式(Debug)和发布模式(Release)的启动流程存在核心差异,直接影响启动速度:

对比维度 Debug 模式 Release 模式
Dart 执行模式 JIT(即时编译) AOT(提前编译)
产物加载 加载 Dart 快照,支持热重载 加载 AOT 编译产物(.so/Framework),直接执行
引擎优化 关闭部分渲染优化、线程优化 开启全量优化(线程合并、绘制优化、内存优化)
启动速度 较慢(JIT 初始化 + 快照加载) 较快(AOT 产物直接执行,无编译开销)
额外功能 支持 Hot Reload、DevTools 调试 无调试功能,体积更小、性能更优

总结

Flutter 项目启动是一个跨平台、分层级、多线程协同的复杂流程,核心步骤可概括为 4 个关键阶段:

  1. 原生平台初始化:提供容器(Activity/ViewController)和视图载体(FlutterView),初始化引擎依赖;
  2. Flutter 引擎初始化:启动 Skia 渲染、Dart 虚拟机等核心组件,创建 4 个核心线程,完成与原生的绑定;
  3. Dart 运行时初始化:创建根 Isolate,加载 Dart 编译产物,执行 main() 函数;
  4. Flutter 框架与 UI 渲染:初始化框架绑定,执行 runApp(),通过「构建-布局-绘制-合成-渲染」管线完成首屏显示。

理解该流程有助于你优化 Flutter 项目启动速度(如预加载 Flutter 引擎、延迟初始化非核心业务、优化首屏 Widget 构建),以及排查启动阶段的跨平台兼容问题。

十倍性能优化!一次终端语法高亮库的 AI 折腾与收获

2026年1月4日 15:39
我最近写了一个小框架 Chroma,用 Swift 在终端里做代码高亮;顺手还以它为基底做了一个(实验性的)cat 替代品 ca,能以带高亮的方式在终端里显示代码文本内容 (几乎和 bat 一样,只是又一个“I can, why not”的项目)。这篇文章想做三件事:先简单宣传一下(真的很短),然后重点聊聊这次实践中的主要收获:在 AI 驱动的迭代方式下,把性能优化这件事做“到底”变得前所未有地容易;最后再补一些在做 ca 期间学到的命令行设计和主题生态方面的东西。 Chroma / ca:一个很小的 promotion Chroma 的目标非常朴素:给它一段代码和一个语言标识,它就返回一段可以直接 print 的 ANSI 彩色字符串。 语法高亮这种东西其实早就被写烂了:Rust 有 syntect,Python 有 Pygments,前端世界里更是 highlight.js 一类的工具满天飞。但我在 Swift 生态里一直没找到一个足够顺手、又能对终端输出细节(diff / 行号 / 行背景 / 缩进)有足够掌控力的选择,于是干脆自己(准确地说:靠 AI)糊了一个。...

2026 码农漫游:AI 辅助 Swift 代码修复指南

2026年1月4日 11:28

在这里插入图片描述

☔️ 引子

这是一个雨夜,霓虹灯的光晕在脏兮兮的窗玻璃上晕开,像极了那个该死的 View Hierarchy 渲染不出高斯模糊的样子。

在新上海的地下避难所里,老王(Old Wang)吐出一口合成烟雾,盯着全息屏幕上不断报错的终端。作为人类反抗军里仅存的几位「精通 Apple 软件开发」的工程师之一,他负责给 AI 霸主「智核(The Core)」生成的垃圾代码擦屁股。

在这里插入图片描述

门被撞开了,年轻的女黑客莉亚(Liya)气喘吁吁地冲进来,手里攥着一块存满代码的神经晶片。“老王!救命!‘智核’生成的 SwiftUI 代码在 iOS 26 上又崩了!反抗军的通讯 App 根本跑不起来!”

老王冷笑一声,掐灭了烟头。“我就知道。那些被捧上神坛的 LLM(大型语言模型),不管是 Claude、Codex 还是 Gemini,写起 Python 来是把好手,但一碰到 Swift,就像是穿着溜冰鞋走钢丝——步步惊心。”

在本篇博文中,您将学到如下内容:

  • ☔️ 引子
    • 🤖 为什么 AI 总是在 Swift 上「鬼打墙」?
    • 🎨 1. 别再用过时的调色盘了
    • 📐 2. 只有切掉棱角,才能圆滑处世
    • 🔄 3. 监控变化,不要缺斤少两
    • 📑 4. 标签页的「指鹿为马」
    • 👆 5. 别什么都用「戳一戳」
    • 🧠 6. 扔掉旧时代的观察者
    • ☁️ 7. 数据的陷阱
    • 📉 8. 性能的隐形杀手
    • 🔠 9. 字体排印的法西斯
    • 🔗 10. 导航的死胡同
    • 🏷️ 11. 按钮的自我修养
    • 🔢 12. 数组的画蛇添足
    • 📂 13. 寻找文件的捷径
    • 🧭 14. 导航栈的改朝换代
    • 💤 15. 睡个好觉
    • 🧮 16. 格式化的艺术
    • 🏗️ 17. 不要把鸡蛋放在一个篮子里
    • 🖼️ 18. 渲染的新欢
    • 🏋️ 19. 字重的迷惑行为
    • 🚦 20. 并发的万金油(也是毒药)
    • 🎭 21. 主角光环是默认的
    • 📐 22. 几何的诅咒
  • 尾声:数字幽灵的低语

他把晶片插入接口,全息投影在空中展开。“坐下,莉亚。今天我就给你上一课,让你看看所谓的‘人工智能’是如何在 Swift 的并发地狱快速迭代中翻车的。”

在这里插入图片描述


🤖 为什么 AI 总是在 Swift 上「鬼打墙」?

老王指着屏幕上乱成一锅粥的代码说道:“这不怪它们。Swift 和 SwiftUI 的进化速度比变异病毒还快。再加上 Python 和 JavaScript 的训练数据浩如烟海,而 Swift 的高质量语料相对较少,AI 常常会产生幻觉。更别提 Swift 的 Concurrency(并发) 模型,连人类专家都头秃,更别说这些只会概率预测的傻大个了。”

在这里插入图片描述

“听着,莉亚,”老王严肃地说,“要想在 iOS 18 甚至更高版本的废土上生存,你必须学会识别这些‘智障操作’。我们不谈哲学,只谈生存。以下就是我从死人堆里总结出来的代码排雷指南。”


🎨 1. 别再用过时的调色盘了

💀 AI 的烂代码: foregroundColor() ✨ 老王的修正: foregroundStyle()

“看这里,”老王指着一行代码,“AI 还在用 foregroundColor()。这就像是还在用黑火药做炸弹。虽然字数一样,但前者已经是个行将就木的Deprecated API。把它换成 foregroundStyle()!后者才是未来,它支持渐变(Gradients)等高级特性。别让你的 UI 看起来像上个世纪的产物。”

在这里插入图片描述

📐 2. 只有切掉棱角,才能圆滑处世

💀 AI 的烂代码: cornerRadius() ✨ 老王的修正: clipShape(.rect(cornerRadius:))

“又是一个老古董。cornerRadius() 早就该进博物馆了。现在的标准是使用 clipShape(.rect(cornerRadius:))。为什么?因为前者是傻瓜式圆角,后者能让你通过 uneven rounded rectangles(不规则圆角矩形)玩出花来。在这个看脸的世界,细节决定成败。”

🔄 3. 监控变化,不要缺斤少两

💀 AI 的烂代码: onChange(of: value) { ... } (单参数版本) ✨ 老王的修正: onChange(of: value) { oldValue, newValue in ... }

老王皱起眉头:“这个 onChange 修改器,AI 经常只给一个参数闭包。这在旧版本是‘不安全’的,现在已经被标记为弃用。要么不传参,要么接受两个参数(新旧值)。别搞得不清不楚的,容易出人命。”

在这里插入图片描述

📑 4. 标签页的「指鹿为马」

💀 AI 的烂代码: tabItem() ✨ 老王的修正: 新的 Tab API

“如果看到老旧的 tabItem(),立刻把它换成新的 Tab API。这不仅仅是为了所谓的‘类型安全(Type-safe)’,更是为了适配未来——比如那个传闻中的 iOS 26 搜索标签页设计。我们要领先‘智核’一步,懂吗?”

👆 5. 别什么都用「戳一戳」

💀 AI 的烂代码: 滥用 onTapGesture() ✨ 老王的修正: 使用真正的 Button

“AI 似乎觉得万物皆可 onTapGesture()。大错特错!除非你需要知道点击的具体坐标或者点击次数,否则统统给我换成标准的 Button。这不仅是为了让 VoiceOver(旁白)用户能活下去,也是为了让 visionOS 上的眼球追踪能正常工作。别做一个对残障人士不友好的混蛋。”

🧠 6. 扔掉旧时代的观察者

💀 AI 的烂代码: ObservableObject ✨ 老王的修正: @Observable

“莉亚,看着我的眼睛。除非你对 Combine 框架有什么特殊的各种癖好,否则把所有的 ObservableObject 都扔进焚化炉,换成 @Observable 宏。代码更少,速度更快,这就好比从燃油车换成了核动力战车。”

在这里插入图片描述

☁️ 7. 数据的陷阱

💀 AI 的烂代码: SwiftData 模型中的 @Attribute(.unique) ✨ 老王的修正: 小心使用!

“这是一个隐蔽的雷区。如果在 SwiftData 模型定义里看到 @Attribute(.unique),你要警惕——这玩意儿跟 CloudKit 八字不合。别到时候数据同步失败,你还在那儿傻乎乎地查网络连接。”

📉 8. 性能的隐形杀手

💀 AI 的烂代码: 将视图拆分为「计算属性(Computed Properties)」 ✨ 老王的修正: 拆分为独立的 SwiftUI Views

“为了图省事,AI 喜欢把大段的 UI 代码塞进计算属性里。这是尸位素餐!尤其是在使用 @Observable 时,计算属性无法享受智能视图失效(View Invalidation)的优化。把它们拆分成独立的 SwiftUI 结构体!虽然麻烦点,但为了那 60fps 的流畅度,值得。”

🔠 9. 字体排印的法西斯

💀 AI 的烂代码: .font(.system(size: 14)) ✨ 老王的修正: Dynamic Type (动态字体)

“有些 LLM(尤其是那个叫 Claude 的家伙)简直就是字体界的独裁者,总喜欢强行指定 .font(.system(size: ...))。给我搜出这些毒瘤,全部换成 Dynamic Type。如果是 iOS 26+,你可以用 .font(.body.scaled(by: 1.5))。记住,用户可能眼花,别让他们看瞎了。”

在这里插入图片描述

🔗 10. 导航的死胡同

💀 AI 的烂代码: 列表里的内联 NavigationLink ✨ 老王的修正: navigationDestination(for:)

“在 List 里直接写 NavigationLink 的目标地址?那是原始人的做法。现在的文明人使用 navigationDestination(for:)。解耦!解耦懂不懂?别把地图画在脚底板上。”


老王喝了一口已经凉透的咖啡,继续在这堆赛博垃圾中挖掘。

🏷️ 11. 按钮的自我修养

💀 AI 的烂代码:Label 做按钮内容 ✨ 老王的修正: 内联 API Button("Title", systemImage: "plus", action: ...)

“期待看到 AI 用 Label 甚至纯 Image 来做按钮内容吧——这对 VoiceOver 用户来说简直是灾难。用新的内联 API:Button("Tap me", systemImage: "plus", action: whatever)。简单,粗暴,有效。”

🔢 12. 数组的画蛇添足

💀 AI 的烂代码: ForEach(Array(x.enumerated()), ...) ✨ 老王的修正: ForEach(x.enumerated(), ...)

“看到这个 Array(x.enumerated()) 了吗?这就是脱裤子放屁。直接用 ForEach(x.enumerated(), ...) 就行了。省点内存吧,虽然现在的内存不值钱,但程序员的尊严值钱。”

在这里插入图片描述

📂 13. 寻找文件的捷径

💀 AI 的烂代码: 冗长的文件路径查找代码 ✨ 老王的修正: URL.documentsDirectory

“那些又臭又长的查找 Document 目录的代码,统统删掉。换成 URL.documentsDirectory。一行代码能解决的事,绝不写十行。”

🧭 14. 导航栈的改朝换代

💀 AI 的烂代码: NavigationView ✨ 老王的修正: NavigationStack

NavigationView 已经死了,有事烧纸。除非你要支持 iOS 15 那个上古版本,否则全部换成 NavigationStack。”

💤 15. 睡个好觉

💀 AI 的烂代码: Task.sleep(nanoseconds:) ✨ 老王的修正: Task.sleep(for: .seconds(1))

“‘智核’ 似乎很喜欢纳秒,可能它觉得自己算得快。但你要用 Task.sleep(for:),配合 .seconds(1) 这种人类能读懂的单位。别再像个僵尸一样数纳秒了。”

在这里插入图片描述

🧮 16. 格式化的艺术

💀 AI 的烂代码: C 风格格式化 String(format: "%.2f", ...) ✨ 老王的修正: Swift 原生格式化 .formatted()

“我知道 C 风格的字符串格式化很经典,但它不安全。把它换成 Swift 原生的 Text(abs(change), format: .number.precision(.fractionLength(2)))。虽然写起来长一点,但它像穿了防弹衣一样安全。”

🏗️ 17. 不要把鸡蛋放在一个篮子里

💀 AI 的烂代码: 单个文件塞入大量类型 ✨ 老王的修正: 拆分文件

“AI 喜欢把几十个 struct 和 class 塞进一个文件里,这简直是编译时间毁灭者。拆开它们!除非你想在编译的时候有时间去煮个满汉全席。”

🖼️ 18. 渲染的新欢

💀 AI 的烂代码: UIGraphicsImageRenderer ✨ 老王的修正: ImageRenderer

“如果你在渲染 SwiftUI 视图,别再用 UIKit 时代的 UIGraphicsImageRenderer 了。拥抱 ImageRenderer 吧,这是它的主场。”

在这里插入图片描述

🏋️ 19. 字重的迷惑行为

💀 AI 的烂代码: 滥用 fontWeight() ✨ 老王的修正: 区分 bold()fontWeight(.bold)

“三大 AI 巨头都喜欢滥用 fontWeight()。记住,fontWeight(.bold)bold() 渲染出来的结果未必一样。这就像‘微胖’和‘壮实’的区别,微妙但重要。”

🚦 20. 并发的万金油(也是毒药)

💀 AI 的烂代码: DispatchQueue.main.async ✨ 老王的修正: 现代并发模型

“一旦 AI 遇到并发问题,它就会像受惊的鸵鸟一样把头埋进 DispatchQueue.main.async 里。这是不可原谅的懒惰!那是旧时代的创可贴,现在的我们有更优雅的 Actor 模型。”

🎭 21. 主角光环是默认的

💀 AI 的烂代码: 到处加 @MainActor ✨ 老王的修正: 默认开启

“如果你在写新 App,Main Actor 隔离通常是默认开启的。不用像贴符咒一样到处贴 @MainActor。”

在这里插入图片描述

📐 22. 几何的诅咒

💀 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-)

Swift 6.2 列传(第十五篇):王语嫣的《万剑归宗》与 InlineArray

2026年1月4日 11:24

在这里插入图片描述

摘要:当动态数组的随性(Array)与元组的刻板(Tuple)陷入两难,高性能的内联数组(InlineArray)横空出世。Swift 6.2 引入的 SE-0453 就像是武学中的“万剑归宗”,旨在以固定大小的内存布局,解决性能与灵活性的鱼和熊掌不可兼得之困。

0️⃣ 🐼 序章:琅嬛福地的“内存”迷局

琅嬛福地,天山童姥遗留的虚拟数据中心。

这里是存储着天下所有数据结构秘籍的宝库。大熊猫侯佩穿梭在巨大的全息卷轴之间,背景音乐是《天龙八部》的BGM,他一边走一边摸了摸头顶——黑毛油光锃亮,头绝对不秃,安全感十足。

他之所以来这,是因为上一回任务结束后,他仍对竹笋的存储问题耿耿于怀。

“我那四根‘镇山之宝’级竹笋,必须以最快的速度取用!”侯佩对着一卷写着 Array 的秘籍大声嚷道,“用普通的 Array,虽然方便,但那动态分配内存的方式,让我每次取笋都感觉像是在丐帮的袋子里掏东西,随性得很,但太慢了!

“若追求极致速度,何不用 Tuple(元组)?”

在这里插入图片描述

一个清冷的声音传来。侯佩循声望去,只见一位容貌比数据流还精致的少女站在光影之中,她皓齿明眸,手中正拿着一本《九阴真经》的代码版本,正是王语嫣

王语嫣,熟读天下武学秘籍,对各种数据结构了如指掌。她的爱好就是整理和分类这些代码秘籍,特点是理论知识丰富到可以开宗立派,但从未亲手实践(编写)过一行代码

“王姑娘!”侯佩的眼睛瞬间变成了心形(花痴属性发作),“元组是快,它内存连续且固定,但您看,我如果要用下标循环遍历我的四根竹笋,myTuple.0myTuple.1……这写法简直是望洋兴叹,既不优雅,又不支持循环!”

在本次冒险中,您将学到如下内容:

  • 0️⃣ 🐼 序章:琅嬛福地的“内存”迷局
  • 1️⃣ 💥 混血秘籍:InlineArray 的诞生 (SE-0453)
  • 2️⃣ 📜 固若金汤:InlineArray 的使用法门
    • 招式一:明确指定大小与类型
    • 招式二:让编译器“心领神会”
    • 下标读写:行云流水
  • 3️⃣ 🚧 乾坤已定:固定大小的代价
    • 迭代之困:不入流的限制
  • 4️⃣ 🐼 熊猫的黑色幽默与抉择
  • 5️⃣ 🛑 尾声:慕容复的伪装与“向后看”的难题

王语嫣轻轻叹了一口气:“是啊,鱼和熊掌不可兼得。内存布局(Performance)和下标访问(Usability),历来是程序员江湖的千年难题。”

在这里插入图片描述


1️⃣ 💥 混血秘籍:InlineArray 的诞生 (SE-0453)

就在侯佩和王语嫣陷入技术哲学的死循环时,一道新的全息卷轴从天而降,正是 SE-0453 秘籍。

“快看,这是最新的‘混血’数据结构,”侯佩激动地喊道,“它叫 InlineArray(内联数组),它把元组的‘固定大小’与数组的‘自然下标’完美地融合了!”

InlineArray 最核心的奥义在于:它将固定数量的元素直接存储在结构体内部,没有动态分配的开销,从而实现了结构体级别的内存连续性媲美 C 语言数组的存取速度

💡 前置条件(SE-0452): 要实现这种固定大小的泛型(Generic),Swift 6.2 还必须引入另一个重要的前提:SE-0452:Integer Generic Parameters(整数泛型参数)。这使得我们可以用一个整数值来约束泛型类型的大小,比如 InlineArray<4, String> 中的 4,这在以前的 Swift 版本中是无法想象的。

在这里插入图片描述

2️⃣ 📜 固若金汤:InlineArray 的使用法门

王语嫣作为理论大师,立刻解析了这段秘籍。

创建 InlineArray 有两种法门:

招式一:明确指定大小与类型

我们可以像使用泛型一样,明确告知编译器:“我要一个固定大小为 4String 类型数组。”

// 明确告诉编译器:我要一个固定大小为 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" // 火星变木星,改写数据,毫不费力

在这里插入图片描述

3️⃣ 🚧 乾坤已定:固定大小的代价

王语嫣很快指出了这种“神功”的限制:“侯大哥,此功法虽然内力雄厚(性能卓越),但限制也多。既然是固定大小,那么它就失去了数组的动态伸缩性。”

在这里插入图片描述

侯佩一听,赶紧问道:“那是不是不能再多塞一根竹笋进去了?”

王语嫣点头:“正是。InlineArray 没有 append()remove(at:) 方法。 它的容量在诞生之初就已是天数,无法更改。”

迭代之困:不入流的限制

更让人头疼的是它的“不入流”限制:

🚨 技术哲学InlineArray 不兼容传统的 Sequence(序列)和 Collection(集合)协议。

“为什么?”侯佩不解,“它不是数组吗?”

在这里插入图片描述

“因为它的设计目标是极致性能和编译时确定性。”王语嫣解释道,“为了避免遵循这些协议可能带来的抽象层开销,它选择‘自绝经脉’。如果你想遍历它,你必须通过它的 indices 属性,配合下标访问来实现。”

// 侯佩:虽然不方便,但为了性能,忍了!
for i in names1.indices {
    // 必须通过索引 i 来访问,不能直接用 for element in names1
    print("Hello, \(names1[i])!") 
}

侯佩总结道:“这就像是说,虽然它是武林高手,但它拒绝参加武林大会(不遵循 Collection 协议),如果你想请教它,必须先拿到它的拜帖(indices)才行。”

在这里插入图片描述

4️⃣ 🐼 熊猫的黑色幽默与抉择

“哎呀,这世道,连数据结构都得看颜值和出身。”侯佩叹了口气,把竹笋收进了虚拟的 InlineArray 容器里,感觉身轻如燕,连走路都带风了。

“不过话说回来,”侯佩看向王语嫣,“我还是觉得这种硬编码的语法有点不够‘熊猫化’(不够懒)。听说社区里有人想搞个更直观的语法?”

在这里插入图片描述

王语嫣提起了一件江湖轶事(SE-0483):

插曲:夭折的提案: “有一个叫做 SE-0483 的提议,想要引入类似 var names: [5 x String] = .init(repeating: "Anonymous") 的简洁语法,来表示一个固定包含 5 个 String 的数组。但由于反馈意见认为它过于突兀且不够 Swift 风格,目前已被‘打回重修’。”

侯佩嘿嘿一笑:“果然,任何新秘籍的推广,都会遇到‘保守派’的阻力。不过,能用,速度快,头不秃,对我来说就够了。”

在这里插入图片描述

5️⃣ 🛑 尾声:慕容复的伪装与“向后看”的难题

就在侯佩沉浸在高性能竹笋容器的喜悦中时,王语嫣突然脸色大变。

“侯大哥!我刚才在整理慕容复留下的数据卷轴时,发现了一个惊天的秘密!”

她指着屏幕上的一段文本,那是一篇关于“兴复大燕”的宏大计划书。

在这里插入图片描述

“我想用 Regex(正则表达式) 查找卷轴中所有提到他名字的地方。但是,我不想要匹配到那些他用来伪装自己身份的称呼,比如‘公冶乾’、‘包不同’这些名字后面的‘慕容复’。”王语嫣急道,“我只想匹配到那些,前面紧跟着‘我的挚爱’这四个字的‘慕容复’!”

侯佩挠了挠头:“你的意思是,你想找到一个模式,但这个模式必须满足它前面有一个特定的前置条件,而这个前置条件本身,又不被纳入匹配结果?”

在这里插入图片描述

“对!”王语嫣焦急万分,“我的 Regex 功夫只能‘向前看’(Lookahead),却无法完美地**‘向后看’**,我不能确定文本中这三个字前面是不是真的有‘我的挚爱’。”

侯佩望着卷轴深处那段充满秘密的代码,神秘地一笑:“王姑娘,你不用再对着旧秘籍望洋兴叹了。下一章,Swift 6.2 就要教我们一招绝顶的侦查武功:Regex lookbehind assertions(正则表达式向后查找断言)!”

在这里插入图片描述

(欲知后事如何,且看下回分解:Regex lookbehind assertions —— 如何在不匹配前文的情况下,精确判断前文的存在性,找到王语嫣真正的“挚爱”。)

Make Good New Things

2026年1月2日 08:00

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 GrahamGwern 那样,通过写作对某一话题进行深度挖掘,以确保自己真正掌握了真理。

除了写作,还可以开发 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 浪潮,则让飞机成为了大众的交通工具。它的操作逻辑与传统的地面交通截然不同,能力也更强悍。要发挥它的最大价值,你需要熟悉与它交互的最佳实践,找到属于你的那架「飞机」,然后让它载着你,飞往以前想都不敢想的地方。

❌
❌