一: 内存访问冲突
如果两个操作同时满足下面几个条件,就会产生内存访问冲突:
- 至少一个是写入操作
- 访问的是同一块内存
- 访问的时间重叠(比如在同一个方法内)
比如说像下面这种操作就会产生内存访问冲突:
test(a: &num1, b: &num1)
传入同一个参数.就会同时读取和写入同一块内存,产生内存访问冲突.
那么如何解决内存访问冲突?只要让这两个操作不是同一块内存即可:
内存访问冲突典型:
以上两种情况都会产生内存访问冲突,因为元组和结构体的成员都在同一块连续的内存中.虽然访问的是不同的成员,但本质仍然是同一块内存.
指针
swift
中也有专门的指针类型,这些都被定性为不安全的unsafe
,常用的有以下四种类型:
UnsafePointer<T>
: 带泛型的不可修改指针,比如说UnsafePointer<Int>
等价于 OC 的const Int*
UnsafeMutablePointer<T>
: 带泛型的可修改的指针,比如说UnsafeMutablePointer<Int>
等价于 OC 的Int*
UnsafeRawPointer
: 不带泛型的不可修改指针,也就是普通指针,相当于 OC 中的const void*
UnsafeMutableRawPointer
: 不带泛型的可修改指针,相当于 OC 中的void*
指针的读取和写入
泛型指针和非泛型指针的读取和写入操作是不一样的,我们先看一下泛型指针的读取和写入操作:
再来看看非泛型指针的读取和写入:
指针的应用
在 OC 的 NSArray
中,系统提供了一个遍历数组元素的方法,它里面就用到了指针类型:
我们在Swift
开发中难免会用到OC中的NSArray
,如果我们想在swift
语言的开发中使用 OC 那种遍历方法呢?
可以看到,OC 中的 Bool *
在swift
中变成了UnsafeMutablePointer<ObjCBool>
.正是我们上面讲的可变泛型指针.
还有另一种遍历方法,不需要用到指针:
.
获得指向某个变量的指针
如果我们想获取指向某个变量的指针,应该怎么做到呢?swift
也提供了相关的API
:
@inlinable public func withUnsafePointer<T, Result>(to value: inout T, _ body: (UnsafePointer<T>) throws -> Result) rethrows -> Result
@inlinable public func withUnsafeMutablePointer<T, Result>(to value: inout T, _ body: (UnsafeMutablePointer<T>) throws -> Result) rethrows -> Result
这两个API
要求传入两个参数:第一个参数是想要获得哪个变量指针的地址;第二个参数是传入一个闭包,这个闭包要求传入一个参数,并且有一个返回值.
咱们仔细看这两个API
,会发现这个API
的返回值就是第二个闭包参数的返回值.
闭包的第一个参数可以理解为是系统设置的,指向age
变量的指针:
下面我们来看看这两个API
是怎么用的:
因为闭包的第一个参数恰恰也是闭包的返回值,而闭包的返回值恰恰又是函数的返回值,所以简写如下:
var age = 10
var ptr = withUnsafePointer(to: &age) { $0 }
print(ptr.pointee)
获取一个指向某个变量的可变类型的指针跟上面一样,我们直接贴出代码:
var age = 10
var ptr = withUnsafeMutablePointer(to: &age) { $0 }
ptr.pointee = 20
print(ptr.pointee)
上面获取的指向某个变量的指针是泛型的,如果我们想要非泛型的指针应该怎么做呢?
还是同样的API
,只不过在闭包内把返回值改为非泛型的.
非泛型的指针类型有一个初始化方法:传入一个泛型指针,初始化为非泛型指针:
所以我们只需在闭包体内转换即可:
var age = 10
var ptr1 = withUnsafePointer(to: &age) { UnsafeMutableRawPointer(mutating: $0)}
ptr1.storeBytes(of: 20, as: Int.self)
var ptr2 = withUnsafePointer(to: &age) { (p) -> UnsafeRawPointer in
UnsafeRawPointer(p)
}
print(ptr2.load(as: Int.self))
获得指向堆空间实例的指针
如果我们想获取指向堆空间实例的指针,应该怎么做到呢?还是用上面的方法试一下.
class Person{
var name: String
var age: Int
init(name: String, age: Int) {
self.name = name
self.age = age
}
}
var p1 = Person(name: "Jack Ma", age: 56)
var ptr1 = withUnsafePointer(to: &p1) { $0 }
如上代码,ptr1
指针指向的是Person
对象在堆空间的内存吗?我们通过汇编窥探一下:
再来看看指针ptr1
中存储的是p1
变量的地址,还是Person
实例在堆空间的地址:
通过打印可以发现,ptr1
指向的是全局变量p1
的地址,并不是Person
实例的堆空间地址.
怎样获取一个指向堆空间实例地址的指针呢?
要想获得一个指向堆空间实例地址的指针需要用到另一个函数:
UnsafeRawPointer(bitPattern: <#T##Int#>)
UnsafeRawPointer(bitPattern: Int)
传入一个内存地址,返回一个指向此内存地址的指针.
所以现在我们的思路就要先获取一个指向堆空间实例对象的内存地址,怎么获取堆空间实例对象的内存地址呢?
class Person{
var name: String
var age: Int
init(name: String, age: Int) {
self.name = name
self.age = age
}
}
var p1 = Person(name: "Jack Ma", age: 56)
//获取指向 p1 的非泛型指针
var ptr1 = withUnsafePointer(to: &p1) { UnsafeRawPointer($0) }
//取出 p1 指向的内存地址
//取出 p1 的前 8 个 字节,也就是实例对象在堆空间的内存地址
var personAdress = ptr1.load(as: UInt.self)
//获取指向堆空间实例对象的指针
var heapAdress = UnsafeRawPointer(bitPattern: personAdress)
上面代码就实现了我们的目的.heapAdress
就是一个指向堆空间实例内存地址的指针.
创建指针
我们也可以自己向堆空间申请一块内存,让一个指针变量指向这块内存.
第一种做法是使用malloc
向堆空间申请一块内存,它会返回一个UnSafeMutableRawPointer
类型的指针.需要注意的是malloc
需要导入Foundation
框架:
import Foundation
//向堆空间申请16个字节的内存
var ptr1 = malloc(16)
//把 20 放到前 8 个字节
ptr1?.storeBytes(of: 20, as: Int.self)
上面代码我们就向堆空间申请了16个字节的内存,并把20存储在了前8个字节中.我们如何使用后8个字节的内存呢?
系统提供了一个带偏移量的方法,可以访问其他内存:ptr1?.storeBytes(of: T, toByteOffset: Int, as: T.Type)
:
import Foundation
//向堆空间申请16个字节的内存
var ptr1 = malloc(16)
//把 20 放到前 8 个字节
ptr1?.storeBytes(of: 20, as: Int.self)
//把 30 放到后 8 个字节
ptr1?.storeBytes(of: 30, toByteOffset: 8, as: Int.self)
//读取前8个字节
print(ptr1?.load(fromByteOffset: 0, as: Int.self) ?? 0)
//读取后8个字节
print(ptr1?.load(fromByteOffset: 8, as: Int.self) ?? 0)
第二种方法是使用UnsafeMutableRawPointer.allocate (byteCount: alignment:)
方法.
示例:
//像堆空间申请24个字节
var ptr1 = UnsafeMutableRawPointer.allocate(byteCount: 24, alignment: 1)
//存储前8个字节
ptr1.storeBytes(of: 10, as: Int.self)
//存储中间8个字节
ptr1.storeBytes(of: 20, toByteOffset: 8, as: Int.self)
我们要访问其他字节的内存空间,除了使用上面说的偏移量方法外,还有一种方法:advanced(by:)
,这个方法会传入一个Int
类型的字节数,然后直接返回指向这个字节处的指针:
示例:
//像堆空间申请16个字节
var ptr1 = UnsafeMutableRawPointer.allocate(byteCount: 24, alignment: 1)
//存储前8个字节
ptr1.storeBytes(of: 10, as: Int.self)
//存储中间8个字节
ptr1.storeBytes(of: 20, toByteOffset: 8, as: Int.self)
//存储最后8个字节
ptr1.advanced(by: 16).storeBytes(of: 30, as: Int.self)
print(ptr1.load(as: Int.self))
print(ptr1.advanced(by: 8).load(as: Int.self))
print(ptr1.advanced(by: 16).load(as: Int.self))
上面介绍的都是非泛型的指针类型,其实泛型的指针类型也是可以自己创建的:
var ptr1 = UnsafeMutablePointer<Int>.allocate(capacity: 3)
可以看到,在创建泛型类型的指针类型时,传入的参数并不是字节数,而是容量.因为泛型已经确定了具体的类型,所以只要确定了容量就确定了字节数.
示例:
var ptr1 = UnsafeMutablePointer<Int>.allocate(capacity: 3)
//初始化前8个字节
ptr1.initialize(to: 10)
print(ptr1.pointee)
//释放
ptr1.deinitialize(count: 1)
ptr1.deallocate()
上面的代码我们就创建了一个可以存放3个Int
类型的指针.并且前8个字节存放了10.后面16个字节我们怎么访问呢?
我们可以使用successor()
来访问下一个容量单位的内存:
示例:
var ptr1 = UnsafeMutablePointer<Int>.allocate(capacity: 3)
//初始化
ptr1.initialize(to: 10)
ptr1.successor().initialize(to: 20) //等价于 (ptr1 + 1).initialize(to: 20)
ptr1.successor().successor().initialize(to: 30) //等价于 (ptr1 + 2).initialize(to: 30)
//读取方法一
print(ptr1.pointee)
print(ptr1.successor().pointee)
print(ptr1.successor().successor().pointee)
//读取方法二
print(ptr1.pointee)
print((ptr1 + 1).pointee)
print((ptr1 + 2).pointee)
//读取方法三
print(ptr1[0])
print(ptr1[1])
print(ptr1[2])
//反初始化
ptr1.deinitialize(count: 3)
//释放
ptr1.deallocate()
由于是泛型指针类型,它的操作不是以字节数为单位,而是以单个容量为单位,所以可以使用ptr + 1
和ptr1[0]
来直接操作容量块.
需要注意的是,我们在使用
initialize()初始化内存后,一定要使用
deinitialize()反初始化释放内存,不然会引起内存泄露.
指针之间的切换
通过上面我们自己创建指针类型可以发现,在操作泛型指针类型时明显要比操作非泛型指针类型要方便的多.所以,为了简便,我们有时可能会需要将非泛型指针转换为泛型指针.
但是我们查看泛型指针的初始化方法,没有发现传入非泛型指针,初始化为一个泛型指针的初始化方法:
所以我们要找其他方法来转换,系统提供了这么一个方法,可以将非泛型指针转换为泛型指针assumingMemoryBound(to: )
:
实例:
//创建一个非泛型指针
var ptr1 = UnsafeMutableRawPointer.allocate(byteCount: 16, alignment: 1)
//前8个字节转泛型Int操作
ptr1.assumingMemoryBound(to: Int.self).pointee = 10
//后8个字节转泛型Double操作
(ptr1 + 8).assumingMemoryBound(to: Double.self).pointee = 20.0
//读取后8个字节
print((ptr1 + 8).assumingMemoryBound(to: Double.self).pointee)
在swift
中还有个专门用来进行类型转换的函数unsafeBitCast(x: T , to: U.Type)
.它的意思是传入一个变量,转换为另一种类型.
所以我们也可以直接使用这种转换方法:
unsafeBitCast(ptr1, to: UnsafeMutablePointer<Int>.self).pointee = 30
unsafeBitCast(x: T , to: U.Type)
是一种忽略数据类型的转换,它不会因为数据类型的变化而改变原来的内存数据.
什么意思呢?我么来看看下面代码:
var num1 = 10
var num2 = Double(10)
print(num1)
print(num2)
上面代码也是一种类型转化,把10转换成了Double
类型的10.0
,我们来看看num1
和num2
内存中存储的是什么数据:
可以看到,10转换为Double
类型的10.0后,底层的数据结构变成了0x4024000000000000
,这就是浮点数10.0
在内存中的格式,这是因为浮点数在内存中的存储很复杂,浮点数的存储采用的是:符号位+阶码+尾数的形式.
像上面通过初始化的方法把10转换为浮点数10.0的方法是安全的,因为它会根据数据类型的不同,通过一定的计算改变内存数据.
而通过unsafeBitCast(x: T , to: U.Type)
转换是不安全的,因为它不会根据数据类型的不同改变原来的内存数据.原来的内存数据是什么,它就直接拿过来.
我们通过下面代码试验一下:
var num1 = 10
var num2 = unsafeBitCast(num1, to: Double.self)
转换后num2
的内存数据如下:
可以看到,num1
通过unsafeBitCast
转换为Double
类型后,num2
的内存数据还是0x000000000000000a
.并不是0x4024000000000000
.
我们打印看看,转换后num2
是什么:
可以看到num2
的数据为5e-323
,毫无疑问是不正确的.
既然unsafeBitCast
是不安全的,那我们通过它把非泛型指针类型转换为泛型指针类型,会不会也不安全呢?
不会的,因为不管是非泛型指针类型还是泛型指针类型,他们的本质就是地址.他们在内存中就是一串
0x0x1005b1a7d这样的地址.所以通过
unsafeBitCast 转换后还是同样的内存地址.