类与结构体(上)中我们得知,对象的前的本质是HeapObject结构,前16个字节存放的是 metadata 和 refcount,在Swift中就是使用refcount来追踪和管理内存。
1、强引用
当实例对象被一个强指针指向的时候就会产生一个强引用,会使对象引用计数加1,接下来通过下面的例子来窥探引用计数的本质
通过x/4gx 打印出
sg指针的内存结构,每次赋值打印一次,分别打印三次,对应赋值sg1之前,sg2之前,sg2之后的结果
如图,在引用到sg2的过程中,refCounts 值的变化为 0x0000000000000003 -> 0x0000000200000003 -> 0x0000000400000003,在第一次初始化的时候引用计数并不是1,被强引用的时候单纯的在refcount的那8个字节中累加,那么这是为什么呢?让我们通过源码来探究一下。
1.1 引用计数的本质
通过 HeapObject 找到 refcount成员是InlineRefCounts类型
进入
InlineRefCounts,是RefCounts的别名
在
RefCounts 中定义了一个RefCountBits的泛型 ,这里也就是上面别名中传入的InlineRefCountBits类型
InlineRefCountBits 这个类型又是 RefCountBitsT<RefCountIsInline> 的别名
进入
RefCountBitsT 类,这里只有一个属性,就是 BitsType bits ,并且它是用 RefCountBitsInt的Type属性来定义,所以refcount的本质其实就是 RefCountBitsInt的Type 的结构
可以看到RefCountBitsInt是一个UInt64位的位域信息,对于Swift的引用计数还是OC里面的引用计数都是一个64位的位域信息。
从结构上还是没办法知道为什么初始化的时候的值不是 0x0000000000000002 而是 0x0000000000000003,那么就从实例对象的初始化过程来分析
1.2 引用计数的增加
我们可以通过源码看一下,全局搜索 _swift_retain_,在 HeapObject.cpp 文件中找到它的实现,如下:
在进行强引用的时候,本质上是调用
refCounts 的 increment 方法,也就是引用计数 +1。我们来看一下 increment 的实现:
看到关键的代码,在
increment 中调用了 incrementStrongExtraRefCount,我们再去看看 incrementStrongExtraRefCount 的实现:
注意看,在前面我们已经知道
StrongExtraRefCountShift = 33,外部传进来的 inc = 1。假设此时 bits = 0,那此时就是 1 << 33 = 0x2。如果还有变量对其进行强引用,就是 1 += 1 << 33 --> 2 << 33 = 0x4。
到这里, incrementStrongExtraRefCount 的实现就对应了前面讲的赋值 sg1,sg2 后,refCounts 高 32 位开始的变化。
2、弱引用
弱引用不会对实例保持强引用,因此ARC可以释放被引用的实例。声明属性或者变量时,在前面加上weak关键字即可生成一个弱引用。 由于弱引用不会强保持对实例的引用,所以说实例被释放了弱引用仍旧引用着这个实例也是有可能的。因此,ARC会在被释放实例时自动地设置为nil,所以若引用的变量一定得是可选类型。
由上面的结果看出,在弱引用后,引用计数出现了很大的变化,这是怎么产生的呢?
通过汇编,我们可以看到,用
weak 修饰之后,sg 变成了一个可选项,并且,之后会调用一个 swift_weakInit 函数,紧接着调用 swift_release 函数,将 sg 的实例释放掉了。
我们来看一下 swift_weakInit 函数在源码中是怎么实现的,在 HeapObject.cpp 文件中,swift_weakInit 的实现如下
...
WeakReference *swift::swift_weakInit(WeakReference *ref, HeapObject *value) {
ref->nativeInit(value);
return ref;
}
...
通过源码,可以知道用 weak 修饰之后,在内部会生成 WeakReference 类型的变量,并在 swift_weakInit 中调用 nativeInit 函数。nativeInit 的实现如下:
void nativeInit(HeapObject *object) {
auto side = object ? object->refCounts.formWeakReference() : nullptr;
nativeValue.store(WeakReferenceBits(side), std::memory_order_relaxed);
}
在这里,它调用了 refCounts 的 formWeakReference 函数,形成了弱引用,我们再来看一下 formWeakReference 的实现:
// SideTableRefCountBits specialization intentionally does not exist.
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)
{
// 1. 取出原来的 refCounts-引用计数的信息
auto oldbits = refCounts.load(SWIFT_MEMORY_ORDER_CONSUME);
// Preflight failures before allocating a new side table.
// 2. 判断原来的 refCounts 是否有当前的引用计数
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.
// 如果没有并且正在析构直接返回 nil
return nullptr;
}
// Preflight passed. Allocate a side table.
// FIXME: custom side table allocator
// 3. 创建一个散列表
HeapObjectSideTableEntry *side = new HeapObjectSideTableEntry(getHeapObject());
auto newbits = InlineRefCountBits(side);
// 4. 对原来的散列表以及正在析构的一些处理
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);
} while (! refCounts.compare_exchange_weak(oldbits, newbits, std::memory_order_release, std::memory_order_relaxed));
return side;
}
散列表的创建可分为 4 步:
- 取出原来的
refCounts引用计数的信息 - 判断原来的
refCounts是否有散列表,如果有直接返回,如果没有并且正在析构直接返回 nil - 创建一个散列表
- 对原来的散列表以及正在析构的一些处理
3、无主引用
无主引用和弱引用类似,区别是无主引用会牢牢保持对实例地址的引用,实例被销毁时无主引用不会被置为nil,所以就会造成野指针,总之,无主引用假定是永远有值的。
根据苹果的官方文档的建议。当我们知道两个对象的生命周期并不相关,但是不会去管当前引用的地址是否有值,那么我们必须使用weak。相反,非强引用对象拥有和强引用对象同样或者更长的生命周期的话,则应该使用unowned。
...
4、闭包的循环引用
如果我们在class的内部定义一个闭包,当前闭包访问属性的过程中,就会对我们当前的实例对象进行捕获:
class Swagger {
var a = 18
deinit{
print("deinit")
}
}
var sg: Swagger? = Swagger()
var closure = {
sg!.a = 20
}
这样就是造成循环引用,程序结束并不会打印deinit
如何解决循环引用
- 使用weak修饰闭包传入的参数,其中参数的类型是optional
var closure = { [weak sg] in
sg!.a = 20
}
- 使用unowned修饰闭包参数,与weak的区别在于unowned不允许被设置为nil,即总是假定有值的
var closure = { [unowned sg] in
sg!.a = 20
}
捕获列表
默认情况下,闭包表达式从其周围的范围捕获常量和变量,并强引用这些值。你可以使用捕获列表来显示控制如何在闭包中捕获值。在参数列表之前,捕获列表被写为用逗号括起来的表达式列表,并用方括号括起来。如果使用捕获列表,则即使省略参数名称,参数类型和返回类型,也必须使用in关键词。
对于捕获列表中的每个常量,闭包会利用周围范围内具有相同名称的常量/变量,来初始化捕获列表中定义的常量。有以下几点说明:
- 捕获列表中的常量是值拷贝,而不是引用
- 捕获列表中的常量的相当于复制了变量age的值
- 捕获列表中的常量是只读的,即不可修改
创建闭包时,内部作用域中的age会用外部作用域中age的值进行初始化,但它们的值未以任何特殊方式连接。这意味着更改外部作用域中的a的值不会影响内部作用域中age的值,也不会更改封闭内部的值,也不会更改封闭外部的值。相比之下,只有一个名为height的变量,外部作用域中的height,在闭包内部或者外部进行的更改在两个地方均可见。
func test(){
var age = 10
var height = 100
var closure = { [age] in
print(age)
print(height)
}
age = 18
height = 180
closure()
}
test()
//10
//180