阅读视图

发现新文章,点击刷新页面。

Swift 中 unowned self 的隐晦陷阱:为什么“无主引用”可能毁掉你的 App

若你只想记住一句话:“当闭包生命周期可能长于 self 时,永远不要使用 unowned。”

从一段崩溃代码说起

// 看似无害的「定时器」
class HomeViewController: UIViewController {
    private var timer: Timer?
    override func viewDidLoad() {
        super.viewDidLoad()
        // ⚠️ 崩溃隐患:unowned self
        timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [unowned self] _ in
            self.updateUI()   // 若 1s 内用户退出页面 → 野指针崩溃
        }
    }
    deinit { print("HomeViewController 已释放") }
    private func updateUI() { /* ... */ }
}

运行步骤:

  1. 用户进入页面 → Timer 持有闭包 → 闭包持有 unowned self。
  2. 用户在 0.8s 时点击返回 → HomeViewController 被释放 → 内存地址变野。
  3. 1s 到时系统再回调闭包 → 访问野指针 → EXC_BAD_ACCESS

unowned 与 weak 的底层区别

关键字 运行时结构体 引用计数变化 对象销毁后访问结果 开销
weak WeakReference 不 +1 自动置 nil
unowned Unmanaged 不 +1 野指针(非 nil

Swift 源码层面(swift/stdlib/public/core/HeapObject.cpp):

  • weak 会被登记到 side-table,对象销毁时所有 weak 指针被批量置 nil。
  • unowned 仅保存原始地址,销毁时 不做任何后置清理,访问时通过 swift_unknownObjectUnownedTakeStrong 尝试“复活”对象;若失败则直接崩溃。

官方文档没说清楚的 3 个场景

  1. 动画/网络回调
// UIView.animate 的逃逸闭包
func flyEmoji() {
    UIView.animate(withDuration: 2, animations: { [unowned self] in
        self.emojiView.alpha = 0
    })        // 若 2s 内用户退出页面 → 崩溃
}
  1. Combine / RxSwift
// Combine 订阅
cancellable = publisher
    .sink { [unowned self] value in
        self.handle(value)   // 上游发值时 self 可能已死
    }
  1. Swift Concurrency
// Task 闭包
Task { [unowned self] in
    await self.loadData()    // 任务未结束时 self 被释放 → 崩溃
}

工程级“逃生舱” checklist

已将您提供的场景与推荐写法整理为 Markdown 表格:

场景 推荐写法 备注
定时器 [weak self] + timer.invalidate() on deinit 双重保险
动画 [weak self] + 判断 self?.viewIfLoaded != nil 避免操作已卸载视图
Combine [weak self] + cancellable.cancel() on deinit 及时取消订阅
SwiftUI 直接使用 struct + @State 无引用循环问题

扩展:用“WeakBox” 消灭可选链

/// 让 weak 引用也能像 unowned 一样方便,但更安全
final class WeakBox<T: AnyObject> {
    weak var value: T?
    init(_ value: T) { self.value = value }
}

extension HomeViewController {
    func loadData() {
        let box = WeakBox(self)
        timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
            guard let self = box.value else { return }
            self.updateUI()
        }
    }
}

小结

  1. unowned 的唯一优点是 微不可计的性能提升;
  2. 当闭包寿命 可能 超过 self 寿命时,unowned = 定时炸弹;
  3. 在 UI 层、网络层、异步层,默认使用 weak;
  4. 若性能 profiling 证明 weak 成为热点,再考虑局部替换为 unowned,并加单元测试覆盖生命周期边界。

非逃逸闭包(non-escaping closure)—— 唯一 100% 安全的 unowned 场景

Swift 默认闭包是非逃逸的,编译器可以证明:闭包返回后,self 一定还活着。

// 数组的 forEach(非逃逸)
class Counter {
    var sum = 0
    func accumulate(_ nums: [Int]) {
        // 安全:forEach 同步执行,执行完 self 才退出作用域
        nums.forEach { [unowned self] n in
            self.sum += n
        }
    }
}

底层原理:

SIL(Swift Intermediate Language)在 optimize_lifetime 阶段会插入 fix_lifetime 指令,确保 self 在闭包调用期间不会被提前释放。

手动延长生命周期 —— withExtendedLifetime

// 同步网络请求回调,临时用 unowned 避免循环,但又要防崩溃
final class SyncAPIClient {
    func getUser() -> User? {
        var result: User?
        // 保证 self 在 block 返回前不会被释放
        withExtendedLifetime(self) {
            self.networkBlockAndWait { [unowned self] data in
                result = self.parse(data)   // 安全:self 被延长
            }
        }
        return result
    }
}

withExtendedLifetime 的源码实现:

@inlinable
public func withExtendedLifetime<T, Result>(
    _ x: T, 
    _ body: () throws -> Result
) rethrows -> Result {
    defer { _fixLifetime(x) }   // 防止编译器提前释放
    return try body()
}

原文资料

  1. blog.jacobstechtavern.com/p/the-case-…

lld 22 ELF changes

For those unfamiliar, lld is theLLVM linker, supporting PE/COFF, ELF, Mach-O, and WebAssembly ports.These object file formats differ significantly, and each port mustfollow the conventions of the platform's system linker. As a result, theports share limited code (diagnostics, memory allocation, etc) and havelargely separate reviewer groups.

With LLVM 22.1 releasing soon, I've added some notes to the https://github.com/llvm/llvm-project/blob/release/22.x/lld/docs/ReleaseNotes.rstas an lld/ELF maintainer. As usual, I've reviewed almost all the patchesnot authored by me.

For the first time, I used an LLM agent (Claude Code) to help lookthrough commits(git log release/21.x..release/22.x -- lld/ELF) and draftthe release notes. Despite my request to only read lld/ELF changes,Claude Code also crafted notes for other ports, which I retained sincetheir release notes had been quite sparse for several releases. Changesback ported to the 21.x release are removed(git log --oneline llvmorg-22-init..llvmorg-21.1.8 -- lld).

