Swift进阶-指针

Swift进阶-类与结构体
Swift-函数派发
Swift进阶-属性
Swift进阶-指针
Swift进阶-内存管理
Swift进阶-TargetClassMetadata和TargetStructMetadata数据结构源码分析
Swift进阶-Mirror解析
Swift进阶-闭包
Swift进阶-协议
Swift进阶-泛型
Swift进阶-String源码解析
Swift进阶-Array源码解析

前言

为什么说指针不安全?

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

1.指针类别

Swift中的指针分为两类:

typed pointer: 指定数据类型指针,即UnsafePointer,其中T表示泛型
raw pointer: 未指定数据类型的指针(原生指针) ,即UnsafeRawPointer

与OC中的指针的对比

OC swift 解释
const T * unsafePointer 指针及所指向的内容都不可变
T * unsafeMutablePointer 指针及所指向的内容都可变
const void * unsafeRawPointer 无类型指针,指向的值必须是常量(指向的内存区域未定)
void * unsafeMutableRawPointer 无类型指针,指向的内存区域未定,也叫通用指针(指向的内存区域未定)

首先我们了解一下内存的字节对齐、实际大小、步长

struct Teacher {
    var age: Int = 18
    var sex: Bool = true
}
print(MemoryLayout.alignment)    // 8 字节对齐
print(MemoryLayout.size)     // 9 实际大小
print(MemoryLayout.stride)   // 16 步长
  • 字节对齐alignment: cpu的一个64位寻址周期读出来的是8字节。

  • 实际大小size: 一个Teacher()占用内存的真实大小 size = Int + Bool / 9 = 8 + 1。

  • 步长stride: 比如说我要存储连续的Teacher实例,从当前Teacher()的起始位置到下一个Teacher()的起始位置,就是一个Teacher实例的步长。这里输出16是因为存储一个Teacher()需要的内存实际大小是9,又因为内存的字节对齐是8,所以要跨两个8字节对齐才足够存储一个Teacher实例

1.1 原生指针 raw pointer

使用原生指针需要注意2点:

a.对于原生指针的内存管理是需要手动管理
b.原生指针在使用完需要手动释放

// 定义一个未知类型的指针:本质是分配32字节大小的空间,指定对齐方式是8字节对齐
let p = UnsafeMutableRawPointer.allocate(byteCount: 32, alignment: 8)

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

for i in 0...3 {
    //p是当前内存的首地址,通过内存平移来获取值
    let value = p.load(fromByteOffset: i * 8, as: Int.self) //取值
    print("index: \(i), value: \(value)")
}

p.deallocate() //使用完需要手动释放

预想值是打印的value是0~3,但实际情况却是:

index: 0, value: 3
index: 1, value: 1152921504606846976
index: 2, value: 8319395793567416323
index: 3, value: 246290604621824

原因是没有在指定的位置去设值,导致每次都对第一个内存空间设值。
解决:存储时,通过advanced(by:)指定的步长。

// 定义一个未知类型的指针:本质是分配32字节大小的空间,指定对齐方式是8字节对齐
let p = UnsafeMutableRawPointer.allocate(byteCount: 32, alignment: 8)

for i in 0...3 {
    p.advanced(by: i * MemoryLayout.stride).storeBytes(of: i, as: Int.self) //移动对应的步长,存值
}

for i in 0...3 {
    //p是当前内存的首地址,通过内存平移来获取值
    let value = p.load(fromByteOffset: i * 8, as: Int.self) //取值
    print("index: \(i), value: \(value)")
}

p.deallocate() //使用完需要手动释放

/* print
index: 0, value: 0
index: 1, value: 1
index: 2, value: 2
index: 3, value: 3
*/
1.2 类型指针 typed pointer

1.2.1 通过 withUnsafePointer(to:) 方法获取地址

class Teacher {
    var name: String
    init(name: String) {
        self.name = name
    }
}

class ViewController: UIViewController{
    override func viewDidLoad() {
        super.viewDidLoad()
        var t = Teacher(name: "安老师")
        withUnsafePointer(to: &t) { print($0) }
        // 和上面是一样的,只是方式不同
        withUnsafePointer(to: &t) { point in
            print(point)
        }
    }
}

输出的是变量t的栈内存地址(这与Teacher(name: "安老师")在堆内存的地址不一样)

如果我想访问指针的具体内容 pointee

// 得到的是Teacher实例
withUnsafePointer(to: &t) { print($0.pointee) } 
//  访问Teacher实例的name属性 - 安老师
withUnsafePointer(to: &t) { print($0.pointee.name) } 

