指针

151 阅读9分钟

指针

在学习指针之前的我们需要先来了解一下指针为什么是不安全的?

  • ⽐如我们在创建⼀个对象的时候,是需要在堆分配内存空间的。但是这个内存空间的声明周期是有限的,也就意味着如果我们使⽤指针指向这块内容空间,如果当前内存空间的⽣命周期到了(引 ⽤计数为0),那我们我们系统会对当前的内存空间进行一个回收,那么我们当前的指针就变成了未定义的⾏为了(俗称野指针)。
  • 我们创建的内存空间是有边界的,⽐如我们创建⼀个⼤⼩为10的数组,这个时候我们通过指针访问 到了 index = 11 的位置,这个时候是不是就越界了,访问了⼀个未知的内存空间。
  • 指针类型与内存的值类型不⼀致,也是不安全的。 例如内存里面存的是Int类型的(8字节),而指针确实Int8类型的,那么就会造成精度的缺失。

以上说明了我们使用指针的时候要小心再小心.

我们稍微了解了一些指针之后我们就开始学习指针吧

指针类型

Swift中的指针分为两类:

  • typed pointer 指定数据类型指针,
  • raw pointer 未指定数据类型的指 针(原⽣指针),意味着我们不知道指针所指向的是什么样数据类型的。

基本上我们接触到的指针类型有以下⼏种 ,Mutable表示可变否则不可变

SwiftObject-C说明
unsafePointerconst T *指针及所指向的内容都不可变
unsafeMutablePointerT *指针及其所指向的内存内容均可变
unsafeRawPointerconst void *指针指向的内存区域未定
unsafeMutableRawPointervoid *指针指向的内存区类型未定。
unsafeBufferPointer表达一个连续的内存内存地址
unsafeMutableBufferPointer< T>
unsafeRawBufferPointer原生类型的连续的内存内存地址
unsafeMutableRawBufferPointer
原生指针的使用

我们⼀起来看⼀下如何使⽤ Raw Pointer 来存储 4 个整形的数据,这⾥我们需要选取的UnsafeMutableRawPointer

//创建一个内存空间 4个8字节32字节,步长8
let p = UnsafeMutableRawPointer.allocate(byteCount: 32, alignment: 8)
//接下俩我们要往里面存了 利用for循环 of后面是存储的书,as后面是存储的数据类型
for i in 0...3{
    p.storeBytes(of: i, as: Int.self)
}
//取出来 也就是加载数据类型
for i in 0..<4{
  let value = p.load(fromByteOffset: i*8, as: Int.self)
    print(value)
}

image-20220106185739725

以上可以看出allocate是分配一块内存但是并不会初始化,这个可以查阅官方文档,底下的控制台可以看出我们操作的一直是第一个8字节,如果你有耐心可以在p.storeBytes加一个断点 每次都x/8g一下会看到第一个8字节从0变到3,所以上文代码需要修正一下

for i in 0...3{
    p.storeBytes(of: i, as: Int.self)
}

该代码修正为

for i in 0...3{
//    p.storeBytes(of: i, as: Int.self)
//    移动指针 注意要移动步长,也就是实际分配的内存大小
    p.advanced(by: i * MemoryLayout<Int>.stride).storeBytes(of: i, as: Int.self)
}

Lg

print(MemoryLayout<Int>.size)
print(MemoryLayout<Int>.stride)
print(MemoryLayout<Int>.alignment)

image-20220107122206130

  • size: 实际使用的内存大小
  • stride: 内存对齐后实际分配的内存大小,也叫步长
  • alignment: 对齐参数
泛型指针的使用

这⾥的泛型指针相⽐较原⽣指针来说,其实就是指定当前指针已经绑定到了具体的类型。同样的,我们还是通过⼀个例⼦来解释⼀下。

在进⾏泛型指针访问的过程中,我们并不是使⽤ load 和 store ⽅法来进⾏存储操作。这⾥我们使⽤到当前泛型指针内置的变量 pointee

pointee 可理解为解引(dereference),即用 * 符号获得指针指向内存区域的值。

获取 UnsafePointer 的⽅式有两种。⼀种⽅式就是通过已有变量获取