I'll delve into some of the key changes.

  • --print-gc-sections=<file> has been added toredirect garbage collection section listing to a file, avoidingcontamination of stdout with other linker output. (#159706)
  • A VersionNode lexer state has been added for betterversion script parsing. This brings the lexer behavior closer to GNU ld.(#174530)
  • Unversioned undefined symbols now use version index 0, aligning withGNU ld 2.46 behavior. (#168189)
  • .data.rel.ro.hot and .data.rel.ro.unlikelyare now recognized as RELRO sections, allowing profile-guided staticdata partitioning. (#148920)
  • DTLTO now supports archive members and bitcode members of thinarchives. (#157043)
  • For DTLTO,--thinlto-remote-compiler-prepend-arg=<arg> has beenadded to prepend an argument to the remote compiler's command line. (#162456)
  • Balanced Partitioning (BP) section ordering now skips input sectionswith null data, and filters out section symbols. (#149265) (#151685)
  • For AArch64, fixed a crash when using--fix-cortex-a53-843419 with synthetic sections andimproved handling when patched code is far from the short jump. (#170495)
  • For AArch64, added support for the R_AARCH64_FUNCINIT64dynamic relocation type for relocating word-sized data using the returnvalue of a function. (#156564)
  • For AArch64, added support for the R_AARCH64_PATCHINSTrelocation type to support deactivation symbols. (#133534)
  • For AArch64, added support for reading AArch64 Build Attributes andconverting them into GNU Properties. (#147970)
  • For ARM, fixed incorrect veneer generation for wraparound branchesat the high end of the 32-bit address space branching to the low end.(#165263)
  • For LoongArch, -r now synthesizesR_LARCH_ALIGN at input section start to preserve alignmentinformation. (#153935)
  • For LoongArch, added relocation types for LA32R/LA32S. (#172618) (#176312)
  • For RISC-V, added infrastructure for handling vendor-specificrelocations. (#159987)
  • For RISC-V, added support for statically resolved vendor-specificrelocations. (#169273)
  • For RISC-V, -r now synthesizesR_RISCV_ALIGN at input section start to preserve alignmentinformation during two-stage linking. (#151639)This is an interesting relocatablelinking challenge for linker relaxation.

Besides me, Peter Smith (smithp35) and Jessica Clarke (jrtc27) havedone a lot of reviews.

jrtc27 has done great work simplifying the dynamic relocation system,which is highly appreciated.

I should call out https://github.com/llvm/llvm-project/pull/172618: forthis relatively large addition, the author and approver are from thesame company and contributing to their architecture, and neither theauthor nor the approver is a regular lld contributor/reviewer. Theauthor did not request review from regular reviewers and landed thepatch just 3 minutes after their colleague's approval. I left a commentasking to keep the PR open for other maintainers to review.

Distributed ThinLTO

Distributed ThinLTO(DTLTO) enables distributing ThinLTO backend compilations toexternal systems (e.g., Incredibuild, distcc-like tools) during the linkstep. This feature was contributed by PlayStation, who had offered it asa proprietary technology before upstreaming.

The traditional distributed ThinLTO is implemented in Bazel in buck2.Bazel-style distribution (build system orchestrated)uses a multi-step workflow:

1
2
3
4
5
6
7
8
9
# Compile to bitcode (made parallel by build system)
clang -c -O2 -flto=thin a.c b.c
# Thin link
clang -flto=thin -fuse-ld=lld -Wl,--thinlto-index-only=a.rsp,--thinlto-emit-imports-files -Wl,--thinlto-prefix-replace=';lto/' a.o b.o
# Backend compilation (distributed by build system) with dynamic dependencies
clang -c -O2 -fthinlto-index=lto/a.o.thinlto.bc a.o -o lto/a.o
clang -c -O2 -fthinlto-index=lto/b.o.thinlto.bc b.o -o lto/b.o
# Final native link
clang -fuse-ld=lld @a.rsp # a.rsp contains lto/a.o and lto/b.o

The build system sees the index files from step 2 as outputs andschedules step 3 jobs across the build cluster. This requires a buildsystem that handles dynamic dependencies—outputs ofstep 2 determine inputs to step 3.

DTLTO (linker orchestrated) integrates steps 2-4into a single link invocation:

1
2
clang -flto=thin -c a.c b.c
clang -flto=thin -fuse-ld=lld -fthinlto-distributor=<distributor> *.o

LLD performs the thin-link internally, generates a JSON jobdescription for each backend compilation, invokes the distributorprocess, waits for native objects, and links them. The distributor isresponsible for farming out the compilations to remote machines.

DTLTO works with any build system but requires a separate distributorprocess that speaks its JSON protocol. DTLTO is essentially "ThinLTOdistribution for projects that don't use Bazel".

Pointer Field Protection

R_AARCH64_PATCHINST is a static relocation type usedwith Pointer Field Protection (PFP), which leverages Armv8.3-A PointerAuthentication (PAC) to protect pointer fields in structs.

Consider the following C++ code:

1
2
3
4
5
6
7
8
9
struct cls {
~cls();
long *ptr;
private:
long *ptr2;
};

long *load(cls *c) { return c->ptr; }
void store(cls *c, long *ptr) { c->ptr = ptr; }

With Pointer Field Protection enabled, the compiler generates PACinstructions to sign and authenticate pointers:

1
2
3
4
5
6
7
8
9
10
load:
ldr x8, [x0] // Load the PAC-signed pointer from c->ptr
autda x8, x0 // Authenticate and strip the PAC, R_AARCH64_PATCHINST __pfp_ds__ZTS3cls.ptr
mov x0, x8
ret

store:
pacda x1, x0 // Sign ptr using c as a discriminator, R_AARCH64_PATCHINST __pfp_ds__ZTS3cls.ptr
str x1, [x0]
ret

Each PAC instruction is associated with anR_AARCH64_PATCHINST relocation referencing adeactivation symbol (the __pfp_ds_ prefixstands for "pointer field protection deactivation symbol"). By default,__pfp_ds__ZTS3cls.ptr is an undefined weak symbol in everyrelocatable file.

However, if the field's address escapes in any translation unit(e.g., someone takes &c->ptr), the compiler definesthe deactivation symbol as an absolute symbol (ELFSHN_ABS). When the linker sees a defined deactivationsymbol, it patches the PAC instruction to a NOP(R_AARCH64_PATCHINST acts as R_AARCH64_ABS64when the referenced symbol is defined), disabling the protection forthat field. This is necessary because external code could modify thepointer without signing it, which would cause authenticationfailures.

The linker allows duplicate definitions of absolute symbols if thevalues are identical.

R_AARCH64_FUNCINIT64 is a related static relocation typethat produces an R_AARCH64_IRELATIVE dynamic relocation (GNU indirectfunction). It initializes function pointers in static data at loadtime by calling a resolver function that returns the PAC-signedaddress.

PFP is AArch64-specific because it relies on Pointer Authentication(PAC), a hardware feature introduced in Armv8.3-A. PAC providesdedicated instructions (pacda, autda, etc.)that cryptographically sign pointers using keys stored in systemregisters. x86-64 lacks an equivalent mechanism—Intel CET providesshadow stacks and indirect branch tracking for control-flow integrity,but cannot sign arbitrary data pointers stored in memory.

Takeaways:

  • Security features need linker support. This is because many featuresrequire aggregated information across all translation units. In thiscase, if any TU exposes a field's address, the linker disablesprotection for this field everywhere The implementation isusually lightweight.
  • Relocations can do more than fill in addresses:R_AARCH64_PATCHINST conditionally patches instructions toNOPs based on symbol resolution. This is a different paradigm fromtraditional "compute address, write it" relocations.

RISC-V vendor relocations

RISC-V's openness encourages vendors to add custom instructions.Qualcomm has the uC extensions for their microcontrollers; CHERIoT addscapability-based security.

The RISC-V psABI adopted the vendor relocation system:

1
2
Relocation 0: R_RISCV_VENDOR      references symbol "QUALCOMM"
Relocation 1: R_RISCV_QC_ABS20_U (vendor-specific type)

The R_RISCV_VENDOR marker identifies the vendornamespace via its symbol reference. The subsequent relocation uses avendor-specific type number that only makes sense within that namespace.Different vendors can reuse the same type numbers without conflict.

In lld 22:

  • Infrastructure for vendor relocations was added (#159987).The implementation folds vendor namespace information into the upperbits of RelType, allowing existing relocation processingcode to work with minimal changes.
  • Support for statically-resolved vendor relocations was added (#169273),including Qualcomm and Andes relocation types. The patch landed withoutinvolving the regular lld/ELF reviewer pool. For changes that setarchitectural precedents, broader consensus should be sought beforemerging. I've commentedon this.

The RISC-Vtoolchain conventions document the vendor relocation scheme.

There's a maintainability concern: accepting vendor-specificrelocations into the core linker sets a precedent. RISC-V is uniquelyfragmented compared to other LLVM backends-x86, AArch64, PowerPC, andothers don't have nearly as many vendors adding custom instructions andrelocations. This fragmentation is a direct consequence of RISC-V's opennature and extensibility, but it creates new challenges for upstreamtoolchain maintainers. Accumulated vendor-specific code could become asignificant maintenance burden.

GNU ld compatibility

Large corporate users of lld/ELF don't care about GNU ldcompatibility. They add features for their own use cases and move on. Idiligently coordinate with binutils maintainers and file featurerequests when appropriate. When lld implements a new option or behavior,I often file corresponding GNU ld feature requests to keep the toolsaligned.

This coordination work is largely invisible but essential for thebroader toolchain ecosystem. Users benefit when they can switch betweenlinkers without surprises.


Link: lld 21 ELFchanges

呼吸能改变很多事

我们都知道应该去做那些「难而正确」的事。我们熟读各类方法论:建立系统而非仅盯着目标;先确立身份认同再行动;利用福格行为模型微调习惯……但在现实的引力面前,这些道理往往显得苍白。

因为正确的事通常伴随着当下的痛感(或者枯燥),且反馈周期漫长。相比之下,大脑更原始的本能总是倾向于那些即时满足的选项。

当注意力即将滑向短期快感时,我们需要的不是宏大的意志力,而是一个微小的「阻断器」。深呼吸,就是这个阻断器。

这并非玄学,而是有着明确的生理机制。当我们焦虑或冲动时,身体由处于「战斗或逃跑」的交感神经主导。而深呼吸——特别是呼气长于吸气的呼吸——能激活迷走神经,强制启动副交感神经系统。这就像是给高速运转的大脑物理降温,将我们从应激状态强行拉回「休息与消化」的理智状态。

给自己设一个微小的「绊脚石」:在想要下意识点开那个红色 App 之前,或者伸手拿烟之前,深呼吸 3 次

这里的难点在于「记得」。在多巴胺渴望飙升的瞬间,理智往往是缺席的。所以,不要指望意志力,要依靠环境暗示。比如给手机套一根橡皮筋,利用这个物理触感的停顿,给自己 3 秒钟的窗口期。如果不做这个动作,惯性会带走你;做了这个动作,选择权才更容易回到手中。

如果说深呼吸是急救包,那么冥想就是长期的肌肉训练。

冥想的核心功效,是培养一种「旁观者」的视角。它能帮我们对抗「注意力经济」的掠夺,打断「刺激-反应」的自动化回路。经过冥想训练的大脑,具备更敏锐的「觉察力」。当你下意识地刷手机时,大脑会突然亮起一盏灯:看,我产生了一个想寻求刺激的念头。

在这「被看见」的一瞬间,你就不再是情绪和欲望的奴隶,而是它们的主人。

现代的注意力经济通过高密度的感官轰炸(短视频、爆款标题),不仅拉高了多巴胺阈值,还让多巴胺受体变得极度迟钝。我们像耐药性极强的病人,对低刺激的事物(读书、深度思考、发呆)感到无法忍受的枯燥。

我们丧失了「无聊」的能力,而无聊恰恰是创造力的温床。冥想本质上是一件主动拥抱无聊的事。通过坚持冥想,你在为大脑进行「多巴胺排毒」,恢复受体的敏感度,重新学会如何与「无刺激」的状态相处,并从中获得平静的喜悦。

除了有点无聊,践行冥想最大的阻力,往往来自于我们把它看得太重,试图寻找一段「不被打扰」的完美时间,更高效的策略可能是「缝隙冥想」。

比如通勤的地铁上、排队等咖啡的间隙、或者是午休结束准备开始工作的瞬间。这些时刻原本也是「垃圾时间」,通常会被用来刷手机填补空白。如果能用这 3-5 分钟来关注呼吸,不需要特意调整坐姿,也不必追求绝对的安静,只要把注意力从纷乱的思绪中收回来。

这种「微冥想」虽然短暂,但因为它发生在高频的、甚至有些嘈杂的日常场景中,反而更能训练我们在混乱中随时「调用」平静的能力。


归根结底,我们无法控制外界的信息洪流,也难以完全屏蔽生活的噪音,但我们始终拥有控制自己呼吸的自由。

在刺激和反应之间,有一个空间。在那个空间里,藏着我们要选择的反应。在我们的反应里,藏着我们的成长和自由。

呼吸,就是撑开这个空间的支柱。它不是为了让你变成一台更高效的执行机器,而是为了让你在这个加速的世界里,依然能保有停顿的权利。在这个微小的停顿里,你不再是算法的猎物,而是自己的主人。

RxSwift 中 asDriver() 详解

本文从「是什么、为什么用、怎么用」三个层面,详细讲解 RxSwift 中的 asDriver() 方法,适配新手理解,兼顾实用性和易懂性。

一、asDriver() 核心含义

asDriver() 是 RxSwift 中专门用于将 Observable ** 序列(或其他可转换序列,如 ** Single Maybe )转换为 ** Driver ** 序列 的方法。

简单来说,它是一个「序列类型转换器」:输入为 Observable,输出为 Driver,且转换过程中会自动为序列附加 Driver 特有的「UI 友好型安全特性」,无需手动额外配置。

补充前提:Driver 并非全新序列类型,而是 Observable 的「专用子类/特殊封装」,专门针对 UI ** 绑定场景**(如更新 Label 文本、刷新 TableView 数据、修改按钮状态等)设计,因此也被称为「UI 友好型序列」。

二、asDriver() 自动附加的 3 个关键安全特性

这是 Driver 的核心价值,也是 asDriver() 的核心意义——自动规避 UI 开发中常见的 3 个坑,减少冗余代码:

1. 确保在主线程订阅/发送事件

UI 操作必须在主线程执行(否则会崩溃或界面错乱),asDriver() 会自动将序列的事件派发切换到主线程,无需手动编写 observe(on: MainScheduler.instance)

2. 确保序列不会发送 Error 事件

Driver 序列仅支持发送 next(传递数据)和 completed(序列结束)事件,不支持发送 Error ** 事件**。若原始 Observable 可能发送错误,asDriver() 要求必须提供「错误兜底值」(或错误处理逻辑),避免 UI 绑定链路因错误中断。

3. 具备「共享订阅」(共享事件流)特性

对应 RxSwift 中的 share(replay: 1, scope: .whileConnected),多个观察者订阅同一个 Driver 序列时,不会重复执行上游逻辑(如网络请求、数据库查询),仅共享一份事件流,既节省资源,又能保证多个 UI 组件拿到的数据一致。

三、为什么需要用 asDriver()(适用场景)

核心场景:当需要将 Rx 序列的结果绑定到 UI 控件时,优先使用 ** asDriver() ** 转换为 ** Driver ** 序列

反例:直接用 Observable 绑定 UI(需手动处理安全问题)


// 直接用 Observable 绑定 UI,需手动处理 3 个安全问题
someObservable
    .observe(on: MainScheduler.instance) // 1. 切换到主线程
    .catch { error in Observable.just("默认值") } // 2. 捕获错误,避免链路中断
    .share(replay: 1, scope: .whileConnected) // 3. 共享订阅
    .bind(to: label.rx.text)
    .disposed(by: disposeBag)

正例:用 asDriver() 绑定 UI(自动处理安全问题,代码简洁)


// asDriver() 自动处理安全问题,代码更简洁
someObservable
    .asDriver(onErrorJustReturn: "默认值") // 转换为 Driver,指定错误兜底值
    .drive(label.rx.text) // Driver 专用绑定方法(替代 bind)
    .disposed(by: disposeBag)

四、asDriver() 的常见用法

asDriver() 有两个常用重载方法,对应不同错误场景:

场景 1:原始序列可能发送错误(需兜底值)

使用 asDriver(onErrorJustReturn:),传入「错误时序列要发送的默认值」,错误会被自动捕获,序列发送默认值后正常结束,不中断链路。


// 示例:网络请求结果绑定到 UI(网络请求可能失败,需兜底)
let networkRequest: Observable<String> = fetchDataFromServer() // 假设是网络请求序列

networkRequest
    .asDriver(onErrorJustReturn: "网络请求失败,请重试") // 错误兜底文本
    .drive(label.rx.text) // 绑定到 UILabel
    .disposed(by: disposeBag)

场景 2:原始序列不可能发送错误(如本地数据、UI 事件)

直接使用asDriver(),无需传入兜底值(因序列不会产生错误)。


// 示例:按钮点击事件转换为 Driver(按钮点击不会有错误)
let buttonTap: Observable<Void> = button.rx.tap

buttonTap
    .asDriver() // 无需兜底值,直接转换
    .drive(onNext: {
        print("按钮被点击了")
        // 执行 UI 操作,无需担心线程问题
    })
    .disposed(by: disposeBag)

五、补充:Driver 专用绑定方法 drive()

序列被 asDriver() 转换为 Driver 后,推荐使用 drive() 绑定,而非 bind(),原因如下:

  • 语义更清晰:drive() 意为「驱动 UI 变化」,明确适配 UI 绑定场景;

  • 更安全:drive() 内部已默认处理 UI 绑定的相关细节,避免意外绑定到非 UI 场景。

六、总结

  1. asDriver() 是 RxSwift 中**「将 ** Observable ** 转换为 ** Driver ** 序列」**的专用方法,核心服务于 UI 绑定场景;

  2. 转换后自动获得 3 个安全特性:主线程执行、不发送错误、共享订阅,减少手动冗余代码;

  3. 用法区分:序列可能出错用 asDriver(onErrorJustReturn:),不可能出错用 asDriver(),UI 绑定优先用 drive()

lld 22 ELF changes

LLVM 22 will be released. As usual, I maintain lld/ELF and have addedsome notes to https://github.com/llvm/llvm-project/blob/release/22.x/lld/docs/ReleaseNotes.rst.I've meticulously reviewed nearly all the patches that are not authoredby me. I'll delve into some of the key changes.

  • --print-gc-sections=<file> has been added toredirect garbage collection section listing to a file, avoidingcontamination of stdout with other linker output. (#159706)
  • A VersionNode lexer state has been added for betterversion script parsing. This brings the lexer behavior closer to GNU ld.(#174530)
  • Unversioned undefined symbols now use version index 0, aligning withGNU ld 2.46 behavior. (#168189)
  • .data.rel.ro.hot and .data.rel.ro.unlikelyare now recognized as RELRO sections, allowing profile-guided staticdata partitioning. (#148920)
  • DTLTO now supports archive members and bitcode members of thinarchives. (#157043)
  • For DTLTO,--thinlto-remote-compiler-prepend-arg=<arg> has beenadded to prepend an argument to the remote compiler's command line. (#162456)
  • Balanced Partitioning (BP) section ordering now skips input sectionswith null data, and filters out section symbols. (#149265) (#151685)
  • For AArch64, fixed a crash when using--fix-cortex-a53-843419 with synthetic sections andimproved handling when patched code is far from the short jump. (#170495)
  • For AArch64, added support for the R_AARCH64_FUNCINIT64dynamic relocation type for relocating word-sized data using the returnvalue of a function. (#156564)
  • For AArch64, added support for the R_AARCH64_PATCHINSTrelocation type to support deactivation symbols. (#133534)
  • For AArch64, added support for reading AArch64 Build Attributes andconverting them into GNU Properties. (#147970)
  • For ARM, fixed incorrect veneer generation for wraparound branchesat the high end of the 32-bit address space branching to the low end.(#165263)
  • For LoongArch, -r now synthesizesR_LARCH_ALIGN at input section start to preserve alignmentinformation. (#153935)
  • For LoongArch, added relocation types for LA32R/LA32S. (#172618) (#176312)
  • For RISC-V, added infrastructure for handling vendor-specificrelocations. (#159987)
  • For RISC-V, added support for statically resolved vendor-specificrelocations. (#169273)
  • For RISC-V, -r now synthesizesR_RISCV_ALIGN at input section start to preserve alignmentinformation during two-stage linking. (#151639)

Link: lld 21 ELFchanges

冷水澡是一座小金矿

今年夏天我大概尝试了 3 个月的冷水澡,感觉还不错,一开始不太适应,但慢慢也习惯了,后来天气一冷,就又换回了热水澡。

前几天在看 Andrew Huberman 的视频,他提到了洗冷水澡的诸多好处,尤其是对多巴胺的正面影响,就又开始了冷水澡的尝试,不过这次的挑战明显比夏天的大。

为了避免步子迈太大,我采用了热冷交替法:3 分钟热水(扩张血管),1 分钟冷水(收缩血管),然后做 2-3 个循环,最后以冷水结束。

有了 3 分钟的热水打底,让 1 分钟的冷水变得更能接受些,但当 10 来度的冷水浇在身上时,还是会忍不住地喊出来,甚至跳起来。第一轮的冷水冲击力最大,之后的会稍微好一些,但依旧在舒适区之外。

这看似有点受虐的几分钟,如果从投资的角度看,回报还是挺丰厚的。

硬件的「重启」

人类的身体不是为了恒温环境而设计的。在漫长的进化史上,舒适是异常,寒冷才是常态。当你拧开冷水龙头,实际上是在激活一段古老的代码。

更重要的是,它改变了你的化学环境。冷水能让多巴胺水平显著提升,这种提升不像糖或咖啡因那样会有随后的崩溃(crash),它是一种平稳的、持续的清醒,可以持续 2-3 个小时。

在注意力成为稀缺资源的今天,能够通过一种物理手段,在不服用任何药物的情况下获得这种精神上的敏锐度,这本身就是一种巨大的优势。

软件的「补丁」

洗冷水澡的理念跟斯多葛学派推崇的「自愿不适」(Voluntary Discomfort)也非常 match。但为什么要自找苦吃呢?

因为过度的舒适会让我们变得脆弱。当习惯了恒温、软床和热食,「基准线」会被抬高。一旦环境稍微变得恶劣,就会感到痛苦。冷水澡是一种重置基准线的方式。当你能从容面对冷水时,生活中的其他麻烦——交通堵塞、难缠的邮件、糟糕的天气——似乎就没那么难以忍受了。

这其实是在训练一种极其核心的能力:切断刺激与反应之间的自动连接。

当冷水击中你时,身体的自动反应是恐慌和急促呼吸。这和你在面对工作危机或社交压力时的反应是一样的。通过在冷水中强迫自己冷静下来、控制呼吸,实际上是在重写你的神经回路。你在告诉大脑:「虽然这很不舒服,但我依然掌舵。」

吃掉那只青蛙

早晨通常是一场意志力的博弈。大脑倾向于选择阻力最小的路径。而洗冷水澡是一个反向操作。

这不仅仅是洗澡,可能也是当天的第一个决定。你站在那里,理智告诉你应该拧开冷水,但本能告诉你不要。当你最终执行了理智的命令,你就赢得了一场微小的胜利。

这一胜虽然微不足道,但它至关重要。因为它设定了当天的基调:你不是一个顺从冲动的人,你是一个能够为了长远利益而克服短期不适的人。这种自我认同会像雪球一样滚动,影响你接下来在工作和生活中的每一个选择。

这可能就是为什么这种简单的习惯能带来惊人回报的原因。在这个充满捷径和舒适陷阱的世界里,愿意主动选择不适的人,拥有了一种隐形的竞争优势。


洗冷水澡对场地、器材的要求很低,只需要一个淋浴头,就可以开始,而它带来的回报,又非常丰厚。这就是塔勒布在《反脆弱》中提到的「非对称收益」(低风险,高收益)。这么好的投资机会,不想试试吗?

主线程Runloop

Runloop循环的代码,不是在主线程上运行的吗

Runloop 的这个无限循环代码,就是直接运行在主线程上的,这是核心关键 ——主线程的本质,就是执行 Runloop 无限循环的线程,二者是完全绑定的,不存在 “Runloop 循环在其他线程,主线程做别的事” 的情况。

一、先明确核心对应关系

iOS 中主线程的入口函数,最终就是启动 Runloop 的无限循环,伪代码的对应关系是:

// 主线程的入口(UIApplicationMain函数底层)
int main(int argc, char * argv[]) {
    @autoreleasepool {
        // 1. 初始化UIApplication、AppDelegate等核心对象
        // 2. 启动「主线程的Runloop」,执行无限循环
        CFRunLoopRunMain(); // 这个函数内部,就是Runloop的while(1)无限循环
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

简单说:主线程启动后,唯一的核心工作就是执行 Runloop 的无限循环,主线程的所有任务(UI 事件、渲染、定时器、网络回调等),都必须在这个循环体内部执行

二、关键:“主线程执行 Runloop 循环” 的具体含义

你可以把主线程理解为一个 “执行代码的容器” ,把Runloop 的 while (1)理解为这个容器里永远在运行的 “主程序” ——

  1. 主线程被创建后,立刻加载并执行 Runloop 的无限循环代码;
  2. 这个循环代码霸占了主线程的执行权,主线程后续所有的操作,都是在这个循环体的内部被触发和执行的(比如处理触摸事件、执行帧渲染、调用你的业务代码);
  3. 只要 Runloop 的循环不退出,主线程就会一直存活(APP 前台运行时,主线程 Runloop 永不退出);如果循环退出,主线程就会执行完毕并销毁(APP 退出时才会发生)。

三、结合 “休眠” 再理解:主线程休眠 = Runloop 循环暂停

之前说的Runloop 让线程休眠,本质就是让主线程本身进入休眠态

  • 当 Runloop 执行到mach_msg(内核休眠接口)时,主线程会从运行态切换为休眠态,此时主线程不再执行任何代码(包括 Runloop 的循环体),CPU 也会释放;
  • 当内核收到唤醒信号(如 CADisplayLink、触摸事件),会把主线程切回运行态,Runloop 的 while (1) 循环会继续执行后续的任务处理、渲染等逻辑。

一句话讲清:Runloop 的循环代码是主线程的 “核心程序”,主线程的生命周期和 Runloop 的循环绑定,Runloop 的休眠 / 唤醒就是主线程的休眠 / 唤醒。

四、再补一个易混点:主线程的任务,都在 Runloop 循环体内执行

你在主线程写的所有代码(比如viewDidLoad、按钮点击事件、dispatch_async(dispatch_get_main_queue(), ^{...})),都不是直接 “塞” 到主线程,而是被加入到 Runloop 的任务队列中,等待 Runloop 的循环体执行到对应阶段时,再被主线程执行。

举个例子,按钮点击的执行流程:

  1. 点击按钮,系统将 “点击事件” 加入主线程 Runloop 的事件队列
  2. 内核唤醒主线程,Runloop 的 while (1) 循环继续执行;
  3. Runloop 从事件队列中取出 “点击事件”,让主线程执行你写的点击回调代码;
  4. 回调执行完毕,Runloop 检查是否有 UI 更新 / 其他任务,无则让主线程再次休眠。

最终总结

  1. Runloop 的无限循环直接运行在主线程,是主线程的核心执行代码;
  2. 主线程的生命周期与 Runloop 循环绑定,循环不退出,主线程就一直存活;
  3. Runloop 的 “休眠 / 唤醒” 就是主线程的休眠 / 唤醒,无任务时主线程休眠,有任务时被唤醒执行循环体;
  4. 主线程的所有任务(包括你的业务代码、UI 渲染、事件处理),都在 Runloop 循环体内部被执行。

Runloop的无限循环

不是说runloop是无限循环吗,为啥主线程还会休眠?

Runloop 的 “无限循环” 和 “休眠” 并不矛盾,反而这两个特性是 Runloop 设计的核心精髓 ——Runloop 的无限循环是 “带休眠的循环”,不是空转的死循环,休眠正是为了让无限循环不消耗多余资源。

简单说结论:主线程 Runloop 的 while (1) 是 “逻辑上的无限循环”,但循环内部会主动触发线程休眠,直到有事件唤醒才继续执行循环体,而非一直空转执行代码

一、先破误区:“无限循环”≠“一直执行代码”

普通的空转死循环是这样的(会把 CPU 占满):

// 纯空转死循环:CPU 100%占用,线程一直跑
while (1) {
    // 什么都不做,持续循环
}

Runloop 的无限循环是 “智能循环” ,伪代码更接近这样:

// Runloop的无限循环:有任务就执行,无任务就休眠
while (1) {
    // 1. 检查是否有待处理的事件/任务
    if (有任务) {
        处理任务(事件、渲染、回调等);
    } else {
        // 2. 无任务时,让线程进入「休眠状态」,释放CPU资源
        内核层面让线程休眠,等待唤醒信号;
    }
}

Runloop 的 “无限” 体现在这个 while (1) 的逻辑永远不退出(主线程 Runloop 默认永不停止),但 ** 循环体的执行是 “按需触发”** 的 —— 无任务时根本不执行循环体的核心逻辑,而是让线程休眠。

二、关键原理:Runloop 如何实现 “休眠 - 唤醒”(内核层面)

Runloop 的休眠不是自己实现的,而是基于操作系统内核的 IO 多路复用机制(iOS/macOS 下是mach_msg,底层封装了 select/poll/kqueue),这是跨平台事件驱动的通用方案,核心流程分 3 步:

  1. 无任务时,Runloop 调用mach_msg向内核发送 “休眠请求” :主线程会从运行态切换为休眠态(也叫等待态),此时线程不会占用 CPU 的任何时间片,CPU 可以去处理其他线程 / 进程的任务,资源消耗几乎为 0;
  2. 有事件时,内核主动唤醒线程:当有能唤醒 Runloop 的事件出现时(如 CADisplayLink 刷新、触摸事件、网络回调、定时器),内核会向该线程发送唤醒信号,触发mach_msg返回,线程从休眠态切回运行态;
  3. 唤醒后继续执行循环体:Runloop 接收到唤醒信号后,继续执行 while (1) 的循环体,处理对应的任务,完成后再次检查是否有新任务,无任务则再次休眠。

对主线程来说:CADisplayLink(屏幕刷新定时器)是最常见的唤醒源(60Hz 下 16.67ms 唤醒一次),其次是用户的触摸、滑动等 UI 事件。

三、结合帧渲染理解:休眠 - 唤醒 - 渲染的完整循环(60Hz)

把 Runloop 的休眠、唤醒和帧渲染结合,主线程的完整工作流程就非常清晰了(无其他 UI 事件时):

  1. 休眠阶段:主线程 Runloop 无任务,调用mach_msg进入休眠,CPU 释放;

  2. 定时唤醒:16.67ms 后,CADisplayLink 触发,内核唤醒主线程,Runloop 的 while (1) 继续执行;

  3. 任务检查:Runloop 检查是否有 UI 更新标记(CATransaction);

    • 有更新:执行布局→绘制→合成→显示的一帧渲染流程,完成后再次检查无新任务,进入休眠;
    • 无更新:直接跳过渲染阶段,再次调用mach_msg进入休眠;
  4. 重复循环:每 16.67ms 被 CADisplayLink 唤醒一次,重复上述步骤。

四、额外补充:为什么 Runloop 要设计 “休眠” 机制?

如果主线程 Runloop 的无限循环没有休眠,而是一直空转检查任务,会导致两个严重问题:

  1. CPU 占用率飙升:一个空转的 while (1) 会把单个 CPU 核心占满(100%),主线程是核心线程,会直接导致设备发热、耗电、卡顿;
  2. 系统资源浪费:CPU 一直被 Runloop 的空转占用,无法分配给其他线程 / 进程(如后台网络、音频播放),整个 APP 的运行效率会大幅降低。

休眠机制完美解决了这两个问题:无任务时线程不占 CPU,有任务时及时唤醒,实现了 “按需执行,资源最优”,这也是 Runloop 能成为 iOS 事件驱动核心的关键原因。

最终一句话总结

主线程 Runloop 是逻辑上的无限循环(while (1) 永不退出),但物理上的执行是按需的—— 无任务时通过内核让线程休眠,有事件(如屏幕刷新、UI 触摸)时被唤醒继续执行循环体,休眠和无限循环是 Runloop 的两个核心特性,而非矛盾点。

Routine 就是人生的复利

我们通常会觉得 Routine 是「自由」的反义词,它让人联想到枯燥的重复、僵化的日程表,或者那种一眼就能望到头的生活,这是一个巨大的误解。

在谈论 Routine 之前,我们先来看看复利公式:

把它放到人生这个大背景下,天赋、机遇和单次努力的强度,构成了那个基础的增长率 。很多雄心勃勃的人都盯着 看。他们试图通过一次通宵工作、一个绝妙的点子或者一次爆发式的冲刺来获得巨大的

但这很难持久。

Routine 的作用,是掌控指数

在很多方面,Routine 就像是定投。如果只有在「感觉来了」的时候才去写作,或者在「状态很好」的时候才去锻炼,你的 是断断续续的,增长曲线是锯齿状的。

而一个好的 Routine 是一种承诺:无论我今天感觉如何,无论 是大是小,那个 都会自动加一。

足够大时,它就不再是线性的累加,而是指数级的爆发。那些伟大的小说家每天早起写几千字,不是因为他们每天都有几千字的灵感,而是因为他们把写作变成了一种像刷牙一样的生理节律。他们不对抗它,只是执行它。

这就是 Routine 的本质:它把困难的事情自动化,从而让时间站在你这一边。


很多人认为「我不设计 Routine,是因为我不想被束缚」。但真相是:根本不存在「没有 Routine」这回事。

如果你不主动设计你的生活流程,你的身体和环境会为你设计一套。这套「默认 Routine」通常由你的生物本能(懒惰)和外部世界(诱惑)共同编写。

当你拿起手机无意识地刷了两个小时,这本身就是一个极其高效和稳固的 Routine。

既然「默认 Routine」也是自动化的,为什么我们还会感到疲惫?

因为你并没有完全放弃。内心依然有一个想要变好的声音,它在不断地试图把这辆正滑向舒适区的车拉回来。这就是决策疲劳的来源:一场无休止的内战。

  • 你的本能说:“再休息一会。”
  • 你的理智说:“不行,该干活了。”
  • 你的本能说:“那先倒杯水?”
  • 你的理智说:“好吧,但喝完马上开始。”

这种讨价还价非常消耗能量,宝贵的精力被浪费在了「决定要做什么」上,而不是「做这件事」本身。

一个好的 Routine(比如「穿上跑鞋 = 出门」)是一条不可协商的规则。它绕过了「谈判」环节,直接进入行动。在这个过程中,你宝贵的意志力没有被内耗掉,而是被完整地保留给了真正重要的问题:如何解决这个难题?如何把作品打磨得更好?


如果把人生看作是一项长期的投资,建立 Routine,意味着你从一个短视的投机者,变成了一个长期的价值投资者。

投机者依赖心情、运气、状态、灵感(),而投资者诉诸系统和时间()。

当你开始设计 Routine 时,实际上是在重新分配资产。你不再说「没办法,我就是管不住自己」,而是开始像分析一张糟糕的资产负债表一样分析你的生活:「哦,看来我在‘压力大’这个触发条件下,会自动买入‘逃避’这项资产。我需要调整策略,把它换成‘运动’。」

这并不容易。改变习惯总是痛苦的,但如果坚持下去,哪怕只是每天微调一点点,复利的力量就会显现。

最开始,你只是改变了起床后的 30 分钟。一年后,你会发现你变了一个人。

你不仅改变了你做的事,你改变了你是谁。

所以,不要再把 Routine 看作是一张枯燥的时间表。它是你为了结束内耗、夺回控制权而制定的投资策略。它是你对抗熵增、对抗平庸、对抗混乱的武器。

Routine 就是人生的复利。

聊聊swift闭包

Swift闭包语法详解

代码示例

var b:() -> Void = {() -> Void in
    print("匿名函数")
}
b()

详细解释

这是一个Swift中的闭包(Closure)语法,具体解释如下:

代码分解

var b:() -> Void = {() -> Void in
    print("匿名函数")
}
b()

语法分析

  1. 变量声明

    • var b:声明一个变量 b
    • :() -> Void:指定变量的类型是一个函数类型,() -> Void 表示:
      • ():不接受任何参数
      • -> Void:返回 Void 类型(即不返回值,相当于空类型)
  2. 闭包赋值

    • = {() -> Void in:赋值给一个闭包表达式
      • {}:闭包的开始和结束
      • () -> Void:闭包的类型签名(与变量类型匹配)
      • in:关键字,表示参数列表和函数体的分隔符
  3. 闭包体

    • print("匿名函数"):闭包执行的代码
  4. 调用

    • b():调用这个闭包函数

等价写法

这个闭包还可以写成更简洁的形式:

完整写法(上面使用的)

var b: () -> Void = { () -> Void in
    print("匿名函数")
}

简化写法(省略类型,因为可以从上下文推断)

var b: () -> Void = {
    print("匿名函数")
}

尾随闭包写法(如果作为参数传递)

func executeClosure(closure: () -> Void) {
    closure()
}

executeClosure {
    print("匿名函数")
}

关键概念

  • 闭包:可以捕获和存储其所在上下文中变量和常量的引用
  • 匿名函数:没有名字的函数,可以直接赋值给变量或作为参数传递
  • 类型推断:Swift可以根据上下文自动推断闭包的类型

执行结果

执行这段代码会输出:匿名函数

闭包的特点

  1. 可以捕获上下文:闭包可以捕获和存储其所在作用域中的变量和常量
  2. 可以作为参数传递:可以作为函数的参数传递给其他函数
  3. 可以作为返回值:函数可以返回闭包作为返回值
  4. 有三种形式
    • 全局函数:有名字但不能捕获值
    • 嵌套函数:有名字且能捕获其封闭函数域内的值
    • 闭包表达式:无名字的轻量级语法,能捕获其上下文中变量或常量的值

命名函数 vs 闭包表达式的区别

示例代码对比

第一种写法:命名函数赋值
func test3(param:[Int]) -> String
{
    var temp:String = ""
    for item in param {
        temp = temp + String(item)
    }
    return temp
}
var e:([Int]) -> String = test3
print(e([2,3]))  // 输出:23
第二种写法:闭包表达式
var f:([Int]) -> String = {
    (a:[Int]) -> String in
    var temp:String = ""
    for item in a {
        temp = temp + String(item)
    }
    return temp
}
print(f([4,5,6]))  // 输出:456

核心区别

1. 定义方式
  • 命名函数:使用 func 关键字定义,有函数名,可以在其他地方重复调用
  • 闭包表达式:直接在赋值时定义,没有函数名,是一次性使用的匿名函数
2. 作用域和生命周期
  • 命名函数:有独立的生命周期,可以在定义后多次调用
  • 闭包表达式:赋值给变量后,通过变量名调用,变量的生命周期决定了闭包的生命周期
3. 内存占用
  • 命名函数:函数本身占用内存,但赋值给变量时只是引用传递
  • 闭包表达式:闭包本身和捕获的上下文都会占用内存
4. 使用场景
  • 命名函数:适用于需要重用的逻辑,函数体复杂,功能独立的情况
  • 闭包表达式:适用于简单的回调逻辑,作为参数传递,或需要捕获上下文的场景
5. 语法特点
  • 命名函数:有独立的函数签名,可以有默认参数等
  • 闭包表达式:语法更简洁,可以省略类型标注(类型推断)

实际应用场景

闭包在Swift开发中非常常用,比如:

  • 异步回调
  • 集合类型的操作(如map、filter、sort)
  • UI事件处理
  • 定时器回调
  • 网络请求回调

性能考虑

  • 命名函数:通常性能更好,因为没有额外的上下文捕获开销
  • 闭包表达式:如果捕获了大量上下文变量,可能产生循环引用或内存泄漏风险

最佳实践

  1. 使用命名函数当:

    • 函数逻辑复杂且需要重用
    • 函数需要有明确的名称便于调试
    • 不需要捕获外部上下文
  2. 使用闭包表达式当:

    • 需要简单的回调逻辑
    • 作为参数传递给其他函数
    • 需要捕获和修改外部变量
    • 用于内联定义,提高代码可读性

闭包作为函数参数的进阶用法

示例代码分析

1. 无参数闭包
func test(param:() -> Void)
{
    param()
}

// 调用方式:尾随闭包语法
test{
    print("test")
}

分析

  • test函数接受一个() -> Void类型的闭包参数
  • 使用尾随闭包语法调用:test{ print("test") }
  • 省略了param:标签,直接在大括号中定义闭包
2. 单参数闭包
func test2(param:(Int)-> Void)
{
    param(10)
}

// 完整写法
test2(param: {(value:Int) -> Void in
    print(value)
})

// 简化写法(尾随闭包)
test2{ (value) in
    print(value)
}

分析

  • test2函数接受(Int) -> Void类型的闭包
  • 完整写法:明确指定参数类型和返回类型
  • 简化写法:省略参数类型(类型推断),使用尾随闭包语法
3. 多参数有返回值闭包
func test3(param:(Int, Int) -> Int)
{
    print(param(10,30))
}

// 完整写法
test3(param:{(item1, item2) -> Int in
    return item1 + item2
})

// 极简写法
test3(param:{
    $0 + $1  // 使用$0、$1表示第一个和第二个参数
})

分析

  • test3函数接受(Int, Int) -> Int类型的闭包
  • 完整写法:明确参数名和返回类型
  • 极简写法:使用$0$1等简写参数名,省略returnin

记忆要点

闭包语法简化规则:
  1. 参数类型省略:如果能从上下文推断,可以省略参数类型
  2. 返回类型省略:单表达式闭包可以省略return
  3. 参数名简化:可以使用$0$1等代替参数名
  4. 圆括号省略:单参数时可以省略参数圆括号
尾随闭包语法规则:
  • 当闭包是函数的最后一个参数时,可以使用尾随闭包语法
  • 省略参数标签,直接在大括号中定义闭包
  • 使代码更简洁易读

实际运行结果

// test{ print("test") } 输出:test

// test2{ (value) in print(value) } 输出:10

// test3{ $0 + $1 } 传入(10,30) 输出:40

常见应用场景详解

1. 网络请求异步回调
// 模拟网络请求函数
func fetchUserData(userId: String, completion: @escaping (User?, Error?) -> Void) {
    DispatchQueue.global().async {
        // 模拟网络延迟
        Thread.sleep(forTimeInterval: 1.0)

        // 模拟成功获取用户数据
        let user = User(name: "张三", age: 25)
        DispatchQueue.main.async {
            completion(user, nil)
        }
    }
}

// 使用示例
fetchUserData(userId: "123") { user, error in
    if let user = user {
        print("获取用户成功:\(user.name), 年龄:\(user.age)")
        // 在主线程更新UI
        self.updateUI(with: user)
    } else if let error = error {
        print("获取用户失败:\(error.localizedDescription)")
    }
}

实际应用:iOS开发中所有网络请求几乎都使用闭包回调,如URLSession、Alamofire等。

2. 数组和集合操作
let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

// map:转换每个元素
let doubled = numbers.map { $0 * 2 }  // [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]

// filter:过滤元素
let evenNumbers = numbers.filter { $0 % 2 == 0 }  // [2, 4, 6, 8, 10]

// reduce:累积计算
let sum = numbers.reduce(0) { $0 + $1 }  // 55

// sort:排序
let sorted = numbers.sorted { $0 > $1 }  // [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]

// 链式调用
let result = numbers
    .filter { $0 > 5 }
    .map { $0 * $0 }
    .reduce(0, +)  // 平方和:36+49+64+81+100=330

实际应用:处理数据转换、过滤、统计等操作。

3. UI事件处理
import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        // UIButton点击事件
        let button = UIButton(type: .system)
        button.setTitle("点击我", for: .normal)
        button.addAction(UIAction { [weak self] _ in
            self?.handleButtonTap()
        }, for: .touchUpInside)

        // UITextField文本变化监听
        let textField = UITextField()
        textField.addAction(UIAction { _ in
            print("文本变化:\(textField.text ?? "")")
        }, for: .editingChanged)

        // 定时器
        Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
            print("定时器触发")
        }
    }

    func handleButtonTap() {
        print("按钮被点击了!")
        // 处理点击逻辑
    }
}

实际应用:所有UI控件的事件响应都使用闭包。

4. 动画和过渡效果
import UIKit

class AnimationViewController: UIViewController {

    let animatedView = UIView()

    override func viewDidLoad() {
        super.viewDidLoad()

        // UIView动画
        UIView.animate(withDuration: 0.5) {
            self.animatedView.alpha = 0.5
            self.animatedView.transform = CGAffineTransform(scaleX: 1.2, y: 1.2)
        } completion: { finished in
            if finished {
                print("动画完成")
                // 执行后续操作
            }
        }

        // UIViewPropertyAnimator
        let animator = UIViewPropertyAnimator(duration: 1.0, curve: .easeInOut) {
            self.animatedView.center = CGPoint(x: 200, y: 300)
        }

        animator.addCompletion { position in
            switch position {
            case .end:
                print("动画正常结束")
            case .current:
                print("动画被中断")
            case .start:
                print("动画开始")
            @unknown default:
                break
            }
        }

        animator.startAnimation()
    }
}

实际应用:所有iOS动画效果都依赖闭包。

5. 多线程和并发
import Foundation

class DataManager {

    // 异步数据处理
    func processDataAsync(data: [Int], completion: @escaping ([Int]) -> Void) {
        DispatchQueue.global(qos: .userInitiated).async {
            // 耗时操作
            let processedData = data.map { $0 * $0 }

            // 回到主线程回调
            DispatchQueue.main.async {
                completion(processedData)
            }
        }
    }

    // 并行处理
    func processInParallel(data: [Int], completion: @escaping ([Int]) -> Void) {
        let group = DispatchGroup()
        var results = [Int]()
        let lock = DispatchQueue(label: "com.example.lock")

        for item in data {
            group.enter()
            DispatchQueue.global().async {
                // 模拟耗时计算
                let result = item * item * item
                lock.sync {
                    results.append(result)
                }
                group.leave()
            }
        }

        group.notify(queue: .main) {
            completion(results.sorted())
        }
    }
}

// 使用示例
let manager = DataManager()
manager.processDataAsync(data: [1, 2, 3, 4, 5]) { results in
    print("处理结果:\(results)")  // [1, 4, 9, 16, 25]
}

实际应用:后台数据处理、文件操作等都需要多线程。

6. 通知中心
import Foundation

class NotificationManager {

    init() {
        // 监听键盘通知
        NotificationCenter.default.addObserver(
            forName: UIResponder.keyboardWillShowNotification,
            object: nil,
            queue: .main
        ) { notification in
            if let keyboardFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect {
                print("键盘高度:\(keyboardFrame.height)")
                // 调整UI布局
            }
        }

        // 自定义通知
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(handleCustomNotification(_:)),
            name: NSNotification.Name("CustomNotification"),
            object: nil
        )
    }

    @objc func handleCustomNotification(_ notification: Notification) {
        if let userInfo = notification.userInfo {
            print("收到自定义通知:\(userInfo)")
        }
    }

    func postCustomNotification() {
        NotificationCenter.default.post(
            name: NSNotification.Name("CustomNotification"),
            object: self,
            userInfo: ["message": "Hello from notification!"]
        )
    }
}

实际应用:系统通知和应用内组件通信。

7. 错误处理
enum NetworkError: Error {
    case invalidURL
    case noData
    case parsingError
}

func fetchData(from urlString: String, completion: @escaping (Result<Data, NetworkError>) -> Void) {
    guard let url = URL(string: urlString) else {
        completion(.failure(.invalidURL))
        return
    }

    URLSession.shared.dataTask(with: url) { data, response, error in
        if let error = error {
            completion(.failure(.noData))
            return
        }

        guard let data = data else {
            completion(.failure(.noData))
            return
        }

        completion(.success(data))
    }.resume()
}

// 使用示例
fetchData(from: "https://api.example.com/data") { result in
    switch result {
    case .success(let data):
        print("获取数据成功,大小:\(data.count) bytes")
        // 处理数据
    case .failure(let error):
        switch error {
        case .invalidURL:
            print("无效的URL")
        case .noData:
            print("没有数据")
        case .parsingError:
            print("解析错误")
        }
    }
}

实际应用:网络请求、文件操作等可能出错的操作。

8. 依赖注入和配置
class NetworkService {

    typealias RequestCompletion = (Result<Data, Error>) -> Void

    private let session: URLSession
    private let baseURL: URL

    init(session: URLSession = .shared, baseURL: URL) {
        self.session = session
        self.baseURL = baseURL
    }

    func request(endpoint: String, completion: @escaping RequestCompletion) {
        let url = baseURL.appendingPathComponent(endpoint)
        session.dataTask(with: url) { data, _, error in
            if let error = error {
                completion(.failure(error))
            } else if let data = data {
                completion(.success(data))
            }
        }.resume()
    }
}

// 配置不同的环境
let developmentConfig = NetworkService(
    session: URLSession.shared,
    baseURL: URL(string: "https://dev-api.example.com")!
)

let productionConfig = NetworkService(
    session: URLSession.shared,
    baseURL: URL(string: "https://api.example.com")!
)

实际应用:框架配置、测试mock等。

9. 内存管理和循环引用
class ViewModel {

    var data: [String] = []
    var onDataUpdate: (() -> Void)?

    func loadData() {
        // 模拟异步数据加载
        DispatchQueue.global().async { [weak self] in  // 捕获弱引用
            let newData = ["Item 1", "Item 2", "Item 3"]

            DispatchQueue.main.async {
                self?.data = newData
                self?.onDataUpdate?()  // 调用闭包
            }
        }
    }

    deinit {
        print("ViewModel 被释放")
    }
}

class ViewController {

    let viewModel = ViewModel()

    init() {
        // 使用 [weak self] 避免循环引用
        viewModel.onDataUpdate = { [weak self] in
            self?.updateUI()
        }
    }

    func updateUI() {
        print("UI已更新")
    }

    deinit {
        print("ViewController 被释放")
    }
}

实际应用:MVC/MVVM架构中View和ViewModel的通信。

10. 函数式编程模式
// 函数组合
func compose<A, B, C>(_ f: @escaping (B) -> C, _ g: @escaping (A) -> B) -> (A) -> C {
    return { x in f(g(x)) }
}

// 柯里化
func curry<A, B, C>(_ f: @escaping (A, B) -> C) -> (A) -> (B) -> C {
    return { a in { b in f(a, b) } }
}

// 使用示例
let add = { (a: Int, b: Int) -> Int in a + b }
let curriedAdd = curry(add)
let add5 = curriedAdd(5)
let result = add5(3)  // 8

let double = { (x: Int) -> Int in x * 2 }
let square = { (x: Int) -> Int in x * x }
let doubleThenSquare = compose(square, double)
let result2 = doubleThenSquare(3)  // (3*2)^2 = 36

实际应用:函数式编程风格的代码组织。

练习建议

  1. 记忆口诀

    • "有尾随,无标签;可省略,推断强"
    • "参数多,用$0;单表达式,return省略"
  2. 常见错误避免

    • 闭包参数类型与函数期望不匹配
    • 忘记处理闭包中的错误
    • 循环引用导致的内存泄漏
  3. 调试技巧

    • 使用print语句调试闭包执行
    • 注意闭包捕获的变量作用域
    • 使用weak self避免循环引用

闭包逻辑实现的本质分析

核心概念:逻辑定义 vs 逻辑执行

1. 闭包定义了逻辑,函数调用了逻辑
// 函数定义:只负责调用闭包,不实现具体逻辑
func processData(data: [Int], operation: (Int) -> String) {
    for item in data {
        let result = operation(item)  // 这里调用闭包,执行逻辑
        print("处理结果:\(result)")
    }
}

// 闭包定义:实现具体逻辑
let doubleAndFormat = { (number: Int) -> String in
    let doubled = number * 2
    return "数字 \(number) 的两倍是 \(doubled)"
}

// 使用:传递闭包给函数
let numbers = [1, 2, 3, 4, 5]
processData(data: numbers, operation: doubleAndFormat)

输出结果

处理结果:数字 1 的两倍是 2
处理结果:数字 2 的两倍是 4
处理结果:数字 3 的两倍是 6
处理结果:数字 4 的两倍是 8
处理结果:数字 5 的两倍是 10
2. 分析执行流程
func downloadImage(url: String, completion: @escaping (UIImage?) -> Void) {
    // 第1步:函数开始执行,准备异步操作
    print("开始下载图片:\(url)")

    DispatchQueue.global().async {
        // 第2步:模拟网络请求(这里不实现具体逻辑)
        print("正在从网络获取图片...")
        Thread.sleep(forTimeInterval: 1.0)

        // 第3步:模拟获取图片成功
        let image = UIImage(systemName: "photo")

        // 第4步:回到主线程,调用闭包
        DispatchQueue.main.async {
            print("网络请求完成,调用completion闭包")
            completion(image)  // 执行闭包:UI更新逻辑在这里执行
        }
    }
}

// 闭包定义UI更新逻辑
let imageView = UIImageView()
downloadImage(url: "https://example.com/image.jpg") { downloadedImage in
    // 第5步:闭包执行,实现UI更新逻辑
    print("闭包执行:更新UI")
    imageView.image = downloadedImage
    imageView.setNeedsDisplay()
}

执行顺序分析

  1. downloadImage函数开始执行(网络请求准备)
  2. 异步执行网络请求(不包含UI逻辑)
  3. 网络请求完成,回到主线程
  4. 调用闭包completion(image) - 这是函数调用闭包的地方
  5. 闭包执行imageView.image = downloadedImage - 这是UI逻辑实现的地方

关键理解点

谁负责实现逻辑?
  1. 函数(调用者)

    • 定义接口和调用时机
    • 负责数据准备和传递
    • 决定何时调用闭包
  2. 闭包(被调用者)

    • 实现具体的业务逻辑
    • 处理传递的数据
    • 执行最终的操作
实际开发中的职责分离
class NetworkManager {
    // 函数:只负责网络请求,不负责数据处理
    func fetchUserData(completion: @escaping (Result<User, Error>) -> Void) {
        // 网络请求逻辑在这里实现
        URLSession.shared.dataTask(with: userURL) { data, _, error in
            // 数据处理逻辑在这里实现
            if let error = error {
                completion(.failure(error))
            } else if let data = data {
                // 解析逻辑在这里实现
                do {
                    let user = try JSONDecoder().decode(User.self, from: data)
                    completion(.success(user))
                } catch {
                    completion(.failure(error))
                }
            }
        }.resume()
    }
}

class UserViewController: UIViewController {
    private let networkManager = NetworkManager()

    func loadUser() {
        // UI逻辑在这里实现
        networkManager.fetchUserData { [weak self] result in
            switch result {
            case .success(let user):
                // UI更新逻辑在这里实现
                self?.updateUI(with: user)
            case .failure(let error):
                // 错误处理UI逻辑在这里实现
                self?.showError(error)
            }
        }
    }

    private func updateUI(with user: User) {
        nameLabel.text = user.name
        ageLabel.text = "\(user.age)岁"
        // 更多UI更新逻辑...
    }

    private func showError(_ error: Error) {
        let alert = UIAlertController(title: "错误", message: error.localizedDescription, preferredStyle: .alert)
        present(alert, animated: true)
    }
}

记忆总结

函数 vs 闭包的职责分工

方面 函数(调用者) 闭包(被调用者)
逻辑实现 框架逻辑、流程控制 具体业务逻辑、数据处理
执行时机 决定何时执行 被调用时执行
关注点 何时做、如何传递数据 做什么、如何处理数据
可复用性 固定流程,灵活回调 每次使用可以不同逻辑

经典比喻

  • 函数是"饭店厨师":负责准备食材和烹饪环境
  • 闭包是"顾客点的菜谱":告诉厨师具体做什么菜
  • 调用闭包是"上菜":厨师按照菜谱做出菜给顾客

代码层面的理解

// 函数定义了"做什么事"的框架
func doSomething(action: () -> Void) {
    print("准备做事")
    action()  // 具体"怎么做"由闭包决定
    print("事情完成")
}

// 闭包实现了"怎么做"的具体逻辑
doSomething {
    print("这是我要做的事情")  // 具体逻辑在这里
}

这样理解就能清楚:函数提供执行环境和时机,闭包提供具体的执行逻辑

Swift闭包 vs Objective-C Block 对比

面试常问的核心区别

1. 语言和语法差异

Swift闭包

// 基本语法
let closure: (Int, Int) -> Int = { (a, b) in
    return a + b
}

// 简化语法
let simplified = { $0 + $1 }

// 尾随闭包
func calculate(a: Int, b: Int, operation: (Int, Int) -> Int) -> Int {
    return operation(a, b)
}

let result = calculate(a: 10, b: 5) { $0 - $1 }

Objective-C Block

// 基本语法
int (^block)(int, int) = ^(int a, int b) {
    return a + b;
};

// 使用
int result = block(10, 5);

// 作为参数传递
- (void)calculateWithA:(int)a b:(int)b operation:(int(^)(int, int))operation {
    int result = operation(a, b);
    NSLog(@"Result: %d", result);
}

// 调用
[self calculateWithA:10 b:5 operation:^int(int x, int y) {
    return x * y;
}];
2. 类型系统和类型推断
特性 Swift闭包 Objective-C Block
类型标注 支持类型推断,大多可省略 需要明确类型标注
返回值 支持多返回值、元组 只能单返回值
可选类型 原生支持Optional 需要使用指针
泛型 强大泛型支持 有限泛型支持
3. 内存管理和循环引用

Swift闭包

class ViewController: UIViewController {
    var completionHandler: (() -> Void)?

    func setup() {
        // 避免循环引用
        completionHandler = { [weak self] in
            self?.updateUI()
        }

        // 或使用 unowned(当self一定存在时)
        completionHandler = { [unowned self] in
            self.updateUI()
        }
    }
}

Objective-C Block

@interface ViewController ()
@property (nonatomic, copy) void (^completionHandler)(void);
@end

@implementation ViewController

- (void)setup {
    // 避免循环引用
    __weak typeof(self) weakSelf = self;
    self.completionHandler = ^{
        [weakSelf updateUI];
    };

    // 或使用 __unsafe_unretained
    __unsafe_unretained typeof(self) unsafeSelf = self;
    self.completionHandler = ^{
        [unsafeSelf updateUI];
    };
}

@end
4. 变量捕获机制
特性 Swift闭包 Objective-C Block
值捕获 自动捕获,支持修改 自动捕获,默认const
引用捕获 inout关键字 __block修饰符
作用域 清晰的作用域规则 需要注意block作用域

Swift捕获示例

func createCounter() -> () -> Int {
    var count = 0
    return {
        count += 1  // 可以修改捕获的变量
        return count
    }
}

Objective-C捕获示例

int (^createCounter)(void) {
    __block int count = 0;  // 需要__block修饰符才能修改
    return ^int{
        count += 1;
        return count;
    };
}
5. 性能和优化
特性 Swift闭包 Objective-C Block
编译优化 LLVM优化,更好的内联 GCC优化,较少内联
运行时开销 较小(值类型优化) 较大(堆分配)
调试体验 更好的错误信息 较难调试
6. 使用场景和生态

Swift闭包优势场景

  • 现代iOS开发(iOS 8+)
  • 函数式编程
  • 协议和泛型结合
  • SwiftUI开发

Objective-C Block优势场景

  • 遗留代码库维护
  • 与C/C++交互
  • 底层系统编程

优缺点对比总结

Swift闭包的优点:
  1. 语法简洁:类型推断、尾随闭包、简化参数
  2. 类型安全:编译时类型检查,更少运行时错误
  3. 现代化特性:泛型、协议、Optional等
  4. 性能更好:优化程度更高
  5. 易学易用:学习曲线平缓
Swift闭包的缺点:
  1. 学习成本:需要掌握新语法
  2. 兼容性:无法直接与Objective-C代码交互
  3. 迁移成本:从Block迁移需要重写
Objective-C Block的优点:
  1. 成熟稳定:经过多年验证
  2. 兼容性好:与C/Objective-C完美集成
  3. 底层控制:更直接的内存控制
Objective-C Block的缺点:
  1. 语法复杂:类型标注繁琐
  2. 类型不安全:容易出现类型错误
  3. 调试困难:错误信息不清晰
  4. 现代性不足:缺少泛型等特性

面试回答要点

如果问到选择哪个

  • 新项目:推荐Swift闭包
  • 遗留项目:继续使用Block
  • 混合项目:视情况而定

核心理解

  • Swift闭包是Block的现代化版本
  • 两者解决相同问题,但Swift更优雅
  • 掌握两者有助于理解编程范式演进

迁移指南

从Block到Swift闭包的常见转换:

// Objective-C Block
[self fetchDataWithCompletion:^(NSData *data, NSError *error) {
    if (error) {
        [self showError:error];
    } else {
        [self processData:data];
    }
}];
// Swift闭包
fetchData { data, error in
    if let error = error {
        showError(error)
    } else if let data = data {
        processData(data)
    }
}

Swift 属性包装器

我们来看 The Swift Programming Language (6.2.3) 中的例子。

@propertyWrapper
struct TwelveOrLess {
    private var number = 0
    
    var wrappedValue: Int {
        get { return number }
        set { number = min(newValue, 12) }
    }
}

结构 TwelveOrLess 是属性包装器,属性包装器可以是 class、struct 和 enum。属性包装器需要有个属性 wrappedValue,表示被包装的值。TwelveOrLess 的 wrappedValue 属性是计算属性,读写私有的存储属性 number,其 setter 确保 number 小于或等于 12。

struct SmallRectangle {
    @TwelveOrLess var height: Int
    @TwelveOrLess var width: Int
}

结构 SmallRectangle 应用包装器 TwelveOrLess 到属性 height 和 width,编译器重写代码为:

struct SmallRectangle {
    private var _height = TwelveOrLess()
    private var _width = TwelveOrLess()
    var height: Int {
        get { return _height.wrappedValue }
        set { _height.wrappedValue = newValue }
    }
    var width: Int {
        get { return _width.wrappedValue }
        set { _width.wrappedValue = newValue }
    }
}

生成 _height 和 _width 存储属性,存储包装器 TwelveOrLess 的实例。height 和 width 成为计算属性,访问 _height 和 _width 的 wrappedValue。

v2-a712b4a4c7d80561227b6bc40f5c8608_1440w.png

编译器还会为 SmallRectangle 生成 memberwise 初始化器,此时生成的初始化器为

init(
    height: TwelveOrLess = TwelveOrLess(),
    width: TwelveOrLess = TwelveOrLess()
)

参数 height 和 width 的类型为包装器类型 TwelveOrLess,TwelveOrLess 的初始化器为默认初始化器 init()。 如果 TwelveOrLess 增加初始化器 init(wrappedValue: Int),

@propertyWrapper
struct TwelveOrLess {   
    private var number = 0
    
    init(wrappedValue: Int) {
        self.number = min(wrappedValue, 12)
    }

    var wrappedValue: Int {
        get { return number }
        set { number = min(newValue, 12) }
    }
}

则 SmallRectangle 的初始化器为

init(
    height: Int,
    width: Int
)

参数 height 和 width 的类型为原始类型 Int。

如果 TwelveOrLess 增加初始化器 init(),

@propertyWrapper
struct TwelveOrLess {   
    private var number = 0
    
    init() {
        number  = 1
    }
    
    init(wrappedValue: Int) {
        self.number = min(wrappedValue, 12)
    }

    var wrappedValue: Int {
        get { return number }
        set { number = min(newValue, 12) }
    }
}

则 SmallRectangle 的初始化器为

init(
    height: TwelveOrLess = TwelveOrLess(),
    width: TwelveOrLess = TwelveOrLess()
)

此时想让 SmallRectangle 的 memberwise 初始化器参数类型为原始类型 Int,需要修改为

struct SmallRectangle {
    @TwelveOrLess var height: Int = 1
    @TwelveOrLess var width: Int = 1
}

编译器生成的代码为

struct SmallRectangle {
    private var _height = TwelveOrLess(wrappedValue: 1)
    private var _width = TwelveOrLess(wrappedValue: 1)
    var height: Int {
        get { return _height.wrappedValue }
        set { _height.wrappedValue = newValue }
    }
    var width: Int {
        get { return _width.wrappedValue }
        set { _width.wrappedValue = newValue }
    }
    
    init(height: Int, width: Int) {
        self.height = height
        self.width = width
    }
}

总结:对一个 struct 的某个属性应用包装器,要使 memberwise 初始化器对应参数类型为原始类型,需要如下条件之一,

  • 属性包装器有初始化器 init(wrappedValue:),并且没有 init()
  • 属性有初始值,像 @TwelveOrLess var height: Int = 1

否则,memberwise 初始化器参数类型为包装器类型。

除了被包装的值,属性包装器可以通过定义一个 projected value 暴露额外的功能。

@propertyWrapper
struct SmallNumber {
    private var number: Int
    private(set) var projectedValue: Bool
    
    var wrappedValue: Int {
        get { return number }
        set {
            if newValue > 12 {
                number = 12
                projectedValue = true
            } else {
                number = newValue
                projectedValue = false
            }
        }
    }
    
    init() {
        self.number = 0
        self.projectedValue = false
    }
}

上面代码中,SmallNumber 结构增加了一个属性 projectedValue,用来记录包装器是否调整了被包装的值。

struct SomeStructure {
    @SmallNumber var someNumber: Int
}

var someStructure = SomeStructure()
print(someStructure.$someNumber)
// 打印 false
someStructure.someNumber = 55
print(someStructure.$someNumber)
// 打印 true

通过在被包装的属性名前增加 $ 来访问包装器的 projectedValue。

BSBacktraceLogger源码解析

借助AI辅助。

源码地址

github.com/bestswifter…

逐行注释

//
//  BSBacktraceLogger.m
//  BSBacktraceLogger
//
//  Created by 张星宇 on 16/8/27.
//  Copyright © 2016年 bestswifter. All rights reserved.
//

// ==================== 头文件引入说明 ====================

// 导入自定义头文件,包含对外公开的接口声明
#import "BSBacktraceLogger.h"

// 【Mach内核】导入Mach内核相关头文件
// Mach是XNU内核的核心部分,提供了底层的线程、任务、内存管理等功能
// 这个头文件包含了thread_t(线程类型)、task_t(任务/进程类型)、kern_return_t(内核返回值类型)等
#import <mach/mach.h>

// 【动态链接】导入动态链接库相关函数
// 主要包含dladdr()函数,用于将内存地址转换为符号信息(本代码自己实现了类似功能)
// Dl_info结构体定义也在这里,用于存储符号信息(文件名、函数名、地址等)
#include <dlfcn.h>

// 【POSIX线程】导入POSIX线程库(pthread)
// pthread是跨平台的线程标准,Mach线程是macOS/iOS特有的底层线程
// 本代码需要在NSThread、pthread和Mach线程之间进行转换
#include <pthread.h>

// 【系统类型】导入系统基础类型定义
// 如size_t、ssize_t等基础类型
#include <sys/types.h>

// 【限制值】导入系统限制值定义
// 如ULONG_MAX(无符号长整型最大值)、UINT_MAX(无符号整型最大值)等
#include <limits.h>

// 【字符串操作】导入C标准库字符串操作函数
// 如strcmp(字符串比较)、strrchr(查找字符)等
#include <string.h>

// 【动态链接器】导入dyld(dynamic linker)相关函数
// dyld负责加载和链接动态库,提供了获取已加载镜像(动态库/可执行文件)信息的函数
// 如_dyld_image_count()、_dyld_get_image_header()等
#include <mach-o/dyld.h>

// 【符号表】导入Mach-O文件格式中符号表相关的结构体定义
// nlist/nlist_64是符号表项的结构体,包含符号名、地址、类型等信息
#include <mach-o/nlist.h>

// ==================== CPU架构适配宏定义 ====================
// 不同CPU架构的寄存器名称和结构不同,需要通过条件编译来适配

#pragma -mark DEFINE MACRO FOR DIFFERENT CPU ARCHITECTURE

// 【ARM64架构】适用于iPhone 5s及以后的iOS设备、Apple Silicon Mac
#if defined(__arm64__)

// ARM64指令地址去标签宏
// 背景知识:ARM64架构中,指针的低位可能被用来存储额外信息(称为"标签位")
// 低2位通常用于标识指针类型或其他元数据,需要清除这些位才能得到真实地址
// ~(3UL) 等于 11111...11100(二进制),与操作后会清除低2位
#define DETAG_INSTRUCTION_ADDRESS(A) ((A) & ~(3UL))

// 线程状态结构体的元素数量
// ARM_THREAD_STATE64_COUNT 是系统定义的常量,表示ARM64架构下线程状态结构体包含多少个字段
#define BS_THREAD_STATE_COUNT ARM_THREAD_STATE64_COUNT

// 线程状态类型标识符
// 用于告诉thread_get_state()函数我们想要获取哪种类型的线程状态
#define BS_THREAD_STATE ARM_THREAD_STATE64

// 帧指针寄存器(Frame Pointer Register)
// __fp 是ARM64架构中的x29寄存器,专门用作帧指针
// 帧指针指向当前函数的栈帧起始位置,通过它可以遍历整个调用栈
#define BS_FRAME_POINTER __fp

// 栈指针寄存器(Stack Pointer Register)
// __sp 是ARM64架构中的SP寄存器,指向栈顶
// 栈是向下增长的,SP总是指向最后一个压入栈的数据
#define BS_STACK_POINTER __sp

// 程序计数器/指令指针寄存器(Program Counter)
// __pc 是ARM64架构中的PC寄存器,指向当前正在执行的指令地址
// 这是我们获取调用栈的起点
#define BS_INSTRUCTION_ADDRESS __pc

// 【ARM32架构】适用于iPhone 5及之前的旧设备(已基本淘汰)
#elif defined(__arm__)

// ARM32指令地址去标签宏
// ARM32的低1位用于标识Thumb模式(ARM有两种指令集:ARM和Thumb)
// 如果低位是1,表示Thumb模式;如果是0,表示ARM模式
// ~(1UL) 等于 11111...11110(二进制),与操作后会清除低1位
#define DETAG_INSTRUCTION_ADDRESS(A) ((A) & ~(1UL))

// 线程状态结构体的元素数量(32位ARM)
#define BS_THREAD_STATE_COUNT ARM_THREAD_STATE_COUNT

// 线程状态类型标识符(32位ARM)
#define BS_THREAD_STATE ARM_THREAD_STATE

// 帧指针寄存器(32位ARM使用r7寄存器作为帧指针)
// ARM32有16个通用寄存器r0-r15,其中r7在iOS中被约定用作帧指针
#define BS_FRAME_POINTER __r[7]

// 栈指针寄存器(r13,也称为SP)
#define BS_STACK_POINTER __sp

// 程序计数器寄存器(r15,也称为PC)
#define BS_INSTRUCTION_ADDRESS __pc

// 【x86_64架构】适用于64位Intel Mac、iOS模拟器
#elif defined(__x86_64__)

// x86架构的指令地址不需要去标签,直接返回原值
// x86架构不像ARM那样在指针中嵌入额外信息
#define DETAG_INSTRUCTION_ADDRESS(A) (A)

// 线程状态结构体的元素数量(x86_64)
#define BS_THREAD_STATE_COUNT x86_THREAD_STATE64_COUNT

// 线程状态类型标识符(x86_64)
#define BS_THREAD_STATE x86_THREAD_STATE64

// 帧指针寄存器(x86_64使用rbp寄存器)
// rbp是Base Pointer的缩写,专门用作帧指针
#define BS_FRAME_POINTER __rbp

// 栈指针寄存器(rsp = Stack Pointer)
#define BS_STACK_POINTER __rsp

// 指令指针寄存器(rip = Instruction Pointer)
// x86_64使用rip而不是pc来表示指令指针
#define BS_INSTRUCTION_ADDRESS __rip

// 【i386架构】适用于32位Intel Mac、旧的iOS模拟器(已基本淘汰)
#elif defined(__i386__)

// x86架构不需要去标签
#define DETAG_INSTRUCTION_ADDRESS(A) (A)

// 线程状态结构体的元素数量(i386)
#define BS_THREAD_STATE_COUNT x86_THREAD_STATE32_COUNT

// 线程状态类型标识符(i386)
#define BS_THREAD_STATE x86_THREAD_STATE32

// 帧指针寄存器(32位x86使用ebp)
// ebp是Extended Base Pointer的缩写
#define BS_FRAME_POINTER __ebp

// 栈指针寄存器(esp = Extended Stack Pointer)
#define BS_STACK_POINTER __esp

// 指令指针寄存器(eip = Extended Instruction Pointer)
#define BS_INSTRUCTION_ADDRESS __eip

#endif

// ==================== 通用宏定义 ====================

// 【返回地址转调用指令地址】
// 背景知识:当函数A调用函数B时:
//   1. CPU会将"返回地址"(return address)压入栈,这个地址指向函数A中CALL指令的下一条指令
//   2. 如果我们想知道是在哪里调用的函数B,需要找到CALL指令本身的地址
//   3. CALL指令的地址 = 返回地址 - 1(或更多,取决于指令长度)
// 这个宏就是做这个转换:先去标签,然后减1
#define CALL_INSTRUCTION_FROM_RETURN_ADDRESS(A) (DETAG_INSTRUCTION_ADDRESS((A)) - 1)

// 【根据指针大小定义格式化字符串】
// __LP64__ 表示"Long and Pointer are 64-bit",用于区分32位和64位系统
#if defined(__LP64__)

// 64位系统的格式定义
// 堆栈跟踪输出格式示例:
// 0   MyApp                           0x0000000100001234 main + 52
// %-4d:序号,左对齐,占4个字符
// %-31s:模块名,左对齐,占31个字符
// 0x%016lx:地址,16位十六进制(前面补0)
// %s:符号名(函数名)
// %lu:偏移量(无符号长整型)
#define TRACE_FMT         "%-4d%-31s 0x%016lx %s + %lu"

// 指针的完整格式:0x0000000100001234(16位十六进制,前面补0)
#define POINTER_FMT       "0x%016lx"

// 指针的短格式:0x100001234(不补0)
#define POINTER_SHORT_FMT "0x%lx"

// 符号表结构体类型(64位)
// nlist_64包含:符号名索引、符号类型、段索引、描述符、符号地址(64位)
#define BS_NLIST struct nlist_64

#else

// 32位系统的格式定义
// 输出格式示例:
// 0   MyApp                           0x00001234 main + 52
#define TRACE_FMT         "%-4d%-31s 0x%08lx %s + %lu"

// 指针的完整格式:0x00001234(8位十六进制)
#define POINTER_FMT       "0x%08lx"

// 指针的短格式
#define POINTER_SHORT_FMT "0x%lx"

// 符号表结构体类型(32位)
// nlist包含:符号名索引、符号类型、段索引、描述符、符号地址(32位)
#define BS_NLIST struct nlist

#endif

// ==================== 数据结构定义 ====================

// 【栈帧结构体】
// 背景知识:什么是栈帧(Stack Frame)?
// 每次函数调用时,系统会在栈上分配一块内存区域,称为"栈帧",用于存储:
//   - 函数的局部变量
//   - 函数参数
//   - 返回地址(调用者的下一条指令)
//   - 前一个栈帧的地址(用于回溯)
//
// 栈帧通过帧指针(Frame Pointer)连接成链表结构:
//   main的栈帧 ← funcA的栈帧 ← funcB的栈帧 ← 当前函数栈帧
//
// BSStackFrameEntry就是这个链表节点的简化版本,只包含:
//   - previous: 指向调用者(上一个函数)的栈帧
//   - return_address: 返回到调用者的地址
typedef struct BSStackFrameEntry{
    // 指向前一个栈帧的指针(const表示这个指针本身不可修改)
    // 通过这个指针可以一层层往回遍历,直到找到main函数甚至更底层
    const struct BSStackFrameEntry *const previous;
    
    // 返回地址:当前函数执行完毕后,应该返回到哪里继续执行
    // 这个地址指向调用者的代码中,CALL指令的下一条指令
    // uintptr_t是无符号整型,其大小与指针相同(32位系统是32位,64位系统是64位)
    const uintptr_t return_address;
} BSStackFrameEntry;

// ==================== 静态全局变量 ====================

// 【主线程ID】
// mach_port_t 是Mach内核中的"端口"类型,用于进程间通信
// 在Mach中,每个线程都有一个唯一的端口ID(实际上就是一个整数)
// 这个变量在+load方法中初始化,保存主线程的Mach端口ID,用于后续快速识别主线程
static mach_port_t main_thread_id;

// ==================== 类实现 ====================

@implementation BSBacktraceLogger

// 【类加载方法】
// +load方法的特点:
//   1. 在程序启动时,类被加载到内存时自动调用(比main函数还早)
//   2. 每个类只调用一次
//   3. 即使类没有被使用也会调用
//   4. 调用顺序:父类 → 子类 → 分类
+ (void)load {
    // mach_thread_self() 返回当前线程的Mach端口ID
    // 因为+load在主线程中执行,所以这里获取的就是主线程ID
    // 保存这个ID是因为后续判断主线程时比较方便(直接比较ID,而不需要通过名称匹配)
    main_thread_id = mach_thread_self();
}

#pragma -mark Implementation of interface
// ==================== 公共接口实现 ====================

// 【获取指定NSThread的调用栈】
// 参数:thread - NSThread对象(OC层的线程抽象)
// 返回:格式化的调用栈字符串
+ (NSString *)bs_backtraceOfNSThread:(NSThread *)thread {
    // 分两步:
    // 1. bs_machThreadFromNSThread: 将OC的NSThread转换为底层的Mach线程ID
    // 2. _bs_backtraceOfThread: 根据Mach线程ID获取调用栈
    // 为什么要转换?因为真正的线程操作需要使用Mach内核API,它只认Mach线程ID
    return _bs_backtraceOfThread(bs_machThreadFromNSThread(thread));
}

// 【获取当前线程的调用栈】
// 应用场景:在检测到异常或需要记录日志时,快速获取当前位置的调用栈
+ (NSString *)bs_backtraceOfCurrentThread {
    // [NSThread currentThread] 返回当前正在执行的线程对象
    return [self bs_backtraceOfNSThread:[NSThread currentThread]];
}

// 【获取主线程的调用栈】
// 应用场景:卡顿检测 - 当检测到主线程卡顿时,获取主线程调用栈分析卡在哪里
+ (NSString *)bs_backtraceOfMainThread {
    // [NSThread mainThread] 返回主线程对象
    return [self bs_backtraceOfNSThread:[NSThread mainThread]];
}

// 【获取所有线程的调用栈】
// 应用场景:
//   1. 死锁检测 - 查看所有线程状态,分析是否存在相互等待
//   2. 性能分析 - 定期采样所有线程,统计CPU热点
//   3. 崩溃日志 - 在崩溃时记录所有线程状态
+ (NSString *)bs_backtraceOfAllThread {
    // thread_act_array_t 是线程ID数组的类型(实际上是 thread_t* 指针)
    thread_act_array_t threads;
    
    // mach_msg_type_number_t 是Mach消息类型的数量类型(实际上是unsigned int)
    mach_msg_type_number_t thread_count = 0;
    
    // task_t 是Mach中"任务"的类型,任务就是进程的Mach术语
    // mach_task_self() 返回当前进程的任务端口
    const task_t this_task = mach_task_self();
    
    // 【task_threads函数】获取指定任务(进程)的所有线程
    // 参数1:任务端口(进程)
    // 参数2:输出参数,返回线程ID数组
    // 参数3:输出参数,返回线程数量
    // 返回值:kern_return_t 是内核函数的返回值类型,KERN_SUCCESS表示成功
    kern_return_t kr = task_threads(this_task, &threads, &thread_count);
    
    // 检查是否成功获取线程列表
    if(kr != KERN_SUCCESS) {
        return @"Fail to get information of all threads";
    }
    
    // 创建可变字符串,用于拼接所有线程的回溯信息
    // %u 是无符号整型格式符
    NSMutableString *resultString = [NSMutableString stringWithFormat:@"Call Backtrace of %u threads:\n", thread_count];
    
    // 遍历所有线程
    for(int i = 0; i < thread_count; i++) {
        // threads[i] 是第i个线程的Mach端口ID
        // 获取该线程的调用栈并追加到结果字符串
        [resultString appendString:_bs_backtraceOfThread(threads[i])];
    }
    
    // 返回不可变副本(防止外部修改)
    return [resultString copy];
}

#pragma -mark Get call backtrace of a mach_thread
// ==================== 核心:获取Mach线程的调用栈回溯 ====================

// 【核心函数】根据Mach线程ID获取调用栈
// 参数:thread - thread_t类型,即Mach线程的端口ID(实际上是一个整数)
// 返回:格式化的调用栈字符串
NSString *_bs_backtraceOfThread(thread_t thread) {
    // 【步骤1:准备缓冲区】
    // 创建一个数组,用于存储最多50层的函数调用地址
    // uintptr_t 是"unsigned integer pointer type"的缩写,保证能容纳一个指针
    // 为什么是50?这是一个经验值,通常调用栈不会超过50层
    uintptr_t backtraceBuffer[50];
    
    // 当前处理到第几层调用栈
    int i = 0;
    
    // 创建结果字符串,包含线程ID(用于识别是哪个线程)
    NSMutableString *resultString = [[NSMutableString alloc] initWithFormat:@"Backtrace of Thread %u:\n", thread];
    
    // 【步骤2:获取线程的寄存器状态】
    // _STRUCT_MCONTEXT 是机器上下文结构体,包含所有寄存器的当前值
    // mcontext = machine context(机器上下文)
    // 为什么需要它?因为调用栈的起点是当前寄存器的值(PC、FP、SP等)
    _STRUCT_MCONTEXT machineContext;
    
    // 调用函数获取线程状态并填充到machineContext中
    // 如果失败(比如线程已销毁、权限不足等),返回错误信息
    if(!bs_fillThreadStateIntoMachineContext(thread, &machineContext)) {
        return [NSString stringWithFormat:@"Fail to get information about thread: %u", thread];
    }
    
    // 【步骤3:提取当前指令地址(PC寄存器)】
    // PC(Program Counter)= 程序计数器,指向当前正在执行的指令
    // 这是调用栈的第0层(最内层,即当前函数)
    const uintptr_t instructionAddress = bs_mach_instructionAddress(&machineContext);
    backtraceBuffer[i] = instructionAddress;
    ++i;
    
    // 【步骤4:提取链接寄存器(LR,仅ARM架构)】
    // LR(Link Register)= 链接寄存器,ARM架构特有
    // 背景知识:ARM架构中,当函数A调用函数B时:
    //   - 返回地址不是压入栈,而是保存在LR寄存器中(为了提高性能)
    //   - 如果函数B还要调用函数C,那么会先把LR的值压入栈,再更新LR
    // 所以LR通常包含直接调用者的地址(调用栈的第1层)
    uintptr_t linkRegister = bs_mach_linkRegister(&machineContext);
    if (linkRegister) {
        backtraceBuffer[i] = linkRegister;
        i++;
    }
    
    // 【安全检查】确保指令地址有效
    if(instructionAddress == 0) {
        return @"Fail to get instruction address";
    }
    
    // 【步骤5:开始遍历栈帧链表】
    // 初始化栈帧结构体(全部清零)
    BSStackFrameEntry frame = {0};
    
    // 获取帧指针(FP寄存器的值)
    // FP指向当前函数的栈帧,栈帧的开头就是BSStackFrameEntry结构体
    const uintptr_t framePtr = bs_mach_framePointer(&machineContext);
    
    // 检查帧指针是否有效,并尝试读取第一个栈帧
    // bs_mach_copyMem是安全的内存读取函数(使用内核API,可以读取其他线程的内存)
    if(framePtr == 0 ||
       bs_mach_copyMem((void *)framePtr, &frame, sizeof(frame)) != KERN_SUCCESS) {
        return @"Fail to get frame pointer";
    }
    
    // 【步骤6:循环遍历栈帧链表】
    // 栈帧链表:当前栈帧 → 调用者栈帧 → 调用者的调用者栈帧 → ... → main → _start
    for(; i < 50; i++) {
        // 保存当前栈帧的返回地址(即"从哪里调用过来的")
        backtraceBuffer[i] = frame.return_address;
        
        // 终止条件(满足任一条件就停止遍历):
        // 1. 返回地址为0 - 已经到达栈底
        // 2. 前一个栈帧指针为0 - 没有更上层的调用者了
        // 3. 无法读取前一个栈帧 - 内存访问失败(可能栈帧已损坏)
        if(backtraceBuffer[i] == 0 ||
           frame.previous == 0 ||
           bs_mach_copyMem(frame.previous, &frame, sizeof(frame)) != KERN_SUCCESS) {
            break;
        }
    }
    
    // 【步骤7:符号化所有地址】
    // 记录实际获取到多少层调用栈
    int backtraceLength = i;
    
    // Dl_info 是动态链接信息结构体(定义在<dlfcn.h>),包含:
    //   - dli_fname: 文件名(如"MyApp"、"libSystem.dylib")
    //   - dli_fbase: 文件加载的基地址
    //   - dli_sname: 符号名(函数名,如"main"、"-[ViewController viewDidLoad]")
    //   - dli_saddr: 符号地址(函数的起始地址)
    Dl_info symbolicated[backtraceLength];
    
    // bs_symbolicate函数将每个地址转换为符号信息
    // 参数1:地址数组
    // 参数2:输出符号信息数组
    // 参数3:数组长度
    // 参数4:跳过的条目数(0表示不跳过)
    bs_symbolicate(backtraceBuffer, symbolicated, backtraceLength, 0);
    
    // 【步骤8:格式化输出】
    // 遍历所有调用栈层级,格式化成可读字符串
    for (int i = 0; i < backtraceLength; ++i) {
        // bs_logBacktraceEntry格式化单条调用栈信息
        // 输出格式类似:0   MyApp    0x0000000100001234 main + 52
        [resultString appendFormat:@"%@", bs_logBacktraceEntry(i, backtraceBuffer[i], &symbolicated[i])];
    }
    
    // 添加结尾换行
    [resultString appendFormat:@"\n"];
    
    // 返回不可变副本
    return [resultString copy];
}

#pragma -mark Convert NSThread to Mach thread
// ==================== NSThread转Mach线程 ====================

// 【线程转换函数】将NSThread对象转换为Mach线程ID
// 背景知识:iOS/macOS中有三种线程抽象:
//   1. NSThread - OC层的线程封装,提供面向对象的接口
//   2. pthread - POSIX标准的线程,跨平台(Unix/Linux/macOS都支持)
//   3. Mach thread - Mach内核的原生线程,macOS/iOS特有,性能最高
// NSThread内部实际上封装了pthread,而pthread底层又是Mach thread
// 但它们之间没有直接的API可以互相转换,所以需要用一些技巧
//
// 参数:nsthread - NSThread对象
// 返回:thread_t - Mach线程ID(实际上是mach_port_t类型,即端口号)
thread_t bs_machThreadFromNSThread(NSThread *nsthread) {
    // 【准备工作】
    // 用于存储pthread线程名称的缓冲区(最多256字符)
    char name[256];
    
    // 线程数量
    mach_msg_type_number_t count;
    
    // 线程列表数组
    thread_act_array_t list;
    
    // 获取当前进程的所有Mach线程
    // mach_task_self() 返回当前进程的任务端口
    task_threads(mach_task_self(), &list, &count);
    
    // 【转换策略:通过线程名称匹配】
    // 因为没有直接的API,所以采用"临时修改线程名称"的方法:
    // 1. 给NSThread设置一个唯一的名称(时间戳)
    // 2. 遍历所有Mach线程,将其转换为pthread
    // 3. 通过pthread的名称找到匹配的线程
    // 4. 恢复原始名称
    
    // 获取当前时间戳(精确到微秒),确保唯一性
    // timeIntervalSince1970 返回自1970-01-01 00:00:00到现在的秒数(浮点数)
    NSTimeInterval currentTimestamp = [[NSDate date] timeIntervalSince1970];
    
    // 保存NSThread的原始名称(稍后需要恢复)
    NSString *originName = [nsthread name];
    
    // 将线程名称临时设置为时间戳字符串
    // 例如:"1609459200.123456"
    [nsthread setName:[NSString stringWithFormat:@"%f", currentTimestamp]];
    
    // 【特殊处理:主线程】
    // 主线程ID在+load方法中已经保存,可以直接返回,无需遍历
    if ([nsthread isMainThread]) {
        return (thread_t)main_thread_id;
    }
    
    // 【遍历所有Mach线程进行匹配】
    for (int i = 0; i < count; ++i) {
        // pthread_from_mach_thread_np: 将Mach线程转换为pthread
        // _np后缀表示"non-portable"(非可移植),即macOS/iOS特有的扩展函数
        pthread_t pt = pthread_from_mach_thread_np(list[i]);
        
        // 【冗余检查】这段代码实际上永远不会执行
        // 因为如果isMainThread为true,前面已经return了
        // 这可能是代码重构时遗留的冗余逻辑
        if ([nsthread isMainThread]) {
            if (list[i] == main_thread_id) {
                return list[i];
            }
        }
        
        // 如果pthread转换成功(有些Mach线程可能没有对应的pthread)
        if (pt) {
            // 清空名称缓冲区
            name[0] = '\0';
            
            // pthread_getname_np: 获取pthread的线程名称
            // 参数1:pthread
            // 参数2:输出缓冲区
            // 参数3:缓冲区大小
            pthread_getname_np(pt, name, sizeof name);
            
            // strcmp: 字符串比较函数,相等返回0
            // 如果pthread的名称与我们设置的时间戳匹配,说明找到了对应的Mach线程
            if (!strcmp(name, [nsthread name].UTF8String)) {
                // 【找到匹配】恢复原始线程名称
                [nsthread setName:originName];
                
                // 返回Mach线程ID
                return list[i];
            }
        }
    }
    
    // 【未找到匹配】恢复原始名称
    [nsthread setName:originName];
    
    // 返回当前线程ID作为后备方案
    // mach_thread_self() 返回当前正在执行的线程ID
    // 这种情况通常不应该发生,除非传入的NSThread对象有问题
    return mach_thread_self();
}

#pragma -mark GenerateBacbsrackEnrty
// ==================== 格式化调用栈条目 ====================

// 【格式化单条调用栈】将地址和符号信息格式化为可读字符串
// 输出示例:0   MyApp       0x0000000100001234 main + 52
//          ^   ^            ^                  ^    ^ ^
//          |   |            |                  |    | |
//        序号 模块名        地址              函数名 + 偏移量
//
// 参数:
//   entryNum - 调用栈序号(0表示最内层,数字越大越外层)
//   address - 内存地址
//   dlInfo - 符号信息(包含文件名、函数名、地址等)
// 返回:格式化的字符串
NSString* bs_logBacktraceEntry(const int entryNum,
                               const uintptr_t address,
                               const Dl_info* const dlInfo) {
    // 文件地址缓冲区(当文件名为空时用于存储地址的字符串形式)
    char faddrBuff[20];
    
    // 符号地址缓冲区(当符号名为空时用于存储地址的字符串形式)
    char saddrBuff[20];
    
    // 【提取文件名】
    // dlInfo->dli_fname 是完整路径,如"/System/Library/Frameworks/UIKit.framework/UIKit"
    // bs_lastPathEntry 提取最后一部分,如"UIKit"
    const char* fname = bs_lastPathEntry(dlInfo->dli_fname);
    
    // 如果文件名为空(符号信息不完整)
    if(fname == NULL) {
        // sprintf: 格式化输出到字符串
        // POINTER_FMT 是根据32/64位定义的格式(如"0x%08lx"或"0x%016lx")
        // dlInfo->dli_fbase 是文件的基地址(加载到内存的起始地址)
        sprintf(faddrBuff, POINTER_FMT, (uintptr_t)dlInfo->dli_fbase);
        
        // 使用地址字符串作为文件名
        fname = faddrBuff;
    }
    
    // 【计算偏移量】
    // 偏移量 = 当前地址 - 符号起始地址
    // 例如:main函数起始地址是0x100001200,当前地址是0x100001234,偏移量就是52字节
    uintptr_t offset = address - (uintptr_t)dlInfo->dli_saddr;
    
    // 【提取符号名】
    // dlInfo->dli_sname 是函数名,如"main"、"-[ViewController viewDidLoad]"
    const char* sname = dlInfo->dli_sname;
    
    // 如果符号名为空(可能是被strip剥离了符号,或者是动态生成的代码)
    if(sname == NULL) {
        // 使用文件基地址的字符串形式作为符号名
        sprintf(saddrBuff, POINTER_SHORT_FMT, (uintptr_t)dlInfo->dli_fbase);
        sname = saddrBuff;
        
        // 重新计算偏移量(相对于文件基地址而不是符号地址)
        offset = address - (uintptr_t)dlInfo->dli_fbase;
    }
    
    // 【格式化输出】
    // PRIxPTR 是可移植的指针格式符(定义在<inttypes.h>),根据平台自动选择正确的格式
    // 输出格式:
    //   %-30s: 文件名,左对齐,占30个字符
    //   0x%08" PRIxPTR ": 地址,十六进制,至少8位(32位)或16位(64位)
    //   %s: 符号名
    //   + %lu: 偏移量(无符号长整型)
    //   \n: 换行符
    return [NSString stringWithFormat:@"%-30s  0x%08" PRIxPTR " %s + %lu\n" ,fname, (uintptr_t)address, sname, offset];
}

// 【路径提取函数】从完整路径中提取文件名
// 例如:"/System/Library/Frameworks/UIKit.framework/UIKit" → "UIKit"
//       "MyApp" → "MyApp"(如果没有路径分隔符,返回原字符串)
//
// 参数:path - 完整路径字符串
// 返回:文件名字符串(指向原字符串的某个位置,不是新分配的内存)
const char* bs_lastPathEntry(const char* const path) {
    // 空指针检查
    if(path == NULL) {
        return NULL;
    }
    
    // strrchr: 从右往左查找指定字符
    // 查找最后一个'/'字符的位置
    char* lastFile = strrchr(path, '/');
    
    // 三元运算符:
    // 如果找到'/',返回'/'后面的部分(lastFile + 1跳过'/'字符)
    // 如果没找到'/',说明path本身就是文件名,直接返回
    return lastFile == NULL ? path : lastFile + 1;
}

#pragma -mark HandleMachineContext
// ==================== 机器上下文处理 ====================

// 【获取线程状态】将线程的寄存器状态填充到机器上下文结构体
// 这是获取调用栈的第一步:需要知道线程当前的寄存器值(PC、FP、SP等)
//
// 参数:
//   thread - Mach线程ID
//   machineContext - 输出参数,_STRUCT_MCONTEXT结构体指针,用于存储寄存器状态
// 返回:bool - true表示成功,false表示失败
bool bs_fillThreadStateIntoMachineContext(thread_t thread, _STRUCT_MCONTEXT *machineContext) {
    // 【mach_msg_type_number_t】
    // 状态结构体的大小(以"自然单位"为单位,通常是4字节)
    // BS_THREAD_STATE_COUNT 是根据CPU架构定义的宏,表示状态结构体包含多少个字段
    mach_msg_type_number_t state_count = BS_THREAD_STATE_COUNT;
    
    // 【thread_get_state】Mach内核函数,获取线程状态
    // 这是一个非常底层的系统调用,可以获取线程的完整寄存器快照
    //
    // 参数:
    //   thread - 线程ID
    //   BS_THREAD_STATE - 状态类型(ARM_THREAD_STATE64、x86_THREAD_STATE64等)
    //   (thread_state_t)&machineContext->__ss - 输出缓冲区
    //       __ss是"saved state"的缩写,表示保存的CPU状态
    //       thread_state_t 是void*的别名,所以需要类型转换
    //   &state_count - 输入/输出参数,输入时是缓冲区大小,输出时是实际写入的大小
    //
    // 返回值:kern_return_t - 内核函数的返回值
    //   KERN_SUCCESS (0) - 成功
    //   KERN_INVALID_ARGUMENT - 参数无效
    //   KERN_FAILURE - 其他失败
    kern_return_t kr = thread_get_state(thread, BS_THREAD_STATE, (thread_state_t)&machineContext->__ss, &state_count);
    
    // 检查是否成功
    return (kr == KERN_SUCCESS);
}

// 【提取帧指针】从机器上下文中读取帧指针寄存器的值
// 帧指针(Frame Pointer)指向当前函数的栈帧起始位置
// 通过帧指针可以遍历整个调用栈链表
//
// 参数:machineContext - 机器上下文(mcontext_t是_STRUCT_MCONTEXT的typedef别名)
// 返回:uintptr_t - 帧指针的值(一个内存地址)
uintptr_t bs_mach_framePointer(mcontext_t const machineContext){
    // machineContext->__ss 是"saved state",包含所有寄存器的值
    // BS_FRAME_POINTER 是根据CPU架构定义的宏:
    //   ARM64: __fp(x29寄存器)
    //   ARM32: __r[7](r7寄存器)
    //   x86_64: __rbp
    //   i386: __ebp
    return machineContext->__ss.BS_FRAME_POINTER;
}

// 【提取栈指针】从机器上下文中读取栈指针寄存器的值
// 栈指针(Stack Pointer)指向栈顶(最后压入的数据)
// 注意:栈是向下增长的,所以栈顶的地址比栈底小
//
// 参数:machineContext - 机器上下文
// 返回:uintptr_t - 栈指针的值
uintptr_t bs_mach_stackPointer(mcontext_t const machineContext){
    // BS_STACK_POINTER 根据CPU架构定义:
    //   ARM: __sp(Stack Pointer)
    //   x86: __rsp(x86_64)或__esp(i386)
    return machineContext->__ss.BS_STACK_POINTER;
}

// 【提取指令地址】从机器上下文中读取程序计数器/指令指针的值
// 程序计数器(PC)指向当前正在执行的指令地址
// 这是调用栈回溯的起点
//
// 参数:machineContext - 机器上下文
// 返回:uintptr_t - 当前指令地址
uintptr_t bs_mach_instructionAddress(mcontext_t const machineContext){
    // BS_INSTRUCTION_ADDRESS 根据CPU架构定义:
    //   ARM: __pc(Program Counter)
    //   x86_64: __rip(Instruction Pointer)
    //   i386: __eip
    return machineContext->__ss.BS_INSTRUCTION_ADDRESS;
}

// 【提取链接寄存器】从机器上下文中读取链接寄存器的值(仅ARM架构有效)
// 链接寄存器(LR, Link Register)是ARM架构特有的寄存器
// 当函数A调用函数B时,返回地址会保存在LR中(而不是压入栈)
// 这样做的好处:
//   1. 性能更好(寄存器访问比内存访问快得多)
//   2. 简化了叶子函数(不调用其他函数的函数)的栈帧管理
//
// 参数:machineContext - 机器上下文
// 返回:uintptr_t - 链接寄存器的值(x86架构返回0,因为没有LR)
uintptr_t bs_mach_linkRegister(mcontext_t const machineContext){
#if defined(__i386__) || defined(__x86_64__)
    // x86架构没有链接寄存器
    // 函数调用时,返回地址直接压入栈
    return 0;
#else
    // ARM架构:返回LR寄存器的值
    // __lr 是ARM架构中的x30寄存器(ARM64)或r14寄存器(ARM32)
    return machineContext->__ss.__lr;
#endif
}

// 【安全的内存复制】从指定内存地址复制数据到目标地址
// 为什么需要这个函数?
//   1. 直接使用指针访问其他线程的内存可能会崩溃(权限问题、野指针等)
//   2. vm_read_overwrite是内核级别的内存读取,更安全、更可靠
//   3. 可以跨线程读取内存(读取其他线程的栈内存)
//
// 参数:
//   src - 源地址(要读取的内存地址)
//   dst - 目标地址(复制到哪里)
//   numBytes - 要复制的字节数
// 返回:kern_return_t - 内核函数返回值(KERN_SUCCESS表示成功)
kern_return_t bs_mach_copyMem(const void *const src, void *const dst, const size_t numBytes){
    // vm_size_t 是虚拟内存大小类型(实际上就是unsigned long)
    // 用于记录实际复制了多少字节
    vm_size_t bytesCopied = 0;
    
    // 【vm_read_overwrite】虚拟内存读取覆写函数(Mach内核API)
    // 这是一个非常底层的系统调用,可以读取任意进程的内存
    //
    // 参数:
    //   mach_task_self() - 目标任务(进程),这里是当前进程
    //   (vm_address_t)src - 源地址,要读取的内存位置
    //   (vm_size_t)numBytes - 要读取的字节数
    //   (vm_address_t)dst - 目标地址,读取的数据写入到哪里
    //   &bytesCopied - 输出参数,实际复制的字节数
    //
    // 返回值:
    //   KERN_SUCCESS - 成功
    //   KERN_INVALID_ADDRESS - 地址无效(如野指针、空指针)
    //   KERN_PROTECTION_FAILURE - 权限不足
    //   KERN_NO_SPACE - 目标空间不足
    return vm_read_overwrite(mach_task_self(), (vm_address_t)src, (vm_size_t)numBytes, (vm_address_t)dst, &bytesCopied);
}

#pragma -mark Symbolicate
// ==================== 符号化:地址转换为可读符号 ====================

// 【符号化】将内存地址数组转换为符号信息数组
// 符号化就是"地址 → 可读信息"的过程:
//   0x0000000100001234 → "MyApp: main + 52"
//
// 为什么需要符号化?
//   - 内存地址对人类来说毫无意义
//   - 符号化后可以知道:哪个模块、哪个函数、距离函数起始地址多少字节
//   - 方便调试、性能分析、崩溃分析
//
// 参数:
//   backtraceBuffer - 输入,内存地址数组
//   symbolsBuffer - 输出,符号信息数组(Dl_info结构体数组)
//   numEntries - 数组长度
//   skippedEntries - 跳过的条目数(通常为0)
void bs_symbolicate(const uintptr_t* const backtraceBuffer,
                    Dl_info* const symbolsBuffer,
                    const int numEntries,
                    const int skippedEntries){
    // 当前处理的索引
    int i = 0;
    
    // 【特殊处理第一个地址】
    // 如果没有跳过条目,且索引有效
    if(!skippedEntries && i < numEntries) {
        // 第一个地址是当前指令地址(PC寄存器),直接符号化,不需要调整
        // 为什么不需要调整?因为PC指向的就是当前正在执行的指令
        bs_dladdr(backtraceBuffer[i], &symbolsBuffer[i]);
        i++;
    }
    
    // 【处理剩余的返回地址】
    // 从第二个地址开始,都是返回地址(return address)
    for(; i < numEntries; i++) {
        // 【关键点】返回地址需要-1才能得到调用指令的地址
        // 背景知识:
        //   当函数A调用函数B时,CPU执行CALL指令:
        //     1. 将"CALL的下一条指令地址"压入栈(这就是返回地址)
        //     2. 跳转到函数B
        //   所以返回地址指向的是CALL之后的指令,不是CALL本身
        //   为了知道是在哪里调用的,需要返回地址-1得到CALL指令的地址
        //
        // CALL_INSTRUCTION_FROM_RETURN_ADDRESS宏做两件事:
        //   1. DETAG_INSTRUCTION_ADDRESS: 去除ARM架构的标签位
        //   2. -1: 得到CALL指令地址
        bs_dladdr(CALL_INSTRUCTION_FROM_RETURN_ADDRESS(backtraceBuffer[i]), &symbolsBuffer[i]);
    }
}

// 【自定义dladdr】将内存地址转换为符号信息
// dladdr是系统函数(定义在<dlfcn.h>),但这里自己实现了一遍
// 为什么要自己实现?
//   1. 学习Mach-O文件格式和符号表结构
//   2. 可以添加自定义逻辑(如特殊符号处理)
//   3. 更好地理解符号化过程
//
// 参数:
//   address - 要查询的内存地址
//   info - 输出参数,Dl_info结构体指针,用于存储符号信息
// 返回:bool - true表示成功找到符号,false表示失败
bool bs_dladdr(const uintptr_t address, Dl_info* const info) {
    // 【步骤0:初始化输出结构体】
    // Dl_info结构体定义(来自<dlfcn.h>):
    //   typedef struct dl_info {
    //       const char *dli_fname;  // 文件名(模块名)
    //       void *dli_fbase;        // 文件基地址
    //       const char *dli_sname;  // 符号名(函数名)
    //       void *dli_saddr;        // 符号地址(函数起始地址)
    //   } Dl_info;
    info->dli_fname = NULL; // 文件名
    info->dli_fbase = NULL; // 文件基地址
    info->dli_sname = NULL; // 符号名(函数名)
    info->dli_saddr = NULL; // 符号地址
    
    // 【步骤1:查找包含该地址的镜像(动态库/可执行文件)】
    // 背景知识:iOS/macOS程序运行时会加载多个镜像:
    //   - 可执行文件本身(如MyApp)
    //   - 系统库(如UIKit.framework、libSystem.dylib)
    //   - 第三方库
    // bs_imageIndexContainingAddress 遍历所有镜像,找到包含该地址的那一个
    const uint32_t idx = bs_imageIndexContainingAddress(address);
    
    // 如果没找到(UINT_MAX表示失败),返回false
    // 这种情况很少发生,除非传入了无效地址
    if(idx == UINT_MAX) {
        return false;
    }
    
    // 【步骤2:获取镜像的Mach-O头部】
    // _dyld_get_image_header 是dyld提供的函数,返回指定索引的镜像头部
    // mach_header是Mach-O文件的头部结构体,包含:
    //   - magic: 魔数(标识文件类型和字节序)
    //   - cputype: CPU类型(ARM、x86等)
    //   - cpusubtype: CPU子类型
    //   - filetype: 文件类型(可执行文件、动态库等)
    //   - ncmds: 加载命令的数量
    //   - sizeofcmds: 所有加载命令的总大小
    const struct mach_header* header = _dyld_get_image_header(idx);
    
    // 【步骤3:处理ASLR(地址空间布局随机化)】
    // ASLR(Address Space Layout Randomization)是一种安全机制:
    //   - 每次程序启动时,镜像加载到内存的地址是随机的
    //   - 这样可以防止黑客利用固定地址进行攻击
    //
    // 地址关系:
    //   实际地址 = 文件中的地址 + ASLR偏移(slide)
    //
    // _dyld_get_image_vmaddr_slide 返回ASLR偏移量
    const uintptr_t imageVMAddrSlide = (uintptr_t)_dyld_get_image_vmaddr_slide(idx);
    
    // 计算去除ASLR偏移后的地址(文件中的原始地址)
    const uintptr_t addressWithSlide = address - imageVMAddrSlide;
    
    // 【步骤4:获取段基地址】
    // 段基地址用于计算符号表在内存中的位置
    // 符号表存储在__LINKEDIT段中,通过段基地址可以将文件偏移转换为内存地址
    const uintptr_t segmentBase = bs_segmentBaseOfImageIndex(idx) + imageVMAddrSlide;
    
    // 如果段基地址为0,说明获取失败
    if(segmentBase == 0) {
        return false;
    }
    
    // 【步骤5:填充基本信息】
    // _dyld_get_image_name 返回镜像的完整路径
    // 例如:"/System/Library/Frameworks/UIKit.framework/UIKit"
    info->dli_fname = _dyld_get_image_name(idx);
    
    // 文件基地址就是Mach-O头部地址
    info->dli_fbase = (void*)header;
    
    // 【步骤6:查找符号表并匹配最接近的符号】
    // 最佳匹配的符号表项(初始化为NULL,表示还没找到)
    const BS_NLIST* bestMatch = NULL;
    
    // 最小距离(初始化为最大值)
    // 我们要找的是:地址 >= 符号地址,且距离最小的那个符号
    uintptr_t bestDistance = ULONG_MAX;
    
    // 【步骤7:获取第一个加载命令的地址】
    // Mach-O文件结构:
    //   [Mach Header] [Load Commands] [Segments/Sections] [Data]
    // 加载命令紧跟在头部之后
    uintptr_t cmdPtr = bs_firstCmdAfterHeader(header);
    
    // 如果命令指针为0,说明头部损坏
    if(cmdPtr == 0) {
        return false;
    }
    
    // 【步骤8:遍历所有加载命令,查找符号表命令】
    // header->ncmds 是加载命令的数量
    for(uint32_t iCmd = 0; iCmd < header->ncmds; iCmd++) {
        // 将指针转换为load_command结构体
        // load_command是所有加载命令的基类,包含:
        //   - cmd: 命令类型(LC_SEGMENT、LC_SYMTAB等)
        //   - cmdsize: 命令大小
        const struct load_command* loadCmd = (struct load_command*)cmdPtr;
        
        // 【查找符号表命令】
        // LC_SYMTAB表示这是一个符号表命令
        if(loadCmd->cmd == LC_SYMTAB) {
            // 转换为symtab_command结构体
            // symtab_command包含:
            //   - symoff: 符号表在文件中的偏移
            //   - nsyms: 符号数量
            //   - stroff: 字符串表在文件中的偏移
            //   - strsize: 字符串表大小
            const struct symtab_command* symtabCmd = (struct symtab_command*)cmdPtr;
            
            // 【计算符号表在内存中的地址】
            // 符号表地址 = 段基地址 + 文件偏移
            // BS_NLIST是根据32/64位定义的符号表项类型(nlist或nlist_64)
            const BS_NLIST* symbolTable = (BS_NLIST*)(segmentBase + symtabCmd->symoff);
            
            // 【计算字符串表在内存中的地址】
            // 字符串表存储所有符号的名称字符串
            // 符号表项中只存储字符串的索引,真正的字符串在字符串表中
            const uintptr_t stringTable = segmentBase + symtabCmd->stroff;
            
            // 【遍历符号表,查找最佳匹配】
            for(uint32_t iSym = 0; iSym < symtabCmd->nsyms; iSym++) {
                // nlist/nlist_64结构体:
                //   - n_un.n_strx: 符号名在字符串表中的索引
                //   - n_type: 符号类型(函数、变量、调试符号等)
                //   - n_sect: 符号所在的段索引
                //   - n_desc: 描述符(引用类型、可见性等)
                //   - n_value: 符号地址(函数/变量的地址)
                
                // 【过滤外部符号】
                // 如果n_value为0,说明这是一个外部符号(引用其他库的符号)
                // 外部符号的地址在当前镜像中不存在,需要跳过
                if(symbolTable[iSym].n_value != 0) {
                    // 符号的基地址(函数/变量的起始地址)
                    uintptr_t symbolBase = symbolTable[iSym].n_value;
                    
                    // 计算地址与符号基地址的距离
                    uintptr_t currentDistance = addressWithSlide - symbolBase;
                    
                    // 【最佳匹配算法】
                    // 条件1:地址必须 >= 符号基地址(即地址在符号的范围内)
                    // 条件2:距离必须 <= 当前最佳距离(找最接近的符号)
                    //
                    // 为什么这样做?
                    // 例如:
                    //   main函数起始地址:0x100001200
                    //   其他函数起始地址:0x100001300
                    //   查询地址:0x100001234
                    // 那么最佳匹配是main(距离0x34),而不是其他函数(距离为负)
                    if((addressWithSlide >= symbolBase) &&
                       (currentDistance <= bestDistance)) {
                        // 更新最佳匹配
                        bestMatch = symbolTable + iSym;
                        // 更新最小距离
                        bestDistance = currentDistance;
                    }
                }
            }
            
            // 【步骤9:提取符号信息】
            // 如果找到了匹配的符号
            if(bestMatch != NULL) {
                // 符号地址 = 文件中的地址 + ASLR偏移
                info->dli_saddr = (void*)(bestMatch->n_value + imageVMAddrSlide);
                
                // 【提取符号名】
                // 符号名地址 = 字符串表基地址 + 符号名索引
                // (intptr_t)强制转换是为了正确处理指针算术
                info->dli_sname = (char*)((intptr_t)stringTable + (intptr_t)bestMatch->n_un.n_strx);
                
                // 【处理C/C++符号前缀】
                // C/C++编译器会在符号名前加下划线(_)
                // 例如:main → _main,printf → _printf
                // 为了输出更友好,跳过开头的下划线
                if(*info->dli_sname == '_') {
                    info->dli_sname++;
                }
                
                // 【处理stripped符号】
                // 如果符号地址等于文件基地址,且类型为3(N_SECT)
                // 说明这是一个section符号(不是真正的函数/变量符号)
                // 这种情况通常发生在符号被strip剥离后
                // 剥离符号可以减小文件大小,但会导致调试困难
                if(info->dli_saddr == info->dli_fbase && bestMatch->n_type == 3) {
                    info->dli_sname = NULL;
                }
                
                // 找到符号后跳出循环(不需要继续遍历其他加载命令)
                break;
            }
        }
        
        // 移动到下一个加载命令
        // cmdPtr += loadCmd->cmdsize 等价于 cmdPtr = cmdPtr + cmdsize
        cmdPtr += loadCmd->cmdsize;
    }
    
    // 返回成功(即使没找到符号,也返回true,因为至少找到了文件名)
    return true;
}

// ==================== 辅助函数:Mach-O文件解析 ====================

// 【获取第一个加载命令地址】
// Mach-O文件结构:[Header][Load Commands][Segments][Data]
// 这个函数计算加载命令的起始地址
//
// 参数:header - Mach-O头部指针
// 返回:uintptr_t - 第一个加载命令的地址(0表示头部损坏)
uintptr_t bs_firstCmdAfterHeader(const struct mach_header* const header) {
    // 根据魔数(magic number)判断文件类型和字节序
    switch(header->magic) {
        // 【32位Mach-O】
        case MH_MAGIC:    // 0xfeedface - 32位小端序
        case MH_CIGAM:    // 0xcefaedfe - 32位大端序(字节序相反)
            // 32位Mach-O头部大小是sizeof(struct mach_header)
            // header + 1 会跳过整个头部,指向加载命令起始位置
            return (uintptr_t)(header + 1);
            
        // 【64位Mach-O】
        case MH_MAGIC_64: // 0xfeedfacf - 64位小端序
        case MH_CIGAM_64: // 0xcffaedfe - 64位大端序
            // 64位Mach-O头部大小是sizeof(struct mach_header_64)
            // 需要先转换为mach_header_64*,然后+1跳过头部
            return (uintptr_t)(((struct mach_header_64*)header) + 1);
            
        default:
            // 未知魔数,说明文件损坏或不是Mach-O文件
            return 0;  // Header is corrupt
    }
}

// 【查找包含指定地址的镜像索引】
// 背景知识:程序运行时会加载多个镜像(可执行文件、动态库)
// 每个镜像被加载到内存的不同区域,这个函数找出地址属于哪个镜像
//
// 参数:address - 要查询的内存地址
// 返回:uint32_t - 镜像索引(UINT_MAX表示未找到)
uint32_t bs_imageIndexContainingAddress(const uintptr_t address) {
    // 【获取镜像总数】
    // _dyld_image_count() 返回当前进程加载的镜像数量
    // 镜像(image)包括:
    //   - 主程序可执行文件(如MyApp)
    //   - 动态库(如UIKit.framework、libSystem.dylib)
    //   - 插件、bundle等
    const uint32_t imageCount = _dyld_image_count();
    
    // Mach-O头部指针
    const struct mach_header* header = 0;
    
    // 【遍历所有镜像】
    for(uint32_t iImg = 0; iImg < imageCount; iImg++) {
        // _dyld_get_image_header(i) 返回第i个镜像的Mach-O头部
        header = _dyld_get_image_header(iImg);
        
        // 检查头部是否有效
        if(header != NULL) {
            // 【处理ASLR偏移】
            // 计算去除ASLR偏移后的地址(文件中的原始虚拟地址)
            uintptr_t addressWSlide = address - (uintptr_t)_dyld_get_image_vmaddr_slide(iImg);
            
            // 获取第一个加载命令的地址
            uintptr_t cmdPtr = bs_firstCmdAfterHeader(header);
            
            // 如果命令指针无效,跳过这个镜像
            if(cmdPtr == 0) {
                continue;
            }
            
            // 【遍历该镜像的所有加载命令,查找段命令】
            // 段(segment)是内存中的一块连续区域,包含多个节(section)
            // 常见的段:
            //   - __TEXT: 代码段(只读、可执行)
            //   - __DATA: 数据段(可读写)
            //   - __LINKEDIT: 链接信息段(符号表、字符串表等)
            for(uint32_t iCmd = 0; iCmd < header->ncmds; iCmd++) {
                // 当前加载命令
                const struct load_command* loadCmd = (struct load_command*)cmdPtr;
                
                // 【处理32位段命令】
                if(loadCmd->cmd == LC_SEGMENT) {
                    // 转换为segment_command结构体
                    // segment_command包含:
                    //   - segname: 段名(如"__TEXT"、"__DATA")
                    //   - vmaddr: 虚拟内存地址
                    //   - vmsize: 虚拟内存大小
                    //   - fileoff: 文件偏移
                    //   - filesize: 文件大小
                    const struct segment_command* segCmd = (struct segment_command*)cmdPtr;
                    
                    // 【检查地址是否在该段的虚拟地址范围内】
                    // 条件:vmaddr <= address < vmaddr + vmsize
                    if(addressWSlide >= segCmd->vmaddr &&
                       addressWSlide < segCmd->vmaddr + segCmd->vmsize) {
                        // 找到了!返回镜像索引
                        return iImg;
                    }
                }
                // 【处理64位段命令】
                else if(loadCmd->cmd == LC_SEGMENT_64) {
                    // segment_command_64与segment_command结构类似
                    // 只是地址和大小字段是64位的
                    const struct segment_command_64* segCmd = (struct segment_command_64*)cmdPtr;
                    
                    // 检查地址是否在该段的范围内
                    if(addressWSlide >= segCmd->vmaddr &&
                       addressWSlide < segCmd->vmaddr + segCmd->vmsize) {
                        // 找到了!返回镜像索引
                        return iImg;
                    }
                }
                
                // 移动到下一个加载命令
                cmdPtr += loadCmd->cmdsize;
            }
        }
    }
    
    // 【没有找到】
    // 返回UINT_MAX表示失败
    // 这种情况很少发生,除非:
    //   1. 传入了无效地址(野指针、栈溢出等)
    //   2. 地址指向动态分配的内存(malloc分配的堆内存)
    //   3. 地址指向系统保留区域
    return UINT_MAX;
}

// 【获取镜像的段基地址】
// 段基地址用于将文件偏移转换为虚拟内存地址
// 公式:虚拟内存地址 = 段基地址 + 文件偏移
//
// 为什么需要段基地址?
// - 符号表、字符串表等信息存储在文件中,用文件偏移表示
// - 程序运行时需要访问这些信息,必须转换为内存地址
// - 段基地址 = __LINKEDIT段的虚拟地址 - 文件偏移
//
// 参数:idx - 镜像索引
// 返回:uintptr_t - 段基地址(0表示失败)
uintptr_t bs_segmentBaseOfImageIndex(const uint32_t idx) {
    // 获取镜像的Mach-O头部
    const struct mach_header* header = _dyld_get_image_header(idx);
    
    // 获取第一个加载命令的地址
    uintptr_t cmdPtr = bs_firstCmdAfterHeader(header);
    
    // 如果命令指针无效,返回0
    if(cmdPtr == 0) {
        return 0;
    }
    
    // 【遍历所有加载命令,查找__LINKEDIT段】
    // __LINKEDIT段(Link Edit Segment)包含:
    //   - 符号表(Symbol Table)
    //   - 字符串表(String Table)
    //   - 重定位信息(Relocation Info)
    //   - 代码签名(Code Signature)
    // 这些信息在运行时需要被dyld访问
    for(uint32_t i = 0; i < header->ncmds; i++) {
        // 当前加载命令
        const struct load_command* loadCmd = (struct load_command*)cmdPtr;
        
        // 【处理32位段命令】
        if(loadCmd->cmd == LC_SEGMENT) {
            // 转换为segment_command结构体
            const struct segment_command* segmentCmd = (struct segment_command*)cmdPtr;
            
            // strcmp: 字符串比较,相等返回0
            // SEG_LINKEDIT是系统定义的常量:"__LINKEDIT"
            if(strcmp(segmentCmd->segname, SEG_LINKEDIT) == 0) {
                // 【计算段基地址】
                // 段基地址 = 虚拟内存地址 - 文件偏移
                //
                // 为什么这样计算?
                // 假设__LINKEDIT段:
                //   - 文件中的偏移:0x4000
                //   - 加载到内存的地址:0x100004000
                //   - 段基地址 = 0x100004000 - 0x4000 = 0x100000000
                //
                // 那么,符号表在文件中的偏移是0x5000,其内存地址就是:
                //   0x100000000 + 0x5000 = 0x100005000
                return segmentCmd->vmaddr - segmentCmd->fileoff;
            }
        }
        // 【处理64位段命令】
        else if(loadCmd->cmd == LC_SEGMENT_64) {
            // segment_command_64与segment_command结构类似
            const struct segment_command_64* segmentCmd = (struct segment_command_64*)cmdPtr;
            
            // 查找__LINKEDIT段
            if(strcmp(segmentCmd->segname, SEG_LINKEDIT) == 0) {
                // 计算段基地址(64位版本,需要类型转换)
                return (uintptr_t)(segmentCmd->vmaddr - segmentCmd->fileoff);
            }
        }
        
        // 移动到下一个加载命令
        cmdPtr += loadCmd->cmdsize;
    }
    
    // 【没有找到__LINKEDIT段】
    // 返回0表示失败
    // 这种情况不应该发生,因为所有Mach-O文件都有__LINKEDIT段
    return 0;
}

@end

Coding Agent 时代,App 的核心竞争力是什么?

以 Claude Code 为代表的 Coding Agent 对软件行业的重塑已成定局。它们的可用性已然突破临界点,使得代码生成的边际成本显著下降,比如 Claude Code 本身已经已经全部由 Claude Code 编写了。过去需要一周的硬编码工作,现在可能缩短为半天;过去因技术门槛高而不敢涉猎的领域,现在变得触手可及。

效率的提升带来的是竞争规则的改变,当「实现能力」不再是短板,App 的核心竞争力将发生怎样的迁移?

Agent 的强大,本质上意味着功能性复制的成本显著降低。如果你的护城河仅仅是「写了一个别人写不出的功能」,除非这个功能有极高的技术门槛,否则,其他竞争对手可以用 Agent 在短时间内复刻出一个八九不离十的产品,以更低的价格,甚至免费,来吸引用户。

这正是经典的「智猪博弈」升级版:以前是大猪(创新者)踩踏板,一两只小猪(跟随者)在食槽边等;现在是一二十只全副武装的小猪在那等着。你费尽心思设计的复杂功能,可能通过几轮 Prompt 就被对方解构并重现。

在这个局面下,需要重点关注的,是那些 AI 无法生成、无法复制且具有时间复利 的东西。

1. 从功能实现转向用户洞察

代码是可以被复制的显性知识,但关于「为什么要这样做」的隐性知识是 AI 难以窃取的。

产品的初衷是为了解决特定问题。你需要比同行更深刻地理解你的用户群:他们的使用场景、痛点、情绪触发点以及那些「非理性的诉求」。AI 可以完美执行 How,但无法推导出 Why。

这种基于深刻洞察和独特审美提出的解决方案,是单纯的 UI 克隆无法比拟的。

2. 数据资产与迁移壁垒

用户使用你的产品越久,沉淀的历史记录、个性化偏好、内容积累就越多,迁移成本也就越高。即使竞争对手 1:1 复制了你的功能,他们也无法复制用户在你这里留下的数据上下文。

因此,产品的设计逻辑应从「提供工具」转向「沉淀资产」。让产品越用越懂用户,这种基于数据的个性化体验,是冷冰冰的 AI 克隆版无法比拟的。

3. 开发者角色的演进

代码层面,随着代码量的增加,保障代码的可维护性、可演进性和产品质量变得更加重要,这一方面需要加深对 Coding Agent 的理解,提升熟练度,另一方面也需要深厚的软件开发功底,还要非常熟悉业务。

4. 情感连接与分发网络

对于产品,不仅要解决问题,还要带来愉悦感。这种细微的交互体验、情感共鸣,是建立品牌忠诚度的关键,也是用户愿意自发传播的动力。

分发能力也很重要,在行业中积累的信誉、与 KOL 建立的友好关系、在 Apple/Google 生态中建立的信任权重,这些都是 AI 无法通过算法生成的「社会资本」。


Claude Code 并没有让 App 开发这件事变得没有价值,它只是消灭了平庸的重复造轮子,将竞争的维度拉向了两端:一端是更底层的系统架构与质量保障,另一端是更上层的用户洞察与品牌情感。夹在中间单纯靠「写代码」生存的空间,会被挤压地越来越小,甚至消失。

4-1.【协议导向编程】什么是协议导向编程(POP),它解决了传统面向对象设计中的哪些问题?

1️⃣ 什么是协议导向编程(POP)

协议导向编程 是 Swift 提出的编程范式,它的核心思想是:

  • 通过协议(protocol)定义接口和行为,而不是依赖类继承。
  • 通过协议扩展(protocol extensions)提供默认实现,减少重复代码。
  • 关注行为组合而非继承层级,强调“能力”的组合而不是类的层级关系。

简单理解:

在 POP 中,我们关注对象 能做什么(行为),而不是它 是什么(类型/类继承)。


Swift 中的协议示例

protocol Flyable {
    func fly()
}

protocol Swimmable {
    func swim()
}

// 给 Flyable 提供默认实现
extension Flyable {
    func fly() {
        print("I can fly!")
    }
}

// 使用协议组合
struct Bird: Flyable, Swimmable {
    func swim() {
        print("I can swim too!")
    }
}

let duck = Bird()
duck.fly()   // 输出: I can fly!
duck.swim()  // 输出: I can swim too!

✅ 这里你可以看到:

  • 没有继承自基类。
  • 通过协议组合实现多种能力。
  • 默认实现避免重复代码。

2️⃣ POP 解决了传统 OOP 的哪些问题

在传统面向对象编程(OOP)中,我们通常使用 类继承 来复用代码:

class Animal { ... }
class Bird: Animal { ... }
class Duck: Bird { ... }

OOP 的局限

  1. 继承层级僵化

    • 类只能单继承(Swift 中类也只支持单继承)。
    • 行为复用受限。
    • 如果想让一个类同时拥有多种能力(fly、swim、run),继承树会变得复杂。
  2. 代码复用困难

    • 父类提供通用方法,但子类可能需要修改。
    • 多个不相关类要重复实现相同功能。
  3. 类型耦合强

    • 子类依赖父类,导致灵活性下降。
    • 改变父类可能影响整个继承链。

POP 的优势

问题 POP 的解决方案
单继承限制 协议可以任意组合,轻松实现多能力组合(多重“继承”效果)
代码重复 协议扩展提供默认实现,避免重复实现
耦合性强 类型依赖协议接口而非具体类,实现低耦合
灵活性差 POP 强调行为组合,可在不改变继承关系的前提下扩展功能
测试难度大 使用协议作为依赖(依赖注入),单元测试更容易模拟/mock

Swift POP 的典型实践

  1. 使用协议定义能力(Abilities)

    • Flyable, Swimmable, Runnable 等。
  2. 协议扩展提供默认实现

    • 避免每个类型都重复实现。
  3. 通过协议组合创建对象

    • 组合多种能力而不依赖继承层级。
  4. 依赖抽象而非具体类型

    • 代码更灵活,可测试性高。

总结

协议导向编程(POP) 的核心思想是:

“面向协议编程,而不是面向类编程”
通过协议组合和扩展,实现行为复用和灵活的类型组合,从而解决了传统 OOP 的继承僵化、代码重复、耦合度高等问题。

Swift 官方甚至强调:

Swift 是协议导向语言,而不是类继承语言

Neo-Cupertino 档案:撕开 Actor 的伪装,回归 Non-Sendable 的暴力美学

在这里插入图片描述

当前时间:2077年,一个阴雨连绵的周二 地点:Neo-Cupertino,第 42 区,“无限循环” 咖啡馆 人物

  • Jet:资深架构师,义眼闪烁着蓝光的代码老兵,热衷于复古的 Swift 语法。
  • Nova:刚入行的初级工程师,满脑子是最新的神经链接框架,但经常被编译器暴揍。
  • 反派“The Race Condition” (竞态幽灵),一个游荡在系统内存缝隙中的古老 AI 病毒,专门吞噬不安全的变量。

窗外的霓虹灯光透过雨幕,在 Jet 的合成皮风衣上投下斑驳的阴影。他抿了一口手中的高浓度咖啡因液,看着面前焦头烂额的 Nova。

“我的编译器又在尖叫了,” Nova 把全息屏幕推向 Jet,上面红色的错误提示像鲜血一样流淌,“我只是想在一个 Actor 里用个简单的类,结果 Swift 的并发检查像个疯狗一样咬着我不放。我是不是该把所有东西都加上 @MainActor 算了?”

在这里插入图片描述

Jet 叹了口气,那是见惯了无数次堆栈溢出后的沧桑。“滥用主线程隔离?那是饮鸩止渴。Nova,你被恐惧蒙蔽了双眼。来,看看这份 2025 年的加密文档。那时候我们管这个叫——Non-Sendable First Design。”

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

  • 🦾 序章:尴尬却必要的兴奋
  • 🧠 核心概念重载 (Quick Refresher)
  • 🔒 困兽之斗?不,是绝对领域 (Getting Stuck)
  • ⚖️ 权衡:光与影 (The Pros and Cons)
    • 💎 优势 #1:极致的简单 (Simplicity)
    • 🌐 优势 #2:通用性 (Generality)
    • 🕳️ 弱点 #1:启动任务的陷阱 (Starting Tasks)
  • 💊 并不完美的解药
  • 🛑 启动任务本身就是一种充满危险的诱惑
    • 🕸️ 弱点 #2:诡异的属性 (Weird Properties)
  • 🎬 终章:回归本源 (First for a Reason)

Jet 手指轻挥,将文档投影在两人中间,开始了他的解构。

在这里插入图片描述


🦾 序章:尴尬却必要的兴奋

Jet 指着文档的开头说道:“这作者是个老实人。他承认自己对 Non-Sendable Types (非跨域传输类型) 的痴迷程度简直到了走火入魔的地步,甚至有点尴尬。”

“这就好比给自己的战术命名一样,听起来有点自以为是。但他把这种设计理念称为 ‘Non-Sendable First Design’。虽然名字听起来像是什么二流黑客的代号,但其核心思想在当年可是振聋发聩。”

在这里插入图片描述

“在那个年代,因为语言特性的限制,大家对 Non-Sendable 类型避之唯恐不及。它们虽然有用,但用起来就像是在满是碎玻璃的地上跳舞——人体工程学极差。直到有一天,NonisolatedNonsendingByDefault(即 Swift 6 中的‘可接近并发’特性)横空出世。这一切都变了!Non-Sendable 类型突然变得顺滑无比,仿佛这才是它们原本的宿命。”


🧠 核心概念重载 (Quick Refresher)

“在深入之前,我们要先进行一次思维格式化。” Jet 的义眼转动,调出了基础理论图谱。

Swift 解决 Data Race (数据竞争) 的手段非常硬核:它要求在编译时就确立对非线程安全数据的保护。这种抽象被称为 Isolation (隔离)

  • 实现层:可能是锁 (Lock)、队列 (Queue),或者是专用的线程。
  • 运行时:由 Actor 负责具体的保护机制。

“编译器其实是个‘脸盲’,” Jet 解释道,“它不知道某个 Actor 到底是怎么运作的(比如 MainActor 其实是把任务倒进主线程这个大漏斗里),它只知道一点:Actor 能护犊子。”

在这里插入图片描述

Swift 将数据世界一分为二:

  1. Sendable (可传输类型):天生的战士,线程安全,可以在并发的枪林弹雨中随意穿梭,无需保护。
  2. Non-Sendable (非可传输类型):共享的可变状态。它们是我们程序中最有趣、最核心的部分,比如你的用户数据、缓存状态。但它们也是脆弱的,就像没有穿护甲的平民,必须被保护。

“很多人误以为 Actor 只是用来‘后台运行’的工具,这完全是买椟还珠。Actor 的真正使命,是充当 Non-Sendable 数据的保镖,防止它们被竞态幽灵吞噬。”


🔒 困兽之斗?不,是绝对领域 (Getting Stuck)

“听着,Nova。世界上最好的安保系统,如果不允许任何人进出,那也没用。但在 Swift Concurrency 的法则里,有一个非常有趣的特性:”

如果一个 Actor 拥有(通常是创建)了一个 Non-Sendable 类型,这个类型就被‘困’住了。

“想象一下,” Jet 描绘道,“一个由类和协议组成的庞大网络,彼此交织,协同工作。它们可以在 Actor 的围墙内为所欲为。但编译器这个冷酷的守门人,绝对禁止你不安全地将它们扔到墙外。”

在这里插入图片描述

“这听起来像是限制,但这正是 Non-Sendable 类型的强大之处——画地为牢,反而成就了绝对的安全。”


⚖️ 权衡:光与影 (The Pros and Cons)

Jet 调出了对比数据面板。“作者曾纠结于如何展示这些,最后他决定返璞归真,先给你看甜头,再告诉你陷阱。注意,这里默认的语境是 nonisolated (非隔离) 的。”

💎 优势 #1:极致的简单 (Simplicity)

“看这段代码,Nova。它简单得像是一首儿歌。”

class Counter {
    var state = 0
    
    func reset() {
        self.state = 0
    }
}

“这就是一个普通的类,甚至还有可变状态。再看这一步:”

class Counter {
    // ...

    // 这里加上了 async
    func toggle() async {
        self.state += 1
    }
}

extension Counter: Equatable {
    static func == (lhs: Counter, rhs: Counter) -> Bool {
        lhs.state == rhs.state
    }
}

Jet 敲着桌子强调:“这里有两个关键点!”

  1. Async 方法:在旧时代,nonisolated + async 意味着代码会跑去后台线程,但这对于 Non-Sendable 类型来说是个悖论(它不能离开 Actor 的保护)。这曾经是个第22条军规式的死锁。但现在,有了 NonisolatedNonsendingByDefault,这个问题迎刃而解。
  2. 协议一致性 (Protocol Conformance):看那个 Equatable。这对于普通类来说易如反掌。但如果是 MainActor 隔离的类型?那简直是噩梦,你得处理各种隔离上下文的匹配问题。

Non-Sendable 类型拥有隔离类型所不具备的纯粹和简单。

在这里插入图片描述

🌐 优势 #2:通用性 (Generality)

“这需要一点悟性,” Jet 眯起眼睛,“这关乎同步访问 (Synchronous Access)。”

“如果你给类型加上了 @MainActor,那只有主线程的朋友才能同步访问它。这就像是个 VIP 俱乐部。但 Non-Sendable 类型是通用的雇佣兵。”

actor ActorClient {
    // ActorClient 拥有这个 counter
    private let counter = Counter()
    
    func accessSynchronously() {
        // ✅ 这是完全可能的!
        // 因为 counter 是 Non-Sendable,它被"困"在了 ActorClient 的隔离域内,
        // 所以 ActorClient 可以像操作自家后院一样同步操作它。
        counter.reset() 
    }
}

“它的接口保持不变,不管是谁‘拥有’它。这就是海纳百川的通用性。”

在这里插入图片描述


🕳️ 弱点 #1:启动任务的陷阱 (Starting Tasks)

此时,全息投影变成了警告的红色。

“这就是那个让我开始反思的地方,” Jet 指着下面的代码,“也是新手最容易踩的雷区。”

class Counter {
    // ...

    func printStateEventually() {
        Task {
            // ❌ 错误: 将闭包作为 'sending' 参数传递...
            // 编译器会阻止你,因为你试图在新的 Task 里捕获非 Sendable 的 self
            print(self.state)
        }
    }
}

“为什么不编译?把它翻译成古老的 GCD 你就懂了:”

// ⚠️ 警告: 在 @Sendable 闭包中捕获了 non-Sendable 类型 'Counter'
DispatchQueue.global().async {
    print(self.state)
}

“这就像你在没有任何保护措施的情况下,试图把一个易碎的花瓶扔到正在高速运转的传送带上。在一个 nonisolated 的函数里启动 Task,意味着上下文是不确定的。没有线程安全的保证,编译器绝不会放行。

在这里插入图片描述

💊 并不完美的解药

“为了解决这个问题,我们要么回到原来的队列(像 GCD 那样),要么把 isolation 参数传进去。”

class Counter {
    // 显式传递隔离上下文,这代码丑得像被辐射过的变异体
    func printStateEventually(isolation: isolated any Actor) {
        Task {
            _ = isolation // 这里的魔法是为了继承隔离域
            print(self.state)
        }
    }
}

“这方案有毒,” Jet 摇摇头,“第一,它有传染性,调用者必须提供 Actor。第二,这语法太啰嗦。最重要的是……”

在这里插入图片描述

🛑 启动任务本身就是一种充满危险的诱惑

“Jet 突然严肃起来:“有人曾告诉我,不应该在这种类型内部创建非结构化的 Task。以前我觉得那是废话,现在我觉得那是金玉良言。”

“在类型内部悄悄启动一个外部无法等待 (await) 的任务,这是埋雷。这让代码变得难以观测、难以测试。这其实是一种语法盐 (Syntactic Salt)——语言故意让你难受,是为了告诉你:别这么干!

“正确的做法是使用 async 方法,保持在结构化并发的世界里。如果非要开新任务,让调用者去开。”

在这里插入图片描述


🕸️ 弱点 #2:诡异的属性 (Weird Properties)

“还有一些边缘情况,” Jet 快速带过,“比如 lazy var、属性包装器 (Property Wrappers) 或者宏 (Macros)。如果在这些东西里混合复杂的并发要求,你会发现自己进退维谷。”

class Counter {
    // 这种延迟加载在并发环境下可能极其复杂
    lazy var internal = {
        SpecialCounter(isolatedParam: ???) 
    }()
}

“这虽然罕见,但一旦遇到,就像是撞上了隐形墙。前车之鉴,不可不防。”

在这里插入图片描述


🎬 终章:回归本源 (First for a Reason)

雨渐渐停了,Neo-Cupertino 的黎明即将来临。Jet 关闭了全息投影,看着若有所思的 Nova。

“‘Non-Sendable First’,不是让你永远只用这一招,而是把它作为起点。”

在这里插入图片描述

“在 Swift 5.5 之前,所有东西本质上都是 nonisolated 的。这才是‘常态’。只要不涉及跨线程,它们就是最轻量、最高效的选择。”

“当然,当你遇到真正的并发需求,当你发现无法在 Actor 之间安全传递数据时,再考虑加上 SendableActor 约束。但在那之前……” Jet 站起身,整理了一下衣领。

不要为了并不存在的并发问题,去背负隔离带来的沉重枷锁。 利用 Region Isolation (区域隔离) 的特性,让编译器帮你推导安全性。这种感觉就像是作弊,但却是合法的。”

在这里插入图片描述

Nova 看着屏幕上终于变绿的编译通过提示,眼中闪过一丝光芒。“所以,大道至简?”

“没错,” Jet 转身走进晨雾中,留下最后一句话,“如果觉得复杂,说明你走错了路。Non-Sendable 的世界,简单得让人上瘾。”

在这里插入图片描述

赛博深渊(上):用 Apple Foundation Models 提炼“禁忌知识”的求生指南

在这里插入图片描述

🍎 引子

新九龙城的雨从未停过。霓虹灯的废气在湿漉漉的街道上晕染开来,像极了那个死于代码过载的倒霉蛋老王流出的脑浆。

在贫民窟第 404 区的一间昏暗安全屋里,一名代号为“老 K”的黑客正对着一块发着幽蓝光芒的屏幕,手指在键盘上敲击出残影。墙角的服务器风扇发出濒死的哀鸣,仿佛随时都会起飞爆炸。

在这里插入图片描述

在这个被巨型企业“果核公司(Fruit Corp)”统治的赛博世界里,数据就是生命,而算力就是货币。

老 K 刚刚从公司的主机里偷出了一份代号为“创世纪”的加密文本,文件大得惊人,如果不赶紧在本地进行 “摘要提炼”,追踪程序(那些被称为“猎犬”的 AI)顺着网线摸过来,把他那可怜的脑机接口烧成灰烬只是时间问题。

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

  • 🍎 引子
  • 🧠 缩水的“大脑”与本地化的艺术
  • 🛠️ 装备检查:不要试图在烤面包机上运行核程序
  • 🕵️‍♂️ 侦测可用性:敌我识别系统
  • 💉 注入代码:瞒天过海
  • 🎭 模拟测试:在母体中演练
  • 🎆 尾声:黎明前的微光

但他不能上传到云端处理。云端是“它们”的地盘。他必须在本地,用那个刚刚解禁的传说级武器——Apple Foundation Models


🧠 缩水的“大脑”与本地化的艺术

“听着,菜鸟,”老 K 转过头,仿佛打破了第四面墙,对着作为学徒的你说道,“想要活命,就得学会如何在端侧(On-device)跑大模型。”

Apple Foundation Models 提供了一种能在用户终端设备(不管是你的义体植入终端,还是手里的 iPhone/Mac)上本地运行的大语言模型(LLM)

在这里插入图片描述

你要知道,传统的 LLM 就像是住在数据中心里的巨型怪兽,吃的是高功率 GPU,喝的是海量的显存和电力。想要把这种怪兽塞进你的口袋里,不仅需要勇气,还需要黑科技。

这里有两个关键的 “瘦身” 魔法:

  1. 参数削减 (Reducing Parameters):把脑子里那些没用的神经元切掉,只保留核心逻辑。
  2. 模型量化 (Quantizing Model Values):如果说原来每个神经元需要 32 位浮点数的高精度,现在我们把它压缩到 4 位或 8 位。虽然精度略有下降,但体积小到可以忽略不计。

在这里插入图片描述

⚠️ 风险提示:这些被“阉割”过的模型依然保留了 LLM 的通病——幻觉 (Hallucinations)。它可能会一本正经地胡说八道,就像喝了假酒的算命先生。但在提炼摘要、理解文本、甚至简单的数据生成任务上,它依然是把好手。

好了,废话少说,让我们开始改装你的代码,让它能驾驭这股力量。


🛠️ 装备检查:不要试图在烤面包机上运行核程序

想要驾驭 Apple Intelligence,你的硬件得跟得上。

“别拿你那台老古董来丢人现眼。”老 K 吐了一口烟圈。

硬性指标:

  • 操作系统:必须是 macOS 26.0 或更高版本(对于 iOS 设备,则是 iOS 26/iPadOS 26)。
  • 硬件支持:设备必须原生支持 Apple Intelligence(那种山寨的义体插件不仅没用,还会炸)。
  • 开关状态:用户必须在设置里手动开启了 Apple Intelligence。

在这里插入图片描述

☠️ 避坑指南:如果你在 macOS 26 的虚拟机里折腾,大概率会翻车。Apple Foundation Models 对虚拟化环境过敏。找台真机,或者支持该特性的物理设备。

在这里插入图片描述


🕵️‍♂️ 侦测可用性:敌我识别系统

我们的目标是那个 Share Extension(分享扩展)。我们要让它变成一个能吞噬长文本并吐出精华摘要的黑洞。

在这里插入图片描述

现在的 App 像个傻子,只会把你选中的文字原样复读一遍。我们要改造它。

首先,在 SummarizeExtension 文件夹下创建一个新的 SwiftUI 视图,命名为 ModelCheckView.swift。这就像是我们的看门狗,用来检测当前环境是否安全。

在这里插入图片描述

在该文件顶部引入这一行禁忌的咒语:

import FoundationModels // 引入 Apple 的本地模型库

然后在结构体里加入这些属性:

// 这里的 sharedText 是我们要处理的“赃物”(文本)
let sharedText: String
// 任务完成后的回调,毕竟我们得知道什么时候跑路
let onDone: () -> Void
// 核心:获取系统默认的语言模型实例
let model = SystemLanguageModel.default 

接下来,把原本的 body 替换成这一段充满求生欲的代码:

// 1. 检查模型的可用性状态
switch model.availability {

// 2. 状态:可用。谢天谢地,我们可以干活了
case .available:
  // 这里调用真正干活的摘要视图(后面我们会讲)
  SummaryView(sharedText: sharedText, onDone: onDone)

// 3. 各种不可用的“死法”
case .unavailable(.deviceNotEligible):
  Text("⚠️ 你的设备太老了,跑不动 Apple Intelligence。")
  
case .unavailable(.appleIntelligenceNotEnabled):
  Text("🛑 Apple Intelligence 虽已就绪,但你还没打开开关。快去设置!")
  
case .unavailable(.modelNotReady):
  Text("⏳ 模型还没热身完毕,稍后再试。") 
  // 注意:在模拟器里看到这个,通常意味着你的宿主机不支持,或者你在虚拟机里套娃。

// 4. 未知错误,最可怕的一种
case .unavailable:
  Text("👾 未知错误阻止了 Apple Intelligence 的运行。可能是赛博幽灵作祟。")
}

在这里插入图片描述

老 K 的技术旁白

  • model.availability:这是你的盖革计数器。一定要先 switch 它,不然直接调用模型会导致程序崩溃,就像在没有氧气的地方点火一样。
  • unavailable 的各种姿势:一定要给用户(或者你自己)清晰的反馈。与其让 App 闪退,不如告诉他“你的装备不行”。

在这里插入图片描述


💉 注入代码:瞒天过海

现在,我们要把这个检测机制植入到 ShareViewController.swift 里。这个文件是连接古老的 UIKit 世界和新锐 SwiftUI 世界的桥梁。

找到 showSwiftUIView(with:) 方法,我们要玩一招“偷梁换柱”。

// 用我们新的 ModelCheckView 替换掉原来的直接调用
let wvc = UIHostingController(
  rootView: ModelCheckView(
    sharedText: text,
    onDone: closeExtension // 事情办完就销毁现场
  )
)

这样,当你在 Safari 里选中一段长得令人发指的文本,点击“分享 -> LocalSummarizer”时,系统会先经过我们的 ModelCheckView 查岗。

在这里插入图片描述


🎭 模拟测试:在母体中演练

“别急着上线,现在的网络全是眼线。”老 K 按住你的手,“先在 Xcode 的模拟环境中跑一遍。”

即使你的设备牛逼哄哄,你也得测试一下如果用户没开功能会怎样。

在这里插入图片描述

  1. 在 Xcode 顶部选择 SummarizeExtension 方案。
  2. 点击 Edit Scheme...
  3. Run -> Options 标签页下,找到 Simulated Foundation Models Availability

在这里插入图片描述

这就是你的上帝模式开关。你可以把它设为:

  • Apple Intelligence Not Enabled:看看你的 App 会不会乖乖提示用户去开启。
  • Off:这是默认值,反映你真实设备的状态。

在这里插入图片描述

⚠️ 警告:测完记得把它改回 Off。别到时候明明设备支持,却因为这里没改回来而对着屏幕怀疑人生,那种感觉就像是找了半天眼镜结果眼镜架在鼻子上一样蠢。

在这里插入图片描述

为了让主 App (LocalSummarizer) 也能自检,我们顺手改一下 ContentView.swift

import FoundationModels

// ... 在 body 里 ...
switch SystemLanguageModel.default.availability {
case .available:
  Text("✅ 此设备已准备好接入 Apple Foundation Models。")
// ... 其他 case 照抄上面的 ...
}

在这里插入图片描述


🎆 尾声:黎明前的微光

老 K 敲下最后一个回车键。屏幕闪烁了一下,那段数百万字的冗长文本,瞬间被 Apple Foundation Models 压缩成了寥寥几百字的精华摘要。

没有联网,没有上传,一切都在悄无声息中完成。

在这里插入图片描述

“搞定了。”老 K 拔掉数据线,嘴角露出一丝不易察觉的微笑,“猎犬们还在云端嗅探我们的踪迹,殊不知我们已经带着情报,大摇大摆地从它们眼皮子底下溜走了。”

在这里插入图片描述

窗外的雨似乎小了一些。在这个被数据淹没的时代,掌握本地大模型技术,不仅仅是为了开发几个花哨的功能,更是为了在这个监视无处不在的赛博丛林里,保留最后一份隐私和自由。

在这里插入图片描述

你看着手里显示着 "Summary Generated" 的屏幕,深知这只是开始。下一章,我们将利用这股力量,去触碰更深层的禁忌……

(未完待续...)

理财学习笔记(2):不懂不投

这是本系列的第 2 篇,主题是:不懂不投。

我们刚开始投资理财的时候,通常会寻求以下这些方法来找到投资标的。

常见的错误办法

1、问朋友。我们通常会问那些看起来投资理财收益比较高的朋友,问他们应该买什么股票。
对于朋友推荐的股票,我们通常会“无脑”买入。但如果有一天,股票突然大幅回撤,我们通常就会陷入恐慌。我们会怀疑:这个朋友到底靠不靠谱?他之前赚钱是靠运气,还是因为现在判断出了问题?接着,我们就会陷入各种猜忌、焦虑和紧张中,最后甚至睡不着觉。如果股票持续下跌,我们甚至可能割肉离场。所以说,跟着朋友买其实并不那么靠谱。

2、看走势。我们可能会去看某些股票或基金的历史走势。看到它在过去三年或五年涨得很好,我们就买入。这也是理财 App 或者某些理财经理推荐的首选理由:它过去 X 年涨幅 XX,排名 XX。

但这很容易陷入“价值陷阱”,比如:

  1. 周期性误判:有些股票仅仅是在某个周期内表现优秀。比如房地产在过去十年涨得很好,但这并非因为单体公司有多好,而是因为当时整个大环境让所有房企都很赚钱。如果你仅仅因为过去业绩好而买入,一旦遭遇经济下滑或泡沫破裂,就会面临巨大的损失。

  2. 均值回归陷阱:很多股票或基金某年表现出色,仅仅是因为那一年的风格与它匹配。所有行业都有“大小年”之分,未来遇到“小年”时,表现自然就会变差。我把这叫做“均值回归”。

这就好比考试:你的平均水平可能是第三名。发挥好的时候能考第一名,发挥不好则可能掉到第五名,但你始终是在第三名上下徘徊。

很多基金经理或股票的表现也是在自身价值上下震荡。如果你在高点买入,在回撤时就会损失惨重,甚至被深套。

3、跟风。跟风是 A 股散户的常见操作,某个时间什么热,就跟风买什么,涨了就快速卖掉,主打一个击鼓传花,赌谁是最后接盘的大傻子。

这种情况下,我们假设你的胜率是 50%。每次获胜挣 20%,每次赌失败亏 20%。如果你进行了 10 次这样的操作,那你整体的收益期望就是 (1.2^5)*(0.8^5)=0.82,所以你折腾了半天,最后 1 块钱的本金变成了 0.82 元。

当然,如果有人认为自己跟风总是赢,这也是有可能的,但是因为自己不敢长期持有,只要涨一点点就卖,其实每次挣的是一点点收益。但是如果偶尔遇到亏损的时候,自己舍不得卖掉,就会一次亏很多。做这种短线操作的人,需要极强的止损纪律,大部分人也是很难做到的。

不懂不投

所以回到股票投资,我觉得投资理财一定要自己懂才行。如果你完全不懂或一知半解,这些都会成为你的陷阱。因为:

  1. 心理层面:不懂的人往往“拿不住”。当股票大幅下跌时,无论是否割肉,你都会极度焦虑、睡不好觉,担心本金损失。
  2. 投资层面:如果你懂,面对下跌说不定还能逆势加仓;即便不加仓,至少能睡个好觉。

此外,世界上还有很多投资陷阱。有些人甚至专门为“制造陷阱”而生,比如搞资金盘、割韭菜或传销。这些行为有些是非法的,有些则游走在法律边缘。如果大家没有能力分辨这些陷阱,很容易就在投资理财中遭遇严重的亏损。

小结

小结一下,常见的错误投资:

  • 问朋友。其实本质上信的是朋友的业绩,朋友如果业绩下滑,就会怀疑。
  • 看走势。其实本质上是用过去业绩替代未来判断,不靠谱。
  • 跟风。纯投机,50% 胜率下期望是负的。

心理层面,只有懂了,才可能拿得住,睡得着觉。

另外,真正懂也可以避免很多骗局。

以上。

NSProcessInfoThermalState 的作用

借助AI辅助。

NSProcessInfoThermalState 的作用

NSProcessInfoThermalState 是一个用于表示设备散热状态的枚举类型。它帮助应用程序了解设备当前的温度水平,从而采取适当的措施来防止设备过热。

枚举值及含义

这个枚举包含四个级别:

  1. Nominal(正常) - 设备温度处于可接受水平,对用户无负面影响

  2. Fair(一般) - 设备温度略有升高,风扇可能开始工作并发出声音

  3. Serious(严重) - 设备温度明显升高,风扇全速运转,系统性能可能受到影响

  4. Critical(临界) - 设备温度显著升高,设备需要冷却降温

使用方式

// 获取当前散热状态
let thermalState = ProcessInfo.processInfo.thermalState

// 监听散热状态变化通知
NotificationCenter.default.addObserver(
    forName: ProcessInfo.thermalStateDidChangeNotification,
    object: nil,
    queue: nil
) { notification in
    // 处理散热状态变化
}

应用场景

当散热状态升高时,应用程序应该采取措施降低系统负载:

  • 减少 CPU/GPU 使用率
  • 降低帧率
  • 减少 I/O 操作
  • 使用低质量的视觉效果
  • 暂停后台任务

这对于保护设备硬件、提升用户体验(避免设备过热导致的性能下降或风扇噪音)非常重要,特别是在进行密集计算或图形渲染的应用中。

3-27.【函数式编程】讨论 Combine 的 Publisher 如何体现 Functor / Monad 特性。

1️⃣ Functor 特性在 Publisher 中的体现

Functor 核心:能够把函数映射到容器/上下文中的值,同时保持结构不变。

在 Combine 中:

  • Publisher 可以看作一个 容器,异步发出值
  • 使用 map 可以对 Publisher 发出的每个元素进行函数映射
  • 不改变 Publisher 的结构,只改变输出值

示例

import Combine
import Foundation

let publisher = [1, 2, 3].publisher // ArrayPublisher<Int>

let mapped = publisher.map { $0 * 2 }

let cancellable = mapped.sink { value in
    print("Mapped value:", value)
}
// 输出:Mapped value: 2
//       Mapped value: 4
//       Mapped value: 6
  • map 只改变 输出值,不改变 Publisher 的类型
  • 多次 map 可组合,满足 Functor 规律:
let f: (Int) -> Int = { $0 + 1 }
let g: (Int) -> Int = { $0 * 10 }

let left = publisher.map(f).map(g)
let right = publisher.map { g(f($0)) }
  • left == right ✅ 组合律满足

2️⃣ Monad 特性在 Publisher 中的体现

Monad 核心:支持 bind(flatMap)操作,能够把返回容器的函数平铺,避免嵌套容器。

在 Combine 中:

  • flatMap 对应 Monad 的 bind
  • 输入:(Output) -> Publisher<NewOutput, Failure>
  • 输出:单一 Publisher,把可能产生的多个嵌套 Publisher 平铺
  • 支持链式组合多个异步操作

示例

假设我们有两个异步 Publisher 函数:

func fetchNumber(_ id: Int) -> AnyPublisher<Int, Never> {
    Just(id * 2).eraseToAnyPublisher()
}

func fetchString(_ number: Int) -> AnyPublisher<String, Never> {
    Just("Number is (number)").eraseToAnyPublisher()
}

// 使用 flatMap 链式组合
let cancellable2 = [1, 2, 3].publisher
    .flatMap { fetchNumber($0) }   // Monad bind,平铺 Publisher
    .flatMap { fetchString($0) }
    .sink { print($0) }

解释

  1. [1,2,3].publisher → Publisher

  2. .flatMap(fetchNumber) → Publisher

    • 每个值会生成一个新的 Publisher
    • flatMap 平铺输出,而不是产生嵌套 Publisher<Publisher>
  3. .flatMap(fetchString) → Publisher

  4. 最终输出一个 扁平 Publisher,可以订阅


3️⃣ Functor vs Monad 对比在 Combine 中

特性 Functor (map) Monad (flatMap)
操作类型 (Output) -> NewOutput (Output) -> Publisher<NewOutput, Failure>
结构变化 不改变 Publisher 层级 平铺嵌套 Publisher
适用场景 对值做同步转换 异步链式调用,生成新的 Publisher
示例 .map { $0 * 2 } .flatMap { fetchNumber($0) }

4️⃣ 总结

  1. Functor

    • Publisher.map → 映射输出值,保持 Publisher 结构
    • 遵循恒等和组合律
  2. Monad

    • Publisher.flatMap → 绑定新的 Publisher,平铺嵌套,短路失败可处理
    • 支持函数式异步流水线,链式组合

💡 核心思想

Combine 的 Publisher 就是一个 异步容器,Functor 和 Monad 的特性保证了:

  1. 可以安全映射数据 (map)
  2. 可以安全组合异步操作 (flatMap)
  3. 异步值处理保持函数式风格,副作用集中在 sinkassign
❌