• Swift —— 指针


    1.指针

    为什么说指针是不安全的呢?主要以下几点:

    • 比如我们在创建一个对象的时候,是需要在堆区分配内存空间的,但是这个内存空间的生命周期是有限的,也就意味着如果我们使用指针指向这块内存空间,如果当前内存空间的生命周期到了(也就是引用计数为0了),那么当前的指针就成了未定义的行为,也就是野指针。
    • 我们创建的内存空间是有边界的,比如创建一个大小为10的数组,这个时候通过指针访问到index = 11 的位置,这个时候就越界了,访问了一个未知的内存空间。
    • 指针类型和内存空间的值类型不一致,也是不安全的。

    2. 指针类型

    swift中的指针分为2类,typed pointer (指定数据类型指针)raw pointer(未指定数据类型指针,也叫原生指针),基本上我们接触的指针类型有以下几种

    • unsafePointer,相当于oc中的const T *,指针以及指向内容都不可变
    • unsafeMutablePointer,相当于oc中的 T *,指针以及指向内容都可变
    • unsafeRawPointer,相当于oc中的const Void *,指针指向的内存区域未定
    • unsafeMutableRawPointer,相当于oc中的const Void *,指针指向的内存区域未定
    • unsafeBufferPointer,连续的内存空间
    • unsafeMutableBufferPointer
    • unsafeRawBufferPointer
    • unsafeMutableRawBufferPointer

    3. 原始指针的使用

    接下来使用raw pointer 储存4个整形的数据,这里使用unsafeMutablePointer.

    let p = UnsafeMutableRawPointer.allocate(byteCount: 32, alignment: 8)
    
    for i in  0..<4 {
        p.storeBytes(of: i, as: Int.self)
    }
    
    
    for i in  0..<4 {
        let value = p.load(fromByteOffset: i * 8, as: Int.self)
        print("index\(i) value:\(value)")
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    但是这里运行后发现取出来的值和想要的不一样,这是因为存的时候指针没有位移相应的位置,也就是步长。
    在这里插入图片描述
    在IOS里面有测量当前大小的工具,分别是

    • MemoryLayout.size
    • MemoryLayout.stride
    • MemoryLayout.alignment

    例如下面代码

    struct LGTeacher {
        var age: Int = 18
    }
    
    print(MemoryLayout<LGTeacher>.size)
    print(MemoryLayout<LGTeacher>.stride)
    print(MemoryLayout<LGTeacher>.alignment)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    运行后发现都是8

    在这里插入图片描述
    而在结构体添加一个bool属性

    struct LGTeacher {
        var age: Int = 18
        var sex: Bool = true
    }
    
    • 1
    • 2
    • 3
    • 4

    然后重新运行,发现这里打印的值就不一样了。这里可以知道,size是struct结构体的大小,stride是占用内存的实际大小,alignment是对其的大小。
    在这里插入图片描述
    所以这里知道,我们储存的时候先要移动 i * 步长信息的位置,然后在储存值。

    for i in  0..<4 {
        p.advanced(by: i * MemoryLayout<Int>.stride).storeBytes(of: i , as: Int.self)
    }
    
    • 1
    • 2
    • 3

    这样运行后就可以看到打印的是期望的值了
    在这里插入图片描述
    当然,在使用完指针之后,需要调用deallocate来释放内存空间。

    p.deallocate()
    
    • 1

    4. 泛型指针的使用

    泛型指针,也叫类型指针,指定当前指针已经绑定到了具体的类型。

    获取withUnsafePointer的方式有两种,一种是通过已有变量获取:

    如果闭包是最后一个参数的话,可以写成尾随闭包。

    var age = 18
    withUnsafePointer(to: &age){ ptr in
        print(ptr)
    }
    
    • 1
    • 2
    • 3
    • 4

    指针的具体内容使用pointee访问,pointee代表指针指向的具体数据类型,所以下列代码是可行的。

    age = withUnsafePointer(to: &age){ ptr in
        ptr.pointee + 21
    }
    
    • 1
    • 2
    • 3

    但是需要注意的是,这里的pointee是只读属性,所以下面的代码是不可行的。

    age = withUnsafePointer(to: &age){ ptr in
        ptr.pointee +=  21
    }
    
    • 1
    • 2
    • 3

    但是如果使用的是withUnsafeMutablePointer的话,那么pointee就可变了。

    withUnsafeMutablePointer(to: &age) { ptr in
        ptr.pointee +=  21   
    }
    
    • 1
    • 2
    • 3

    这个时候可以看到age的值也变了。
    在这里插入图片描述
    一种是直接内存分配:

    var age = 10
    let tPtr = UnsafeMutablePointer<Int>.allocate(capacity: 1)
    tPtr.initialize(to: age)
    
    • 1
    • 2
    • 3

    这里也有两种方式来初始指针内存,一种是使用:

    struct LGTeacher {
        var age: Int
        var height: Double
    }
    var tPtr = UnsafeMutablePointer<LGTeacher>.allocate(capacity: 5)
    
    tPtr[0] = LGTeacher(age: 18, height: 180.0)
    tPtr[1] = LGTeacher(age: 18, height: 180.0)
    tPtr.deinitialize(count: 5)
    tPtr.deallocate()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    一种是使用:

    struct LGTeacher {
        var age: Int
        var height: Double
    }
    var tPtr = UnsafeMutablePointer<LGTeacher>.allocate(capacity: 5)
    
    tPtr.initialize(to: LGTeacher(age: 18, height: 180))
    tPtr.advanced(by: MemoryLayout<LGTeacher>.stride).initialize(to: LGTeacher(age: 18, height: 180))
    
    tPtr.deinitialize(count: 2)
    tPtr.deallocate()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    这里使用完指针后,需要调用deinitialize和deallocate。deinitialize把内存空间全部抹成0,也就是数据清零。deallocate回收内存空间。在实际开发中可以用defer来执行deinitialize和deallocate。

    5. 指针读取macho中的属性名称

    之前在macho通过地址的操作找到了属性名称,现在通过指针来读取macho中的属性名称。这里先获取到types里面的地址。

    class LGTeacher {
        var age: Int = 18
        var name: String = "hello"
    }
    
    var size: UInt = 0
    
    var ptr = getsectdata("__TEXT", "__swift5_types", &size)
    
    print(ptr)
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    运行后发现这里地址和macho里面的地址是一样的。
    在这里插入图片描述
    在这里插入图片描述
    接下来需要用到虚拟内存地址等,所以需要去获取。这里先找到machoHeader的内存地址。然后需要拿到虚拟内存地址,其在__LINKEDIT里面,需要用到里面的VM Address File Offset,所以通过getsegbyname拿到__LINKEDIT,看到这里返回segment_command_64,看到segment_command_64的结构,可以看到vmaddrfileoff,使用vmaddr 减去fileoff才是加载的基地址。之前的ptr是加过基地址的,所以ptr需要减去linkBaseAddress。

    class LGTeacher {
        var age: Int = 18
        var name: String = "hello"
    }
    
    var size: UInt = 0
    
    var ptr = getsectdata("__TEXT", "__swift5_types", &size)
    
    var mhHeaderPtr = _dyld_get_image_header(0)
    var secCommondPtr = getsegbyname("__LINKEDIT")
    var linkBaseAddress: uint64 = 0
    if let vmaddress = secCommondPtr?.pointee.vmaddr, let fileoff = secCommondPtr?.pointee.fileoff {
        linkBaseAddress = vmaddress - fileoff
    }
    
    var offset: uint64 = 0
    if let unwrappedPtr = ptr {
        let intRepresentation = uint64(bitPattern: Int64(Int(bitPattern: unwrappedPtr)))
        offset = intRepresentation - linkBaseAddress
        print(offset)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    在这里插入图片描述
    运行后这里得到16164,转换为16进制就得到0X3F24。
    在这里插入图片描述
    那么程序编译之后就得到了swift5_types里面的地址为0X3F24。

    在这里插入图片描述
    接下来要拿到0X3F24地址里面的内容。这里先拿到程序运行的首地址,那么加上offset就得到了0X3F24地址里面的内容存放在内存当中的真实地址。然后将地址转换为指针类型,使用pointee获得里面的内容。

    6. 内存绑定

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

    • assumingMemoryBound(to:) : 有些时候我们处理代码的过程中,只有原始指针(没有保留指针类型),但此刻对于处理代码的我们来说明确知道指针的类型,这个时候就可以使用assumingMemoryBound来告诉编译器预期的类型(注意:这里只是让编译器绕过类型检查,并没有发生实际类型的转换)。有时候,我们的指针类型是相似的,而我们不想通过一系列操作来转换类型,这个时候就可以使用assumingMemoryBound告诉编译器预期的类型来绕过类型检查。
      例如下面的代码是无法运行的,但是他们的指针类型是类似的,本质上是一样的。
    func testPoint(_ p: UnsafePointer<Int>) {
        print(p)
    }
    
    let tuple = (10,20)
    withUnsafePointer(to: tuple) { (tuplePtr:UnsafePointer<(Int,Int)>) in
        testPoint(tuplePtr)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    那么如果这里使用assumingMemoryBound来告诉编译器预期的类型是Int,那么这里就可以运行了。
    在这里插入图片描述

    • bind Memory(to:capacity:) : 用于更改内存绑定的类型,如果当前内存还没有内存绑定,则将首次绑定为该类型,否则就重新绑定为该类型,并且内存中的所有的值都会变成改类型。
      不同与assumingMemoryBound的是,这里发生了实际类型的转换。
      在这里插入图片描述

    • withMemoryRebound(o:capacity:body:) :当我们给外部函数传递参数的时候,不免会有一些数据类型的差距。如果我们进行类型转换,必然要来回复制数据。这个时候可以使用withMemoryRebound来临时更改内存绑定类型。
      下面就将uint8临时更改内存绑定类型为Int8类型了,减少了代码的复杂度。

    func testPoint(_ p: UnsafePointer<Int8>) {
        print(p)
    }
    
    let UInt8Ptr = UnsafePointer<uint8>.init(bitPattern: 10)
    
    UInt8Ptr?.withMemoryRebound(to: Int8.self, capacity: 1) { (ptr: UnsafePointer<Int8>) in
        testPoint(ptr)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    7. 内存管理

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

    class LGTeacher {
        var age: Int = 18
        var name: String = "ls"
    }
    
    var s = LGTeacher()
    print(Unmanaged.passUnretained(s as AnyObject).toOpaque())
    
    print("end")
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    运行后发现是3.
    在这里插入图片描述
    到源码中查看refCount,看到是InlineRefCounts类型。
    在这里插入图片描述
    然后找到InlineRefCounts,发现是模版类,接受一个泛型参数。
    在这里插入图片描述
    RefCounts里面的API都是操作RefCountBits这个泛型参数,所以RefCounts是对引用计数的一个包装,而引用计数类型是传进来的参数。
    在这里插入图片描述
    之后找到InlineRefCountBits,这里看到真实操作的类是RefCountBitsT
    在这里插入图片描述
    要知道引用计数是什么,就要看RefCountBitsT里面的属性,这里就看到引用计数bits,他的类型BitsType是由RefCountBitsInttype属性定义的。
    在这里插入图片描述
    查找RefCountBitsInt可以发现type是一个uint64_t的位域信息。

    在这里插入图片描述
    那么创建一个实例的时候,引用计数是多少呢?看到_swift_allocObject_,这里使用HeapObject创建实例。
    在这里插入图片描述
    然后看到这里面对refCounts进行了赋值InlineRefCounts::Initialized。
    在这里插入图片描述
    然后进来发现是枚举类型
    在这里插入图片描述
    往下看枚举类型传进去的值是0,1,而RefCountBits就是RefCountBitsT类型。
    在这里插入图片描述
    接下来找RefCountBitsT的初始化函数,那么就看到这里是strongExtraCount是0,unownedCount是1.
    在这里插入图片描述
    那么之前的0x0000000000000003代表的就是PureSwiftDeallocShift为1,UnownedRefCountShift为1了。
    在这里插入图片描述
    这里在代码中添加两个强引用。

    class LGTeacher {
        var age: Int = 18
        var name: String = "ls"
    }
    
    var s = LGTeacher()
    var s1 = s
    var s2 = s
    print(Unmanaged.passUnretained(s as AnyObject).toOpaque())
    
    print("end")
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    运行后打印看到这里就是4了,这是因为2存储在了高33位。
    在这里插入图片描述
    64位位置信息:
    在这里插入图片描述
    验证一下,将s变为可选参数然后后面置位nil,

    class LGTeacher {
        var age: Int = 18
        var name: String = "ls"
    }
    
    var s:LGTeacher? = LGTeacher()
    print( Unmanaged.passUnretained((s as AnyObject)).toOpaque())
    s = nil
    
    print("end")
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    运行后可以看到这里变成了0x0000000100000003,也就是IsDeinitingShift的位置变为了1.
    在这里插入图片描述

    在这里插入图片描述
    那么强引用是怎么去添加的呢?这里看到是在swift_retain方法里面调用了increment方法,
    在这里插入图片描述
    increment方法里面则是调用了incrementStrongExtraRefCount。
    在这里插入图片描述
    在incrementStrongExtraRefCount就是1左移33位。
    在这里插入图片描述

    8.循环引用

    刚才说到了强引用,那么使用强引用就会有一个问题,就是循环引用。
    下面就是一个案例,这里t持有subject,subject也持有t,这样就造成了循环引用,导致无法释放。

    class LGTeacher{
        var age: Int = 18
        var name: String = "Kody"
        var subject: LGSubject?
    }
    class LGSubject{
        var subjectName: String
        var subjectTeacher: LGTeacher
        init(_ subjectName: String, _ subjectTeacher: LGTeacher) {
            self.subjectName = subjectName
            self.subjectTeacher = subjectTeacher
    } }
    var t = LGTeacher()
    var subject = LGSubject.init("Swift ", t)
    t.subject = subject
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    Swift提供了两种方法来解决在使用类的属性时所遇到的循环引用问题:弱引用和无主引用

    8.1 弱引用

    弱引用不会对其引用的实例保持强引用,因而不会阻止ARC释放被引用的实例,这个特性阻止了引用变为循环引用。声明属性或者变量时,在前面加上weak关键字声明这是一个弱引用。

    由于弱引用不会强保持对实例的引用,所以说实例被释放了弱引用依旧引用着这个实例也是有可能的。因此,ARC会在被引用的实例被释放时自动的设置弱引用为nil,由于弱引用需要允许他们的值为nil,所以他们一定的是可选类型。

    那么用弱引用的对象的引用计数会有什么变化呢?输入下面代码

    class LGTeacher{
        var age: Int = 18
        var name: String = "Kody"
    }
    weak var t = LGTeacher()
    print( Unmanaged.passUnretained((t as AnyObject)).toOpaque())
    print("end")
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    运行后发现这里对比正常对象多调用了swift_weakInit.
    在这里插入图片描述
    看到swift_weakInit在底层调用的是nativeInit.
    swift_weakIn
    而弱引用就是形成一个散列表。
    在这里插入图片描述
    所以formWeakReference本质上就是创建一个散列表。
    在这里插入图片描述
    而在allocateSideTable里面就会判断是否有散列表,有的话就得到散列表并返回,如果在析构就返回nullptr。
    在这里插入图片描述
    如果没有的话,那么往下就会创建散列表,看到这里的类型是HeapObjectSideTableEntry。
    在这里插入图片描述
    找到HeapObjectSideTableEntry,这里发现,swift里面有两种引用计数:InlineRefCountBits和SideTableRefCountBits。在HeapObject里面如果没有弱引用就是存的InlineRefCountBits,如果用弱引用存的就是HeapObjectSideTableEntry这个实例对象,HeapObjectSideTableEntry里面就有弱引用的信息。
    在这里插入图片描述
    找到SideTableRefCounts。
    在这里插入图片描述
    找到SideTableRefCountBits,发现这里也是继承自RefCountBitsT,不过这里多了一个weakBits,
    在这里插入图片描述
    添加弱引用,查看引用计数的变化。

    var t = LGTeacher()
    
    print( Unmanaged.passUnretained((t as AnyObject)).toOpaque())
    weak var t1 = t
    print("end")
    
    • 1
    • 2
    • 3
    • 4
    • 5

    运行后看到变成了0xc0000000200e6308,这里62和63位都变成了1.
    在这里插入图片描述
    在这里插入图片描述
    在看到allocateSideTable里面生成sidetable的方法InlineRefCountBits。看到这里把散列表位置像右移了3位,然后把62和63位都变为1.
    在这里插入图片描述
    那么把之前的地址0xc0000000200e6308左移三位,得到0x100731840,看到这里0x100731840前八个字节存放的HeapObject的地址,然后0x0000000000000003 和 0x0000000000000002存的是strongCount和weakCount。
    在这里插入图片描述

    8.2 无主引用

    无主引用也不会对其引用的实例保持强引用,但是无主引用不是一个可选类型,它假定是永远有值的,所以无主引用相对于弱引用来说不够那么安全。如果强引用的双方,生命周期没有关联,使用weak,比如delegate。如果其中一个对象,另外一个对象也要跟着销毁,就要使用unowned。weak比unowned来说更安全,而unowned则性能更好,一般来说,使用weak就可以了。

    9. 闭包循环引用

    swift中,闭包会默认捕获外部的变量。这里可以看出来,闭包内部对外部变量的修改会改变外部原始变量的值。
    在这里插入图片描述
    而这里看到,LGTeacher的deinit没有被调用,这是因为t是全局变量。
    在这里插入图片描述
    这个时候,deinit就被调用了。
    在这里插入图片描述
    而如果让LGTeacher持有这个闭包,那么deinit就不会被调用了,因为闭包和对象形成了循环引用。
    在这里插入图片描述
    那么如何解决这个问题呢?这里在捕获列表使用weak修饰t就可以了。
    在这里插入图片描述
    当然,这里也可以使用unowned
    在这里插入图片描述
    当声明了一个捕获列表,编译器会在当前程序运行的上下文中找到与之同名的变量或者常量,然后进行初始化,这里也就是把0赋值给了闭包中的age,而闭包中的age和外面的age是不同的东西,所以外面的age在变化,闭包中的值也不会变化。这里虽然捕获值是在调用closure的时候,但是因为使用了捕获列表,所以对于age的变量,不再捕获age = 10,而是在定义的上下文中用age的值来初始化捕获列表中age的值。
    在这里插入图片描述
    捕获列表中的age是一个常量,所以这里也无法改变age的值。
    在这里插入图片描述
    对于值类型来说,是调用的时候在捕获值,所以这里打印11,13,14.
    在这里插入图片描述
    OC中有强弱共舞,那么Swift中有没有呢? 下面就是Swift中的强弱共舞,这里将t可选值做了模式匹配,将值做了解包的操作,然后赋值给了strongSelf。

    class LGTeacher{
        var age: Int = 18
        var name: String = "Kody"
        var closure: (() -> ())?
        deinit {
            print("deinit")
        }
    }
    
    func testClosure() {
        let t = LGTeacher()
    
        t.closure = { [weak t] in
            if let strongSelf = t {
                print( strongSelf.age)
            }
        }
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    这里还有另一个写法,就是使用withExtendedLifetime。

    class LGTeacher{
        var age: Int = 18
        var name: String = "Kody"
        var closure: (() -> ())?
        deinit {
            print("deinit")
        }
    }
    
    func testClosure() {
        let t = LGTeacher()
    
        t.closure = { [weak t] in
            withExtendedLifetime(t) {
                print(t!.age)
            }
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
  • 相关阅读:
    47_ue4进阶末日生存游戏开发[基础游戏循环]
    Day1-数组第一部分、二分查找|LeetCode 704、35、34|代码随想录刷题
    图片加载失败后,怎样让那个图标不显示呢?
    激光雷达技术详解:MATLAB代码实践与遥感测距原理通俗解析
    Cilium系列-13-启用XDP加速及Cilium性能调优总结
    .net core/5/6/7中WPF如何优雅的开始开发
    VRChat 2024年裁员原因与背景深度分析
    (附源码)计算机毕业设计SSM基于课程群的实验管理平台
    AngularJS 退役,Angular 接棒
    微生物共现网络可视化:实现布局自由
  • 原文地址:https://blog.csdn.net/LinShunIos/article/details/122336893