鲍志飞的技术笔记

iOS 开发、Swift 并发、SwiftUI,以及一些日常工程实践的记录。

weak self 什么时候真的需要

iOS · Swift

很多人写闭包时习惯性地加上 [weak self],把它当成一种「保险」。但无脑加 weak 有代价:多一层可选解包,还可能让本该执行的逻辑因为 self 提前释放而被跳过。

判断依据只有一条:这个闭包最终会不会被 self 持有。构成循环引用需要闭环——self 持有闭包,闭包又强引用 self。

不需要 weak self 的典型场景,因为闭包由系统或临时对象持有,执行完就释放:

// DispatchQueue 持有闭包,执行完即释放,不构成环
DispatchQueue.main.async {
    self.tableView.reloadData()
}

// UIView 动画同理
UIView.animate(withDuration: 0.3) {
    self.headerView.alpha = 0
}

需要 weak self 的场景,是闭包被 self(或 self 的属性)长期持有:

class FeedViewController: UIViewController {
    private var cancellable: AnyCancellable?   // self 持有它

    func bind() {
        cancellable = viewModel.$items
            .sink { [weak self] items in       // 没有 weak 就成环
                self?.render(items)
            }
    }
}

同一类的还有:赋给自身属性的回调闭包(self.onDone = { self.xxx })、注册进长生命周期单例的观察者、以及会重复触发的 Timer。

顺带一提 unowned:它省掉了可选解包,但 self 已释放时会直接崩溃。只在你能确信「闭包的生命周期不会超过 self」时才用,否则老实用 weak。

SwiftUI 的状态该用哪个包装器

iOS · SwiftUI

SwiftUI 的属性包装器容易混淆,但选择规则其实可以压缩成两个问题:数据是值类型还是引用类型?这个视图是数据的拥有者,还是只是使用者?

最容易踩的一个坑:在视图里用 @ObservedObject 创建对象。

struct ProfileView: View {
    // ❌ 视图每次重建都会新建一个 ViewModel,状态莫名其妙丢失
    @ObservedObject var viewModel = ProfileViewModel()
}

SwiftUI 的 View 是值类型,会被频繁重新构造。@ObservedObject 不负责保管对象的生命周期,于是每次重建都得到一个全新的 ViewModel——表现出来就是「输入到一半的内容被清空」「列表莫名回到顶部」这类诡异现象。

struct ProfileView: View {
    // ✅ StateObject 只在视图首次出现时初始化一次
    @StateObject private var viewModel = ProfileViewModel()
}

反过来,如果 ViewModel 是从父视图传进来的,就该用 @ObservedObject——用 @StateObject 反而会让它无视外部传入的新对象。

冷启动时间都花在哪

iOS · 性能

冷启动以 main() 为界分成两段,优化手段完全不同。

pre-main 阶段(系统在跑,代码还没执行):

main 之后:didFinishLaunching 里的初始化、首屏视图构建、首屏数据加载。这段是自己的代码,也最容易失控——各种 SDK 初始化习惯性地全堆在这里。

实测方式,别凭感觉:

常见的有效改动:合并动态库为静态库、把 +load 换成 +initialize 或延迟到首次使用、非首屏必需的 SDK 挪到启动后再初始化、首屏别做同步 IO 和同步网络请求。

force push 用 --force-with-lease

工程实践 · Git

rebase 之后本地历史被改写,普通 push 会被拒绝,于是很多人顺手就是 git push -f。这个习惯在多人协作时是有风险的。

--force 的语义是「不管远程现在是什么,用我的覆盖掉」。如果在你 rebase 期间同事往同一分支推了新提交,你这一下会直接抹掉他的提交,而且 push 成功、毫无提示。

--force-with-lease 多了一道检查:只有当远程分支还停在我上次看到的位置时,才允许覆盖。

git push --force-with-lease origin feature/login

如果期间有人推了新东西,这条命令会失败并提示 stale info,你就知道该先 fetch 看看发生了什么,而不是把别人的工作冲掉。

有个细节容易忽略:--force-with-lease 依赖本地的远程追踪引用。如果你在 push 前跑了 git fetch,追踪引用已经更新到最新,这道保护就失效了——它会认为「我看到的就是最新的」。所以别在 force push 前习惯性 fetch。

更省事的做法是设成默认:

git config --global alias.pushf "push --force-with-lease"