Swift的指针和内存管理

指针

为什么指针不安全

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

指针类型

Swift中的指针分为两类, typed pointer指定数据类型指针,raw pointer未指定数据类型的指针(即原⽣指针)。基本上我们接触到的指针类型有⼀下⼏种:

SwiftObject-C说明
unsafePointer <T>const T *指针可变,所指向的内容都不可变
unsafeMutablePointer<T>T *指针及其所指向的内存内容均可变
unsafeRawPointerconst void *指针指向的内存区域未定
unsafeMutableRawPointervoid *指针指向的内存区域未定
unsafeBufferPointer<T>开辟连续的内存地址
unsafeMutableBufferPointer<T>
unsafeRawBufferPointer
unsafeMutableRawBufferPointer

下面我们就体验一下这几种类型的指针:

原生指针的使用

首先我们使用Raw Pointer来存储4个整形的数据,我们需要选取的是 UnsafeMutableRawPointer
下面我们输入一段代码

//存储4个Int 4 * 8 = 32byteCount 8字节alignment 对齐
let p = UnsafeMutableRawPointer.allocate(byteCount: 32, alignment: 8)

for i in 0..<4 {
    //存储 i
    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)")
}

打印结果为:
在这里插入图片描述
结果和我们想象的不一致,造成这个的原因,我们先从下面这段代码说起:

struct LLPerson {
    var age: Int = 18    //8字节
    var sex: Bool = true //1字节
}

print(MemoryLayout<LLPerson>.size)      //打印大小
print(MemoryLayout<LLPerson>.stride)    //打印步长
print(MemoryLayout<LLPerson>.alignment) //打印对齐

输出结果为:
在这里插入图片描述
size是结构体的实际大小,stride步长信息由于要8字节对齐,所以步长信息是16字节,就是在内存中连续存储LLPerson所需要的内存。
在上面的例子中,p是起始存储地址,在for循环中p.storeBytes(of: i, as: Int.self)就是无限循环向p地址存储i,所以打印的index0 value3i=3时存储进去的,其他的值都是未初始化数据的值。接下来我们改造代码:

//存储4个Int 4 * 8 = 32byteCount 8字节alignment 对齐
let p = UnsafeMutableRawPointer.allocate(byteCount: 32, alignment: 8)

for i in 0..<4 {
    //存储 i
//    p.storeBytes(of: i, as: Int.self)
	//advanced 移动指针地址 移动 i * Int类型步长
    p.advanced(by: i * MemoryLayout<Int>.stride).storeBytes(of: i, as: Int.self)
}

for i in 0..<4 {
    let value = p.load(fromByteOffset: i * MemoryLayout<Int>.stride, as: Int.self)
    print("index\(i) value\(value)")
}

//使用了allocate 需要使用deallocate销毁
p.deallocate()

打印结果为:

index0 value0
index1 value1
index2 value2
index3 value3

这种情况就是正确的。

泛型指针的使用

泛型指针相⽐较原⽣指针来说,其实就是指定当前指针已经绑定到了具体的类型,首先从一段代码看起:

var age = 18

age = withUnsafePointer(to: &age){ ptr in
    return ptr.pointee + 21
//    ptr.pointee + 21   也可以直接返回
}

print(age)   //打印39

使用withUnsafePointer,这种写法是可以修改age的值的,但是当我们在闭包内部修改ptr.pointee的值时,
在这里插入图片描述
编译器编译是不通过的,同时提醒我们他不是一个mutable,即不是一个可变的,那我们可以使用对应的withUnsafeMutablePointer,改造如下:

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

print(age)  //打印结果为39

在我们泛型指针的使用中,常用的方法有如下这些:
在这里插入图片描述
下面我们可以使用以下实例使用以下这些方法:
当我们使用UnsafeMutablePointer创建内存时

struct LLPerson {
    var age: Int
    var height: Double
}

var tPtr = UnsafeMutablePointer<LLPerson>.allocate(capacity: 5)
//和C函数的数组一样,使用tPtr[0]来存值
tPtr[0] = LLPerson(age: 18, height: 20.0)  
tPtr[1] = LLPerson(age: 19, height: 21.0)

defer {  //实际开发中可以用defer包裹
	tPtr.deinitialize(count: 5)  //和deallocate承兑出现的  抹去数值
	tPtr.deallocate()   //回收内存
}

这种方式和C语言中的数组一样,使用[]来存取值。同时我们还可以使用advanced这种方式来初始化值

tPtr.advanced(by: MemoryLayout<LLPerson>.stride).initialize(to: LLPerson(age: 18, height: 20.0))