如果我想改变属性的值:

var t = Teacher(name: "安老师")
withUnsafePointer(to: &t) {
    return $0.pointee.name = "林老师"  // 把return省略也可以
}
print(t.name)

withUnsafePointer(to:)withUnsafeMutablePointer(to:)对于值类型不同:

var  age = 20
/* 报错!不予以修改值
withUnsafePointer(to: &age) { point in
    point.pointee += 1  
}
*/
withUnsafeMutablePointer(to: &age) { point in
    point.pointee += 1
}

1.2.2 类型指针使用API:

泛型指针使用API

通过allocate创建UnsafeMutablePointer,需要注意以下几点:

  • initializedeinitialize需成对使用
  • deinitialize中的count与申请时的capacity需要一致
  • 使用完后必须deallocate
class Teacher {
    var name: String
    init(name: String) {
        self.name = name
    }
}

class ViewController: UIViewController{
    override func viewDidLoad() {
        super.viewDidLoad()
        let tPoint = UnsafeMutablePointer.allocate(capacity: 3)
        defer {
            tPoint.deinitialize(count: 3)
            tPoint.deallocate()
        }
        tPoint.advanced(by: 0).initialize(to: Teacher(name: "黄老师"))
        tPoint.advanced(by: 1).initialize(to: Teacher(name: "刘老师"))
        tPoint.advanced(by: 2).initialize(to: Teacher(name: "潘老师"))
        // 三种方式访问
        print(tPoint.pointee.name)                         // 黄老师   首地址指向就是第一个Teacher实例
        print(tPoint[1].name)                              // 刘老师
        print(tPoint.successor().pointee.name)             // 刘老师  同上一行是一样效果的
        print(tPoint.successor().successor().pointee.name) // 潘老师
    }
}

有三种方式访问:

  • 下标
  • 内存平移
  • successor()
/** 三种方式是效果是一样的 */
let tPoint = UnsafeMutablePointer.allocate(capacity: 3)
defer {
    tPoint.deinitialize(count: 3)
    tPoint.deallocate()
}
// 内存平移
(tPoint + 1).initialize(to: Teacher(name: "刘老师"))
// successor()
tPoint.successor().initialize(to: Teacher(name: "刘老师"))
// advanced(by: 1)
tPoint.advanced(by: 1).initialize(to:  Teacher(name: "刘老师"))

为什么tPoint.advanced(by: 1)可以?为什么不用 tPoint.advanced(by: MemoryLayout.stride)?
注意:
关键在于这句代码 --> let tPoint = UnsafeMutablePointer.allocate(capacity: 3),此时我们是知道tPoint的具体类型的,就是指向Teacher的指针。
在确定指针的类型后,通过步长的移动+1,就表示移动了那个类的实例大小空间+1。

2.内存绑定

swift 提供了三种不同的 API 来绑定/重新绑定指针:

  • assumingMemoryBound(to:)
  • bindMemory(to: capacity:)
  • withMemoryRebound(to: capacity: body:)
2.1 assumingMemoryBound(to:) (只是让编译器绕过类型检查,并没有发⽣实际类型的转换)

有些时候我们处理代码的过程中,只有原始指针(没有保留指针类型),但此刻对于处理代码的我们来 说明确知道指针的类型,我们就可以使⽤ assumingMemoryBound(to:) 来告诉编译器预期的类型。

class ViewController: UIViewController{
    override func viewDidLoad() {
        super.viewDidLoad()
        var tuple = (10, 20)
        withUnsafePointer(to: &tuple) { tuplePtr in
            testPointer(tuplePtr)
        }
    }
}

func testPointer(_ p: UnsafePointer) {
    print(p[0])
    print(p[1])
}

此时在调用 testPointer会报编译器错误 Cannot convert value of type 'UnsafePointer<(Int, Int)>' to expected argument type 'UnsafePointer'

而代码修改一下使用 assumingMemoryBound(to:)

class ViewController: UIViewController{
    override func viewDidLoad() {
        super.viewDidLoad()
        var tuple = (10, 20)
        withUnsafePointer(to: &tuple) { tuplePtr in
            testPointer(UnsafeRawPointer(tuplePtr).assumingMemoryBound(to: Int.self))
        }
    }
}

func testPointer(_ p: UnsafePointer) {
    print(p[0])
    print(p[1])
}

