指针
在学习指针之前的我们需要先来了解一下指针为什么是不安全的?
- ⽐如我们在创建⼀个对象的时候,是需要在堆分配内存空间的。但是这个内存空间的声明周期是有限的,也就意味着如果我们使⽤指针指向这块内容空间,如果当前内存空间的⽣命周期到了(引 ⽤计数为0),那我们我们系统会对当前的内存空间进行一个回收,那么我们当前的指针就变成了未定义的⾏为了(俗称野指针)。
- 我们创建的内存空间是有边界的,⽐如我们创建⼀个⼤⼩为10的数组,这个时候我们通过指针访问 到了 index = 11 的位置,这个时候是不是就越界了,访问了⼀个未知的内存空间。
- 指针类型与内存的值类型不⼀致,也是不安全的。 例如内存里面存的是Int类型的(8字节),而指针确实Int8类型的,那么就会造成精度的缺失。
以上说明了我们使用指针的时候要小心再小心.
我们稍微了解了一些指针之后我们就开始学习指针吧
指针类型
Swift中的指针分为两类:
- typed pointer 指定数据类型指针,
- raw pointer 未指定数据类型的指 针(原⽣指针),意味着我们不知道指针所指向的是什么样数据类型的。
基本上我们接触到的指针类型有以下⼏种 ,Mutable表示可变否则不可变
| Swift | Object-C | 说明 |
|---|---|---|
| unsafePointer | const T * | 指针及所指向的内容都不可变 |
| unsafeMutablePointer | T * | 指针及其所指向的内存内容均可变 |
| unsafeRawPointer | const void * | 指针指向的内存区域未定 |
| unsafeMutableRawPointer | void * | 指针指向的内存区类型未定。 |
| unsafeBufferPointer | 表达一个连续的内存内存地址 | |
| unsafeMutableBufferPointer< T> | ||
| unsafeRawBufferPointer | 原生类型的连续的内存内存地址 | |
| unsafeMutableRawBufferPointer |
原生指针的使用
我们⼀起来看⼀下如何使⽤ Raw Pointer 来存储 4 个整形的数据,这⾥我们需要选取的UnsafeMutableRawPointer
//创建一个内存空间 4个8字节32字节,步长8
let p = UnsafeMutableRawPointer.allocate(byteCount: 32, alignment: 8)
//接下俩我们要往里面存了 利用for循环 of后面是存储的书,as后面是存储的数据类型
for i in 0...3{
p.storeBytes(of: i, as: Int.self)
}
//取出来 也就是加载数据类型
for i in 0..<4{
let value = p.load(fromByteOffset: i*8, as: Int.self)
print(value)
}
以上可以看出allocate是分配一块内存但是并不会初始化,这个可以查阅官方文档,底下的控制台可以看出我们操作的一直是第一个8字节,如果你有耐心可以在p.storeBytes加一个断点 每次都x/8g一下会看到第一个8字节从0变到3,所以上文代码需要修正一下
for i in 0...3{
p.storeBytes(of: i, as: Int.self)
}
该代码修正为
for i in 0...3{
// p.storeBytes(of: i, as: Int.self)
// 移动指针 注意要移动步长,也就是实际分配的内存大小
p.advanced(by: i * MemoryLayout<Int>.stride).storeBytes(of: i, as: Int.self)
}
Lg
print(MemoryLayout<Int>.size)
print(MemoryLayout<Int>.stride)
print(MemoryLayout<Int>.alignment)
- size: 实际使用的内存大小
- stride: 内存对齐后实际分配的内存大小,也叫步长
- alignment: 对齐参数
泛型指针的使用
这⾥的泛型指针相⽐较原⽣指针来说,其实就是指定当前指针已经绑定到了具体的类型。同样的,我们还是通过⼀个例⼦来解释⼀下。
在进⾏泛型指针访问的过程中,我们并不是使⽤ load 和 store ⽅法来进⾏存储操作。这⾥我们使⽤到当前泛型指针内置的变量 pointee
pointee 可理解为解引(dereference),即用 * 符号获得指针指向内存区域的值。
获取 UnsafePointer 的⽅式有两种。⼀种⽅式就是通过已有变量获取
var age = 18
//得到age变量的内存指针
withUnsafePointer(to: &age){ptr in
print(ptr)
}
//获取地址的值 指针和指针的内容都是不可变的 pointee指针所指向的内容
age = withUnsafePointer(to: &age){ ptr in
// pointee是只读的不可变的,所以不能直接赋值
(ptr.pointee) + 21
}
//指针和指针的内容都是可变的
withUnsafeMutablePointer(to: &age){ ptr in
(ptr.pointee) += 21
}
print(age)
以上代码的运行结果是,
withUnsafePointer没有Mutable的情况下pointee是只读类型的,指针指向的内存值不可变。下图可以看到如果改变pointee的值将会报错
还有⼀种⽅式就是直接分配内存
//类型指针
var tPtr = UnsafeMutablePointer<MJYStruct>.allocate(capacity: 5)
tPtr[0] = MJYStruct(age: 18, height: 20.0)
tPtr[1] = MJYStruct(age: 19, height: 23.0)
print(tPtr[1])
print(tPtr.pointee)
tPtr.deinitialize(count: 5)
tPtr.deallocate()
尝试过ponitee的取值,pointee好像是指向该指针的第一个元素,如果要取第二个元素要按照数组的取法,不过在Mutable的加持下可以改变tPtr.pointee的值
tPtr.pointee = tPtr[1]
print(tPtr.pointee)
上述代码中的deinitialize和deallocate是对内存的初始化和回收,下图可以清晰的看到分配内存和回收内存的过程
assumingMemoryBound(to:)
func testPoint(_ p: UnsafePointer<Int>){
print(p[0])
print(p[1])
}
let tuple = (10,20)
withUnsafePointer(to: tuple) { (tuplePtr: UnsafePointer<(Int, Int)>) in
// testPoint(tuplePtr)
testPoint(UnsafeRawPointer(tuplePtr).assumingMemoryBound(to: Int.self))
}
assumingMemoryBound 绕过当前编译器的检查,并没有发生实际的转换,告诉编译器当前内存已经绑定过Int了,这个时候编译器将不会检查
bindMemory(to: capacity:)
⽤于更改内存绑定的类型,如果当前内存还没有类型绑定,则将⾸次绑定为该类型;否则重新绑定该类 型,并且内存中所有的值都会变成该类型。
withMemoryRebound(to: capacity: body:)
当我们在给外部函数传递参数时,不免会有⼀些数据类型上的差距。如果我们进⾏类型转换,必然要来 回复制数据;这个时候我们就可以使⽤ withMemoryRebound(to: capacity: body:) 来临时更 改内存绑定类型。
内存管理
Swift 中使⽤⾃动引⽤计数(ARC)机制来追踪和管理内存。
class MJYTeacher {
var age: Int = 18
var name: String = "卿卿"
}
//初始化一个变量
var t = MJYTeacher()
//接下来我们把t 赋值给t1,t2
var t1 = t
//这个时候我们是不是有3个强引用了,已就意味着此时此刻引用计数为3
var t2 = t
// 打印实例变量的内存指针
print(Unmanaged.passRetained(t as AnyObject).toOpaque())
print("end")
我们再源码中看一下refCounts,首先我们再HeapObject.h文件中搜索refCount 找到以下定义
接下来我们看一下InlineRefCounts
可以看到是一个模板类接收一个泛型参数,这个模板类是当前的RefCounts
我们看到refcounts的类操作的都是传进来的泛型参数,所以当前的RefCounts本质是对当前引用技术的包装,引用技术的具体类型取决于传进来的参数也就是InlineRefCounts,我们将目标聚集在InlineRefCountBits
同样这个也是一个模板函数 查看RefCountBitsT这个类
可以看到RefCountBitsInt是一个UInt64位的位域信息,对于Swift的引用计数还是OC里面的引用计数都是一个64位的位域信息,在这64位位域信息中存储了当前类运行的生命周期相关的引用技术。
当我们创建一个实例对象的时候,当前的引用计数是多少呢?
同样我们聚焦到源代码HeapObject.cpp
查看HeapObject方法
接下俩我们来看一下Initialized
我们可以看到传入的是RefCountBits,接下来目光聚集这个类:
StrongExtraRefCountShift: 33
PureSwiftDeallocShift:1
UnownedRefCountShift: 1
这里的结果是0x0000000400000003,中间有个4 是由于2左移33位得到4,末尾是3 是由于最后两位为11 所以得到3
我们了解了强引用,使用强引用就会造成一个问题:循环引用。我们来看一个经典的循环引用案例:
class MJYTeacher{
var age: Int = 18
var name: String = "卿卿"
var subject: MJYSubject?
}
class MJYSubject{
var subjectName: String
var subjectTeacher: MJYTeacher
init(_ subjectName: String, _ subjectTeacher: MJYTeacher) {
self.subjectName = subjectName
self.subjectTeacher = subjectTeacher
}
}
var t = MJYTeacher()
var subject = MJYSubject.init("Swift进阶", t)
t.subject = subject
那么我们要解决这个循环引用,在swift中拥有两种方法,一个是弱引用(weak),另一个是无主引用(unowned)
弱引用(weak)
通过汇编我们可以知道在初始化的时候多调用了swift_weakInit方法我们在源码中找到这个方法
结论就是里面还是维护的是一张弱引用散列表,是一个可选类型,可以将指针的值置为nil
无主引用(unowned)
在变量前面用unowned声明就是一个无主引用,无主引用不等于弱引用,不是一个可选类型,不能设置为nil
unowned var t = MJYTeacher()
如果弱引用双方,生命周期没有任何关联,那么就使用weak,例如delegate
如果一个对象销毁,另一个对象也跟着销毁,那么使用unowned
一般无脑weak,但是unowned的性能更好,因为不用创建一个散列表,维护这个散列表,直接对refcounts的64位进行操作
Lg: 在查看引用技术是不要在控制台 用po t 因为会对引用技术造成影响,引用计数会+1
闭包
变量捕获
var age = 18
let closure = {
age += 1
}
closure()
print(age)
age这个变量被捕获了所以能在闭包内修改age的值
class MJYTeacher{
var age: Int = 18
var name: String = "卿卿"
var testClosure:(() -> ())?
deinit {
print("MJYTeacher deinit")
}
// var subject: MJYSubject?
}
func test(){
let t = MJYTeacher()
t.testClosure = {
t.age += 1
}
print("end")
}
test()
执行以上代码我们会发现没有打印deinit,因为闭包捕获了t这个变量,t又持有这个闭包,造成了循环引用,那么如何解决呢!
class MJYTeacher{
var age: Int = 18
var name: String = "卿卿"
var testClosure:(() -> ())?
deinit {
print("MJYTeacher deinit")
}
// var subject: MJYSubject?
}
func test(){
let t = MJYTeacher()
t.testClosure = {[weak t] in
t!.age += 1
}
print("end")
}
test()
只要在闭包内声明这个变量是weak或者unowned 就能顺利deinit了
[weak t]是捕获列表
var age = 0
var height = 0.0
let closure = {[age] in
print(age)
print(height)
}
age = 10
height = 1.98
closure()
可以认为声明后age是值引用,不再是地址引用了,并且是let声明的 闭包内不会再被改变了
var height = 0.0
let closure = {[age] in
print(age)
print(height)
}
var age = 0
age = 10
height = 1.98
closure()
print(age)
上下文中去寻找age哦,age的声明在闭包声明之后也是有效的,是在闭包执行的时候去捕获变量
func test(){
let t = MJYTeacher()
t.testClosure = {[weak t] in
if let strongSelf = t{
print(strongSelf.age)
}
withExtendedLifetime(t){
print( t!.age)
}
}
}
withExtendedLifetime延长t的生命周期,周期范围是这个闭包表达式内