Swift3之Weak引用

由于不同的Swift版本引用计数实现会有不同,该文讨论的引用计数原理都基于Swift3

弱引用


iOS开发时经常会遇到循环引用,如果处理不当会导致内存泄露,我们通常会使用weak reference弱引用来解决该问题,因为弱引用不会retain对象,当对象引用计数变为0时,弱引用指针将会被赋nil

实现过程


通常如果实现弱引用,可以让每一个对象维护所有指向该对象的一个弱引用列表,当一个弱引用指向一个对象时,该引用被添加进列表,当弱引用重新赋值或生命期结束,则将其从列表中移除,当一个对象dealloced后,列表中的所有引用会被赋nil。在多线程环境中,需要对获得弱引用和释放对象的操作进行同步,以避免竞态条件,既当一个线程在释放最后一个强引用对象的同时,另一个线程正尝试加载该对象的弱引用。

Objective-C实现的过程为,每一个弱引用是一个指向目标对象的指针,编译器会使用helper函数,来避免直接读写指针,确保读取弱引用对象时不会返回正在被释放的对象指针。

实战


接下来,我们将创建几个方法来观察弱引用的过程。
首先我们想要能够dump出一个对象的内存,如下方法将获取一块内存,将其分成指针大小的块,再将其内容转成16进制,以便于观察:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Swift version: Swift3

func contents(ptr: UnsafeRawPointer, _ length: Int) -> String {
let wordPtr = ptr.assumingMemoryBound(to: UInt.self)

let words = length / MemoryLayout<UInt>.size
let wordChars = MemoryLayout<UInt>.size * 2

let buffer = UnsafeBufferPointer<UInt>(start: wordPtr, count: words)
let wordStrings = buffer.map({ word -> String in
var wordString = String(word, radix: 16)
while wordString.characters.count < wordChars {
wordString = "0" + wordString
}
return wordString
})
return wordStrings.joined(separator: " ")
}

接下来,我们将创建一个dumper函数来打印一个对象实例的内容,参数为一个对象实例,函数返回一个闭包。在函数内部,会创建一个UnsafeRawPointer指针来指向对象,这样能确保不会进行引用计数的操作,且当对象被释放后,我们仍可以dump出指针所指向内存的内容。

1
2
3
4
5
6
7
8
9
10
11
// Swift version: Swift3

func dumperFunc(_ obj: AnyObject) -> ((Void) -> String) {
let objString = String(describing: obj)
let ptr = unsafeBitCast(obj, to: UnsafeRawPointer.self)
let length = class_getInstanceSize(type(of: obj))
return {
let bytes = contents(ptr: ptr, length)
return "\(objString) \(ptr): \(bytes)"
}
}

如下有一个类,它有一个弱引用的属性target,同时,创建两个dummy属性,当dump内存内容时可以更清晰的识别:

1
2
3
4
5
class WeakReferer {
var dummy1 = 0x1234321012343210
weak var target: WeakTarget?
var dummy2: UInt = 0xabcdefabcdefabcd
}

接下来,创建一个该对象的实例,并dump出内存的内容:

1
2
3
let referer = WeakReferer()
let refererDump = dumperFunc(referer)
print(refererDump())

结果为:

1
SwiftLearn.WeakReferer 0x000060000004eb50: 000000010ebb0c50 0000000200000004 1234321012343210 0000000000000000 abcdefabcdefabcd

我们可以看到,dummy1位于第4块,dummy2位于第6块,弱引用位于他们中间,正如我们期待的,其内容为0.

现在我们给它赋一个值看看,我将通过一个do块来控制target的生命周期:

1
2
3
4
5
6
7
8
// 因为target是NSObject对象,所以需要改一下WeakReferer的target属性的类型

do {
let target = NSObject()
referer.target = target
print(target)
print(refererDump())
}

打印结果为:

1
2
<NSObject: 0x7fda6a21c6a0>
WeakReferer 0x00007fda6a000ad0: 00000001050a44a0 0000000200000004 1234321012343210 00007fda6a21c6a0 abcdefabcdefabcd

正如我们所看到的,target对象的指针直接存放在弱引用中。接下来,我们在do块结束之后再打印一下看看target释放后的情况:

1
print(refererDump())
1
WeakReferer 0x00007ffe32300060: 000000010cfb44a0 0000000200000004 1234321012343210 0000000000000000 abcdefabcdefabcd

被赋值为了nil.

接下来,我们再测试一下将target赋值为一个纯Swift对象,看是不是和Objective-CNSObject一样,如下为纯Swift``target

1
class WeakTarget {}

再试一下,看看结果怎样:

1
2
3
4
5
6
7
8
9
let referer = WeakReferer()
let refererDump = dumperFunc(referer)
print(refererDump())
do {
let target = WeakTarget()
referer.target = target
print(refererDump())
}
print(refererDump())

target开始为nil,然后赋给它一个值:

1
2
SwiftLearn.WeakReferer 0x00006000002423a0: 000000010538bc50 0000000200000004 1234321012343210 0000000000000000 abcdefabcdefabcd
SwiftLearn.WeakReferer 0x00006000002423a0: 000000010538bc50 0000000200000004 1234321012343210 000060800002a9c2 abcdefabcdefabcd

接下来,当target离开作用域,我们看看弱引用是否被赋nil

1
SwiftLearn.WeakReferer 0x00006000002423a0: 000000010538bc50 0000000200000004 1234321012343210 000060800002a9c2 abcdefabcdefabcd

咦,怎么没被赋nil,难道是target没有被释放,产生了内存泄露?我们给target对象加上析构函数看看:

1
2
3
class WeakTarget {
deinit { print("WeakTarget deinit") }
}

运行之前的代码,看看结果:

1
2
3
4
SwiftLearn.WeakReferer 0x00006000002423a0: 000000010538bc50 0000000200000004 1234321012343210 0000000000000000 abcdefabcdefabcd
SwiftLearn.WeakReferer 0x00006000002423a0: 000000010538bc50 0000000200000004 1234321012343210 000060800002a9c2 abcdefabcdefabcd
WeakTarget deinit
SwiftLearn.WeakReferer 0x00006000002423a0: 000000010538bc50 0000000200000004 1234321012343210 000060800002a9c2 abcdefabcdefabcd

析构函数被调用了,但是弱引用并没有被赋nil,这跟我们印象中的weak运行过程有出入,我们接着访问一下该值,看是否会产生crash

1
2
3
4
5
6
7
8
9
10
let referer = WeakReferer()
let refererDump = dumperFunc(referer)
print(refererDump())
do {
let target = WeakTarget()
referer.target = target
print(refererDump())
}
print(refererDump())
print(referer.target)
1
2
3
4
5
WeakReferer 0x00007ff7aa20d060: 00000001047a04a0 0000000200000004 1234321012343210 0000000000000000 abcdefabcdefabcd
WeakReferer 0x00007ff7aa20d060: 00000001047a04a0 0000000200000004 1234321012343210 00007ff7aa2157f0 abcdefabcdefabcd
WeakTarget deinit
WeakReferer 0x00007ff7aa20d060: 00000001047a04a0 0000000200000004 1234321012343210 00007ff7aa2157f0 abcdefabcdefabcd
nil

并没有产生crash,打印结果为nil
让我们再仔细的分析一下,首先我们先给WeakTarget对象加上一个dummy属性,dump的时候能更方便的查看内存内容:

1
2
3
4
5
6
7
class WeakTarget {
var dummy = 0x0123456789abcdef

deinit {
print("Weak target deinit")
}
}

