Swift学习(四)指针和内存管理

424 阅读10分钟

一、指针

指针的安全性

指针是不安全的。

  • ⽐如我们在创建⼀个对象的时候,是需要在堆分配内存空间的。但是这个内存空间的声明周期是有 限的,也就意味着如果我们使⽤指针指向这块内容空间,如果当前内存空间的⽣命周期啊到了(引 ⽤计数为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= 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中查找其内存

WX20220108-194201@2x.png 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

WX20220108-211717@2x.png 再看看那这个InlineRefCounts的真面目。

WX20220108-212013@2x.png

WX20220108-212307@2x.png 可以看到class RefCounts实际是对RefCountBits的包装。所以我们还是需要回到InlineRefCountsBits中一探究竟。

WX20220108-212907@2x.png 这个InlineRefCountBits也是个模版类。继续看看RefCountBitsT

WX20220108-213308@2x.png 可以看到有用的属性只有这个bits,再继续看看这个RefCountBitsInt的定义。

WX20220108-213706@2x.png 这里面有3个定义,根据注释看,应该是根据CUP的位数有关,我们直接来看64位的。这个就是一个64位的位域信息。那么在一个对象刚被初始化的时候这个refCounts的初始值是多少呢?继续看源码,来到对象的初始化方法里面。

WX20220108-215221@2x.png

WX20220108-215614@2x.png 这个Initialized实际上是一个枚举值

WX20220108-220102@2x.png

WX20220108-220308@2x.png 这里的RefCountBits可以看到是一个定义的模版,而该模版在调用的时候实际上是typedef RefCountBitsT<RefCountIsInline> InlineRefCountBits; 再继续看RefCountBitsT的构造方法。

WX20220108-220907@2x.png 我们继续看看位移位数的逻辑

WX20220109-000843@2x.png 点进shiftAfterField

image.png 可以看到是个字符串拼接后累加的逻辑。根据以上可以得到这个64位的位域信息由5部分组成如下:

WX20220109-002855@2x.png (图中strongRefCount的位置和UnownedRefCount的位置画反了)

注意其中IsImmortal// overlaps PureSwiftDealloc and UnownedRefCount``IsImmortal信息是与PureSwiftDeallocUnownedRefCount的信息覆盖存储的。也就是说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情况。

WX20220109-004039@2x.png 我们用0x0000000200000003、0x0000000400000003、0x0000000600000003右移33位就能得到强引用计数的值,分别是1、2、3.

swift_retain的过程

上面的代码在打开汇编调试断点的时候我们可以看到如下:

WX20220109-005348@2x.png 底层调用了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;

}

WX20220109-010147@2x.png 会调用refCounts 的refCounts.incrementNonAtomic或者refCounts.increment方法。

WX20220109-010857@2x.png

WX20220109-011009@2x.png 把要加的引用计数(这里是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
    }

打开汇编调试断点,看看底层做了那些操作。 WX20220109-095413@2x.png 在源码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);
}

WX20220109-101009@2x.png 去找SideTable,如果找到就调用incrementWeak()

WX20220109-101050@2x.png 查找sideTab的方法(注释上有写),注意⚠️100行,如果没有就去创建一个,这里我们可以得出sideTable的结构就是HeeapObjectSideTableEntry

WX20220109-100635@2x.png Increment:增加

WX20220109-102416@2x.png 这个SideTableRefCounts就有点眼熟了。

WX20220109-102631@2x.png 他和InlineRefCounts共用一个模版类RefCounts

image.png

image.png 这里的参数是InlineRefCountBits,并且右移了3位。并且把62、63位置为1. 我们可以使用代码和打印内存存储来还愿这个过程。

func func10() {
        let room = OSRoom("", "")
        weak var room1 = room
        print(room)
    }

image.png 在有了一个弱引用之后,Refcounts发生了变化,我们尝试还原一下。 将0xc0000c00003a90dc放入编程计算机中。

image.png 把62.63位置为0,之后再左移3位。

image.png 这个地址就是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。根据这一特性,我们在weakunowned的选择上要注意,如果能确定两个实例对象有相同的生命周期,或者说能保证两者是同时释放的,我们可以使用unowned,如果不能就使用weak。最典型的事例就是delegate

image.png 当然暴力全部使用weak也可以的。只是我们通过本文了解了weakunowend,可以明显得直观得觉察出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