指针&内存管理

230 阅读14分钟

一、指针

定义

  • 指针其实就是内存地址 指针变量是用来存放内存地址的变量,在同一CPU构架下,不同类型的指针变量所占用的存储单元长度是相同的,而存放数据的变量因数据的类型不同,所占用的存储空间长度也不同。

  • 指针描述了数据在内存中的位置,标识了一个占据存储空间的实体,在这一段空间起始位置的相对距离值

指针安全吗

指针不安全

  • 比如我们在创建一个对象的时候,需要在堆上分配内存空间,但是这个内存空间的生命周期是有限的,如果我们使用指针指向这块内存空间,如果当前内存空间的生命周期到了(引用计数变为0),那么我们当前的指针就变成了未定义的了,也就变成了野指针,再次使用会发生意想不到的事。

  • 我们创建的内存空间是有边界的,比如我创建了一个大小为 5 的数组,这个时候我们通过指针访问到了  index = 6 的位置,这个时候就数组越界了,访问了一个未知的内存空间。

  • 指针所指向的类型与内存值的类型不一致,也是不安全的

指针类型

总体来说 Swift里的指针可以分为两类

  • 指定数据类型的指针:typed pointer
  • 未指定数据类型的指针(原生指针):raw pointer

基本上我们接触到的类型如下

SwiftObject-C说明
unsafePointer<T>const T*指针及指针所指向的内容都不可变
unsafeMutablePointer<T>T*指针及指针所指向的内容都可变
unsafeRawPointerconst void*指针所指向的内存未定
unsafeMutableRawPointervoid*指针所指向的内存未定
unsafeBufferPointer<T>
unsafeMutableBufferPointer<T>
unsafeRawBufferPointer
unsafeMutableRawBufferPointer

原生指针

首先我们来存储几个整形变量

//先开辟 4 块内存空间 ,int 大小 8字节
let p = UnsafeMutableRawPointer.allocate(byteCount: 4 * 8, alignment: 8)
for i in 0..<4 {
    // 存储值
    p.storeBytes(of: i, as: Int.self)
}
for i in 0..<4 {
    // 使用 load 方法加载当前指针当中对应的内存数据
    let value = p.load(fromByteOffset: i * 8, as: Int.self)
    print("i = \(i), value = \(value)")
}
// 谁申请谁释放
p.deallocate()

结果是

image.png

为啥和我们想的不一样呢?

这个就要考虑另外一个问题了---步长

struct Teacher {
    var age: Int
    var sex: Bool
}

print(MemoryLayout<Teacher>.size) // 真实大小
print(MemoryLayout<Teacher>.stride) // 步长信息
print(MemoryLayout<Teacher>.alignment) // 对齐信息

打印结果:
9
16
8

为啥呢?

这个是因为 我们size是真正的内存占用的大小, 固 8(Int) + 1(Bool) = 9

步长信息 机器要考虑对齐属性,而我们alignment = 8 的,即 按照8字节对齐,固需要 16字节

所以在上面我们 用 UnsafeMutableRawPointer 存储时要按照步长值 来移动存储的指针。

所以我们通过 advanced(by n: Int) 来移动步长

// 存储值
p.storeBytes(of: i, as: Int.self)
改成
p.advanced(by:i * MemoryLayout<Int>.stride).storeBytes(of: i, as: Int.self)

然后执行就会得到想要的结果

image.png

泛型指针

泛型指针相对于原⽣指针来说,其实就是 当前指针已经绑定到了具体的类型,我们通过一个例子来说明

var age = 20
age = withUnsafePointer(to: &age, { ptr in
    return ptr.pointee + 20
    //ptr.pointee + 20  也是一样的,闭包可以识别是返回值
})
print(age)

打印结果:

40
Program ended with exit code: 0

上面例子我们可以通过这个方法来修改 age , 但是当我们 直接在闭包里 使用 ptr.pointee =  21 会得到提示 编译不给通过

image.png

通过提示我们知道 这个 pointee 是只有 get 方法

若我们想在闭包里直接修改 pointee , 我们需要使用带mutable 属性的

