一、指针
指针的安全性
指针是不安全的。
- ⽐如我们在创建⼀个对象的时候,是需要在堆分配内存空间的。但是这个内存空间的声明周期是有 限的,也就意味着如果我们使⽤指针指向这块内容空间,如果当前内存空间的⽣命周期啊到了(引 ⽤计数为0),那么我们当前的指针是不是就变成了未定义的⾏为了。
- 我们创建的内存空间是有边界的,⽐如我们创建⼀个⼤⼩为10的数组,这个时候我们通过指针访问 到了 index = 11 的位置,这个时候是不是就越界了,访问了⼀个未知的内存空间。
- 指针类型与内存的值类型不⼀致,也是不安全的。
指针的类型
swift中指针分为2种,即类型指针(typeof pointor)、原生指针(row pointor)。
| 指针类型 | 说明 |
|---|---|
| UnsafeRawPointer | 不可变原生指针 |
| UnsafeMutableRawPointer | 可变原生指针 |
| UnsafePointer<'T'> | 不可变类型指针 |
| UnsafeMutablePointer<'T'> | 可变类型原生指针 |
| UnsafeRowBufferPointer | 不可变原生buffer指针 |
| UnsafeMutableRawBufferPointer | 可变原生buffer指针 |
| UnsafeBufferPointer<'T'> | 不可变类型buffer指针 |
| UnsafeMutableBufferPointer<'T'> | 可变类型buffer指针 |
注:可以看到所有的指针都是以Unsafe开头,对于安全性语言Swift来讲,用这种命名方式告诉我们指针式不安全的。一般情况下我们是用不到指针的,但涉及到和OC,C交互的时候才用到指针,但在学习过程中可以多了解一些,有助于我们对底层的理解。
通过一个小例子来看看指针的使用(talk is cheap, show me code)。
//用指针p,存0,1,2,3,4,并且取出来
func func1() {
//连续的32个字节的内存,用来存储4个int类型(每个int占8位),并且8字节对齐。
let p = UnsafeMutableRawPointer.allocate(byteCount: 32, alignment: 8)
//相当于一把尺子,去量Int的长度
let size = MemoryLayout<Int>.size
for i in 0..<4 {
// p.storeBytes(of: i, as: Int.self)
p.storeBytes(of: i, toByteOffset: i * size, as: Int.self)
}
for i in 0..<4 {
let number = p.load(fromByteOffset: i * size, as: Int.self)
print(number)
}
}
上面代码有用到MemoryLayout,这里大致讲下MemoryLayout的几种区别。
| 类型 | 备注 |
|---|---|
| MemoryLayout<‘T’>.size | 实际占用内存大小 |
| MemoryLayout<‘T’>.stride | 步长,相邻两个对象的实际距离 |
| MemoryLayout<‘T’>.alignmet | 对齐字节数 |
func func2() {
struct InlineStudent {
var name: Int
let sex: Bool
}
var size = MemoryLayout<InlineStudent>.size
var stride = MemoryLayout<InlineStudent>.stride
var alignment = MemoryLayout<InlineStudent>.alignment
print(size, stride, alignment)
size = MemoryLayout<Int>.size
stride = MemoryLayout<Int>.stride
alignment = MemoryLayout<Int>.alignment
print(size, stride, alignment)
}
打印结果为:
//InlineStudent 实际占用8+1个字节,每个对象之前的距离是16个字节,是按照8字节对齐的
9 16 8
//Int实际占用8个字节,每个Int之前距离是8个字节,8字节对齐
8 8 8
使用withUnsafePointer可以获取指针的地址。
func func3() {
var a = 10
withUnsafePointer(to: &a) { point in
print(point);
}
print(a);
}
打印为
0x000000016fb798c0
在view Memary中查找其内存
0xa正好是10.
可以用指针的pointee属性访问其内容,并且可以在闭包内返回值来修改。
/// Accesses the instance referenced by this pointer.
///
/// When reading from the `pointee` property, the instance referenced by
/// this pointer must already be initialized.
@inlinable public var pointee: Pointee { get }
func func3() {
var a = 10
withUnsafePointer(to: &a) { point in
print(point);
}
print(a);
a = withUnsafePointer(to: &a, { point in
return point.pointee + 5
})
print(a);
}
了解了指针,在swift中还可以这样定义数组,跟c语言非常相似
func func4() {
struct InFuncStudend {
var age: Int
var height: Double
}
let p = UnsafeMutablePointer<InFuncStudend>.allocate(capacity: 5);
p[0] = InFuncStudend(age: 18, height: 180)
p[1] = InFuncStudend(age: 18, height: 160)
p[2] = InFuncStudend(age: 18, height: 190)
p[3] = InFuncStudend(age: 17, height: 180)
let st4 = InFuncStudend(age: 15, height: 180)
p.advanced(by: 4).initialize(to: st4)
//deinitialize 和 deallocate 成对出现
p.deinitialize(count: 5)
p.deallocate()
}
利用指针我们可以进行swift-dump,这里有个别人写好的,可以学习借鉴。传送门
指针类型绑定
swift中提供了三个API来重新绑定指针。
| api | 作用对象 | 作用范围 |
|---|---|---|
| assumingMemoryBound | 原生指针 | 没有转化,只是为了通过编译器 |
| bindMemory | 原生指针 | 实质转化 |
| withMemoryRebound | 所有指针 | 实质转化,但作用域只限自己后面的小闭包内 |
func func5() {
func inFuncTest(_ p: UnsafePointer<Int>) {
print(p.pointee)
}
var tuple = (10, 20)
// assumingMemoryBound 绕过编译器检查,没有发生实质的转换
withUnsafePointer(to: tuple) { (point: UnsafePointer<(Int, Int)>) in
let p = UnsafeRawPointer(point).assumingMemoryBound(to: Int.self)
inFuncTest(p)
}
//使用结构体转还成Int
struct InFuncStudend {
var age: Int=
var height: Double
}
var st1 = InFuncStudend(age: 18, height: 180)
withUnsafePointer(to: st1) { (point: UnsafePointer<InFuncStudend>) in
let p = UnsafeRawPointer(point).assumingMemoryBound(to: Int.self)
inFuncTest(p)
}
//bindMemory会进行实质的转换
tuple.0 = 11
st1.age = 19
withUnsafePointer(to: tuple) { (point: UnsafePointer<(Int, Int)>) in
let p = UnsafeRawPointer(point).bindMemory(to: Int.self, capacity: 1)
inFuncTest(p)
}
withUnsafePointer(to: st1) { (point: UnsafePointer<InFuncStudend>) in
let p = UnsafeRawPointer(point).bindMemory(to: Int.self, capacity: 1)
inFuncTest(p)
}
// withMemoryRebound的类型转换作用范围只是在闭包内
withUnsafePointer(to: st1) { point in
point.withMemoryRebound(to: Int.self, capacity: 1) { (inPoint: UnsafePointer<Int>) in
inFuncTest(inPoint)
}
}
}
二、内存管理
Swift 中使⽤⾃动引⽤计数(ARC)机制来追踪和管理内存。
引用计数的存储
那么这个引用计数是存放在哪呢?我们回顾下之前的对象的结构HeapObject。
再看看那这个
InlineRefCounts的真面目。
可以看到
class RefCounts实际是对RefCountBits的包装。所以我们还是需要回到InlineRefCountsBits中一探究竟。
这个
InlineRefCountBits也是个模版类。继续看看RefCountBitsT。
可以看到有用的属性只有这个
bits,再继续看看这个RefCountBitsInt的定义。
这里面有3个定义,根据注释看,应该是根据CUP的位数有关,我们直接来看64位的。这个就是一个64位的位域信息。那么在一个对象刚被初始化的时候这个
refCounts的初始值是多少呢?继续看源码,来到对象的初始化方法里面。
这个
Initialized实际上是一个枚举值
这里的RefCountBits可以看到是一个定义的模版,而该模版在调用的时候实际上是
typedef RefCountBitsT<RefCountIsInline> InlineRefCountBits; 再继续看RefCountBitsT的构造方法。
我们继续看看位移位数的逻辑
点进
shiftAfterField
可以看到是个字符串拼接后累加的逻辑。根据以上可以得到这个64位的位域信息由5部分组成如下:
(图中strongRefCount的位置和UnownedRefCount的位置画反了)
注意其中IsImmortal// overlaps PureSwiftDealloc and UnownedRefCount``IsImmortal信息是与PureSwiftDealloc和UnownedRefCount的信息覆盖存储的。也就是说0-31位还是IsImmortal的信息。
我们可以通过代码来证实RefCount的StrongRefCount的结构。
func func6() {
let room = OSRoom("001", "卧室")
print(room)
//查看Refcount
let room1 = room
print(room1)
//查看Refcount
let room2 = room
print(room2)
//查看Refcount
}
在引用计数变化后,分别查看Refcount情况。
我们用0x0000000200000003、0x0000000400000003、0x0000000600000003右移33位就能得到强引用计数的值,分别是1、2、3.
swift_retain的过程
上面的代码在打开汇编调试断点的时候我们可以看到如下:
底层调用了
swift_retain来使引用计数+1.我们继续在与源码中看看swift_retain的实现。在HeapObject.cpp中看到这些关于retain的代码。
SWIFT_ALWAYS_INLINE
static HeapObject *_swift_retain_(HeapObject *object) {
SWIFT_RT_TRACK_INVOCATION(object, swift_retain);
if (isValidPointerForNativeRetain(object))
object->refCounts.increment(1);
return object;
}
HeapObject *swift::swift_retain(HeapObject *object) {
#ifdef SWIFT_STDLIB_SINGLE_THREADED_RUNTIME
return swift_nonatomic_retain(object);
#else
CALL_IMPL(swift_retain, (object));
#endif
}
SWIFT_RUNTIME_EXPORT
HeapObject *(*SWIFT_RT_DECLARE_ENTRY _swift_retain)(HeapObject *object) =
_swift_retain_;
HeapObject *swift::swift_nonatomic_retain(HeapObject *object) {
SWIFT_RT_TRACK_INVOCATION(object, swift_nonatomic_retain);
if (isValidPointerForNativeRetain(object))
object->refCounts.incrementNonAtomic(1);
return object;
}
会调用refCounts 的
refCounts.incrementNonAtomic或者refCounts.increment方法。
把要加的引用计数(这里是1)左移33位,然后再跟原来的
RefCounts相加,得到新的RefCounts.
循环引用
和OC一样,swift中如果A对有强引用,B对A也有强引用,这种情况会出现循环引用,从而导致A、B不能释放,造成内存泄漏。
func func8() {
class OSInFuncRoom: OSRoom {
var bed: OSInFuncBed
init(_ bed: OSInFuncBed) {
self.bed = bed
super.init("1", "卧室")
}
}
class OSInFuncBed {
var room: OSInFuncRoom?
}
let bed = OSInFuncBed()
let room = OSInFuncRoom(bed)
bed.room = room
}
在Swift中解决循环引用有2种方式,分别是弱指针(这种和OC中一样)与无主引用。
使用弱指针解决循环引用
弱引⽤不会对其引⽤的实例保持强引⽤,因⽽不会阻⽌ ARC 释放被引⽤的实例。这个特性阻⽌了引⽤ 变为循环强引⽤。声明属性或者变量时,在前⾯加上 weak 关键字表明这是⼀个弱引⽤。
由于弱引⽤不会强保持对实例的引⽤,所以说实例被释放了弱引⽤仍旧引⽤着这个实例也是有可能的。 因此,ARC 会在被引⽤的实例被释放是⾃动地设置弱引⽤为 nil 。由于弱引⽤需要允许它们的值为 nil , 它们⼀定得是可选类型。
func func9() {
class OSInFuncRoom: OSRoom {
// var bed: OSInFuncBed
//
// init(_ bed: OSInFuncBed) {
// self.bed = bed
// super.init("1", "卧室")
// }
}
class OSInFuncBed {
weak var room: OSInFuncRoom?
}
let bed = OSInFuncBed()
bed.room = OSInFuncRoom("", "")
// let room = OSInFuncRoom(bed)
// bed.room = room
}
打开汇编调试断点,看看底层做了那些操作。
在源码
HeapObject.cpp中查找swift_weakInit函数。
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);
}
去找SideTable,如果找到就调用
incrementWeak()
查找sideTab的方法(注释上有写),注意⚠️100行,如果没有就去创建一个,这里我们可以得出sideTable的结构就是
HeeapObjectSideTableEntry
Increment:增加
这个
SideTableRefCounts就有点眼熟了。
他和
InlineRefCounts共用一个模版类RefCounts。
这里的参数是InlineRefCountBits,并且右移了3位。并且把62、63位置为1.
我们可以使用代码和打印内存存储来还愿这个过程。
func func10() {
let room = OSRoom("", "")
weak var room1 = room
print(room)
}
在有了一个弱引用之后,Refcounts发生了变化,我们尝试还原一下。
将
0xc0000c00003a90dc放入编程计算机中。
把62.63位置为0,之后再左移3位。
这个地址就是sidetable散列表的地址,回到lldb中,打印这块地址。可以发现,第一个8字节存放的是room的地址。第3个8字节是没有进行弱引用之前的RefCounts,第4个8字节是弱引用的数量左移1位。
使用无主引用打破循环引用
无主引用和弱引用使用上的区别主要是,无主引用不是可选类型。无主引用使用关键字
unowned声明。
func func11() {
let room = OSRoom("", "")
unowned var room1 = room
print(room)
}
无主引用有点不安全,像上面的代码,如果room被释放了,room1就被置为nil了。如果再去使用room1有可能会造成crash。根据这一特性,我们在weak和unowned的选择上要注意,如果能确定两个实例对象有相同的生命周期,或者说能保证两者是同时释放的,我们可以使用unowned,如果不能就使用weak。最典型的事例就是delegate。
当然暴力全部使用
weak也可以的。只是我们通过本文了解了weak 和 unowend,可以明显得直观得觉察出unowend的效率和性能是更好的。
unowend的引用数量是存储在RefCounts的1-31位当中的(本文引用计数储存内容提及过,不再赘述)。
闭包中的循环引用
func func13() {
class OSInFuncRoom {
var roomId: String
var closure: (()->())?
init(_ roomId: String) {
self.roomId = roomId
}
deinit {
print("\(#function) OSInFuncRoom deinit")
}
}
let room = OSInFuncRoom("111")
room.closure = {
print(room.roomId)
}
room.closure!()
}
这种情况我们使用捕获列表和weak来打破循环引用,注意使用weak修饰后,room变成了可选值,需要解包使用。
func func14() {
class OSInFuncRoom {
var roomId: String
var closure: (()->())?
init(_ roomId: String) {
self.roomId = roomId
}
deinit {
print("\(#function) OSInFuncRoom deinit")
}
}
let room = OSInFuncRoom("111")
room.closure = {[weak room] in
print(room!.roomId)
}
room.closure!()
}
注意:捕获列表是在定义的时候,在运行的上下文中找到与之同名的变量或者常量,然后使用这个值初始化一个常量在闭包内部使用。
//捕获列表
func func15() {
var x = 10
var y = 10
//只捕获x,不捕获y
let closure = {[x] in
print("\(#function)",x, y)
}
x = 11
y = 11
closure()
}
输出func15() 10 11