美文网首页
Swift进阶-指针

Swift进阶-指针

作者: 顶级蜗牛 | 来源:发表于2022-01-08 12:00 被阅读0次

    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>,其中T表示泛型
    raw pointer: 未指定数据类型的指针(原生指针) ,即UnsafeRawPointer

    与OC中的指针的对比👇

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

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

    struct Teacher {
        var age: Int = 18
        var sex: Bool = true
    }
    print(MemoryLayout<Int>.alignment)    // 8 字节对齐
    print(MemoryLayout<Teacher>.size)     // 9 实际大小
    print(MemoryLayout<Teacher>.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<Int>.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<Teacher>.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<Teacher>.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<Teacher>.stride)?
    注意:
    关键在于这句代码 --> let tPoint = UnsafeMutablePointer<Teacher>.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<Int>) {
        print(p[0])
        print(p[1])
    }
    

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

    而代码修改一下使用 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<Int>) {
        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<T>
    • 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<Int> in
                testPointer(ptr)
            }
       }     
    }
    
    func testPointer(_ p: UnsafePointer<Int8>) {
        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<Int8>)  in
                testPointer(ptr)
                // 超过了闭包作用域,则不是UnsafePointer<Int8>类型
            }
       }     
    }
    
    func testPointer(_ p: UnsafePointer<Int8>) {
        print(p)
    }
    

    相关文章

      网友评论

          本文标题:Swift进阶-指针

          本文链接:https://www.haomeiwen.com/subject/wxglcrtx.html