Swift指针类型
swift的指针类型分为两类,指定数据类型指针typed pointer
和未指定数据类型指针raw pointer
,其中未指定数据类型的指针也叫做原生指针。
- 指定数据类型指针:用
unsafePointer<T>
表示,T
指代泛型。 - 未指定数据类型指针:用u
nsafeRawPointer
表示。
常用的几种指针类型
我们比较经常使用的swift指针主要有以下几种
-
unsafePointer<T>
在OC中等同于const T*
代表这个指针及所指向的内容都不可变。 -
unsafeMutablePointer<T>
在OC中等同于T * 代表这个指针及其所指向的内存内容均可变。 -
unsafeRawPointer
在OC中等同于const void * 代表这个指针指向的内存区域未定。 -
unsafeMutableRawPointer
在OC中等同于void * 代表这个指针指向的内存区域未定。
Swift指针的安全性
在Swift指针中,我们发现使用了unsafe
做前缀修饰,说明这个指针是不安全的。它为什么是不安全的呢?主要有以下三点:
- 我们在创建⼀个对象的时候,是需要在堆分配内存空间的。但是这个内存空间的声明周期是有限的,也就意味着如果我们使⽤指针指向这块内容空间,如果当前内存空间的⽣命周期到了(引⽤计数为0),那么我们当前的指针就变成了未定义的⾏为,也就可能会成为一个野指针。
- 我们创建的内存空间是有边界的,⽐如我们创建⼀个⼤⼩为10的数组,这个时候我们通过指针访问了index = 11的位置,这个时候就会越界,这个指针也就会访问了⼀个未知的内存空间。
- 指针类型与内存的值类型不⼀致,也是不安全的。 比如我们创建了一个
Int
类型的指针,如果这个指针指向了Int16
类型的指针,就会造成数据精度缺失。
原生指针的使用
原生指针常用API
这里我选择UnsafeMutableRawPointer
里面的API做说明
1.创建可变指针
///byteCount:需要多少内存空间
///alignment:内存对齐大小
allocate(byteCount: Int, alignment: Int) -> UnsafeMutableRawPointer
2.存储数据
//of:数据大小
//as:数据类型
storeBytes(of value:T, as:T.type)
3.读取数据
//fromByteOffset: 读取数据偏移大小
//as:数据类型
load(fromByteOffset: as:)
4.移动指针到下一个存储的位置
//by: 可以移动的步长
advanced(by:)
5.销毁可变指针
deallocate()
原生指针案例
let p = UnsafeMutableRawPointer.allocate(byteCount: 32, alignment: 8)
for i in 0..<4 {
p.storeBytes(of: i, as: Int.self)
print(p)
}
for i in 0..<4 {
let value = p.load(fromByteOffset: i * 8, as: Int.self)
print("index \(i), value \(value)")
}
p.deallocate()
//打印结果
index 0, value 3
index 1, value 0
index 2, value 16
index 3, value 0
当我们打印出来结果后,和我们预期想的结果不对。我们在存数据的地方先打印出来p,再使用lldb分析。打印结果如下:
我们可以看到,打印出来的p的内存地址是同一个地址,也就是说我们存储数据的时候把数据都存储在同一个地址里面,而读取数据的时候是有偏移的。因此存储数据的时候也要偏移一定的步长才可以。
我们把存储数据的代码修改一下,看一下结果。
for i in 0..<4 {
p.advanced(by: MemoryLayout<Int>.stride * i).storeBytes(of: i, as: Int.self)
}
//打印结果
index 0, value 0
index 1, value 1
index 2, value 2
index 3, value 3
这次的结果是正确的,我们先通过advanced(by:)
来获取到偏移后的指针地址,然后存储数据,这次是要存储4个Int
类型的数据,所以我们根据Int
类型的步长来进行偏移,然后存储数据。
Swift内存布局
swift提供了一个MemoryLayout
枚举类型来帮助我们查看数据类型的内存布局。
MemoryLayout<T>.size
提供实际存储该数据类型所需要的字节数。MemoryLayout<T>.stride
提供连续存储多个元素指针间的间隔。MemoryLayout<T>.alignment
需要多少内存(其值的倍数)才能将完全对齐的内容保存在内存缓冲区中,也就是内存对齐的方式。 想看Swift里面具体如何进行内存布局,可以参考这篇文章
泛型指针的使用
对于范型指针,其实就是原生指针制定了指针类型,因为确定了类型,就固定了存储类型的stride
和alignment
,我们就不需要通过load
和store
的方式存取,而是通过其内置的 pointee
变量来存取数据。
获取UnsafePointer
的⽅式有两种。⼀种⽅式就是通过已有变量获取
var age = 18
//通过withUnsafePointer直接访问age的地址
withUnsafePointer(to: &age) { ptr in
print(ptr)
}
//打印结果
0x00000001000081b0
修改已有变量的值
var age = 18
//如果想要修改当前age,使用withUnsafePointer只能返回当前修改的值,无法直接修改age的值
age = withUnsafePointer(to: &age) { ptr in
return ptr.pointee + 21
}
//打印结果
39
var b = 42
//如果想直接修改变量的值,可以使用withUnsafeMutablePointer
withUnsafeMutablePointer(to: &b) { ptr in
ptr.pointee += 100
}
print(b)
//打印结果
142
另外一种方式是直接分配内存
var age = 10
//分配一块Int类型的内存空间,这时候还没有开始初始化
let tPtr = UnsafeMutablePointer<Int>.allocate(capacity: 1)
//初始化分配的内存空间
tPtr.initialize(to: age)
print(tPtr.pointee)
//释放已经初始化的内存空间,和initialize成对出现
tPtr.deinitialize(count: 1)
//销毁这块分配好的内存空间,和allocate成对出现
tPtr.deallocate()
我们可以通过以下这张图片来更直观的了解一下:
对于结构体和类,我们应该怎么去初始化指针并存储值呢?
struct LGStruct {
var age: Int
var height: Double
}
var tPtr = UnsafeMutablePointer<LGStruct>.allocate(capacity: 5)
tPtr[0] = LGStruct(age: 18, height: 1.50)
tPtr[1] = LGStruct(age: 20, height: 1.80)
tPtr.deinitialize(count: 5)
tPtr.deallocate()
我们也可以这样初始化
var tPtr = UnsafeMutablePointer<LGStruct>.allocate(capacity: 5)
tPtr.initialize(to: LGStruct(age: 18, height: 1.50))
tPtr.advanced(by: MemoryLayout<LGStruct>.stride).initialize(to: LGStruct(age: 20, height: 1.80))
tPtr.deinitialize(count: 5)
tPtr.deallocate()
内存绑定
在Swift中提供了三种不同的API来绑定指针,或者重新绑定指针
-
assumingMemoryBound(to:) 有些时候我们处理代码的过程中,只有原始指针(没有保留指针类型),但此刻对于处理代码的我们来说明确知道指针的类型,否则就会报错,比如以下代码:
这时我们就可以使⽤
assumingMemoryBound(to:)
来告诉编译器预期的类型。需要注意的是,这⾥只是让编译器绕过类型检查,并没有发⽣实际类型的转换。 -
bindMemory(to: capacity:) ⽤于更改内存绑定的类型,如果当前内存还没有类型绑定,则将⾸次绑定为该类型;否则重新绑定该类型,并且内存中所有的值都会变成该类型。
-
withMemoryRebound(to: capacity: body:) 当我们在给外部函数传递参数时,不免会有⼀些数据类型上的差距。如果我们进⾏类型转换,必然要来回复制数据;这个时候我们就可以使⽤
withMemoryRebound(to: capacity: body:)
来临时更改内存绑定类型。当离开当前的作用域就会失效,重新绑定为原始类型。这可以将临时类型指针的访问和其他代码的作用域分开。
指针案例练习
使用指针读取类的属性名称
我们在上面Swift进阶(三)—— 属性中通过Mach-o文件得到了一个类的属性信息,这次我们使用指针来读取类的属性名称。
首先,我们先定义一个LGTeacher
类,里面只有age
和name
两个属性
class LGTeacher {
var age: Int = 18
var name: String = "FY"
}
我们先去获取程序运行的随机ASLR
地址
var mhHeaderPtr = _dyld_get_image_header(0)
这里我因为使用了MacOS程序,所以用上面代码读取出来的ASLR
地址是和通过image lsit
的lldb指定读取的ASLR
地址是一致的。如果使用了真机运行这段代码,在iOS15的系统中读取出来的地址,和通过image lsit
的lldb指定读取的第三个是一致的。所以你需要改成代码_dyld_get_image_header(3)
。
或者通过以下API去调用。
var excute_header: UnsafePointer<mach_header>?
let count = _dyld_image_count()
print("count (count)")
for i in 0..<count{
let mhHeaderPtr = _dyld_get_image_header(i)
if mhHeaderPtr!.pointee.filetype == MH_EXECUTE {
excute_header = mhHeaderPtr
}
}
print("excute_header (excute_header)")
再去获取Mach-O
文件中__LINKEDIT
的信息,然后得到这个文件的内存地址和文件偏移量,通过内存地址和偏移量可以得到这个虚拟内存的基地址。
var segment_command_linkedit = getsegbyname("__LINKEDIT")
var linkBaseAddress:UInt64 = 0
if let vmaddr = setCommond64Ptr?.pointee.vmaddr, let fileOff = setCommond64Ptr?.pointee.fileoff {
linkBaseAddress = vmaddr - fileOff
}
接着我们获取Mach-O
文件中__swift5_types
字段的内存地址,这里存放的是Swift
结构体、枚举、类的Descriptor
信息。通过代码获取的内存地址是有添加了虚拟内存的基地址的,因此要减去基地址获得这个__swift5_types
字段的内存偏移地址。
var size: UInt = 0
var ptr = getsectdata("__TEXT", "__swift5_types", &size)
var offset:UInt64 = 0
if let unwrappedPtr = ptr {
//把指针类型转成UInt64类型
let intRepresentation = UInt64(bitPattern: Int64(Int(bitPattern: unwrappedPtr)))
offset = intRepresentation - linkBaseAddress
}
再去获取__swift5_types
字段的Data LO
的内存地址。
//Data LO 内存地址
//把ASLR地址转成UInt64类型
let mhHeaderPtr_IntRepresentation = UInt64(bitPattern: Int64(Int(bitPattern: mhHeaderPtr)))
//获取dataLO在内存中存储的地址
var dataLOAddress = mhHeaderPtr_IntRepresentation + offset
var dataLoAddressPtr = withUnsafePointer(to: &dataLOAddress) { return $0 }
//前面代码里面只有一个类,因此dataLO地址的前四个字节就是我们这个类的ClassDescriptor信息的偏移地址。
var dataLoContent = UnsafePointer<UInt32>.init(bitPattern: Int(exactly: dataLOAddress) ?? 0)?.pointee
通过上面得到的ClassDescriptor
信息的偏移地址,我们可以计算出ClassDescriptor
信息的真实内存地址。
let typeDescOffset = UInt64(dataLoContent!) + offset - linkBaseAddress
var typeDescAddress = typeDescOffset + mhHeaderPtr_IntRepresentation
print(typeDescAddress)
通过 Swift进阶(二) —— 方法探究里面我们可以知道ClassDescriptor
的数据结构,因此我们现在定义一个这样的结构体。
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
接下来我们尝试去获取这个类的名称。
//创建一个TargetClassDescriptor类型的指针,指向这个类的ClassDescriptor内存地址。
let classDescriptor = UnsafePointer<TargetClassDescriptor>.init(bitPattern: Int(exactly: typeDescAddress) ?? 0)?.pointee
if let name = classDescriptor?.name {
/*类名属性在classDescriptor中排在flags和parent后面,这两个属性都是UInt32类型,
因此classDescriptor指针要往后移动8个字节,加上类名属性的偏移地址,就是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))
}
}
//打印出来的类名
LGTeacher
在Swift进阶(三)—— 属性中,我们知道属性存储在fieldDescriptor
这个属性中,因此我们先获取fieldDescriptor
的内存地址。ClassDescriptor
的内存地址往后移动16个字节。
let fieldDescriptorRelaticveAddress = typeDescOffset + 16 + mhHeaderPtr_IntRepresentation
从上面文章中我们得到了FieldDescriptor
和FieldRecord
的具体数据结构,我们把它们用结构体表示。
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的内存地址偏移量
let fieldDescriptorOffset = UnsafePointer<UInt32>.init(bitPattern: Int(exactly: fieldDescriptorRelaticveAddress) ?? 0)?.pointee
//获取fieldDescriptor的内存地址
let fieldDescriptorAddress = fieldDescriptorRelaticveAddress + UInt64(fieldDescriptorOffset!)
//创建一个FieldDescriptor类型的指针指向fieldDescriptor
let fieldDercriptor = UnsafePointer<FieldDescriptor>.init(bitPattern: Int(exactly: fieldDescriptorAddress) ?? 0)?.pointee
根据fieldDercriptor的numFields属性判断有多少个fieldRecord。
for i in 0..<fieldDercriptor!.numFields {
//每个fieldRecord占用12字节内存,因此每次访问一个fieldRecord,指针步长需要移动12字节
let stride: UInt64 = UInt64(i * 12)
//属性列表filedRecords的内存偏移首地址为fieldDescriptor往后偏移16字节
let fieldRecordAddress = fieldDescriptorAddress + 16 + stride
//获取每个fieldName属性的偏移地址,为每个fieldRecord首地址往后偏移8个字节
let fieldNameRelactiveAddress = UInt64(2 * 4) + fieldRecordAddress - linkBaseAddress + mhHeaderPtr_IntRepresentation
//获取属性名称存储在内存的地址偏移量
let offset = UnsafePointer<UInt32>.init(bitPattern: Int(exactly: fieldNameRelactiveAddress) ?? 0)?.pointee
//获取属性名称存储在内存的实际地址
let fieldNameAddress = fieldNameRelactiveAddress + UInt64(offset!) - linkBaseAddress
if let cChar = UnsafePointer<CChar>.init(bitPattern: Int(fieldNameAddress)) {
print(String(cString: cChar))
}
}
//打印结果
age
name
使用指针获取类的方法名称
我们现在来使用指针获取类的方法名。
首先,我们定义一个方法类
class LGTeacher {
func test() {
}
func test1() {
}
func test2() {
}
}
通过 Swift进阶(二) —— 方法探究里面我们可以知道方法名的内存地址存储在TargetClassDescriptor
类的size
属性后面,所以我们可以按照上面的方法去获取到TargetClassDescriptor
类的内存地址。然后根据这个内存地址来获取用来存储方法名称的V_Table
属性,V_Table
属性可以用以下的结构体来表示:
struct VTable {
var kind: UInt32
var offset: UInt32
}
//因为我们上面定义一个类的时候,只有方法名,没有属性,因此可以根据size属性获取这个类里面有多少方法。
let numOfVTable = classDescriptor?.size
for i in 0 ..< numOfVTable! {
//计算每个VTable的内存地址偏移量。
let vTableOffset = Int(typeDescOffset) + MemoryLayout<TargetClassDescriptor>.size + Int(i) * MemoryLayout<VTable>.size
//获取每个VTable的内存地址
let vTableAddress = UInt64(vTableOffset) + mhHeaderPtr_IntRepresentation
let vTable = UnsafePointer<VTable>.init(bitPattern: Int(exactly: vTableAddress) ?? 0)?.pointee
//获取Imp的内存地址,也就是VTable的offset属性的内存地址
let ImpAddress = vTableAddress + 4 + UInt64(vTable!.offset) - linkBaseAddress
LGTest.callImp(IMP(bitPattern: UInt(ImpAddress))!)
}
//打印结果
test
test1
teat2
在上面代码中LGTest是一个OC类,我们通过OC的方法调用IMP,来打印方法信息。代码如下:
@interface LGTest : NSObject
+ (void)callImp: (IMP)imp;
@end
@implementation LGTest
+ (void)callImp:(IMP)imp {
imp();
}
@end