在 Swift 中,利用 isKnownUniquelyReferenced 实现**原地修改(In-place Mutation)**是手动实现 写时复制(Copy-on-Write, COW) 的核心技术。
它的作用是检查一个引用类型(Class 实例)的强引用计数是否为 1。如果是 1,说明没有其他变量共享这块内存,直接修改它是安全的;如果大于 1,则必须先拷贝,以维持值类型的语义。
1. 核心逻辑步骤
实现一个具备 COW 特性的结构体,通常遵循以下模板:
-
内部包装:将数据存储在一个私有的
class实例中。 -
写前检查:在
set或mutating方法中,调用isKnownUniquelyReferenced。 -
分支处理:
- 返回
true:直接修改内部class的属性(高性能)。 - 返回
false:创建一个新的class实例并拷贝数据,再进行修改。
- 返回
2. 代码实现示例
假设我们要创建一个处理大数据块的自定义结构体 DataBuffer:
Swift
final class Storage {
var bytes: [UInt8]
init(bytes: [UInt8]) { self.bytes = bytes }
// 提供一个方便克隆的方法
func copy() -> Storage {
return Storage(bytes: self.bytes)
}
}
struct DataBuffer {
private var _storage: Storage
init(bytes: [UInt8]) {
_storage = Storage(bytes: bytes)
}
var bytes: [UInt8] {
get { _storage.bytes }
set {
// 关键点:检查 _storage 是否只有当前 DataBuffer 在引用
if isKnownUniquelyReferenced(&_storage) {
print("原地修改:引用计数为 1")
_storage.bytes = newValue
} else {
print("执行拷贝:存在多个引用")
_storage = _storage.copy()
_storage.bytes = newValue
}
}
}
}
运行结果演示:
Swift
var buffer1 = DataBuffer(bytes: [0, 1, 2])
var buffer2 = buffer1 // 此时两者共享同一个 _storage 实例
buffer1.bytes = [3, 4, 5] // 输出:执行拷贝(因为 buffer2 也在引用)
buffer1.bytes = [6, 7, 8] // 输出:原地修改(此时 buffer1 独占新申请的内存)
3. 重要注意事项与“坑”
A. 仅支持引用类型
isKnownUniquelyReferenced 的参数必须是一个 类(Class)实例的引用。如果你尝试传入一个结构体,编译器会报错。
B. 注意强引用的隐藏陷阱
如果在检查之前,你无意中创建了一个临时的强引用,该函数会返回 false。
Swift
mutating func update() {
let temp = _storage // 增加了一次引用计数!
if isKnownUniquelyReferenced(&_storage) {
// 这里永远不会执行,因为 temp 占着一个计数
}
}
C. Objective-C 类的限制
isKnownUniquelyReferenced 不支持直接处理 NSObject 派生的类。
- 如果你的
Storage继承自NSObject,该函数会直接返回false。 - 解决方法:始终使用纯 Swift 类(不继承自
NSObject)作为 COW 的后端存储。
4. 为什么要这么做?
- 性能优化:避免不必要的深拷贝。对于拥有几万个元素的数组,拷贝一次的代价是 ,而原地修改是 。
- 值语义保证:确保结构体在被赋值给新变量后,对其中一个的修改不会影响另一个,保持数据的独立性。
总结
isKnownUniquelyReferenced 就像是一个内存门卫。它通过查看对象的“户口本”(引用计数),帮你决定是应该“粉刷旧房”(原地修改)还是“另起炉灶”(深拷贝)。
英文版
8-26. [Memory Management] How to use isKnownUniquelyReferenced to determine if an object can be mutated in-place?
In Swift, utilizing isKnownUniquelyReferenced to implement In-place Mutation is the core technique for manually implementing Copy-on-Write (COW) .
Its function is to check whether the strong reference count of a reference type (Class instance) is exactly 1. If it is 1, it means no other variables share this memory, and it is safe to modify it directly. If it is greater than 1, a copy must be made first to maintain value type semantics.
1. Core Logical Steps
To implement a struct with COW characteristics, you generally follow this template:
-
Internal Packaging: Store the data in a private
classinstance. -
Pre-write Check: In the
setobserver or amutatingmethod, callisKnownUniquelyReferenced. -
Branch Handling:
- Returns
true: Modify the internalclassproperties directly (High performance). - Returns
false: Create a newclassinstance, copy the data over, and then perform the modification.
- Returns
2. Code Implementation Example
Suppose we want to create a custom struct DataBuffer that handles large blocks of data:
Swift
final class Storage {
var bytes: [UInt8]
init(bytes: [UInt8]) { self.bytes = bytes }
// Provide a convenience method for cloning
func copy() -> Storage {
return Storage(bytes: self.bytes)
}
}
struct DataBuffer {
private var _storage: Storage
init(bytes: [UInt8]) {
_storage = Storage(bytes: bytes)
}
var bytes: [UInt8] {
get { _storage.bytes }
set {
// Key Point: Check if _storage is uniquely referenced by this DataBuffer
if isKnownUniquelyReferenced(&_storage) {
print("In-place mutation: Reference count is 1")
_storage.bytes = newValue
} else {
print("Executing copy: Multiple references exist")
_storage = _storage.copy()
_storage.bytes = newValue
}
}
}
}
Execution Demo:
Swift
var buffer1 = DataBuffer(bytes: [0, 1, 2])
var buffer2 = buffer1 // Both now share the same _storage instance
buffer1.bytes = [3, 4, 5] // Output: Executing copy (because buffer2 also references it)
buffer1.bytes = [6, 7, 8] // Output: In-place mutation (buffer1 now owns the new memory exclusively)
3. Important Considerations and "Pitfalls"
A. Supports Reference Types Only
The argument for isKnownUniquelyReferenced must be a reference to a class instance. If you attempt to pass a struct, the compiler will throw an error.
B. Beware of Hidden Strong References
If you accidentally create a temporary strong reference before the check, the function will return false.
Swift
mutating func update() {
let temp = _storage // Increments the reference count!
if isKnownUniquelyReferenced(&_storage) {
// This block will never execute because 'temp' holds a count
}
}
C. Limitations with Objective-C Classes
isKnownUniquelyReferenced does not support direct handling of classes derived from NSObject.
- If your
Storageinherits fromNSObject, the function will consistently returnfalse. - Workaround: Always use pure Swift classes (those that do not inherit from
NSObject) as the back-end storage for COW.
4. Why Do We Do This?
- Performance Optimization: Avoid unnecessary deep copies. For an array with tens of thousands of elements, a copy costs , whereas an in-place mutation is .
- Value Semantics Guarantee: Ensures that after a struct is assigned to a new variable, modifying one does not affect the other, maintaining data independence.
Summary
isKnownUniquelyReferenced acts like a memory gatekeeper. By checking the object's "residency permit" (reference count), it helps you decide whether to "repaint the old house" (mutate in-place) or "build a new one" (deep copy).