基础知识
Swift
和Objective-C
都是利用古老且有效的ARC(Automatic Reference Counting)来管理内存,当实例的引用计数为0时,实例将会被析构,实例占有的内存和资源都将重新变得可用。
但,当两个实例发生循环引用时,那么他们的引用计数会一直大于0,那么他们将不会被析构。
为了解决这个问题,Swift
和Objective-C
引入了弱引用,弱引用不会被ARC
计算。也就是说当一个弱引用指向一个引用类型实例时,引用计数不会增加。
Swift中的弱引用
这里以闭包为例,在OC
中,标准的做法是,定义一个弱引用指向闭包外部的实例,然后在闭包内部定义强引用指向这个实例,在闭包执行期间使用它.
类似于下面代码:
__weak typeof(self) weakSelf = self;
void (^myBlock)(NSString *) = ^(NSString * name) {
__strong typeof(self) strongSelf = self;
NSString *name = self.name;
};
为了更方便的处理循环引用,Swift
引入了一个新的概念,用于简化和更加明显的表达在闭包内部,外部变量的捕获:捕获列表(capture list)。使用捕获列表,可以在函数(闭包)的头部定义和指定那些需要用在内部的外部变量,并且指定引用类型(这里是指 unowned
和 weak
)。
例子:
不使用捕获列表时,闭包将会创建一个外部变量的强引用。
var i1 = 1, i2 = 1
var fStrong = {
i1 += 1
i2 += 2
}
fStrong()
print(i1,i2) //Prints 2 and 3
使用捕获列表,闭包内部会创建一个新的可用常量。如果没有指定常量修饰符,闭包将会简单地拷贝原始值到新的变量中,对于值类型和引用类型都是一样的。
var fCopy = { [i1] in
print(i1,i2)
}
fStrong()
print(i1,i2) //打印结果是 2 和 3
fCopy() //打印结果是 1 和 3
在上面的例子中,在调用fStrong
之前定义函数 fCopy
,在该函数定义的时候,私有常量已经被创建了。正如你所看到的,当调用第二个函数时候,仍然打印 i1
的原始值。
对于外部引用类型的变量,在捕获列表中指定 weak
或 unowned
,这个常量将会被初始化为一个弱引用,指向原始值,这种指定的捕获方式就是用来处理循环引用的方式。
class aClass{
var value = 1
}
var c1 = aClass()
var c2 = aClass()
var fSpec = { [unowned c1, weak c2] in
c1.value += 1
if let c2 = c2 {
c2.value += 1
}
}
fSpec()
print(c1.value,c2.value) //Prints 2 and 2
两个 aClass
捕获实例的不同的定义方式,决定了它们在闭包中不同的使用方式。
调用步骤
动作 | unowned | weak |
---|---|---|
预先调用 #1 | 对象进行 unowned_retain 操作 | 创建一个容器,并且对象进行 strong_retain 操作。创建一个可选值,存入到容器中,然后释放可选值 |
预先调用 #2 | strong_retain_unowned,unowned_retain 和 strong_release | strong_retain |
闭包执行 | strong_retain_unowned,unowned_release | load_weak, 打开可选值, strong_release |
调用之后 | unowned_release | strong_release |
- unowned_retain:增加堆对象中的 unowned 引用计数。
- strong_retain_unowned :断言对象的强引用计数大于 0,然后增加这个引用计数。
- strong_retain:增加对象的强引用计数。
- load_weak:不是真正的 ARC 调用,但是它将增加可选值指向对象的强引用计数。
- strong_release:减少对象的强引用计数。如果释放操作把对象强引用计数变为0,对象将被销毁,然后弱引用将被清除。当整个强引用计数和 unowned 引用计数都为0时,对象的内存才会被释放。
- unowned_release:减少对象的 unowned 引用计数。当整个强引用计数和 unowned 引用计数都为 0 时,对象的内存才会被释放。
使用场景
- unowned: 引用使用的场景是,原始实例永远不会为 nil,闭包可以直接使用它,并且直接定义为显式解包可选值。当原始实例被析构后,在闭包中使用这个捕获值将导致崩溃
- 如果捕获原始实例在使用过程中可能为 nil ,必须将引用声明为 weak, 并且在使用之前验证这个引用的有效性。
实现
unomned实现
来源于Swift5.0源码HeapObject.cpp
文件。
HeapObject *swift::swift_unownedRetain(HeapObject *object) {
SWIFT_RT_TRACK_INVOCATION(object, swift_unownedRetain);
/// 检测对象是否存在,不存在直接return
if (!isValidPointerForNativeRetain(object))
return object;
/// 将对象的引用计数加1
object->refCounts.incrementUnowned(1);
return object;
}
void swift::swift_unownedRelease(HeapObject *object) {
SWIFT_RT_TRACK_INVOCATION(object, swift_unownedRelease);
/// 检测对象是否存在,不存在,直接return
if (!isValidPointerForNativeRetain(object))
return;
// Only class objects can be unowned-retained and unowned-released.
/// 检测是否为类的对象
assert(object->metadata->isClassObject());
assert(static_cast<const ClassMetadata*>(object->metadata)->isTypeMetadata());
/// 检测Unowned引用计数是否能减1
if (object->refCounts.decrementUnownedShouldFree(1)) {
auto classMetadata = static_cast<const ClassMetadata*>(object->metadata);
/// 释放Unowned指针,并没有释放该指针指向的内存
swift_slowDealloc(object, classMetadata->getInstanceSize(),
classMetadata->getInstanceAlignMask());
}
}
到此已经有了一个对象的 unowned 引用,另外一个指令,strong_retain_unowned
用来创建一个强引用:
HeapObject *swift::swift_unownedRetainStrong(HeapObject *object) {
SWIFT_RT_TRACK_INVOCATION(object, swift_unownedRetainStrong);
if (!isValidPointerForNativeRetain(object))
return object;
/// 断言来验证对象是否被弱引用,一旦断言通过,将尝试进行增加强引用计数的操作.
/// 一旦对象在进程中已经被释放,尝试将会失败。
assert(object->refCounts.getUnownedCount() &&
"object is not currently unowned-retained");
/// 尝试增加引用计数
if (! object->refCounts.tryIncrement())
/// 引用计数添加失败
swift::swift_abortRetainUnowned(object);
return object;
}
weak
Swift5.0源码
/// 初始化弱引用
WeakReference *swift::swift_weakInit(WeakReference *ref, HeapObject *value) {
ref->nativeInit(value);
return ref;
}
///
void nativeInit(HeapObject *object) {
auto side = object ? object->refCounts.formWeakReference() : nullptr;
nativeValue.store(WeakReferenceBits(side), std::memory_order_relaxed);
}
WeakReferenceBits(HeapObjectSideTableEntry *newValue) {
setNativeOrNull(newValue);
}
/// 创建一个弱引用表,成功则增加弱引用计数。
template <>
HeapObjectSideTableEntry* RefCounts<InlineRefCountBits>::formWeakReference()
{
auto side = allocateSideTable(true);
if (side)
return side->incrementWeak();
else
return nullptr;
}
/// 创建一个对象的散列表,如果该对象释放了,则返回空
// Return an object's side table, allocating it if necessary.
// Returns null if the object is deiniting.
// SideTableRefCountBits specialization intentionally does not exist.
template <>
HeapObjectSideTableEntry* RefCounts<InlineRefCountBits>::allocateSideTable(bool failIfDeiniting)
{
auto oldbits = refCounts.load(SWIFT_MEMORY_ORDER_CONSUME);
// Preflight failures before allocating a new side table.
if (oldbits.hasSideTable()) {
// Already have a side table. Return it.
return oldbits.getSideTable();
}
else if (failIfDeiniting && oldbits.getIsDeiniting()) {
// Already past the start of deinit. Do nothing.
return nullptr;
}
// Preflight passed. Allocate a side table.
// FIXME: custom side table allocator
HeapObjectSideTableEntry *side = new HeapObjectSideTableEntry(getHeapObject());
auto newbits = InlineRefCountBits(side);
do {
if (oldbits.hasSideTable()) {
// Already have a side table. Return it and delete ours.
// Read before delete to streamline barriers.
auto result = oldbits.getSideTable();
delete side;
return result;
}
else if (failIfDeiniting && oldbits.getIsDeiniting()) {
// Already past the start of deinit. Do nothing.
return nullptr;
}
side->initRefCounts(oldbits);
/// 进行 CAS
} while (! refCounts.compare_exchange_weak(oldbits, newbits,
std::memory_order_release,
std::memory_order_relaxed));
return side;
}
增加引用计数
// Increment the weak reference count.
void incrementWeak() {
auto oldbits = refCounts.load(SWIFT_MEMORY_ORDER_CONSUME);
RefCountBits newbits;
do {
newbits = oldbits;
assert(newbits.getWeakRefCount() != 0);
newbits.incrementWeakRefCount();
if (newbits.getWeakRefCount() < oldbits.getWeakRefCount())
swift_abortWeakRetainOverflow();
} while (!refCounts.compare_exchange_weak(oldbits, newbits,
std::memory_order_relaxed));
}
bool decrementWeakShouldCleanUp() {
auto oldbits = refCounts.load(SWIFT_MEMORY_ORDER_CONSUME);
RefCountBits newbits;
bool performFree;
do {
newbits = oldbits;
performFree = newbits.decrementWeakRefCount();
} while (!refCounts.compare_exchange_weak(oldbits, newbits,
std::memory_order_relaxed));
return performFree;
}
SideTable的数据结构
class HeapObjectSideTableEntry {
// FIXME: does object need to be atomic?
std::atomic<HeapObject*> object;
SideTableRefCounts refCounts;
public:
HeapObjectSideTableEntry(HeapObject *newObject)
: object(newObject), refCounts()
{ }
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Winvalid-offsetof"
static ptrdiff_t refCountsOffset() {
return offsetof(HeapObjectSideTableEntry, refCounts);
}
弱引用的访问:
HeapObject *nativeLoadStrong() {
auto bits = nativeValue.load(std::memory_order_relaxed);
return nativeLoadStrongFromBits(bits);
}
HeapObject *nativeLoadStrongFromBits(WeakReferenceBits bits) {
auto side = bits.getNativeOrNull();
return side ? side->tryRetain() : nullptr;
}
到这里大家发现一个问题没有,被引用对象释放了为什么还能直接访问 Side Table?其实 Swift ABI 中 Side Table 的生命周期与对象是分离的,当强引用计数为 0 时,只有 HeapObject 被释放了。
只有所有的 weak 引用者都被释放了或相关变量被置 nil 后,Side Table 才能得以释放,详见:
void decrementWeak() {
// FIXME: assertions
// FIXME: optimize barriers
bool cleanup = refCounts.decrementWeakShouldCleanUp();
if (!cleanup)
return;
// Weak ref count is now zero. Delete the side table entry.
// FREED -> DEAD
assert(refCounts.getUnownedCount() == 0);
delete this;
}
void decrementWeakNonAtomic() {
// FIXME: assertions
// FIXME: optimize barriers
bool cleanup = refCounts.decrementWeakShouldCleanUpNonAtomic();
if (!cleanup)
return;
// Weak ref count is now zero. Delete the side table entry.
// FREED -> DEAD
assert(refCounts.getUnownedCount() == 0);
delete this;
}
所以即便使用了弱引用,也不能保证相关内存全部被释放,因为只要 weak 变量不被显式置 nil,Side Table 就会存在。而 ABI 中也有可以提升的地方,那就是如果访问弱引用变量时发现被引用对象已经释放,就将自己的弱引用销毁掉,避免之后重复无意义的 CAS 操作。当然 ABI 不做这个优化,我们也可以在 Swift 代码里做。
以上就是Swift weak 弱引用机制实现方式的一个简单的分析,可见思路与 Objective-C runtime 还是很类似的,都采用与对象匹配的 Side Table 来维护引用计数。不同的地方就是 Objective-C 对象在内存布局中没有 Side Table 指针,而是通过一个全局的 StripedMap 来维护对象和 Side Table 之间的关系,效率没有 Swift 这么高。另外 Objective-C runtime 在对象释放时会将所有的 __weak 变量都 zero-out,而 Swift 并没有
小结
在这个实现中,获取一个强引用需要更多复杂同步操作,在多线程竞争严重的情况下,会带来性能损耗
结论
- 保守的使用 weak 引用是否明智呢?答案是否定的,无论是从性能的角度还是代码清晰的角度而言。
- 使用正确的捕获修饰符类型,明确的表明代码中的生命周期特性,当其他人或者你自己在读你的代码时不容易误解