指针读取macho中的属性名称

接下来,我们使用相关内容,通过指针读取macho中的属性名称。

import MachO

class LLPerson {
    var age: Int = 18
    var name: String = "LL"
}

var size: UInt = 0
// 获取__swift5_types在内存中的地址
var ptr = getsectdata("__TEXT", "__swift5_types", &size)
//print(ptr)
//获取macho header的起始地址
var mhHeaderPtr = _dyld_get_image_header(0)
//print(mhHeaderPtr)
//segmentcommand的地址
var setCommond64Ptr = getsegbyname("__LINKEDIT")
//print(sectCommond64Ptr)
//计算基地址
var linkBaseAddress: UInt64 = 0
if let vmaddr = setCommond64Ptr?.pointee.vmaddr, let fileOff = setCommond64Ptr?.pointee.fileoff {
    //虚拟内存地址 - 文件偏移量就是基地址
    linkBaseAddress = vmaddr - fileOff
}
//print(linkBaseAddress)

//获取__swift5_types的偏移量
var offset: UInt64 = 0
if let unwrappedPtr = ptr {
    let intRepresentation = UInt64(bitPattern: Int64(Int(bitPattern: unwrappedPtr)))
    //需要减去基地址
    offset = intRepresentation - linkBaseAddress
}

//print(offset)

//获取DataLo的内存地址
//类型转换
let mhHeader_intRepresentation = UInt64(bitPattern: Int64(Int(bitPattern: mhHeaderPtr)))

var dataLoAddress = mhHeader_intRepresentation + offset

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

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

//获取描述文件的偏移量 dataLo + 文件偏移量 - 基地址
let typeDescOffset = UInt64(dataLoContent!) + offset - linkBaseAddress
//获取描述文件的地址 偏移量 + macho的起始地址
let typeDescAddress = typeDescOffset + mhHeader_intRepresentation
//print(typeDescAddress)

//前面获取到的描述文件的结构
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 size: UInt32
}

//将typeDescAddress地址按照TargetClassDescriptor读取获取相关结构
let classDescriptor = UnsafePointer<TargetClassDescriptor>.init(bitPattern: Int(exactly: typeDescAddress) ?? 0)?.pointee
//print(classDescriptor)

if let name = classDescriptor?.name {
    // 拿到结构中name的偏移量  name的值 + typeDescOffset(描述文件的偏移量) + flags(4字节) + parent(4字节)
    let nameOffset = Int64(name) + Int64(typeDescOffset) + 8
//    print(nameOffset)
    // 拿到name的地址 偏移量 + 程序运行起始地址
    let nameAddress = nameOffset + Int64(mhHeader_intRepresentation)
//    print(nameAddress)
    //以CChar形式读取
    if let cChar = UnsafePointer<CChar>.init(bitPattern: Int(nameAddress)) {
        print(String(cString: cChar))
    }
}

//拿到filedDescriptor的相对地址 typeDescOffset(描述文件的偏移量) + flags(4字节) + parent(4字节)+ name(4字节) + accessFunctionPointer(4字节) + 程序运行基地址
let fieldDescriptorRelativeAddress = typeDescOffset + 16 + mhHeader_intRepresentation
//print(fieldDescriptorRelativeAddress)

//filedDescriptor 内存结构
struct FieldDescriptor {
    var mangledTypeName: Int32
    var superclass: Int32
    var Kind: UInt16
    var fieldRecordSize: UInt16
    var numFields: UInt32   //fieldRecords的个数
    // var fieldRecords: [FieldRecord]
}

//fieldRecords 内存结构
struct FieldRecord {
    var Flags: UInt32
    var mangledTypeName: Int32
    var fieldName: UInt32
}

//偏移地址 类型转换
let fieldDescriptorOffset = UnsafePointer<UInt32>.init(bitPattern: Int(exactly: fieldDescriptorRelativeAddress) ?? 0)?.pointee
//print(fieldDescriptorOffset)

// 获取真实地址
let fieldDescriptorAddress = fieldDescriptorRelativeAddress + UInt64(fieldDescriptorOffset!)
//print(fieldDescriptorAddress)

// 转换成FieldDescriptor内存结构
let fieldDescriptor = UnsafePointer<FieldDescriptor>.init(bitPattern: Int(exactly: fieldDescriptorAddress) ?? 0)?.pointee
//print(fieldDescriptor)

