美文网首页Swift探索
Swift探索(四): 指针和内存管理

Swift探索(四): 指针和内存管理

作者: Lee_Toto | 来源:发表于2022-01-10 00:14 被阅读0次

一:指针

1. 指针的定义

Swift 中引用了某个引用类型实例的常量或变量,与 C 语言中的指针类似,不过它并不直接指向某个内存地址,也不要求你使用星号(*)来表明你在创建一个引用。相反,Swift 中引用的定义方式与其它的常量或变量的一样。

指针是不安全的:

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

2. 指针类型

Swift 中的指针分为两类

  • typed pointer 指定数据类型指针
  • raw pointer 未指定数据类型的指针(原生指针)
    基本上我们接触的指针有以下几种
    Swift的指针和OC的指针对比.png
2.1 原生指针

首先来了解一下步长信息

struct Person {
    var age: Int = 18
    var sex: Bool = true
}

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

// 打印结果
9 // 8(int) + 1(bool)
16 // 8 + 8  bool虽然只占用一个字节
8

我们可以看到

  • size 的结果是 9 = int8字节 + bool1 字节
  • stride 的结果是 16, 因为 alignment 的值为 8 ,也就是说是按照 8 字节对齐,所以步长信息为 8 + 8 = 16 字节。

接下来使用原生指针 (Raw Pointer) 存储4个整型的数据
示例代码

// 首先开辟一块内存空间 byteCount: 当前总的字节大小 4 x 8 = 32 alignment: 对齐的大小
let p = UnsafeMutableRawPointer.allocate(byteCount: 4 * 8, alignment: 8)

for i in 0..<4 {
    // 调用 advanced 获取到每个地址排列的过程中应该距离首地址的大小 i x MemoryLayout<Int>.stride
    // 调用 store 方法存储当前的整型数值
    p.advanced(by:i * MemoryLayout<Int>.stride).storeBytes(of: i, as: Int.self)
}

for i in 0..<4 {
    // 调用 load 方法加载当前指针当中对应的内存数据
    let value = p.load(fromByteOffset: i * 8, as: Int.self)
    print("index--\(i), value--\(value)")
}

// 释放创建的连续的内存空间
p.deallocate()

// 打印结果
index--0, value--0
index--1, value--1
index--2, value--2
index--3, value--3
2.2 类型指针

类型指针相较于原生指针来说,其实就是指定当前指针已经绑定到了具体的类型,在进行类型指针访问的过程中,我们不再使用 storeload 方法进行存储操操作,而是直接使用类型指针内置的变量 pointee
获取 UnsafePointer 有两种方式

  • 通过已有变量获取
var age = 18

// 通过 withUnsafePointer 来访问到当前变量的地址
withUnsafePointer(to: &age) { ptr in
    print(ptr)
}

age = withUnsafePointer(to: &age) { ptr in
    //注意这里我们不能直接修改ptr.pointee
    return ptr.pointee + 12
}

var b = 18

// 使用mutable修改ptr.pointee
withUnsafeMutablePointer(to: &b) { ptr in
    ptr.pointee += 10
    print(ptr)
}
  • 直接分配内存
var age = 10

// 分配一块int类型内存空间, 注意当前内存空间还没被初始化
let tPtr = UnsafeMutablePointer<Int>.allocate(capacity: 1)

// 初始化分配内存空间
tPtr.initialize(to: age)

