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 的属性包装器容易混淆,但选择规则其实可以压缩成两个问题:数据是值类型还是引用类型?这个视图是数据的拥有者,还是只是使用者?
@State —— 值类型,视图私有。应该标成 private。
@StateObject —— 引用类型(ObservableObject),当前视图拥有它,负责创建。
@ObservedObject —— 引用类型,从外部传入,当前视图只是使用者。
@EnvironmentObject —— 引用类型,从环境里取,适合跨多层传递。
@Binding —— 对别处状态的读写引用,自己不持有数据。
最容易踩的一个坑:在视图里用 @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 阶段(系统在跑,代码还没执行):
- 加载动态库——dyld 递归加载所有依赖。数量是主要成本,几十个动态库能拖出几百毫秒。
- rebase / binding——修正指针地址、绑定外部符号。跟符号数量正相关。
- ObjC runtime 初始化——注册类、加载 category、处理 selector 唯一性。
- 初始化器——ObjC 的
+load、C++ 静态构造。这些在 main 之前串行执行,写重了直接拖慢启动。
main 之后:didFinishLaunching 里的初始化、首屏视图构建、首屏数据加载。这段是自己的代码,也最容易失控——各种 SDK 初始化习惯性地全堆在这里。
实测方式,别凭感觉:
- pre-main 耗时:在 Xcode 的 Scheme 里加环境变量
DYLD_PRINT_STATISTICS = 1,控制台会打印各阶段耗时。
- 完整启动:用 Instruments 的 App Launch 模板,能看到具体是哪个调用吃掉了时间。
常见的有效改动:合并动态库为静态库、把 +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"