Swift进阶-类与结构体
Swift-函数派发
Swift进阶-属性
Swift进阶-指针
Swift进阶-内存管理
Swift进阶-TargetClassMetadata和TargetStructMetadata数据结构源码分析
Swift进阶-Mirror解析
Swift进阶-闭包
Swift进阶-协议
Swift进阶-泛型
Swift进阶-String源码解析
Swift进阶-Array源码解析
前言
为什么说指针不安全?
- ⽐如我们在创建⼀个对象的时候,是需要在堆分配内存空间的。但是这个内存空间的声明周期是有限的,也就意味着如果我们使⽤指针指向这块内容空间,如果当前内存空间的⽣命周期啊到了(引⽤计数为0),那么我们当前的指针是不是就变成了未定义的⾏为了(野指针)。
- 我们创建的内存空间是有边界的,⽐如我们创建⼀个⼤⼩为10的数组,这个时候我们通过指针访问 到了 index = 11 的位置,这个时候是不是就越界了,访问了⼀个未知的内存空间。
- 指针类型与内存的值类型不⼀致,也是不安全的。(Int * 和 Int8 *类型容易造成精度缺失)
1.指针类别
Swift中的指针分为两类:
typed pointer
: 指定数据类型指针,即UnsafePointer
,其中T
表示泛型
raw pointer
: 未指定数据类型的指针(原生指针) ,即UnsafeRawPointer
与OC中的指针的对比
OC | swift | 解释 |
---|---|---|
const T * | unsafePointer |
指针及所指向的内容都不可变 |
T * | unsafeMutablePointer | 指针及所指向的内容都可变 |
const void * | unsafeRawPointer | 无类型指针,指向的值必须是常量 (指向的内存区域未定) |
void * | unsafeMutableRawPointer | 无类型指针,指向的内存区域未定,也叫通用指针 (指向的内存区域未定) |
首先我们了解一下内存的字节对齐、实际大小、步长
:
struct Teacher {
var age: Int = 18
var sex: Bool = true
}
print(MemoryLayout.alignment) // 8 字节对齐
print(MemoryLayout.size) // 9 实际大小
print(MemoryLayout.stride) // 16 步长
字节对齐
alignment
: cpu的一个64位寻址周期读出来的是8字节。实际大小
size
: 一个Teacher()
占用内存的真实大小 size = Int + Bool / 9 = 8 + 1。步长
stride
: 比如说我要存储连续的Teacher实例
,从当前Teacher()
的起始位置到下一个Teacher()
的起始位置,就是一个Teacher实例的步长。这里输出16是因为存储一个Teacher()需要的内存实际大小是9,又因为内存的字节对齐是8,所以要跨两个8字节对齐才足够存储一个Teacher实例
。
1.1 原生指针 raw pointer
使用原生指针需要注意2点:
a.对于原生指针的内存管理是需要手动管理
b.原生指针在使用完需要手动释放
// 定义一个未知类型的指针:本质是分配32字节大小的空间,指定对齐方式是8字节对齐
let p = UnsafeMutableRawPointer.allocate(byteCount: 32, alignment: 8)
for i in 0...3 {
p.storeBytes(of: i, as: Int.self) //存值
}
for i in 0...3 {
//p是当前内存的首地址,通过内存平移来获取值
let value = p.load(fromByteOffset: i * 8, as: Int.self) //取值
print("index: \(i), value: \(value)")
}
p.deallocate() //使用完需要手动释放
预想值是打印的value是0~3,但实际情况却是:
index: 0, value: 3
index: 1, value: 1152921504606846976
index: 2, value: 8319395793567416323
index: 3, value: 246290604621824
原因是没有在指定的位置去设值,导致每次都对第一个内存空间设值。
解决:存储时,通过advanced(by:)
指定的步长。
// 定义一个未知类型的指针:本质是分配32字节大小的空间,指定对齐方式是8字节对齐
let p = UnsafeMutableRawPointer.allocate(byteCount: 32, alignment: 8)
for i in 0...3 {
p.advanced(by: i * MemoryLayout.stride).storeBytes(of: i, as: Int.self) //移动对应的步长,存值
}
for i in 0...3 {
//p是当前内存的首地址,通过内存平移来获取值
let value = p.load(fromByteOffset: i * 8, as: Int.self) //取值
print("index: \(i), value: \(value)")
}
p.deallocate() //使用完需要手动释放
/* print
index: 0, value: 0
index: 1, value: 1
index: 2, value: 2
index: 3, value: 3
*/
1.2 类型指针 typed pointer
1.2.1 通过 withUnsafePointer(to:)
方法获取地址
class Teacher {
var name: String
init(name: String) {
self.name = name
}
}
class ViewController: UIViewController{
override func viewDidLoad() {
super.viewDidLoad()
var t = Teacher(name: "安老师")
withUnsafePointer(to: &t) { print($0) }
// 和上面是一样的,只是方式不同
withUnsafePointer(to: &t) { point in
print(point)
}
}
}
输出的是变量t的栈内存地址(这与Teacher(name: "安老师")
在堆内存的地址不一样)
如果我想访问指针的具体内容 pointee
:
// 得到的是Teacher实例
withUnsafePointer(to: &t) { print($0.pointee) }
// 访问Teacher实例的name属性 - 安老师
withUnsafePointer(to: &t) { print($0.pointee.name) }
如果我想改变属性的值:
var t = Teacher(name: "安老师")
withUnsafePointer(to: &t) {
return $0.pointee.name = "林老师" // 把return省略也可以
}
print(t.name)
withUnsafePointer(to:)
与 withUnsafeMutablePointer(to:)
对于值类型不同:
var age = 20
/* 报错!不予以修改值
withUnsafePointer(to: &age) { point in
point.pointee += 1
}
*/
withUnsafeMutablePointer(to: &age) { point in
point.pointee += 1
}
1.2.2 类型指针使用API:
通过allocate
创建UnsafeMutablePointer
,需要注意以下几点:
-
initialize
与deinitialize
需成对使用 -
deinitialize
中的count
与申请时的capacity
需要一致 - 使用完后必须
deallocate
class Teacher {
var name: String
init(name: String) {
self.name = name
}
}
class ViewController: UIViewController{
override func viewDidLoad() {
super.viewDidLoad()
let tPoint = UnsafeMutablePointer.allocate(capacity: 3)
defer {
tPoint.deinitialize(count: 3)
tPoint.deallocate()
}
tPoint.advanced(by: 0).initialize(to: Teacher(name: "黄老师"))
tPoint.advanced(by: 1).initialize(to: Teacher(name: "刘老师"))
tPoint.advanced(by: 2).initialize(to: Teacher(name: "潘老师"))
// 三种方式访问
print(tPoint.pointee.name) // 黄老师 首地址指向就是第一个Teacher实例
print(tPoint[1].name) // 刘老师
print(tPoint.successor().pointee.name) // 刘老师 同上一行是一样效果的
print(tPoint.successor().successor().pointee.name) // 潘老师
}
}
有三种方式访问:
- 下标
- 内存平移
- successor()
/** 三种方式是效果是一样的 */
let tPoint = UnsafeMutablePointer.allocate(capacity: 3)
defer {
tPoint.deinitialize(count: 3)
tPoint.deallocate()
}
// 内存平移
(tPoint + 1).initialize(to: Teacher(name: "刘老师"))
// successor()
tPoint.successor().initialize(to: Teacher(name: "刘老师"))
// advanced(by: 1)
tPoint.advanced(by: 1).initialize(to: Teacher(name: "刘老师"))
为什么tPoint.advanced(by: 1)
可以?为什么不用 tPoint.advanced(by: MemoryLayout
?
注意:
关键在于这句代码 --> let tPoint = UnsafeMutablePointer
,此时我们是知道tPoint的具体类型的,就是指向Teacher
的指针。
在确定指针的类型后,通过步长的移动+1,就表示移动了那个类的实例大小空间+1。
2.内存绑定
swift 提供了三种不同的 API 来绑定/重新绑定指针:
- assumingMemoryBound(to:)
- bindMemory(to: capacity:)
- withMemoryRebound(to: capacity: body:)
2.1 assumingMemoryBound(to:)
(只是让编译器绕过类型检查,并没有发⽣实际类型的转换)
有些时候我们处理代码的过程中,只有原始指针(没有保留指针类型),但此刻对于处理代码的我们来 说明确知道指针的类型,我们就可以使⽤ assumingMemoryBound(to:)
来告诉编译器预期的类型。
class ViewController: UIViewController{
override func viewDidLoad() {
super.viewDidLoad()
var tuple = (10, 20)
withUnsafePointer(to: &tuple) { tuplePtr in
testPointer(tuplePtr)
}
}
}
func testPointer(_ p: UnsafePointer) {
print(p[0])
print(p[1])
}
此时在调用 testPointer
会报编译器错误 Cannot convert value of type 'UnsafePointer<(Int, Int)>' to expected argument type 'UnsafePointer
而代码修改一下使用 assumingMemoryBound(to:)
:
class ViewController: UIViewController{
override func viewDidLoad() {
super.viewDidLoad()
var tuple = (10, 20)
withUnsafePointer(to: &tuple) { tuplePtr in
testPointer(UnsafeRawPointer(tuplePtr).assumingMemoryBound(to: Int.self))
}
}
}
func testPointer(_ p: UnsafePointer) {
print(p[0])
print(p[1])
}
元组时值类型,在本质上这块内存空间是连续的,存放Int
类型数据。先把元组转化成指针指向元组的首地址tuple[0],通过assumingMemoryBound(to:)
告诉编译器,当前元组的内存已经绑定过Int了,这时就能骗过编译器检查。
2.2 bindMemory(to: capacity:)
⽤于更改内存绑定的类型,如果当前内存还没有类型绑定,则将⾸次绑定为该类型;否则重新绑定该类型,并且内存中所有的值都会变成该类型。
案例:将一个class实例绑定成class的底层数据结构
- 定义一个Teacher类
class Teacher {
var name: String
init(name: String) {
self.name = name
}
}
- 从swift源码得出class的底层结构是
HeapObject
,所以我们可以自定义一个HeapObject
struct HeapObject {
var metadata: UnsafeRawPointer // 定义一个未知类型的指针
var refCounts: UInt32 // 引用计数
}
- 从swift源码得出
metadata
其实也是一个结构体,所以我自定义一个Metadata
struct Metadata {
var kind: Int
var superClass: Any.Type
var cacheData: (Int, Int)
var data: Int
var classFlags: Int32
var instanceAddressPoint: UInt32
var instanceSize: UInt32
var instanceAlignmentMask: UInt16
var reserved: UInt16
var classSize: UInt32
var classAddressPoint: UInt32
var typeDescriptor: UnsafeMutableRawPointer
var iVarDestroyer: UnsafeRawPointer
}
将teacher绑定到结构体内存中:
- 1.获取实例变量teacher的内存地址,声明成非托管对象
- 2.绑定到结构体内存,返回值是UnsafeMutablePointer
- 3.访问成员变量
class ViewController: UIViewController{
override func viewDidLoad() {
super.viewDidLoad()
let teacher = Teacher(name: "陈老师")
// 通过Unmanaged指定内存管理,类似于OC与CF的交互方式(所有权的转换 __bridge)
// passUnretained 不增加引用计数,即不需要获取所有权
// passRetained 增加引用技术,即需要获取所有权
// toOpaque 不透明的指针
let ptr = Unmanaged.passUnretained(teacher as AnyObject).toOpaque()
// bindMemory更改当前UnsafeMutableRawPointer的指针类型,绑定到具体类型值
// - 如果没有绑定,则绑定
// - 如果已经绑定,则重定向到 HeapObject类型上
let heapObject = ptr.bindMemory(to: HeapObject.self, capacity: 1)
print(heapObject.pointee.metadata)
print(heapObject.pointee.refCounts)
}
}
打印输出heapObject的成员
0x0000000109186768 // metadata输出的就是地址
3
然后继续对matadata进行绑定
class ViewController: UIViewController{
override func viewDidLoad() {
super.viewDidLoad()
let teacher = Teacher(name: "陈老师")
let ptr = Unmanaged.passUnretained(teacher as AnyObject).toOpaque()
let heapObject = ptr.bindMemory(to: HeapObject.self, capacity: 1)
let metadataPtr = heapObject.pointee.metadata.bindMemory(to: Metadata.self, capacity: 1)
print(metadataPtr.pointee)
}
}
打印输出metadata的成员值
Metadata(kind: 4536207176, superClass: _TtCs12_SwiftObject, cacheData: (4541967040, 140909287047168), data: 105553159911586, classFlags: 2, instanceAddressPoint: 0, instanceSize: 32, instanceAlignmentMask: 7, reserved: 0, classSize: 136, classAddressPoint: 16, typeDescriptor: 0x000000010e60e69c, iVarDestroyer: 0x0000000000000000)
2.3 withMemoryRebound(to: capacity: body:)
(临时更改内存绑定类型)
当我们在给外部函数传递参数时,不免会有⼀些数据类型上的差距。如果我们进⾏类型转换,必然要来回复制数据;这个时候我们就可以使⽤ withMemoryRebound(to: capacity: body:)
来临时更 改内存绑定类型。
class ViewController: UIViewController{
override func viewDidLoad() {
super.viewDidLoad()
var num = 10
withUnsafePointer(to: &num) { ptr: UnsafePointer in
testPointer(ptr)
}
}
}
func testPointer(_ p: UnsafePointer) {
print(p)
}
此时编译器报错因为类型不匹配。
使用withMemoryRebound(to: capacity: body:)
修改后:
class ViewController: UIViewController{
override func viewDidLoad() {
super.viewDidLoad()
var num = 10
let ptr = withUnsafePointer(to: &num) {$0}
ptr.withMemoryRebound(to: Int8.self, capacity: 1) { (ptr: UnsafePointer) in
testPointer(ptr)
// 超过了闭包作用域,则不是UnsafePointer类型
}
}
}
func testPointer(_ p: UnsafePointer) {
print(p)
}