// 访问当前内存的值, 直接通过pointee进行访问
print(tPtr.pointee)`

类型指针主要涉及到的api主要有


类型指针api.png

示例

struct Person {
    var age = 18
    var name = "小明"
}

// 方式一
// capacity 内存空间 5个连续的内存空间
var tptr = UnsafeMutablePointer<Person>.allocate(capacity: 5)

// tptr就是当前分配的内存空间的首地址
tptr[0] = Person.init(age: 18, name: "小明")
tptr[1] = Person.init(age: 19, name: "小强")

// 这两个是成对出现的
// 清除内存空间中内容
tptr.deinitialize(count: 5)
// 回收内存空间
tptr.deallocate()

// 方式二
// 开辟2个连续的内存空间
let p = UnsafeMutablePointer<Person>.allocate(capacity: 2)
p.initialize(to: Person())
p.advanced(by: MemoryLayout<Person>.stride).initialize(to: Person(age: 18, name: "小明"))

// 当前程序运行完成后 执行defer
defer {
    // 这两个是成对出现的
    p.deinitialize(count: 2)
    p.deallocate()
}
2.3 内存指针的使用-内存绑定

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

  • assumingMemoryBound(to:)
    有些时候我们处理代码的过程中只有原生指针(没有报错指针类型),但此刻对于处理代码的的我们来说明确知道指针的类型,我们就可以使用 assumingMemoryBound(to:) 来告诉编译器预期的类型。
    (注意:这里只是让编译器绕过类型检查,并没有发生实际的类型转换)
func testPointer(_ p: UnsafePointer<Int>) {
    print(p[0])
    print(p[1])
}

// 这里的元祖是值类型,本质上这块内存空间中存放的就是Int类型的数据
let tuple = (10, 20)
withUnsafePointer(to: tuple) { (tuplePtr: UnsafePointer<(Int, Int)>) in
    // 先将tuplePtr 转换成原生指针, 在调用assumingMemoryBound(to:) 告诉编译器当前内存已经绑定过Int了,这个时候编译器就不会进行检查
    testPointer(UnsafeRawPointer(tuplePtr).assumingMemoryBound(to: Int.self))
}
  • bindMemory(to: capacity:)
    用于更改内存绑定的类型,如果当前内存还没有类型绑定,则将首次绑定为该类型,否则重新绑定该类型,并且内存中所有的值都会变成该类型
func testPointer(_ p: UnsafePointer<Int>) {
    print(p[0])
    print(p[1])
}

// 这里的元祖是值类型,本质上这块内存空间中存放的就是Int类型的数据
let tuple = (10, 20)
withUnsafePointer(to: tuple) { (tuplePtr: UnsafePointer<(Int, Int)>) in
    // 先将tuplePtr 转换成原生指针, 将原生指针转换成UnsafePointer<Int>类型
    testPointer(UnsafeRawPointer(tuplePtr).bindMemory(to: Int.self, capacity: 1))
}
  • withMemoryRebound(to: capacity: body:)
    当我们在给外部函数传递参数时,不免会有一些数据类型上的差距,如果我们进行类型转换,必然要来会复制数据,这个时候就可以调用 withMemoryRebound(to: capacity: body:) 来临时更改内存绑定类型。
func testPointer(_ p: UnsafePointer<Int8>) {
    print(p[0])
    print(p[1])
}

let uint8Ptr = UnsafePointer<uint8>.init(bitPattern: 10)
// 减少代码复杂度
uint8Ptr?.withMemoryRebound(to: Int8.self, capacity: 1) { (int8Ptr: UnsafePointer<Int8>) in
    testPointer(int8Ptr)
    
}

3.利用指针还原Macho文件中的属性和函数表

class Person {
    var age: Int = 18
    var name: String = "小明"
}

var size: UInt = 0
//__swift5_types section 的pFile
var typesPtr = getsectdata("__TEXT", "__swift5_types", &size)

// 获取当前程序运行地址 相当于 LLDB 中 image list 命令
var mhHeaderPtr = _dyld_get_image_header(0)

// 获取 __LINKEDIT 中的内容 其中 getsegbyname 返回的是 UnsafePointer<segment_command_64>,  segment_command_64 就包含了 vmaddr(虚拟内存地址) 和 fileoff(偏移量)
var setCommond64LinkeditPtr = getsegbyname("__LINKEDIT")

// 计算链接的基地址
var linkBaseAddress: UInt64 = 0
if let vmaddr = setCommond64LinkeditPtr?.pointee.vmaddr, let fileOff = setCommond64LinkeditPtr?.pointee.fileoff{
    linkBaseAddress = vmaddr - fileOff
}

// 或者 直接去 LC_SEGMENT_64(__PAGEZERO)中的VM Size
var setCommond64PageZeroPtr = getsegbyname("__PAGEZERO")
if let vmsize = setCommond64PageZeroPtr?.pointee.vmsize {
    linkBaseAddress = vmsize
}

// 获取__TEXT, __swift5_types 在Macho中的偏移量
var typesOffSet: UInt64 = 0
if let unwrappedPtr = typesPtr {
    // 将当前的地址信息转换成UInt64
    let intRepresentation = UInt64(bitPattern: Int64(Int(bitPattern: unwrappedPtr)))
    typesOffSet = intRepresentation - linkBaseAddress
}

// 程序运行的首地址 转换成UInt64类型
let mhHeaderPtr_IntRepresentation = UInt64(bitPattern: Int64(Int(bitPattern: mhHeaderPtr)))

// DataLo的内存地址
var dataLoAddress = mhHeaderPtr_IntRepresentation + typesOffSet

// 转换成指针类型
var dataLoAddressPtr = withUnsafePointer(to: &dataLoAddress){return $0}

// 获取dataLo指针指向的内容
var dataLoContent = UnsafePointer<UInt32>.init(bitPattern: Int(exactly: dataLoAddress) ?? 0)?.pointee

// 获取typeDescriptor的偏移量
let typeDescOffset = UInt64(dataLoContent!) + typesOffSet - linkBaseAddress

// 获取typeDescriptor在程序运行中的地址
var typeDescAddress = typeDescOffset + mhHeaderPtr_IntRepresentation

// typeDescriptor结构体
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 methods: UInt32
}

// 将 typeDescriptor 的内存地址直接转换成指向 TargetClassDescriptor 结构体的指针
let classDescriptor = UnsafePointer<TargetClassDescriptor>.init(bitPattern: Int(exactly: typeDescAddress) ?? 0)?.pointee

if let name = classDescriptor?.name {
    // 获取name的偏移量地址
    let nameOffset = Int64(name) + Int64(typeDescOffset) + 8
    // 获取name在运行中的内存地址
    let nameAddress = nameOffset + Int64(mhHeaderPtr_IntRepresentation)
    if let cChar = UnsafePointer<CChar>.init(bitPattern: Int(nameAddress)){
        print(String(cString: cChar))
    }
}

// 获取属性
// 获取属性相关的filedDescriptor 在运行中的内存地址
let filedDescriptorRelaticveAddress = typeDescOffset + 4 * 4 + mhHeaderPtr_IntRepresentation

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 指针在的内容 就是FieldDescriptor 的偏移量
let fieldDescriptorOffset = UnsafePointer<UInt32>.init(bitPattern: Int(exactly: filedDescriptorRelaticveAddress) ?? 0)?.pointee

// 获取 FieldDescriptor 的在运行中的内存地址
let fieldDescriptorAddress = filedDescriptorRelaticveAddress + UInt64(fieldDescriptorOffset!)

// 将 FieldDescriptor 的内存地址直接转换成指向 FieldDescriptor 结构体的指针
let fieldDescriptor = UnsafePointer<FieldDescriptor>.init(bitPattern: Int(exactly: fieldDescriptorAddress) ?? 0)?.pointee

// 循环遍历属性
for i in 0..<fieldDescriptor!.numFields{
    // FieldRecord 结构体由 3个 4字节组成,并且保持3 * 4 = 12字节对齐
    let stride: UInt64 = UInt64(i * 3 * 4)
    // 计算 fieldRecord 的地址
    let fieldRecordAddress = fieldDescriptorAddress + stride + 16
    // 计算 fieldRecord 结构体中的 name 在程序运行中的内存地址
    let fieldNameRelactiveAddress = UInt64(2 * 4) + fieldRecordAddress - linkBaseAddress + mhHeaderPtr_IntRepresentation
    // 将上面地址的地址转换成指针,并且获取指向的内容 (偏移量)
    let nameOffset = UnsafePointer<UInt32>.init(bitPattern: Int(exactly: fieldNameRelactiveAddress) ?? 0)?.pointee
    
    // 获取 name 的地址
    let fieldNameAddress = fieldNameRelactiveAddress + UInt64(nameOffset!) - linkBaseAddress
    // 将 name 地址转换成指针
    if let cChar = UnsafePointer<CChar>.init(bitPattern: Int(fieldNameAddress)){
        // 打印指针内容
        print(String(cString: cChar))
    }
}


// 获取v-table
// 函数的结构体
struct TargetMethodDescriptor {
    var kind: UInt32
    var offset: UInt32
}

// 获取方法的数量
if let methods = classDescriptor?.methods {
    for i in 0..<methods {
        // 获取v-table的的首地址
        let VTableRelaticveAddress = typeDescOffset + 4 * 13 + mhHeaderPtr_IntRepresentation
        // 获取当前函数的地址
        let currentMethodAddress = VTableRelaticveAddress + UInt64(i) * UInt64(MemoryLayout<TargetMethodDescriptor>.size)
        // 将 当前函数 的内存地址直接转换成指向 TargetMethodDescriptor 结构体的指针
        let currentMethod = UnsafePointer<TargetMethodDescriptor>.init(bitPattern: Int(exactly: currentMethodAddress) ?? 0)?.pointee
        // 获取到imp的地址
        let impAddress = currentMethodAddress + 4 + UInt64(currentMethod!.offset) - linkBaseAddress
        print(impAddress);
    }
}

注意: 在 Xcode 13_dyld_get_image_header(0) 对比在 LLDB 中输入命令 image list,发现没有正确获取到程序运行的基地址,但是在 Xcode 12 中不会出现这样的问题。

_dyld_get_image_header(0)对比image list.png
发现 _dyld_get_image_header(0) 获取到的地址是 image list 中第三个元素的地址,目前还没找到解决办法,如果您正好知道请留意或者私信我,万分感谢。
  • 经过后面的研究这里找到一个方式获取当前程序运行的基地址
var mhHeaderPtr: UnsafePointer<mach_header>?
let count = _dyld_image_count()
for i in 0..<count {
    var excute_header = _dyld_get_image_header(i)
    if excute_header!.pointee.filetype == MH_EXECUTE {
        mhHeaderPtr = excute_header
        break
    }
}

就是循环遍历 _dyld_get_image_header 中的元素判断是不是 mach-o 的执行地址。

二:内存管理

Swift 中使用自动引用计数(ARC)机制来追踪和管理内存,通常情况下,Swift 内存管理机制会一直起作用,你无须自己来考虑内存的管理。ARC 会在类的实例不再被使用时,自动释放其占用的内存。

1. 强引用

class Person {
    let name: String
    init(name: String) {
        self.name = name
        print("\(name) is being initialized")
    }
    deinit {
        print("\(name) is being deinitialized")
    }
}

var p1: Person?
var p2: Person?
var p3: Person?

p1 = Person(name: "小明")

// 打印结果
小明 is being initialized

由于 Person 类的新实例被赋值给了 p1 变量,所以 p1Person 类的新实例之间建立了一个强引用。正是因为这一个强引用,ARC 会保证 Person 实例被保持在内存中不被销毁。
我们接着添加代码

p2 = p1
p3 = p1

现在这一个 Person 实例已经有三个强引用了。
将其中两个变量赋值 nil 的方式断开两个强引用(包括最先的那个强引用),只留下一个强引用,Person 实例不会被销毁

p1 = nil
p2 = nil

只有当最后一个引用被断开时 ARC 才会销毁它

p3 = nil

// 打印结果
小明 is being deinitialized

2. 弱引用

弱引用不会对其引用的实例保持强引用,因而不会阻止 ARC 销毁被引用的实例。这个特性阻止了引用变为循环强引用。声明属性或者变量时,在前面加上 weak 关键字表明这是一个弱引用。
因为弱引用不会保持所引用的实例,即使引用存在,实例也有可能被销毁。因此,ARC 会在引用的实例被销毁后自动将其弱引用赋值为 nil。并且因为弱引用需要在运行时允许被赋值为 nil,所以它们一定是可选类型。

class Person {
    var age: Int = 18
    var name: String = "小明"
}

weak var t = Person()

进入汇编代码

weak的汇编代码.png
我们可以看到这里的实质是调用了 swift_weakInit 函数,根据 Swift 源码的分析,其内部实现其实就是:一个对象在初始化的时候后是没有 SideTable (散列表)的,当我们创建一个弱引用的时候,系统会创建一个 SideTable
实质上 Swift 存在两种引用计算的布局方式
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
      }
    }   
  }

其中

  • InlineRefCountsSideTableRefCounts 共享当前模板类 RefCounts<T>.的实现。
  • InlineRefCountBitsSideTableRefCountBits 共享当前模板类 RefCountBitsT<bool>
  • InlineRefCounts 其实是一个 uint64_t 可以当引用计数也可以当Side Table 的指针
  • SideTableRefCounts 是一种名为 HeapObjectSideTableEntry 的结构体,里面也有 RefCounts 成员,内部是 SideTableRefCountBits ,其实就是原来的 uint64_t 加上一个存储弱引用数的 uint32_t

3. 无主引用

和弱引用类似,无主引用不会牢牢保持住引用的实例。和弱引用不同的是,无主引用在其他实例有相同或者更长的生命周期时使用。你可以在声明属性或者变量时,在前面加上关键字 unowned 表示这是一个无主引用。
但和弱引用不同,无主引用通常都被期望拥有值。所以,将值标记为无主引用不会将它变为可选类型,ARC 也不会将无主引用的值设置为 nil 。总之一句话就是,无主引用假定是永远有值的。

  • 如果两个对象的生命周期完全和对方没关系(其中一方什么时候赋值为 nil ,对对方没有影响),使用 weak
  • 如果能确保:其中一个对象销毁,另一个对象也要跟着销毁,这时候可以(谨慎)使用 unowned

4. 闭包循环引用

闭包会一般默认捕获外部的变量

var age = 18

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

// 打印结果
19

可以看出 闭包的内部对变量的修改将会改变外部原始变量的值

class Person {
    var age: Int = 18
    var name: String = "小明"
    
    var testClosure:(() -> ())?
    
    deinit {
        print("Person deinit")
    }
}

func testARC() {
    let t = Person()
    
    t.testClosure = {
        print(t.age)
    }
    
    print("end")
}

testARC()

// 打印结果
end

我们发现没有打印 Person deinit ,也就意味着 t 并没有被销毁,此时出现了循环引用。解决办法:就是使用捕获列表

func testARC() {
    let t = Person()
    
    t.testClosure = { [weak t] in
        t?.age += 1
    }
//    t.testClosure = { [unowned t] in
//        t.age += 1
//    }
}

5. 捕获列表

默认情况下,闭包表达式从起周围的范围捕获常量和变量,并强引用这些值。可以使用捕获列表来显式控制如何在闭包中捕获值。
在参数列表之前,捕获列表被写为用逗号括起来的表达式列表,并用方括号括起来。如果使用捕获列表,则即使省略参数名称,参数类型和返回类型,也必须使用 in 关键字。
创建闭包时,将初始化捕获列表中的条目。对于捕获列表中的每个条目,将常量初始化为在周围范围内具有相同名称的常量或变量的值。

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

// 打印结果
0
1.85

创建闭包时,内部作用域中的 age 会用外部作用域中的 age 的值进行初始化,但他们的值未以任何特殊方式连接。这意味着更改外部作用域中的 age 的值不会影响内部作用域中的 age 的值,也不会更改封闭内部的值,也不会影响封闭外的值。先比之下,只有一个名为 height 的变量-外部作用域中的 height - 因此,在闭包内部或外部进行的更改在两个均可见。

相关文章

  • Swift探索(四): 指针和内存管理

    一:指针 1. 指针的定义 Swift 中引用了某个引用类型实例的常量或变量,与 C 语言中的指针类似,不过它并不...

  • C 指针内存管理

    // C 指针的内存管理 // C 指针在 Swift中被冠以 unsafe 的另一个是无法对其进行自动的内存管理...

  • 【四】Swift-指针&内存管理

    目录 一、指针 1.为什么说指针是不安全的 2.指针类型 3.原始指针的使用 4.泛型指针的使用 5.内存绑定 二...

  • Swift 指针&内存管理

    指针 为什么说指针不安全 比如我们在创建一个对象的时候,是需要在堆分配内存空间的。但是这个内存空间的声明周期是有限...

  • Swift指针&内存管理

    一、指针    1、指针类型   Swift中的指针分为两类:指定数据类型的指针(typed pointer);未...

  • Swift指针|内存管理

    一、Swift指针 1.Swift指针简介 swift中的指针分为两类 typed pointer 指定数据类型指...

  • swift指针操作

    swift官方不建议使用指针,为了安全起见,而且使用比较麻烦,内存必须自己管理 1、直接创建指针 2、获取指针 可...

  • Swift底层探索(四):内存管理

    内存管理 Swift中使用自动引用计数(ARC)机制来追踪和管理内存。 通过 lldb直接查看refCounted...

  • 关情纸尾---OC-内存管理

    一、引用计数器 二、野指针和空指针 三、set方法的内存管理 四、property的内存管理(代替oc对象的set...

  • swift指针&内存管理-内存绑定

    swift提供了3种不同的API来绑定/重新绑定指针 assumingMemoryBound(to:) bindM...

网友评论

    本文标题:Swift探索(四): 指针和内存管理

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