Swift桥接ObjC类中非懒加载默认属性内存泄漏原因咨询
咱们直接拆解这个泄漏的核心逻辑——问题出在Swift属性初始化时机和ObjC Runtime KVC操作的交互上,结合你提供的场景,具体原因如下:
1. Swift存储属性的默认初始化 vs ObjC KVC执行顺序
Swift的非lazy存储属性有个关键特性:在类实例分配内存后、任何init方法执行前,就会完成默认值的初始化。对你的Pricing类来说:
@objcMembers class Pricing: Mappable { var price: Money = Money.zero() // 实例分配后立即初始化 var vat: Percent = Percent.zero() ... }
当ObjC的initWithJSON:被调用时,price和vat已经持有了Money.zero()和Percent.zero()的实例,引用计数为1。
接下来,initWithJSON:内部通过[self setValue:value forKey:key]赋值新的反序列化实例时,ObjC Runtime会执行以下步骤:
- 调用属性的getter(比如
-[Pricing price],@objcMembers把Swift属性暴露给了ObjC)获取当前值(也就是默认的zero实例) - 对新值执行
retain - 对旧值执行
release
2. Swift-ObjC桥接带来的额外Retain
这里的坑点在于:Swift属性的getter被ObjC调用时,桥接层会自动给返回的对象加一次retain。这是因为ObjC的方法返回对象默认遵循“返回值拥有+1引用计数”的内存管理规则,Swift到ObjC的桥接会自动适配这个规则。
所以默认的zero实例的引用计数变化是:
- 初始化时:retain count = 1
- ObjC getter调用:retain count +=1 → 2
- KVC赋值时release旧值:retain count -=1 → 1
这就导致这个默认实例的引用计数永远无法归零,ARC无法释放它,最终造成内存泄漏。
3. 为什么Lazy属性能解决问题
Lazy属性的初始化逻辑是第一次被访问时才执行。在ObjC的initWithJSON:执行setValue:forKey:时,price和vat还没被访问过,所以默认的zero实例根本没有被创建。反序列化直接赋值了新的实例,自然不存在旧实例被泄漏的问题。
补充:为什么其他场景下Money.zero()无泄漏
在纯Swift代码中使用Money.zero()时,不会经过ObjC Runtime的getter调用,也就不会触发桥接层的额外retain。只有当ObjC代码(比如这里的Mappable分类)通过KVC访问Swift属性时,才会出现这个特殊的引用计数问题。
内容的提问来源于stack exchange,提问作者Simon