var age = 20
withUnsafeMutablePointer(to: &age, { ptr in
    return ptr.pointee21
    //ptr.pointee = 21  也是一样的,闭包可以识别是返回值
})
print(age)

打印结果:

21
Program ended with exit code: 0

我们来看张图 直接分配内存的实例

image.png 例1 直接使用

struct Teacher {
    var age = 20
    var name = "Li"
}

// 使用 capacity 申请 3个连续的内存空间
var tptr = UnsafeMutablePointer<Teacher>.allocate(capacity: 3)

// tptr 就是当前分配的内存空间的首地址
tptr[0] = Teacher.init(age: 20, name: "Li1")
tptr[1] = Teacher.init(age: 21, name: "Li2")

// 谁申请谁释放 这两个要成对出现
tptr.deinitialize(count: 3)
tptr.deallocate()

例2 移动步长

// 开辟3个连续的内存空间
let p = UnsafeMutablePointer<Teacher>.allocate(capacity: 3)

p.initialize(to: Teacher())
//按照步长 移动
p.advanced(by: MemoryLayout<Teacher>.stride).initialize(to: Teacher(age: 20, name: "Li"))

// 运行完成后 执行defer 销毁数据
defer {
    // 这两个是成对出现的
    p.deinitialize(count: 3)
    p.deallocate()
}

利用指针还原 MachO中 类名、属性、函数IMP

import UIKit
import MachO

class Teacher {
    
    var age: Int = 20
    var name: String = "Li"

    func teach () {
        print("teach")
    }
    func teach1 () {
        print("teach1")
    }
    func teach2 () {
        print("teach2")
    }
}

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        let t = Teacher()
        t.teach()
        t.teach1()
        t.teach2()

        var size: UInt = 0
        //获取 __siwift5_types 章节句柄
        var ptr = getsectdata("__TEXT", "__swift5_types", &size)
        //获取当前运行程序的基地址
        var mHeaderPtr = _dyld_get_image_header(0)
        //获取当前程序虚拟地址
        var setCommondPtr = getsegbyname("__LINKEDIT")
        
        var linkVMAddress: UInt64 = 0
        if let vmAddress = setCommondPtr?.pointee.vmaddr, let offset = setCommondPtr?.pointee.fileoff {
            linkVMAddress = vmAddress - offset
//            print(String(format: "linkVMAddress = %08X%08X", linkVMAddress >> 32, linkVMAddress))
        }
        
        //获取偏移量
        var offSet: UInt64 = 0
        if let unwrappedPtr = ptr {
            let intRepresentation = UInt64(bitPattern: Int64(Int(bitPattern: unwrappedPtr)))
            offSet = intRepresentation - linkVMAddress
            
//            print(String(format: "offSet = %08X", offSet))
        }
        
        //获取DataLo
        let mHeaderPtr_IntRepresentation = UInt64(bitPattern: Int64(Int(bitPattern: mHeaderPtr)))
        print(String(format: "mHeaderPtr_IntRepresentation = %08X%08X", mHeaderPtr_IntRepresentation >> 32, mHeaderPtr_IntRepresentation))
        //获取__swift5_types 里 开始4字节的地址信息
        var dataLoAddress = mHeaderPtr_IntRepresentation + offSet
        //转换成指针
        var dataLoAddressPtr = withUnsafePointer(to: &dataLoAddress){return $0}
        //拿到指针指向的内容
        var dataLoContent = UnsafePointer<UInt32>.init(bitPattern: Int(exactly: dataLoAddress) ?? 0)?.pointee
        if let content = dataLoContent {
//            print(String(format: "dataLoContent = %08X", content))
        }
        
        //获取 __const 文件里的 Descriptor 地址
        let typeDescOffset = UInt64(dataLoContent!) + offSet - linkVMAddress
//        print(String(format: "typeDescOffset = %08X", typeDescOffset))
        //需要加上程序的基地址
        var typeDescAddress = typeDescOffset + mHeaderPtr_IntRepresentation