接下来,我们将使用新的代码执行相同的过程并dump出每一步的对象内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let referer = WeakReferer()
let refererDump = dumperFunc(referer)
print(refererDump())
let targetDump: (Void) -> String
do {
let target = WeakTarget()
targetDump = dumperFunc(target)
print(targetDump())

referer.target = target

print(refererDump())
print(targetDump())
}
print(refererDump())
print(targetDump())
print(referer.target)
print(refererDump())
print(targetDump())

我们一个一个看一下输出的内容。一开始,target属性为nil:

1
SwiftLearn.WeakReferer 0x0000608000243450: 000000010cec4c58 0000000200000004 1234321012343210 0000000000000000 abcdefabcdefabcd

target属性设置一个对象实例,target对象实例的内容为:

1
SwiftLearn.WeakTarget 0x00006080000357e0: 000000010cec4d48 0000000200000004 0123456789abcdef

将对象实例赋给target属性,我们能够看到weak属性已经被赋值了,赋的值为target对象地址+2字节,既对于weak指针,它并不直接指向对象的地址,而是指向对象的side table(下文会讲到side table的概念),unowned``strong引用会直接指向对象:

1
SwiftLearn.WeakReferer 0x0000608000243450: 000000010cec4c58 0000000200000004 1234321012343210 00006080000357e2 abcdefabcdefabcd

target对象的内容块中有一个字段自增了2:

1
SwiftLearn.WeakTarget 0x00006080000357e0: 000000010cec4d48 0000000400000004 0123456789abcdef

target被析构:

1
WeakTarget deinit

我们看到引用的对象依然保持着target的指针:

1
SwiftLearn.WeakReferer 0x0000608000243450: 000000010cec4c58 0000000200000004 1234321012343210 00006080000357e2 abcdefabcdefabcd

看上去好像target依然还存活着,我们看到target对象的有一个字段减了2:

1
SwiftLearn.WeakTarget 0x00006080000357e0: 000000010cec4d48 0000000200000002 0123456789abcdef

访问一下target属性,此时会产生nil,尽管内容中的指针并没有被赋nil:

1
nil

我们再打印一下referer对象的内容,发现访问完target属性后,target字段被修改了,赋为了nil

1
SwiftLearn.WeakReferer 0x0000608000243450: 000000010cec4c58 0000000200000004 1234321012343210 0000000000000000 abcdefabcdefabcd

target对象现在完全被清除了:

1
SwiftLearn.WeakTarget 0x00006080000357e0: 000000010cec3370 0000000200000008 000000010cebe200

从上面的运行过程,我们发现某些字段会被增/减,我们进行一些测试,来看一下有没有规律:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
let target = WeakTarget()
let targetDump = dumperFunc(target)
do {
print(targetDump())
weak var a = target
print(targetDump())
weak var b = target
print(targetDump())
weak var c = target
print(targetDump())
weak var d = target
print(targetDump())
weak var e = target
print(targetDump())

var f = target
print(targetDump())
var g = target
print(targetDump())
var h = target
print(targetDump())
var i = target
print(targetDump())
var j = target
print(targetDump())
var k = target
print(targetDump())
}
print(targetDump())

打印结果为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
SwiftLearn.WeakTarget 0x00006080000395a0: 000000010e8aad40 0000000200000004 0123456789abcdef
SwiftLearn.WeakTarget 0x00006080000395a0: 000000010e8aad40 0000000400000004 0123456789abcdef
SwiftLearn.WeakTarget 0x00006080000395a0: 000000010e8aad40 0000000600000004 0123456789abcdef
SwiftLearn.WeakTarget 0x00006080000395a0: 000000010e8aad40 0000000800000004 0123456789abcdef
SwiftLearn.WeakTarget 0x00006080000395a0: 000000010e8aad40 0000000a00000004 0123456789abcdef
SwiftLearn.WeakTarget 0x00006080000395a0: 000000010e8aad40 0000000c00000004 0123456789abcdef
SwiftLearn.WeakTarget 0x00006080000395a0: 000000010e8aad40 0000000c00000008 0123456789abcdef
SwiftLearn.WeakTarget 0x00006080000395a0: 000000010e8aad40 0000000c0000000c 0123456789abcdef
SwiftLearn.WeakTarget 0x00006080000395a0: 000000010e8aad40 0000000c00000010 0123456789abcdef
SwiftLearn.WeakTarget 0x00006080000395a0: 000000010e8aad40 0000000c00000014 0123456789abcdef
SwiftLearn.WeakTarget 0x00006080000395a0: 000000010e8aad40 0000000c00000018 0123456789abcdef
SwiftLearn.WeakTarget 0x00006080000395a0: 000000010e8aad40 0000000c0000001c 0123456789abcdef
SwiftLearn.WeakTarget 0x00006080000395a0: 000000010e8aad40 0000000200000004 0123456789abcdef
WeakTarget deinit

