1、不安全性
- 野指针
在创建⼀个对象的时候,是需要在堆分配内存空间的。但是这个内存空间的声明周期是有限的,也就意味着如果使⽤指针指向这块内容空间,如果当前内存空间的⽣命周期到了被销毁(引⽤计数为0),那么我们当前的指针就变成了未定义的⾏为,也就是野指针。 - 越界
系统在分配内存的时候是有一定大小的,比如数组的大小是 10 ,如果通过指针访问了 index 为 11 的位置则会访问到未知的内存空间。 - 类型不同
系统分配的内存空间都是有一定类型的,指针的类型可能和内存空间值的类型不一致,这也是不安全 ,比如 一个Int8 类型的指针 指向了一个 Int类型的数据,就可能会精度缺失。
2、基础认知
在使用Swift指针时,内存管理需要手动管理,也就是创建的指针在最后需要调用 deallocate。
内存布局方式的三个重要概念:
- size 实际需要的大小(MemoryLayout<T>.size)
- stride 步长,连续存储多个元素指针间的间隔,也可以理解为系统最终分配的大小(MemoryLayout<T>.stride)
- alignment 内存对齐的基数,最终分配的内存空间大小是它的倍数(MemoryLayout<T>.alignment)
3 、类型
Swift中的指针分为两大类, typed pointer 指定数据类型指针, raw pointer 未指定数据类型的指针(原⽣指针)。根据可不可变,内存连续与否等情况可以细分8种:
Swift | Objective-C | 说明 |
---|---|---|
unsafePointer<T> | const T * | 指针及所指向的内容都不可变 |
unsafeMutablePointer<T> | T * | 指针及其所指向的内存内容均可变 |
unsafeRawPointer | const void * | 指针指向的内存区域未定 |
unsafeMutableRawPointer | void * | 指针指向的内存区域未定,指向的内存可变 |
unsafeBufferPointer<T> | 指针指向连续的内存空间,指针及所指向的内容都不可变 | |
unsafeMutableBufferPointer< T> | 指针指向连续的内存空间,指针及其所指向的内存内容均可变 | |
unsafeRawBufferPointer | 指针指向一段连续的内存区域未定 | |
unsafeMutableRawBufferPointer | 指针指向一段连续的内存区域未定,指向的内存可变 |
3.1 原始指针
/*
案例:
用原生指针存储 4 个整形的数据,使用UnsafeMutableRawPointer
步骤:
1、开辟内存空间
2、调用storeBytes方法存储当前的整形数值
3、调用load方法加载当前内存当中
*/
//实际大小
let intSize = MemoryLayout<Int>.size;
//内存对齐基数
let intAlignment = MemoryLayout<Int>.alignment;
//内存对齐后的大小
let intStride = MemoryLayout<Int>.stride;
/*
allocate(byteCount:,alignment:):创建一个指针
- byteCount: 需要多少内存空间,即总的字节大小,应该使用内存对齐后的大小
- alignment: 内存对齐的基数,按照它的倍数对齐
*/
let p = UnsafeMutableRawPointer.allocate(byteCount: intStride * 4, alignment: intAlignment);
/*
advanced(by:) : 移动指针到下一个存储的位置
- by: 移动的步长
storeBytes(of: , as: ) : 存储数据
- of: 数据值
- as: 数据类型
*/
for i in 0..<4{
p.advanced(by: i * intStride).storeBytes(of: i, as: Int.self)
}
/*
load(fromByteOffset: , as: ) : 存储数据
- fromByteOffset: 读取数据移动的步长
- as: 数据类型
*/
for i in 0..<4{
let value = p.load(fromByteOffset: i * intStride, as: Int.self)
print("Int at \(i):\(value)")
}
print(p)
//销毁p指向的空间
p.deallocate();
/*
打印结果:
Int at 0:0
Int at 1:1
Int at 2:2
Int at 3:3
*/
通过上面的例子,可以发现Int的stride为8,同时在内存上,p的存储为连续的Int的stride(8字节)存放。
- 8字节打印说明:因为x/8g是16进制打印8段内存,16进制是4位,一个字节占8位,所以两个16进制就可以表示一个字节,打印的内存结构可知是8字节。
3.2 泛型指针
对于泛型指针,其实就是原生指针绑定了具体的类型,因为确定了类型,就固定了存储类型的 stride 和 alignment,我们就不需要通过 load 和 store 的方式存取,而是通过其内置的 pointee变量来存取。
获取的方式有两种:
- 通过已有变量获取
var age = 18
/*
通过返回当前的修改来修改age
withUnsafePointer<T, Result>(to value: T, _ body: (UnsafePointer<T>)
- value: 初始化的值.
- body: 可以操作指针的闭包表达式,withUnsafePointer<T>,无法直接在闭包中修改
*/
age = withUnsafePointer(to: age, { ptr in
return ptr.pointee + 12
})
print("first age = \(age)")
/*
直接操作pointee变量来修改age的值
withUnsafeMutablePointer<T, Result>(to value: inout T, _ body: (UnsafeMutablePointer<T>)
- value: 初始化的值.
- body: 可以操作指针的闭包表达式,UnsafeMutablePointer<T>直接在闭包中修改
*/
withUnsafeMutablePointer(to: &age) { ptr in
ptr.pointee += 12
}
print("second age = \(age)")
/*
打印结果
first age = 30
second age = 42
*/
- 直接分配内存
var age = 18
/*
allocate(capacity: ) : 分配一块 类型 的内存空间,此时还没有被初始化
- capacity: 分配多少个 类型 大小的连续空间
*/
let ptr = UnsafeMutablePointer<Int>.allocate(capacity: 1)
//初始化分配内存空间,把age拷贝到内存中
ptr.initialize(to: age)
//直接通过访问pointee属性来访问
ptr.pointee += 12
print(ptr.pointee)
//数据清零
ptr.deinitialize(count: 1)
//销毁内存空间
ptr.deallocate()
/*
打印结果:
30
*/
注意:allocate 和 deallocate 、initialize 和 deinitialize 都是成对出现的,如图:
/*
案例:两种不同的方式初始化泛型指针
*/
struct Person {
var age: Int
let name: String
}
let ptr = UnsafeMutablePointer<Person>.allocate(capacity: 2)
//方式一:下标的模式直接访问
ptr[0] = Person(age: 18, name: "张三")
ptr[1] = Person(age: 20, name: "李四")
print(ptr[0])
print(ptr[1])
ptr.deinitialize(count: 2)
ptr.deallocate()
let ptr2 = UnsafeMutablePointer<Person>.allocate(capacity: 4)
//方式二:initialize
ptr2.initialize(to: Person(age: 18, name:"张三"))
//advanced 偏移的方式
ptr2.advanced(by: 1).initialize(to: Person(age: 20, name: "李四"))
//successor 返回下一个连续值的方式(也就是下一个容量的起始地址)
ptr2.successor().successor().initialize(to: Person(age: 22, name: "赵五"))
//直接内存平移的方式(移动一个ptr2 的类型大小)
(ptr2 + 3).initialize(to: Person(age: 24, name: "钱六"))
print(ptr2.advanced(by: 0).pointee)
print(ptr2.advanced(by: 1).pointee)
print(ptr2.advanced(by: 2).pointee)
print(ptr2.advanced(by: 3).pointee)
ptr2.deinitialize(count: 4)
ptr2.deallocate()
/*
打印结果
Person(age: 18, name: "张三")
Person(age: 20, name: "李四")
Person(age: 22, name: "赵五")
Person(age: 24, name: "钱六")
*/
/*
pt2的内存打印结果:
x/12g 0x0000000109b06070
0x109b06070: 0x0000000000000012 0x000089b8e4a0bce5
0x109b06080: 0xa600000000000000 0x0000000000000014
0x109b06090: 0x00009b9be58e9de6 0xa600000000000000
0x109b060a0: 0x0000000000000016 0x000094bae4b5b5e8
0x109b060b0: 0xa600000000000000 0x0000000000000018
0x109b060c0: 0x0000ad85e5b192e9 0xa600000000000000
*/
这里有一个需要注意的点:
为什么ptr2.advanced(by: 1)可以?为什么不用 ptr2.advanced(by: MemoryLayout<Person>.stride)?
原因:
关键在于这句代码 --> let ptr2 = UnsafeMutablePointer<Person>.allocate(capacity: 4),此时我们是知道ptr2的具体类型的,就是指向Person的指针。
在确定指针的类型后,通过步长的移动+1,就表示移动了那个类的实例大小空间+1。
3.3 内存指针
直接使⽤指针需要我们去管理内存,这很繁琐,并且很危险。于是,Unmanaged 出现了。 Unmanaged 能够将由 C API 传递过来的指针进⾏托管,我们可以通过Unmanaged标定它是否接受引⽤计数的分配,以便实现类似⾃动释放的效果;同时,如果不是使⽤引⽤计数,也可以使⽤Unmanaged 提供的 release 函数来⼿动释放,这⽐在指针中进⾏这些操作要简单很多
//案例:将对象转换成结构体指针
struct HeapObject{
var metadata : UnsafeRawPointer
var refCounted1 : UInt32
var refCounted2 : UInt32
}
class Person {
var age: Int = 18
let name: String = "小明"
}
var p = Person()
//使用Unmanaged拿到当前对象的内存指针
let objRawPtr = Unmanaged.passUnretained(p as AnyObject).toOpaque()
//绑定具体类型
let objPtr = objRawPtr.bindMemory(to: HeapObject.self, capacity: 1)
print(objPtr.pointee)
//打印结果
//HeapObject(metadata: 0x0000000100008228, refCounted1: 3, refCounted2: 0)
3.4 内存绑定
Swift 提供了三种不同的 API 来绑定/重新绑定指针:
- assumingMemoryBound(to:)
在代码中我们可能只有原始指针(没有保留指针类型), 但是可以确定指针的类型,可以使用assumingMemoryBound(to:)来告诉编译器预期的类型(注意:这⾥只是让编译器绕过类型检查,并没有发⽣实际类型的转换)
- 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
//1为一个tupleptr大小的空间容量
testPoint(UnsafeRawPointer(tuplePtr).bindMemory(to: Int.self, capacity: 1))
}
- withMemoryRebound(to: capacity: body:)
当我们在给外部函数传递参数时,不免会有一些数据类型的差距。如果我们进行类型转换,必然要来回复制数据,这个时候我们就可以使用withMemoryRebound(to: capacity: body:)来临时更改内存绑定类型。当离开当前的作用域就会失效,重新绑定为原始类型。这可以将临时类型指针的访问和其他代码的作用域分开。
func testPoint(_ p: UnsafePointer<Int8>){
print(p)
}
let uint8Ptr = UnsafePointer<UInt8>.init(bitPattern: 10)
uint8Ptr?.withMemoryRebound(to: Int8.self, capacity: 1, { (int8Ptr: UnsafePointer<Int8>) in
testPoint(int8Ptr)
})
4、案例 - 获取 Mach-O 文件的类名,属性名称和方法信息
/*
案例:
获取 Mach-O 文件的类名,属性和方法信息
步骤:
1.读取类的 Description的地址偏移,根据虚拟基地址算出它在Mach-O的位置
2.然后根据Description的结构来获取 FieldDescriptor在Mach—O的偏移地址,找到FieldDescriptor在Mach—O的位置
3.通过FieldDescriptor 和其结构找到 FieldRecords数组即可获得属性
4.根据Description的结构获取 v-table
*/
class Person {
let age : Int = 18
let tall : Double = 1.75
func getName() {}
}
//类的信息
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 // 方法数量
//V-Table
}
struct FieldDescriptor {
var MangledTypeName: UInt32
var Superclass: UInt32
var Kind: UInt16
var FieldRecordSize: UInt16
var NumFields: UInt32//属性数
// var FieldRecords: [FieldRecord] //暂时代替属性列表 表明这个在这里的位置 方便找到他的地址 顺序读取内容
}
struct FieldRecord{
var Flags: UInt32
var MangledTypeName: UInt32
var FieldName: UInt32 //属性名称
}
//方法结构
struct TargetMethodDescriptor {
var Flags: UInt32
var Impl: UInt32
}
var size : UInt = 0;
//获取 __swift5_types 中正确的内存地址
var swift5_types_ptr = getsectdata("__TEXT", "__swift5_types", &size)
//0x0000000100007e24
//print(swift5_types_ptr)
// 获取 Mach-O 文件中 __LINKEDIT 的信息
var segment_command_linkedit = getsegbyname("__LINKEDIT")
// 获取该段的文件内存的虚拟地址
let vmaddr = segment_command_linkedit?.pointee.vmaddr
// 获取该段的文件偏移量
let fileoff = segment_command_linkedit?.pointee.fileoff
// 计算出链接的基地址(也就是虚拟内存的基地址)
var link_base_address = (vmaddr ?? 0) - (fileoff ?? 0)
//十进制4294967296,即16进制0x100000000
//print(link_base_address)
//swift5_types_ptr是虚拟地址,那么此时,需要计算出实际在Mach-O的偏移量
var offset : UInt64 = 0
if let unwrappedPtr = swift5_types_ptr {
let intRepresentation = UInt64(bitPattern: Int64(Int(bitPattern: unwrappedPtr)))
offset = intRepresentation - link_base_address
//十进制32292,即16进制的7e24
// print(offset)
}
//获取当前程序运行基地址,注意,_dyld_get_image_header获取哪个要根据xcode和macos版本,目前发现可能为0,也可能是3,可用lldb命令 - image list 查看
var app_base_address = _dyld_get_image_header(0);
//把 app_base_address 转换成整型,进行计算
let app_base_address_int_representation = UInt64(bitPattern: Int64(Int(bitPattern: app_base_address)))
//获取DataLo的内存地址,即 __swift5_types 中四个字节在程序内存中存放的地址
var data_load_address = app_base_address_int_representation + offset
// 将 data_load_address 转成指针类型
let data_load_address_ptr = withUnsafePointer(to: data_load_address) { $0 }
//获取 dataLo 四字节存放内容
let data_load_content = UnsafePointer<UInt32>.init(bitPattern: Int(exactly: data_load_address) ?? 0)?.pointee
// 获取 Description 在 Mach-O 文件的信息
let description_offset = offset + UInt64(data_load_content!) - link_base_address
// 获取 Description 在内存中的指针地址
let description_address = description_offset + app_base_address_int_representation
// 将 Description 的指针地址指向 TargetClassDescriptor
let class_description = UnsafePointer<TargetClassDescriptor>.init(bitPattern: Int(exactly: description_address) ?? 0)?.pointee
// 第一目标:获取类名
if let name = class_description?.name{
//name为类名相对于 classDescriptor 在内存中的偏移量(即结构体内的偏移量),前面两个成员变量均32位,占2个四字节,所以为8
let nameAddress = Int64(name) + Int64(description_address) + 8
if let cChar = UnsafePointer<CChar>.init(bitPattern: Int(nameAddress)){
print(String(cString: cChar))
}
}
// 因为 fieldDescriptor 在结构体中排第五个, 前面有四个成员变量的大小每个都是32位 = 4 个 4 字节,所以为 16
let fieldDescriptor_address_int_representation = description_address + 16
// 将 fieldDescriptor_address_int_representation 转成指针地址,这里拿到的地址的值为 fieldDescriptor 的偏移信息
let fieldDescriptorOffset = UnsafePointer<UInt32>.init(bitPattern: Int(exactly: fieldDescriptor_address_int_representation) ?? 0)?.pointee
// fieldDescriptor_address_int_representation + 偏移信息 = fieldDescriptor 的真正的内存地址
let fieldDescriptorAddress = fieldDescriptor_address_int_representation + UInt64(fieldDescriptorOffset!)
//将 fieldDescriptor 内存地址转成 FieldDescriptor
let fieldDescriptor = UnsafePointer<FieldDescriptor>.init(bitPattern: Int(exactly: fieldDescriptorAddress) ?? 0)?.pointee
//第二目标:获取属性
for i in 0..<fieldDescriptor!.NumFields {
//根据步长来循环读取
let a = MemoryLayout<FieldRecord>.stride
let stride: UInt64 = UInt64(i * UInt32(a))
//获取属性信息地址,FieldRecord 前面五个字段,共16字节
let fieldRecordAddress = fieldDescriptorAddress + 16 + stride
//FieldName 前面两个字段,共8字节
let fieldNameRelactiveAddress = fieldRecordAddress + 8
//获取属性名称的偏移量
let offset = UnsafePointer<UInt32>.init(bitPattern: Int(exactly: fieldNameRelactiveAddress) ?? 0)?.pointee
//获取属性名称的实际运行地址
let fieldNameAddress = fieldNameRelactiveAddress + UInt64(offset!) - link_base_address
if let cChar = UnsafePointer<CChar>.init(bitPattern: Int(fieldNameAddress)){
print(String(cString: cChar))
}
}
//目标三:获取方法信息
for i in 0..<class_description!.size {
// VTable offset
let VTable_offset = Int(description_offset) + MemoryLayout<TargetClassDescriptor>.size + MemoryLayout<TargetMethodDescriptor>.size * Int(i)
// 获取 VTable 的地址
let VTable_address = Int(app_base_address_int_representation) + VTable_offset
// 将 VTable_address 转成 TargetMethodDescriptor 结构
let method_descriptor = UnsafePointer<TargetMethodDescriptor>.init(bitPattern: Int(exactly: VTable_address) ?? 0)?.pointee
// 拿到方法的函数地址
let imp_address = VTable_address + 4 + Int((method_descriptor?.Impl ?? 0)) - Int(link_base_address)
// 转成 IMP
let imp: IMP = IMP(bitPattern: UInt(imp_address))!
}