//        print(String(format: "typeDescAddress = %08X%08X", typeDescAddress >> 32, typeDescAddress))
        
        //Descriptor 结构体信息
        struct TargetClassDescriptor{
            var flags: UInt32
            var parent: UInt32
            var name: Int32
            var accessFunctionPointer: Int32
            var fieldDescriptor: Int32
            var superClassType: Int32
            var metadataNegativeSizeInWords: UInt32
            var metadataPositiveSizeInWords: UInt32
            var numImmediateMembers: UInt32
            var numFields: UInt32
            var fieldOffsetVectorOffset: UInt32
            var Offset: UInt32
            var VTableSize: UInt32
        }
        
        
        //地址指向的值
        let classDescriptor = UnsafePointer<TargetClassDescriptor>.init(bitPattern: Int(exactly: typeDescAddress) ?? 0)?.pointee
        
        if let name = classDescriptor?.name{
            //获取名称的偏移量
            let nameOffset = Int64(name) + Int64(typeDescOffset) + 8
//            print(String(format: "nameOffset = %08X", nameOffset))
            //名称真实的 运行地址
            let nameAddress = nameOffset + Int64(mHeaderPtr_IntRepresentation)
//            print(String(format: "nameAddress = %08X%08X", nameAddress >> 32, nameAddress))
            if let cChar = UnsafePointer<CChar>.init(bitPattern: Int(nameAddress)){
                print(String(cString: cChar))
            }
        }
        //获取 属性地址
        let filedDescriptorRelaticveAddress = typeDescOffset + 16 + mHeaderPtr_IntRepresentation
//        print(String(format: "filedDescriptorRelaticveAddress = %08X%08X", filedDescriptorRelaticveAddress >> 32, filedDescriptorRelaticveAddress))

        struct FieldDescriptor  {
            var mangledTypeName: Int32
            var superclass: Int32
            var Kind: UInt16
            var fieldRecordSize: UInt16
            var numFields: UInt32
            var fieldRecords: [FieldRecord]
        }

        struct FieldRecord{
            var Flags: UInt32
            var mangledTypeName: Int32
            var fieldName: UInt32
        }
        
        //获取 fieldDescriptor 偏移量
        var fieldDescriptorOffset = UnsafePointer<UInt32>.init(bitPattern: Int(exactly: filedDescriptorRelaticveAddress) ?? 0)?.pointee
//        print(String(format: "fieldDescriptorOffset = %08X", UInt64(fieldDescriptorOffset!)))
        
        //获取 fieldDescriptor 运行地址
        let fieldDescriptorAddress = UInt64(bitPattern: Int64(filedDescriptorRelaticveAddress)) + UInt64(fieldDescriptorOffset!)
//        print(String(format: "fieldDescriptorAddress = %08X%08X", fieldDescriptorAddress >> 32, fieldDescriptorAddress))
        //转成结构体指针
        let fieldDescriptor = UnsafePointer<FieldDescriptor>.init(bitPattern: Int(exactly: fieldDescriptorAddress) ?? 0)?.pointee

        for i in 0..<fieldDescriptor!.numFields{
            let size: Int = Int(i) * Int(fieldDescriptor!.fieldRecordSize)
            let stride: UInt64 = UInt64(bitPattern: Int64(size))
            let fieldRecordAddress = fieldDescriptorAddress + stride + 16
//            print(String(format: "fieldRecordAddress = %08X%08X", fieldRecordAddress >> 32, fieldRecordAddress))
            let fieldRecord = UnsafePointer<FieldRecord>.init(bitPattern: Int(exactly: fieldRecordAddress) ?? 0)?.pointee
            if let fieldN =  fieldRecord?.fieldName{
//                print(String(format: "fieldName = %08X", fieldN))
            }
            
            // Flags: UInt32 + mangledTypeName: Int32  fieldRecordAddress 已经是运行地址了,不用这个操作(- linkVMAddress + mHeaderPtr_IntRepresentation)
            let fieldNameRelactiveAddress = UInt64(2 * 4) + fieldRecordAddress
            
//            print(String(format: "fieldNameRelactiveAddress = %08X%08X", fieldNameRelactiveAddress >> 32, fieldNameRelactiveAddress))
            let offset = UnsafePointer<UInt32>.init(bitPattern: Int(exactly: fieldNameRelactiveAddress) ?? 0)?.pointee
            if let fieldSet = offset {
//                print(String(format: "fieldOffSet = %08X", fieldSet))
            }
            
            
            let fieldNameAddress = fieldNameRelactiveAddress + UInt64(offset!) - linkVMAddress
//            print(String(format: "fieldNameAddress = %08X%08X", fieldNameAddress >> 32, fieldNameAddress))
            if let cChar = UnsafePointer<CChar>.init(bitPattern: Int(fieldNameAddress)){
                print(String(cString: cChar))///
            }
        }
        
        //函数描述
        struct TargetMethodDescriptor {
          /// Flags describing the method.(MethodDescriptorFlags)
            var Flags: UInt32

          /// The method implementation. (TargetRelativeDirectPointer)
            var Impl: UInt32;
        };
        
        //偏移TargetClassDescriptor 结构体 12 * 4 位数
        let Offset: Int = 12
        
        //函数个数
        let methods: Int = Int(classDescriptor?.VTableSize ?? 0)
        
        //获取 V_table地址
        let methodDescriptorRelaticveAddress = typeDescOffset + UInt64((Offset+1) * 4) + mHeaderPtr_IntRepresentation