元组时值类型,在本质上这块内存空间是连续的,存放Int类型数据。先把元组转化成指针指向元组的首地址tuple[0],通过assumingMemoryBound(to:)告诉编译器,当前元组的内存已经绑定过Int了,这时就能骗过编译器检查

2.2 bindMemory(to: capacity:)

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

案例:将一个class实例绑定成class的底层数据结构

  • 定义一个Teacher类
class Teacher {
    var name: String
    init(name: String) {
        self.name = name
    }
}
  • 从swift源码得出class的底层结构是HeapObject,所以我们可以自定义一个HeapObject
struct HeapObject {
    var metadata: UnsafeRawPointer // 定义一个未知类型的指针
    var refCounts: UInt32 // 引用计数
}
  • 从swift源码得出metadata其实也是一个结构体,所以我自定义一个Metadata
struct Metadata { 
      var kind: Int 
      var superClass: Any.Type 
      var cacheData: (Int, Int) 
      var data: Int 
      var classFlags: Int32 
      var instanceAddressPoint: UInt32 
      var instanceSize: UInt32 
      var instanceAlignmentMask: UInt16 
      var reserved: UInt16 
      var classSize: UInt32 
      var classAddressPoint: UInt32 
      var typeDescriptor: UnsafeMutableRawPointer 
      var iVarDestroyer: UnsafeRawPointer
 }

将teacher绑定到结构体内存中:

  • 1.获取实例变量teacher的内存地址,声明成非托管对象
  • 2.绑定到结构体内存,返回值是UnsafeMutablePointer
  • 3.访问成员变量
class ViewController: UIViewController{
    override func viewDidLoad() {
        super.viewDidLoad()
        let teacher = Teacher(name: "陈老师")
        // 通过Unmanaged指定内存管理,类似于OC与CF的交互方式(所有权的转换 __bridge)
        // passUnretained 不增加引用计数,即不需要获取所有权
        // passRetained 增加引用技术,即需要获取所有权
        // toOpaque 不透明的指针
        let ptr = Unmanaged.passUnretained(teacher as AnyObject).toOpaque()
        // bindMemory更改当前UnsafeMutableRawPointer的指针类型,绑定到具体类型值
        // - 如果没有绑定,则绑定
        // - 如果已经绑定,则重定向到 HeapObject类型上
        let heapObject = ptr.bindMemory(to: HeapObject.self, capacity: 1)
        print(heapObject.pointee.metadata)
        print(heapObject.pointee.refCounts)
    }
}

打印输出heapObject的成员

0x0000000109186768  // metadata输出的就是地址
3

然后继续对matadata进行绑定

class ViewController: UIViewController{
    override func viewDidLoad() {
        super.viewDidLoad()
        let teacher = Teacher(name: "陈老师")
        let ptr = Unmanaged.passUnretained(teacher as AnyObject).toOpaque()
        
        let heapObject = ptr.bindMemory(to: HeapObject.self, capacity: 1)
        
        let metadataPtr = heapObject.pointee.metadata.bindMemory(to: Metadata.self, capacity: 1)
        print(metadataPtr.pointee)
    }
}

打印输出metadata的成员值

Metadata(kind: 4536207176, superClass: _TtCs12_SwiftObject, cacheData: (4541967040, 140909287047168), data: 105553159911586, classFlags: 2, instanceAddressPoint: 0, instanceSize: 32, instanceAlignmentMask: 7, reserved: 0, classSize: 136, classAddressPoint: 16, typeDescriptor: 0x000000010e60e69c, iVarDestroyer: 0x0000000000000000)
2.3 withMemoryRebound(to: capacity: body:)(临时更改内存绑定类型)

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

class ViewController: UIViewController{
    override func viewDidLoad() {
        super.viewDidLoad()
        var num = 10
        withUnsafePointer(to: &num) { ptr: UnsafePointer in
            testPointer(ptr)
        }
   }     
}

func testPointer(_ p: UnsafePointer) {
    print(p)
}

此时编译器报错因为类型不匹配。
使用withMemoryRebound(to: capacity: body:)修改后:

class ViewController: UIViewController{
    override func viewDidLoad() {
        super.viewDidLoad()
        var num = 10
        let ptr = withUnsafePointer(to: &num) {$0}
        ptr.withMemoryRebound(to: Int8.self, capacity: 1) { (ptr: UnsafePointer)  in
            testPointer(ptr)
            // 超过了闭包作用域,则不是UnsafePointer类型
        }
   }     
}

func testPointer(_ p: UnsafePointer) {
    print(p)
}

你可能感兴趣的:(Swift进阶-指针)