为何判断非类协议一致性会导致ViewController无法释放?
嘿,最近我踩了个特别诡异的iOS内存泄漏坑,花了一整天才排查出来,还做了最小复现示例,跟大家唠唠这个问题!
场景背景
我的项目里故事板结构很简单:一个UINavigationController,它的根视图控制器是ViewController,可以push跳转到AnotherViewController。
核心代码逻辑
先给大家看关键代码片段:
首先定义一个无约束的协议:
protocol MyProtocol {}
AnotherViewController遵循这个协议,并且加了deinit方法用来观察是否被释放:
class AnotherViewController: UIViewController, MyProtocol { deinit { print("🍏 Yippee!") // 打印日志确认释放状态 } }
ViewController作为UINavigationController的代理,实现了动画控制器相关方法,里面用switch判断两个视图控制器是否都符合MyProtocol(实际代码里从来没进入过这个分支):
class ViewController: UIViewController, UINavigationControllerDelegate { func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationController.Operation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? { switch (fromVC, toVC) { case (let from as MyProtocol, let to as MyProtocol): // 实际运行中从未进入这个分支 return nil default: return nil } } }
诡异的运行现象
- 点击Push按钮进入
AnotherViewController,控制台输出🍏 Yippee! - 点击返回按钮回到
ViewController,控制台再次输出🍏 Yippee!,但**AnotherViewController根本没被释放**——内存泄漏实锤了!
有效的修复方式
我试了三种方法,都能解决这个泄漏问题:
- 把协议修改为类协议:
protocol MyProtocol: class {} - 直接移除switch中判断是否符合
MyProtocol的case分支 - 让
ViewController也遵循MyProtocol(这时会进入对应的case分支,泄漏也消失了)
关键疑问拆解
相信大家跟我一样好奇,为啥会出现这种奇葩情况?我查了Swift底层的代码生成逻辑,给大家拆解:
1. 为什么非类协议的switch判断会引发泄漏?
Swift里的非类协议(没有: class约束)可以被值类型和引用类型同时遵循。当你在switch的模式匹配里用as MyProtocol做类型转换时,编译器为了兼容值类型,会生成一个**存在容器(Existential Container)**来包装对象实例。这个容器会对AnotherViewController实例添加强引用,而且因为这个引用被导航代理的相关上下文持有,导致引用无法正常释放,最终造成内存泄漏。
2. 为什么类协议能解决问题?
当协议加上: class约束后,编译器明确知道只有类类型能遵循它,这时候做类型转换时不会生成存在容器,而是直接用类的引用方式处理,不会额外增加强引用,自然就不会有泄漏问题。
3. 为什么只有switch中的判断会出问题?
这是因为switch的模式匹配在处理非类协议的类型转换时,编译器的代码生成逻辑和if判断完全不同。用if let from = fromVC as? MyProtocol这种方式时,编译器的引用处理更“轻量”,不会像switch模式匹配那样生成持有强引用的存在容器,所以不会触发泄漏。同样,直接判断类类型(比如as? AnotherViewController)时,因为是明确的类类型,也不会有存在容器的问题。
内容的提问来源于stack exchange,提问作者Fogmeister