从结果,我们能够看到,对于每一个weak引用,第2个块中的第一个数会自增2;而每一个strong引用,第二个数会自增4.

总结一下:

  • 弱指针看起来就是普通的指针。
  • 当一个弱引用对象的deinit执行后,对象并没有被释放,且弱引用指针也没有被赋nil
  • 当弱引用执行完deinit后,访问弱引用对象,则对象指针会被赋nil,且目标对象被释放。
  • 弱引用对象对于每一个弱引用会包含一个引用计数(unowned计数和weak计数为同一个),且与强引用计数分开统计。

Swift代码


接下来,我们看一下Swift实现的源代码。
Swift标准库表示一个在堆上的对象的结构体为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/// The Swift3 heap-object header.
struct HeapObject {
/// This is always a valid pointer to a metadata object.
HeapMetadata const *metadata;

SWIFT_HEAPOBJECT_NON_OBJC_MEMBERS;
// FIXME: allocate two words of metadata on 32-bit platforms

#ifdef __cplusplus
HeapObject() = default;

// Initialize a HeapObject header as appropriate for a newly-allocated object.
constexpr HeapObject(HeapMetadata const *newMetadata)
: metadata(newMetadata)
, refCounts(InlineRefCounts::Initialized)
{ }
#endif
};

Swiftmetadata字段等同于Objective-Cisa字段,事实上,他们是兼容的。接着,使用了一个宏来定义字段,该字段用来管理引用计数:

1
2
3
4
///Swift默认为InlineRefCounts,当有弱引用指向该对象时,InlineRefCounts会变为SideTableRefCounts

#define SWIFT_HEAPOBJECT_NON_OBJC_MEMBERS \
InlineRefCounts refCounts

Swift增加引用计数的方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
// Increment the reference count.
void increment(uint32_t inc = 1) {
auto oldbits = refCounts.load(SWIFT_MEMORY_ORDER_CONSUME);
RefCountBits newbits;
do {
newbits = oldbits;
bool fast = newbits.incrementStrongExtraRefCount(inc);
if (!fast)
return incrementSlow(oldbits, inc);
} while (!refCounts.compare_exchange_weak(oldbits, newbits,
std::memory_order_relaxed));
}

由于引用计数的管理有两种类型InlineRefCountsSideTableRefCounts,当对象只包含strongunowned引用时,使用InlineRefCounts进行计数管理,如果对象拥有了weak引用,则会使用SideTableRefCounts来管理计数。所以如上增加引用计数的函数,会考虑两种情况,fast对应InlineRefCountsslow对应
SideTableRefCounts,为了避免竞态条件,使用了compare_exchange_weak来进行赋值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
HeapObject {
isa
InlineRefCounts {
atomic<InlineRefCountBits> {
strong RC + unowned RC + flags
OR
HeapObjectSideTableEntry*
}
}
}

HeapObjectSideTableEntry {
SideTableRefCounts {
object pointer
atomic<SideTableRefCountBits> {
strong RC + unowned RC + weak RC + flags
}
}
}