//        print(String(format: "methodDescriptorRelaticveAddress = %08X%08X", methodDescriptorRelaticveAddress >> 32, methodDescriptorRelaticveAddress))
        
        for i in 0..<methods {
            let size: Int = Int(i) * Int(MemoryLayout<TargetMethodDescriptor>.size)
            let stride: UInt64 = UInt64(bitPattern: Int64(size))
            let methodRecordAddress = methodDescriptorRelaticveAddress + stride // 4 = Flags: 的字节数
//            print(String(format: "methodRecordAddress = %08X%08X", methodRecordAddress >> 32, methodRecordAddress))
            //获取method偏移量
            let methodRecord = UnsafePointer<TargetMethodDescriptor>.init(bitPattern: Int(exactly: methodRecordAddress) ?? 0)?.pointee
            guard let ImplOffset = methodRecord?.Impl else {break}
//            print(String(format: "ImplOffset = %08X", ImplOffset))
            // 获取 imp 运行地址
            let implAddress = methodRecordAddress + 4 + UInt64(ImplOffset) - linkVMAddress
            
            print(String(format: "method IMPL = %08X%08X", implAddress >> 32, implAddress))
        }
    }
}

运行后的结果

image.png

内存绑定

在Swift中,系统为我们提供了三种API来实现绑定或者修改指针

  • assumingMemoryBound(to:) 在有的开发中我们会遇到只有原始指针,没有保留指针类型。但调用的函数要传入指针类型,这个时候我么就可以用 assumingMemoryBound(to:) 来实现(Tip: 这里只是让编译器绕过检查,并没有发生实际的类型转换)。

例如

image.png

我们定义了一个打印函数,但是接收的参数是 UnsafePointer<Int> 类型,此时直接调用编译器就报错了,不给编译通过。 但是我们通过 withUnsafePointer 可以得到 UnsafePointer<Int>类型,通过下面的调用可以看出,assumingMemoryBound(to:) 是没有做实际的转换,并可以直接调用Print函数了。

image.png

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

image.png

  • withMemoryRebound(to: capacity: body:) 当我们在打印函数传参时,遇到数据类型上的差异时,Swift 也会不给通过编译,若我们通过简单的数据类型的转换再次调用,必定增加不少无效的逻辑,那么这个时候我们可以使用withMemoryRebound(to: capacity: body:) 来实现代码的精简,临时改变数据类型(Tip: 这种写法是不安全的,固使用时,注意精度的范围).

image.png

二、内存管理

引⽤计数

Swift 中和Object-C一样,使⽤⾃动引⽤计数(ARC)机制来追踪和管理内存。

例如:

image.png

在这个图中,我们看到 0x0000000400000003 就是代表了当前对象的refCounts信息。

如何看这个 RefCounts 呢? 看截图说明

image.png

在这个里我们主要看

isImmortal: 是否是永久的 位

UnownedRefCount:无主引用 位

isDeiniting: 是否是在析构 位