var age = 18
//得到age变量的内存指针
withUnsafePointer(to: &age){ptr in
    print(ptr)
}
//获取地址的值 指针和指针的内容都是不可变的 pointee指针所指向的内容
age = withUnsafePointer(to: &age){ ptr in
//    pointee是只读的不可变的,所以不能直接赋值
    (ptr.pointee) + 21
}
//指针和指针的内容都是可变的
withUnsafeMutablePointer(to: &age){ ptr in
    (ptr.pointee) += 21
}
print(age)

以上代码的运行结果是,

image-20220107122433753

withUnsafePointer没有Mutable的情况下pointee是只读类型的,指针指向的内存值不可变。下图可以看到如果改变pointee的值将会报错

image-20220107122322859

还有⼀种⽅式就是直接分配内存

//类型指针
var tPtr = UnsafeMutablePointer<MJYStruct>.allocate(capacity: 5)
​
tPtr[0] = MJYStruct(age: 18, height: 20.0)
tPtr[1] = MJYStruct(age: 19, height: 23.0)
print(tPtr[1])
print(tPtr.pointee)
​
​
tPtr.deinitialize(count: 5)
tPtr.deallocate()

image-20220107133245538

尝试过ponitee的取值,pointee好像是指向该指针的第一个元素,如果要取第二个元素要按照数组的取法,不过在Mutable的加持下可以改变tPtr.pointee的值

tPtr.pointee = tPtr[1]
print(tPtr.pointee)

image-20220107133543372

上述代码中的deinitializedeallocate是对内存的初始化和回收,下图可以清晰的看到分配内存和回收内存的过程

image-20220107135205807

assumingMemoryBound(to:)
func testPoint(_ p: UnsafePointer<Int>){
    print(p[0])
    print(p[1])
}
let tuple = (10,20)
withUnsafePointer(to: tuple) { (tuplePtr: UnsafePointer<(Int, Int)>) in
//    testPoint(tuplePtr)
    testPoint(UnsafeRawPointer(tuplePtr).assumingMemoryBound(to: Int.self))
}

image-20220112162300181

assumingMemoryBound 绕过当前编译器的检查,并没有发生实际的转换,告诉编译器当前内存已经绑定过Int了,这个时候编译器将不会检查

bindMemory(to: capacity:)

image-20220112162947588

⽤于更改内存绑定的类型,如果当前内存还没有类型绑定,则将⾸次绑定为该类型;否则重新绑定该类 型,并且内存中所有的值都会变成该类型。

withMemoryRebound(to: capacity: body:)

image-20220112163624674

当我们在给外部函数传递参数时,不免会有⼀些数据类型上的差距。如果我们进⾏类型转换,必然要来 回复制数据;这个时候我们就可以使⽤ withMemoryRebound(to: capacity: body:) 来临时更 改内存绑定类型。

内存管理

Swift 中使⽤⾃动引⽤计数(ARC)机制来追踪和管理内存。

class MJYTeacher {
    var age: Int = 18
    var name: String = "卿卿"
}
//初始化一个变量
var t = MJYTeacher()
//接下来我们把t 赋值给t1,t2
var t1 = t
//这个时候我们是不是有3个强引用了,已就意味着此时此刻引用计数为3
var t2 = t
// 打印实例变量的内存指针
print(Unmanaged.passRetained(t as AnyObject).toOpaque())
print("end")

image-20220112164253172

我们再源码中看一下refCounts,首先我们再HeapObject.h文件中搜索refCount 找到以下定义

image-20220112170900066

接下来我们看一下InlineRefCounts

image-20220112171008753

可以看到是一个模板类接收一个泛型参数,这个模板类是当前的RefCounts

image-20220112171204546

我们看到refcounts的类操作的都是传进来的泛型参数,所以当前的RefCounts本质是对当前引用技术的包装,引用技术的具体类型取决于传进来的参数也就是InlineRefCounts,我们将目标聚集在InlineRefCountBits

image-20220112170556333

同样这个也是一个模板函数 查看RefCountBitsT这个类

image-20220112171715262

image-20220112181211967

可以看到RefCountBitsInt是一个UInt64位的位域信息,对于Swift的引用计数还是OC里面的引用计数都是一个64位的位域信息,在这64位位域信息中存储了当前类运行的生命周期相关的引用技术。