接下来,看一下weak引用自减计数的函数操作过程,函数内调用decrementWeakShouldCleanUp来进行位数的操作,其返回一个bool值,既当weakstrong unowned计数都变为0时,bool值返回true,说明可以收回内存了,既调用delete释放内存。

1
2
3
4
5
6
7
8
9
10
11
12
13
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;
}

此时,我们应该就比较清楚了,即使strongunowned的计数为0,如果还存在weak弱引用,
那么对象也不会被释放。

接下来,我们可以看一下加载弱引用的过程,Swift通过HeapObject *swift::swift_weakTakeStrong(WeakReference *ref)函数来实现,该函数通过间接调用,最终调用nativeTakeStrongFromBits函数,该函数内部首先会调用getNativeOrNull方法,该方法会从对象的side table中查询对象的计数,当没有strong引用时,说明该对象已经处于DEINITING状态,函数会返回nullptr,否则将调用tryRetain函数来尝试strong对象。

1
2
3
4
5
6
7
8
9
HeapObject *nativeTakeStrongFromBits(WeakReferenceBits bits) {
auto side = bits.getNativeOrNull();
if (side) {
side->decrementWeak();
return side->tryRetain();
} else {
return nullptr;
}
}

当对象实例的deinit()方法被调用时,内部会调用swift_deallocObject函数,而它会通过调用canBeFreedNow函数来判断是否需要释放内存,既满足没有side tableunowned引用为1,strong引用计数为0。

1
2
3
4
5
6
7
bool canBeFreedNow() const {
auto bits = refCounts.load(SWIFT_MEMORY_ORDER_CONSUME);
return (!bits.hasSideTable() &&
bits.getIsDeiniting() &&
bits.getStrongExtraRefCount() == 0 &&
bits.getUnownedRefCount() == 1);
}

综上,如果还存在weak
弱引用,那么肯定还有side table表,即使没有strong引用,也不会被释放。

总结


  1. 弱引用指向对象实例的side table地址。
  2. Objective-C管理引用计数的方式不同,Swift的弱引用计数与strong计数一起管理。
  3. Swift针对对象的析构和对象的释放进行了解耦,一个对象被析构后,会释放它的外部资源,但是有可能不会释放对象本身的内存。
  4. Swift对象的strong引用计数变为0但是weak计数大于0时,对象会被析构但是不会被释放内存。
  5. 当加载一个弱引用时,运行时会检查target的状态
    ,如果target已经是僵尸对象,那么会赋空weak引用,weak计数减一,并返回nil,这个过程是安全的,当weak引用计数变为0时,僵尸对象内存将被释放。

最后总结一下SwiftObjective-C的区别:

  • Swift不需要维护weak列表,这可以简化代码和提升性能。
  • 对于Swiftweak引用,实例对象会在strong引用计数变为0时,内存依然保留,直到所有的weak引用离开作用域。不过这个影响是很小的,因为虽然对象分配的内存依然保留,但是它所有的外部资源(如ArrayDictionary属性)会在strong引用计数变为0时被释放。
  • 由于Swiftweak创建后,对象的引用计数管理会从InlineRefCounts替换为SideTableRefCounts,这也会带来一定的开销,所以如果可以,尽量使用unowned,unowned有点类似于Objective-C__unsafe_unretained,如果unowned指向僵尸对象后再访问,会产生未定义行为。

附录


  1. https://github.com/apple/swift/blob/7913e9821b814956d243e4e03cfe9ddc0e325bc2/stdlib/public/SwiftShims/HeapObject.h
  2. https://github.com/apple/swift/blob/860252fab41392b7de3218e58f7542cb1dc1ce16/stdlib/public/runtime/WeakReference.h
  3. https://github.com/apple/swift/blob/b7d78853112c1279fc7bc5b85853779040f13703/stdlib/public/SwiftShims/RefCount.h
  4. Swift对象生命周期状态机
  5. https://www.mikeash.com/pyblog/friday-qa-2015-12-11-swift-weak-references.html