StrongExtraRefCount: RefCount

UseSlowRC: 是否使用 slowRC

但是这些和我们的 0x0000000400000003 又有啥关系呢? 我们把它转换成 二进制再看

image.png 在这个里面我们的 第2位 和 低 34 位都为 1 。 我们按照 refCounts 图来看, 1 ~ 31 位 是不是就是 1, 则 33 ~ 62 是不是就是 2. 为啥是这两个数字呢,这个的我们来看RefCount的源码。

image.png_swift_allocObject_ 中通过 HeapObject(metadata) 创建了对象, 而 HeapObject 的函数体如下

image.png 参数 InlineRefCounts 经过查找是个模板类

typedef RefCounts<InlineRefCountBits> InlineRefCounts;

typedef RefCountBitsT<RefCountIsInline> InlineRefCountBits;

//通过 RefCountIsInline = true 知道这个值就是传的 Truefalse
enum RefCountInlinedness { RefCountNotInline = false, RefCountIsInline = true };

对于 RefCountBitsT 我们找到其类

image.png 及其 初始化方法 -- 强引用计数 RefCount 创建方法

image.png

再次找到 RefCountBitsInt 定义,看出 在32位机器是 48 = 32位,而在64位机器是 88 = 64位。

image.png

在初始化时我们 传入了 Initialized 这个参数,在源文件中我们找到,其实就是代表了(0, 1)两个参数

image.png

所以通过 RefCountBitsT 类的初始化方法 RefCountBitsT(uint32_t strongExtraCount, uint32_t unownedCount) 我们知道, 在我们类初始化时,(0, 1) 即 标识了strongExtraCount = 0unownedCount = 1 这个初步解释了 我们的refCounts 图 1 ~ 31 位 = 1 的原因.

那为啥 33 ~ 62 = 2 呢,我们继续……

我们知道 在ARC 中引用计数 是 Retain 的,所以我们在 RefCount.h 中找 swift_retain

image.png 我们看到 在 _swift_retain_ 函数里 有个 increment(1) ,我们猜测这个就是增加 引用计数的函数,

image.png

而 在这个函数里又调用了 incrementStrongExtraRefCount函数(StrongExtraRefCountShift= 33的)。 其实在这个函数就是把引用计数 + inc 并左移33位,即是我们的 强引用计数

image.png

固 我们在 执行了 let t1 = tlet t2 = t 后,引用计数count = 2了, 再进行 左移 33位后 就是0x0000000400000000 了。

循环引用

在Swift 和 Object-C里,我们程序碰到循环引用,都会发送内存问题,那么在Swift中我们要如何避免呢?

其实在Swift 中为我们提供了两种方法来解决在使用类的属性时遇到的循环强引用问题

  • 弱引用(weak reference)

弱引⽤不会对其引⽤的实例保持强引⽤,因⽽不会阻⽌ ARC 释放被引⽤的实例。

这个特性阻⽌了引⽤变为循环强引⽤。

声明属性或者变量时,在前⾯加上 weak 关键字表明这是⼀个弱引⽤。

由于弱引⽤不会强保持对实例的引⽤,所以说实例被释放了弱引⽤仍旧引⽤着这个实例也是有可能的。

因此,ARC 会在被引⽤的实例被释放是⾃动地设置弱引⽤为 nil,由于弱引⽤需要允许它们的值为 nil , 所以它们⼀定是可选类型(Option)

例如

class Teacher {
  var age: Int = 20
  var name: String = "Li"
}
weak var t = Teacher()
print(t)

在汇编里我们看到调用了 swift_weakInit 函数 image.png

在Swift源码中 找到swift_weakInit函数, image.png

image.png

image.png

在RefCount.h 源文件中 我们看到这样的描述

Storage layout:

  HeapObject {
    isa
    InlineRefCounts {
      atomic<InlineRefCountBits> {
        strong RC + unowned RC + flags
        OR
        HeapObjectSideTableEntry*
      }
    }
  }

  HeapObjectSideTableEntry {
    SideTableRefCounts {
      object pointer
      atomic<SideTableRefCountBits> {
        strong RC + unowned RC + weak RC + flags
      }
    }
  }

