Swift 指针&内存管理
指针
为什么说指针不安全
-
比如我们在创建一个对象的时候,是需要在堆分配内存空间的。但是这个内存空间的声明周期是有限的,也就意味着如果我们使用指针指向这块内容空间,如果当前内存空间的生命周期到了(引用计数为0),系统就会对当前的内存空间进行回收,但是指针的指向并没有修改,那么这种指针就是野指针,所以当前的指针就变成了未定义的行为 。
-
我们创建的内存空间是有边界的,比如我们创建一个大小为 10 的数组,这个时候我们通过指针访问到了
index=11
的位置,这个时候是不是就越界了 访问了一个未知的内存空间。 -
指针类型与内存的值类型不一致,也是不安全的,例如内存里面存储的是
Int
类型,但是指针是Int8
类型的,这种情况下就会照成精度的缺失。
指针类型
Swift
中的指针分为两类,typedpointer
指定数据类型指针,raw pointer
未指定数据类型的指
针(原生指针)。基本上我们接触到的指针类型有以下几种:
原始指针的使用
我们一起来看一下如何使用 RawPointer
来存储 4 个整形的数据,这里我们需要选取的是 UnsafeMutableRawPointer
。
struct CXPerson {
var age: Int = 18
}
//结构体的真实大小
print(MemoryLayout<CXPerson>.size)
//结构体的步长,存储一个结构体实例要跨越的真实内存长度,结构体遵循 8 字节对齐
print(MemoryLayout<CXPerson>.stride)
//结构体对齐的字节数
print(MemoryLayout<CXPerson>.alignment)
泛型指针的使用
这里的泛型指针相比较原生指针来说,其实就是指定当前指针已经绑定到了具体的类型。同样的,我们还是通过一个例子来解释一下。
在进行泛型指针访问的过程中,我们并不是使用 loaad
和 store
方法来进行存储操作。这里我们使用到当前泛型指针内置的变量 pointee
。获取 UnsafePointer
的方式有两种,下面我们来分别介绍一下。
通过已有变量获取
如上图所示,我们可以通过获取已有变量来修改变量 age
的值。只是方式 1 跟 方式 2 是获取到的指针跟指针指向的内容都是不可变的,所以没法直接修改 ptr.pointee
,但是方式 3 获取到的指针跟指针指向的内容都是可变的,所以可以直接修改 ptr.pointee
。
直接分配内存
// 开辟一块可以连续存储 5 个 CXPerson 类型大小的内存,并返回地址 tptr
let tptr = UnsafeMutablePointer<CXPerson>.allocate(capacity: 5)
//tptr 为起始地址,可以通过下标的方式存储 CXPerson 实例
tptr[0] = CXPerson(age: 10, height: 110)
tptr[1] = CXPerson(age: 10, height: 110)
//也可以通过 initialize 存储
tptr.advanced(by: 2 * MemoryLayout<CXPerson>.stride).initialize(to: CXPerson(age: 10, height: 110))
// deinitialize 与 deallocate 是成对出现的,dein itialize 可以理解为将数据清 0
tptr.deinitialize(count: 5)
// 回收这块内存空间
tptr.deallocate()
内存绑定
swift
提供了三种不同的 API
来绑定/重绑定指针:
assumingMemoryBound(to:)
这里我们定义了一个 testPoint
函数,需要传入一个 UnsafePointer<Int>
类型的指针参数,当我们调用 testPoint
函数并直接传入 tuplePtr
会报错,这里元组是值类型,本质上这块内存空间中存放的就是 Int
类型的数据,这时候 UnsafePointer<Int>
与 UnsafePointer<(Int, Int)>
本质上是一样的,都是指向 tuple
的首地址,所以这个时候我们就可以使用 assumingMemoryBound(to:)
进行转换。
在我们处理代码的过程中,如果只有原始指针(没有保留指针类型),但是我们明确知道指针类型的情况下,我们就可以使用 assumingMemoryBound(to:)
来告诉编译器预期的类型(注意:这里只是让编译器绕过类型检查,并没有发生实际的类型转换),但是在此之前我们需要先使用 UnsafeRawPointer
将 tuplePtr
转换成原生指针,这个时候就不会报错了。
bindMemory(to:capacity:)
使用 bindMemory(to:capacity:)
可以用于更改内存绑定的类型,如果当前内存还没有类型绑定,则将首次绑定为该类型;否则重新绑定该类型,并且内存中所有的值都会变成该类型。
withMemoryRebound(to:capacity:)
func testPoint(_ p: UnsafePointer<Int8>) {
print(p)
}
let Uint8Ptr = UnsafePointer<UInt8>.init(bitPattern: 10)
Uint8Ptr?.withMemoryRebound(to: Int8.self, capacity: 1){ (int8Ptr: UnsafePointer<Int8>) in
testPoint(int8Ptr)
}
当我们在给外部函数传递参数时,不免会有一些数据类型上的差距。如果我们进行类型转换,必然要来回复制数据,这个时候我们可以使用 withMemoryRebound(to:capacity:)
来临时更改内存绑定类型,可以减少代码的复杂度。
内存管理
在 swift
中也是一样,是使用自动引用计数(ARC
)机制来追踪和管理内存。
在对象的内存布局中,其中的 8 个字节是用来存储当前的引用计数的,但是我们通过打印并不能看出具体的引用计数,所以下面我们通过源码来看一下。
首先我们先找到 refCount
的定义,这里我们在 HeapObject.h
文件中搜索:
接着我们追踪 InlineRefCounts
:
这里可以看到 RefCounts
是一个模板类,接收一个泛型参数 InlineRefCountBits
,本质上 RefCounts
在操作 API
的时候其实操作的都是传进来的泛型参数 RefCountBits
,所以 RefCounts
本质上是对引用计数的包装,而引用计数的具体类型取决于传入的泛型参数 InlineRefCountBits
。所以我们追踪 InlineRefCountBits
:
这里可以看到 InlineRefCountBits
是一个模板函数 RefCountBitsT
,传递的参数 RefCountIsInline
要么是 true
要么是 false
, 所以我们引用计数真实操作的类就是 RefCountBitsT
,所以我们追踪 RefCountBitsT
,看一下 RefCountBitsT
中有什么属性信息。
这里我们可以看到就一个属性信息 bits
,但是 bits
是由 RefCountBitsInt
中的 Type
属性来定义的,所以我们追踪 RefCountBitsInt
可以看到 Type
是一个 uint64_t
的位域信息,所以到这里我们可以看到,swift
中引用计数跟 oc
中引用计数都是一个 64 位的位域信息,在里面存储了跟生命周期相关的引用计数。
当我们创建一个实例对象的时候,当前的引用计数是多少呢?这里我们也找到源代码看一下。
在源代码中可以看到初始化方法,追踪初始化方法可以看到初始化赋值,然后追踪 Initialized
的定义,可以看到 Initialized_t
是一个枚举类型, 依据枚举类型可以找到 RefCountBits(0, 1)
,这里 RefCountBits(0, 1)
就是我们上面讲的 RefCountBitsT
,所以我们找到 RefCountBitsT
类,来看一下它的初始化方法:
通过上面的源码追踪我们知道 strongExtraCount
为 0,unownedCount
为 1,在初始化方法中 strongExtraCount
左移了 StrongExtraRefCountShift
位, unownedCount
左移了 UnownedRefCountShift
位。所以在这个过程当中其实就是把强引用计数跟无主引用计数通过位移的方式存储到了 64 位的信息当中。这里 StrongExtraRefCountShift
是 33,UnownedRefCountShift
是 1。
下面我们对代码进行一些修改,来看一下强引用计数的值:
这里可以看到把 p
赋值给 p1
之后强引用计数为 1,赋值给 p2
之后强引用计数为 2。这里大家可能会有疑问,强引用是怎么添加的,这里一样,我们也通过源码来看一下。
通过代码追踪,我们可以看到强引用的时候函数调用流程是 _swift_retain_
-> increment
-> incrementStrongExtraRefCount
,在 incrementStrongExtraRefCount
函数中会对原有的引用计数进行左移 33 位的操作。
循环引用
以上我们了解了强引用的底层实现,但是使用强引用的时候也会遇到循环引用的问题,这里我们来看一个经典的循环引用的案例:
class CXTeacher {
var age: Int = 18
var name: String = "chenxi"
var subject: CXSubject?
}
class CXSubject {
var subjectName: String
var subjectTeacher: CXTeacher
init(_ subjectName: String, _ subjectTeacher: CXTeacher) {
self.subjectName = subjectName
self.subjectTeacher = subjectTeacher
}
}
var t = CXTeacher()
var subject = CXSubject("swift", t)
t.subject = subject
这里我们定义了一个老师的类 CXTeacher
跟一个代表科目的类 CXSubject
,CXTeacher
中包含科目 subject
,而 CXSubject
中又包含 subjectTeacher
,这样就造成了循环引用。Swift
中提供了两种方式来解决这个问题:弱引用(weak reference
) 和无主引用(unowned reference
)。
弱引用
弱引用不会对其引用的实例保持强引用,因而不会阻上 ARC
释放被引用的实例。这个特性阻止了引用变为循环强引用。声明属性或者变量时,在前面加上 weak
关键字表明这是一个弱引用。
由于弱引用不会强保持对实例的引用,所以说实例被采释放了弱引用仍旧引用着这个实例也是有可能的。因此,ARC
会在被引用的实例被释放是自动地设置弱引用为 nil
。由于弱引用需要允许它们的值为 nil
,它们一定得是可选类型。
通过以上代码我们可以看到,weak
修饰的 t
是一个可选类型,而且通过汇编调试的时候可以看到会调用 swift_weakInit
函数,下面我们到源码中具体来看一下。
WeakReference *swift::swift_weakInit(WeakReference *ref, HeapObject *value) {
ref->nativeInit(value);
return ref;
}
声明一个 weak
变量相当于定义了一个 WeakReference
对象。
void nativeInit(HeapObject *object) {
auto side = object ? object->refCounts.formWeakReference() : nullptr;
nativeValue.store(WeakReferenceBits(side), std::memory_order_relaxed);
}
HeapObjectSideTableEntry* RefCounts<InlineRefCountBits>::formWeakReference()
{
auto side = allocateSideTable(true);
if (side)
return side->incrementWeak();
else
return nullptr;
}
nativeInit
中 object->refCounts
调用了 formWeakReference
,而在 formWeakReference
中其实就是创建了一个散列表,所以我们继续追踪以下散列表的创建方法 allocateSideTable
。
HeapObjectSideTableEntry* RefCounts<InlineRefCountBits>::allocateSideTable(bool failIfDeiniting)
{
// 取出原有的 refCounts
auto oldbits = refCounts.load(SWIFT_MEMORY_ORDER_CONSUME);
// 判断 refCounts 是否有引用计数
if (oldbits.hasSideTable()) {
// 如果有就获取引用计数
return oldbits.getSideTable();
}
else if (failIfDeiniting && oldbits.getIsDeiniting()) {
// 如果没有或者正在析构直接返回 nullptr
return nullptr;
}
// 如果没有 SideTable这里需要创建一个新的 HeapObjectSideTableEntry 类型的 side
HeapObjectSideTableEntry *side = new HeapObjectSideTableEntry(getHeapObject());
// 将 side 传入 InlineRefCountBits 中来做初始化操作
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);
} while (! refCounts.compare_exchange_weak(oldbits, newbits,
std::memory_order_release,
std::memory_order_relaxed));
return side;
}
这里我们继续追踪 HeapObjectSideTableEntry
:
通过源码注释可以看到 swift
中存在两种引用计数, 如果只有强引用就是 InlineRefCounts
,包含了 strong RC + unowned RC + flags
,如果存在弱引用就是 HeapObjectSideTableEntry
,包含了 strong RC + unowned RC + weak RC + flags
,就是原先的 64 为信息(strong RC + unowned RC + flags
)上加上一个 32 位的弱引用 信息(weak RC
)。这里 InlineRefCounts
与 HeapObjectSideTableEntry
都是共用一个模板类 RefCountBitsT
。
class HeapObjectSideTableEntry {
// FIXME: does object need to be atomic?
std::atomic<HeapObject*> object;
SideTableRefCounts refCounts;
public:
HeapObjectSideTableEntry(HeapObject *newObject)
: object(newObject), refCounts()
{ }
}
追踪 HeapObjectSideTableEntry
类可以看到 HeapObjectSideTableEntry
中存储了实例对象 object
跟 refCounts
,只是这里 refCounts
是 SideTableRefCounts
类,所以继续追踪 SideTableRefCounts
。
typedef RefCounts<InlineRefCountBits> InlineRefCounts;
typedef RefCounts<SideTableRefCountBits> SideTableRefCounts;
class alignas(sizeof(void*) * 2) SideTableRefCountBits : public RefCountBitsT<RefCountNotInline>
{
uint32_t weakBits;
public:
SWIFT_ALWAYS_INLINE
SideTableRefCountBits() = default;
SWIFT_ALWAYS_INLINE
constexpr SideTableRefCountBits(uint32_t strongExtraCount,
uint32_t unownedCount)
: RefCountBitsT<RefCountNotInline>(strongExtraCount, unownedCount)
// weak refcount starts at 1 on behalf of the unowned count
,
weakBits(1) {}
这里就验证了我们上面的结论 SideTableRefCountBits
与 InlineRefCountBits
一样, 都是共用一个模板类 RefCountBitsT
,所以 SideTableRefCountBits
会继承 RefCountBitsT
的 64 位信息,而且出了 64 位信息之外 SideTableRefCountBits
又多了 32 位的 weakBits
。
无主引用
无主引用与弱引用最大的区别就是 unowned
修饰的变量 t
假定是永远有值的,不是可选类型,所以相对于 weak
,unowned
不够那么安全, 因为如果 t
变为了 nil
程序肯定就会崩溃了。 对于 weak
与 unowned
使用区别总结如下。
-
weak
:如果需要互相引用的实例,生命周期没有任何关联可以使用weak
,例如delegate
。 -
unowned
:如果能确定互相引用的实例,其中一个实例销毁另一个实例也跟着销毁可以使用unowned
,例如我们上面讲的老师跟课程的例子就可以使用unowned
。从性能上来讲unowned
性能能好,因为不用额外创建散列表,直接操作 64 位的信息就行。
class CXTeacher {
var age: Int = 18
var name: String = "chenxi"
unowned var subject: CXSubject?
}
class CXSubject {
var subjectName: String
var subjectTeacher: CXTeacher
init(_ subjectName: String, _ subjectTeacher: CXTeacher) {
self.subjectName = subjectName
self.subjectTeacher = subjectTeacher
}
}
闭包循环引用
闭包一般默认会捕获外部变量,通过上面我们也可以看出,在闭包中对 age
进行加 1,打印的时候 age
变为了 19,闭包对变量的修改将会改变外部原始变量的值。
那么这样的话就会有一个问题,如果我们在 class
的内部定义一个闭包,当前闭包访问属性过程中就会对我们当前实例对象进行捕获。
如上案例中我们在 CXTeacher
类中定义了一个闭包属性 testClosure
,在 testARC
执行闭包,并对 t
的 age
属性进行加 1,这里可以看到 deinit
并没有被调用,这是因为 CXTeacher
与 闭包属性 testClosure
产生了循环因为。这里我们可以使用捕获列表来解决这个问题。
这里我们可以看到 deinit
函数执行了,在这里 [unowned t]
就是捕获列表,那么什么是捕获列表呢?
捕获列表
默认情况下,闭包表达式从其周围的范围捕获常量和变量,并强引用这些值。您可以使用捕获列表来显式控制如何在闭包中捕获值。
在参数列表之前,捕获列表被写为用逗号括起来的表达式列表,并用方括号括起来。如果使用捕获列表,则即使省略参数名称,参数类型和返回类型,也必须使用 in
关键字。
创建闭包时,将初始化捕获列表中的条目。对于捕获死列表中的每个条目,将常量初始化为在周围范围内具有相同名称的常量或变量的值。例如,在面上面的代妈中,捕获列表中包含 age
,但捕获列表中未包含 height
,这使它们具有不同的行为。
创建闭包时,内部作用域中的age会用外部作用域中的 age
的值进行初始化,但它们的值未以任何特殊方式连接。这意味着更改外部作用域中的a的值不会景影响内部作用域中的 age
的值,也不会更改封闭内部的值,也不会影响封闭外部的值。相比之下,只有一个名为 height
的变量,外部作用域中的 height
,因此,在闭包内部或外部进行的更改在两个地方均可见。
__weak __typeof__(self) weakSelf = self;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
__strong __typeof(self) strongSelf = weakSelf;
});
例如以上代码,在 OC
的 block
中我们会用 __strong __typeof(self) strongSelf = weakSelf
来延长 weakSelf
的生命周期,__strong
确保在 Block
内,strongSelf
不会被释放。那么在 swift
中我们可以用如下两种方式实现类似功能。
- 方式 1
func testARC() {
var t = CXTeacher()
t.testClosure = { [weak t] in
if let strongT = t {
print(strongT.age)
}
}
print("end")
}
- 方式 2
func testARC() {
var t = CXTeacher()
t.testClosure = { [weak t] in
withExtendedLifetime(t) { //延长t的生命周期在这个闭包表达式范围内
print(t!.age)
}
}
print("end")
}