// 读取属性
for i in 0..<fieldDescriptor!.numFields {
    // 计算相应偏移 Flags(4字节) + mangledTypeName(4字节)+ fieldName(四字节)= 12
    let stride: UInt64 = UInt64(i * 12)
    //拿到每个FieldRecord的地址 fieldDescriptorAddress + mangledTypeName(4字节)+ superclass(4字节)+ Kind(2字节)+ fieldRecordSize(2字节)+ 每个FieldRecord的偏移
    let fieldRecordAddress = fieldDescriptorAddress + 16 + stride
    //获取fieldName的相对地址  Flags(4字节)+ mangledTypeName(4字节)+ 相对地址 - 基地址 + 程序运行起始地址
    let fieldNameRelactiveAddress = UInt64(2 * 4) + fieldRecordAddress - linkBaseAddress + mhHeader_intRepresentation
    //转换格式
    let offset = UnsafePointer<UInt32>.init(bitPattern: Int(exactly: fieldNameRelactiveAddress) ?? 0)?.pointee
    //获取内存地址 fieldName的相对地址  + 偏移量 - 基地址
    let fieldNameAddress = fieldNameRelactiveAddress + UInt64(offset!) - linkBaseAddress
    // cChar类型读取
    if let cChar = UnsafePointer<CChar>.init(bitPattern: Int(fieldNameAddress)){
        print(String(cString: cChar))
    }
}

接下来,我们通过对比macho文件对比获取的值
在这里插入图片描述
通过getsectdata("__TEXT", "__swift5_types", &size)获取__swift5_types的地址。

在这里插入图片描述
通过getsegbyname("__LINKEDIT")获取segment_commond_64的地址,从segment_command_64的结构:

public struct segment_command_64 {

    public var cmd: UInt32 /* for 64-bit architectures */ /* LC_SEGMENT_64 */

    public var cmdsize: UInt32 /* includes sizeof section_64 structs */

    public var segname: (CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar) /* segment name */
	//VM Address
    public var vmaddr: UInt64 /* memory address of this segment */

    public var vmsize: UInt64 /* memory size of this segment */
	//File Offset
    public var fileoff: UInt64 /* file offset of this segment */

    public var filesize: UInt64 /* amount to map from the file */

    public var maxprot: vm_prot_t /* maximum VM protection */

    public var initprot: vm_prot_t /* initial VM protection */

    public var nsects: UInt32 /* number of sections in segment */

    public var flags: UInt32 /* flags */

    public init()

    public init(cmd: UInt32, cmdsize: UInt32, segname: (CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar), vmaddr: UInt64, vmsize: UInt64, fileoff: UInt64, filesize: UInt64, maxprot: vm_prot_t, initprot: vm_prot_t, nsects: UInt32, flags: UInt32)
}

拿到vmaddr(虚拟内存地址)和fileoff(文件偏移量),相减就可以得到基地址。

在这里插入图片描述
通过减去基地址拿到__swift5_types的偏移量。

在这里插入图片描述
通过程序地址加上偏移量拿到dataLo的地址,从地址拿到相关内容。

在这里插入图片描述
dataLo加上文件偏移再减去基地址,就拿到了描述文件的起始位置。

在这里插入图片描述
通过TargetClassDescriptor结构读取数据。

在这里插入图片描述
从结构中获取name进行读取,同理属性名称也通过该结构读取出来进行打印。
这就是我们从macho中读取的数据。

内存绑定

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

1、assumingMemoryBound(to:)
在这里插入图片描述
传入元组时,会报错。但当我们使用assumingMemoryBound时,

func testPoint(_ p: UnsafePointer<Int>) {
    print(p[0])
    print(p[1])
}
// 定义一个元组
let tuple = (10, 20)
withUnsafePointer(to: tuple) { (tuplePtr: UnsafePointer<(Int, Int)>) in
    testPoint(UnsafeRawPointer(tuplePtr).assumingMemoryBound(to: Int.self))
}

这种情况就可以了。
有些时候我们处理代码的过程中,只有原始指针(没有保留指针类型),但此刻对于处理代码的我们来说明确知道指针的类型,我们就可以使⽤ assumingMemoryBound(to:) 来告诉编译器预期的类型。 (注意:这⾥只是让编译器绕过类型检查,并没有发⽣实际类型的转换)。

2、bindMemory(to: capacity:)

func testPoint(_ p: UnsafePointer<Int>) {
    print(p[0])
    print(p[1])
}
// 定义一个元组
let tuple = (10, 20)
withUnsafePointer(to: tuple) { (tuplePtr: UnsafePointer<(Int, Int)>) in
    testPoint(UnsafeRawPointer(tuplePtr).bindMemory(to: Int.self, capacity: 1))
}

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

3、withMemoryRebound(to: capacity: body:)