所以 我们的 weak 引用计数 是 trong RC + unowned RC + weak RC + flags 这个格式的

我们在前面讲到了强引用的创建,我们再来看看弱引用计数 RefCount 创建

image.png

看例子:

image.png 在这个里,我们的 RefCounts 字段变成了 0xc000000020827b9e最高2比特位 为1

我们再进行如下操作

在 弱引用计数 RefCount 创建时 把 side 地址存在了 右移 3位的 RefCount 表中,那么我们还原则
0xc000000020827b9e 左移3位 拿到 当前的 side表的地址--- 0x10413DCF0

image.png

所以weak 修饰后,其实就是维护了一张weak表(散列表)

  • 无主引用(unowned reference) ⽆主引⽤不会牢牢保持住引⽤的实例,不像weak弱引⽤会在销毁时置为nil。总之,⽆主引⽤假定是永远有值的,当为nil时会崩溃

Weak VS unowned

  1. 如果两个对象的⽣命周期完全和对⽅没关系(其中⼀⽅什么时候赋值为nil,和对⽅没影响),请⽤ weak
  2. 如果你的代码能确保:其中⼀个对象销毁,另⼀个对象也要跟着销毁,这时候,可以(谨慎)⽤ unowned

闭包->循环引用

不论我们在Swift里还是Object-C里,block和闭包都是可能引起循环引用的。

例如

class Teacher {
    var age: Int = 20
    var name: String = "Li"
    var complateClosure: (() -> ())?
    deinit {
        print("Teacher 销毁了")
    }
}

func test() {
    let t = Teacher()
    let ptr = Unmanaged.passUnretained(t as AnyObject).toOpaque()
    print(ptr)
    t.complateBlock = {
        t.age = 21;
    }
    print(t)
}
test()

print("end")

在这个例子中,我们并未看到 Teacher类中的析构函数deinit 中的打印,这是因为我们在 complateBlock 中又使用了 t.age 造成了循环引用问题,导致 test() 执行完毕后, Teacher 实例引用计数不为0, 系统无法回收,导致内存泄露。

image.png 我们打印其在函数里的强引用计数信息,发现 RefCounts图中 StrongExtraRefCount 信息是 0x00000002 了。 这个 RefCounts= 0x0000000200000003 右移33位 就是 1。代表当前对象还有一个强引用关系,所以系统回收不了。

我们要如何避免这个呢?

其实就是要使用上面循环引用中weak弱引用 或者 unowned无主引用

我们把 complateBlock 改成如下

    t.complateBlock = { [weak t] in
        t?.age = 21;
    }

image.png

我们看到 我们的实例变量 t 的 RefCounts 已近被改成 weak散列表了。 通过weak表,我们看到 StrongExtraRefCount 强引用信息确实是没增加,同时 在test()函数结束后,实例也销毁了,所以解决了 我们 闭包 中的 循环引用

我们再 把 complateBlock 改成如下

    t.complateBlock = { [unowned t] in
        t.age = 21;
    }

image.png 我们看到 StrongExtraRefCount 强引用信息没增加,但是在 UnownedRefCount 位实现了 +1操作,即 0x0000000000000003 => 0x0000000000000005 并且 在test()函数结束后,实例也销毁了,同样解决了 我们 闭包 中的 循环引用

需要注意的是:

unowned 是不安全的,但是在 上面的闭包中,可以安全使用是因为在闭包中 遵循了 其中⼀个对象销毁,另⼀个对象也要跟着销毁,这时候,可以(谨慎)⽤ unowned

捕获列表

何为捕获列表?

默认情况下,闭包表达式从其定义的范围内捕获常量或者变量,并强引用这些值,您可以使用捕获列表来显示控制如何在闭包中捕获值。

例如

image.png

闭包中的 [age] in就是 捕获列表使用方法。

使用了捕获列表,也即我们在 闭包中 使用了一个内部变量。这个捕获列表的值会在定义 闭包时就 根据 上下文进行了初始化,固在 闭包定义结束后 再次修改 捕获列表中同名的变量值,但不影响捕获列表的值,因为他们已经不是一个变量了。