当我们创建一个实例对象的时候,当前的引用计数是多少呢?

同样我们聚焦到源代码HeapObject.cpp image-20220112172825356

查看HeapObject方法

image-20220112172931333

接下俩我们来看一下Initialized image-20220112173312179

我们可以看到传入的是RefCountBits,接下来目光聚集这个类:

image-20220112173856405

StrongExtraRefCountShift: 33

PureSwiftDeallocShift:1

UnownedRefCountShift: 1

image-20220112164253172这里的结果是0x0000000400000003,中间有个4 是由于2左移33位得到4,末尾是3 是由于最后两位为11 所以得到3

我们了解了强引用,使用强引用就会造成一个问题:循环引用。我们来看一个经典的循环引用案例:

class MJYTeacher{
    var age: Int = 18
    var name: String = "卿卿"
    var subject: MJYSubject?
    
}
class MJYSubject{
    var subjectName: String
    var subjectTeacher: MJYTeacher
    init(_ subjectName: String, _ subjectTeacher: MJYTeacher) {
        self.subjectName = subjectName
        self.subjectTeacher = subjectTeacher
    }
}
var t = MJYTeacher()
var subject = MJYSubject.init("Swift进阶", t)
t.subject = subject

那么我们要解决这个循环引用,在swift中拥有两种方法,一个是弱引用(weak),另一个是无主引用(unowned)

弱引用(weak)

image-20220112182443328

通过汇编我们可以知道在初始化的时候多调用了swift_weakInit方法我们在源码中找到这个方法

结论就是里面还是维护的是一张弱引用散列表,是一个可选类型,可以将指针的值置为nil

无主引用(unowned)

在变量前面用unowned声明就是一个无主引用,无主引用不等于弱引用,不是一个可选类型,不能设置为nil

unowned var t = MJYTeacher()

如果弱引用双方,生命周期没有任何关联,那么就使用weak,例如delegate

如果一个对象销毁,另一个对象也跟着销毁,那么使用unowned

一般无脑weak,但是unowned的性能更好,因为不用创建一个散列表,维护这个散列表,直接对refcounts的64位进行操作

Lg: 在查看引用技术是不要在控制台 用po t 因为会对引用技术造成影响,引用计数会+1

闭包

变量捕获

var age = 18
let closure = {
    age += 1
}
closure()
print(age)

image-20220112184637641

age这个变量被捕获了所以能在闭包内修改age的值

class MJYTeacher{
    var age: Int = 18
    var name: String = "卿卿"
    var testClosure:(() -> ())?
    deinit {
        print("MJYTeacher deinit")
    }
//    var subject: MJYSubject?
}
func test(){
    let t = MJYTeacher()
    t.testClosure = {
        t.age += 1
    }
   print("end")
}
test()

执行以上代码我们会发现没有打印deinit,因为闭包捕获了t这个变量,t又持有这个闭包,造成了循环引用,那么如何解决呢!

class MJYTeacher{
    var age: Int = 18
    var name: String = "卿卿"
    var testClosure:(() -> ())?
    deinit {
        print("MJYTeacher deinit")
    }
//    var subject: MJYSubject?
}
func test(){
    let t = MJYTeacher()
    t.testClosure = {[weak t] in
        t!.age += 1
    }
   print("end")
}
test()

只要在闭包内声明这个变量是weak或者unowned 就能顺利deinit了

[weak t]是捕获列表

  var age = 0
var height = 0.0
let closure = {[age] in
    print(age)
    print(height)
}
age = 10
height = 1.98
closure()

可以认为声明后age是值引用,不再是地址引用了,并且是let声明的 闭包内不会再被改变了

var height = 0.0
let closure = {[age] in
​
    print(age)
    print(height)
}
var age = 0
age = 10
height = 1.98
closure()
print(age)

上下文中去寻找age哦,age的声明在闭包声明之后也是有效的,是在闭包执行的时候去捕获变量

func test(){
    let t = MJYTeacher()
    t.testClosure = {[weak t] in
        if let strongSelf = t{
            print(strongSelf.age)
        }
        withExtendedLifetime(t){
          print( t!.age)
        }
    }
}

withExtendedLifetime延长t的生命周期,周期范围是这个闭包表达式内