func testPoint(_ p: UnsafePointer<Int8>) {
    print(p)
}

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

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

内存管理

强引用

Swift 中使⽤⾃动引⽤计数(ARC)机制来追踪和管理内存。首先我们从refCounts着手:

class LLPerson {
    var age: Int = 8
    var name: String = "LL"
}

var p = LLPerson()
//固定写法 打印p的内存指针
print(Unmanaged.passUnretained(p as AnyObject).toOpaque())

在这里插入图片描述
我们通过对源码的分析,可以得出refCount一下结构:
在这里插入图片描述
我们可以通过实例观看一下变化:
在这里插入图片描述
通过位移运算左移33位,高34位,每次加2

同时,当我们使用强引用时会造成循环引用。下面我们来分析一下:

class LLTeacher {
    var age: Int = 18
    var name: String = "LL"
    var subject: LLSubject?
    
}

class LLSubject{
    var subjectName: String
    var subjectTeacher: LLTeacher
    
    init(_ subjectName: String, _ subjectTeacher: LLTeacher) {
        self.subjectName = subjectName
        self.subjectTeacher = subjectTeacher
    }
}

var t = LLTeacher()
var subject = LLSubject.init("Swift进阶", t)
t.subject = subject

上⾯做这段代码是不是就产⽣了两个实例对象之前的强引⽤啊,Swift 提供了两种办法⽤来解决你在使⽤类的属性时所遇到的循环强引⽤问题:弱引⽤( weak reference )和⽆主引⽤( unowned reference )

弱引用

弱引⽤不会对其引⽤的实例保持强引⽤,因⽽不会阻⽌ ARC 释放被引⽤的实例。这个特性阻⽌了引⽤变为循环强引⽤。声明属性或者变量时,在前⾯加上 weak 关键字表明这是⼀个弱引⽤。
由于弱引⽤不会强保持对实例的引⽤,所以说实例被释放了弱引⽤仍旧引⽤着这个实例也是有可能的。 因此,ARC 会在被引⽤的实例被释放是⾃动地设置弱引⽤为 nil 。由于弱引⽤需要允许它们的值为 nil , 它们⼀定得是可选类型。

class LLTeacher {
    var age: Int = 18
    var name: String = "LL"
}

weak var t = LLTeacher()

在这里插入图片描述
只是用weak时,调用了swift_weakInit,相当于定义了⼀个 WeakRefrence 对象,创建了一个散列表结构。
是⼀种类名为 HeapObjectSideTableEntry 的结构,⾥⾯也有 RefCounts 成员,是内部是 SideTableRefCountBits,其实就是原来的 uint64_t 加上⼀个存储弱引⽤数的 uint32_t

Unowned

和弱引⽤类似,⽆主引⽤不会牢牢保持住引⽤的实例。但是不像弱引⽤,总之,⽆主引⽤假定是永远有值的。当为nil时会崩溃。
在这里插入图片描述
根据苹果的官⽅⽂档的建议。当我们知道两个对象的⽣命周期并不相关,那么我们必须使⽤ weak。相反,⾮强引⽤对象拥有和强引⽤对象同样或者更⻓的⽣命周期的话,则应该使⽤ unowned

Weak VS unowned

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

打印Vtable

// 原来得到的结构
struct TargetMethodDescriptor {
    var Flags: UInt32  //4字节
    //存储的offset
    var Impl: UInt32  //4字节
}
//classDescriptor!.size存储着方法的个数
for i in 0..<classDescriptor!.size{
	//偏移量 = 描述文件偏移量 + 描述文件结构尺寸 + i * 方法size
    let targetMethodOffset = Int(typeDescOffset) + MemoryLayout<TargetClassDescriptor>.size + Int(i) * MemoryLayout<TargetMethodDescriptor>.size
    //运行起始地址 + 偏移量 = 方法地址
    let targetMethodAddress = mhHeader_intRepresentation + UInt64(targetMethodOffset)
    // 格式化数据结构
    let targetMethod = UnsafePointer<TargetMethodDescriptor>.init(bitPattern: Int(exactly: targetMethodAddress) ?? 0)?.pointee
    // 方法地址 = 地址 + Flags(4字节) + impl偏移量 - 基地址
    let impAddress = targetMethodAddress + 4 + UInt64(targetMethod!.Impl) - linkBaseAddress
    //打印出来就可以
    let imp = IMP(bitPattern: UInt(impAddress))
    LLTest.callImp(imp!)
}

// 相当于执行方法,方法中写入打印函数名称
+ (void)callImp:(IMP)imp {
    imp();
}

一些参考swiftDump

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值