1.指针
为什么说指针是不安全的呢?主要以下几点:
- 比如我们在创建一个对象的时候,是需要在堆区分配内存空间的,但是这个内存空间的生命周期是有限的,也就意味着如果我们使用指针指向这块内存空间,如果当前内存空间的生命周期到了(也就是引用计数为0了),那么当前的指针就成了未定义的行为,也就是野指针。
- 我们创建的内存空间是有边界的,比如创建一个大小为10的数组,这个时候通过指针访问到index = 11 的位置,这个时候就越界了,访问了一个未知的内存空间。
- 指针类型和内存空间的值类型不一致,也是不安全的。
2. 指针类型
swift中的指针分为2类,typed pointer (指定数据类型指针)raw pointer(未指定数据类型指针,也叫原生指针),基本上我们接触的指针类型有以下几种
- unsafePointer,相当于oc中的const T *,指针以及指向内容都不可变
- unsafeMutablePointer,相当于oc中的 T *,指针以及指向内容都可变
- unsafeRawPointer,相当于oc中的const Void *,指针指向的内存区域未定
- unsafeMutableRawPointer,相当于oc中的const Void *,指针指向的内存区域未定
- unsafeBufferPointer,连续的内存空间
- unsafeMutableBufferPointer
- unsafeRawBufferPointer
- unsafeMutableRawBufferPointer
3. 原始指针的使用
接下来使用raw pointer 储存4个整形的数据,这里使用unsafeMutablePointer.
let p = UnsafeMutableRawPointer.allocate(byteCount: 32, alignment: 8)
for i in 0..<4 {
p.storeBytes(of: i, as: Int.self)
}
for i in 0..<4 {
let value = p.load(fromByteOffset: i * 8, as: Int.self)
print("index\(i) value:\(value)")
}
但是这里运行后发现取出来的值和想要的不一样,这是因为存的时候指针没有位移相应的位置,也就是步长。 在IOS里面有测量当前大小的工具,分别是
- MemoryLayout.size
- MemoryLayout.stride
- MemoryLayout.alignment
例如下面代码
struct LGTeacher {
var age: Int = 18
}
print(MemoryLayout<LGTeacher>.size)
print(MemoryLayout<LGTeacher>.stride)
print(MemoryLayout<LGTeacher>.alignment)
运行后发现都是8
而在结构体添加一个bool属性
struct LGTeacher {
var age: Int = 18
var sex: Bool = true
}
然后重新运行,发现这里打印的值就不一样了。这里可以知道,size是struct结构体的大小,stride是占用内存的实际大小,alignment是对其的大小。 所以这里知道,我们储存的时候先要移动 i * 步长信息的位置,然后在储存值。
for i in 0..<4 {
p.advanced(by: i * MemoryLayout<Int>.stride).storeBytes(of: i , as: Int.self)
}
这样运行后就可以看到打印的是期望的值了 当然,在使用完指针之后,需要调用deallocate来释放内存空间。
p.deallocate()
4. 泛型指针的使用
泛型指针,也叫类型指针,指定当前指针已经绑定到了具体的类型。
获取withUnsafePointer的方式有两种,一种是通过已有变量获取:
如果闭包是最后一个参数的话,可以写成尾随闭包。
var age = 18
withUnsafePointer(to: &age){ ptr in
print(ptr)
}
指针的具体内容使用pointee访问,pointee代表指针指向的具体数据类型,所以下列代码是可行的。
age = withUnsafePointer(to: &age){ ptr in
ptr.pointee + 21
}
但是需要注意的是,这里的pointee是只读属性,所以下面的代码是不可行的。
age = withUnsafePointer(to: &age){ ptr in
ptr.pointee += 21
}
但是如果使用的是withUnsafeMutablePointer的话,那么pointee就可变了。
withUnsafeMutablePointer(to: &age) { ptr in
ptr.pointee += 21
}
这个时候可以看到age的值也变了。 一种是直接内存分配:
var age = 10
let tPtr = UnsafeMutablePointer<Int>.allocate(capacity: 1)
tPtr.initialize(to: age)
这里也有两种方式来初始指针内存,一种是使用:
struct LGTeacher {
var age: Int
var height: Double
}
var tPtr = UnsafeMutablePointer<LGTeacher>.allocate(capacity: 5)
tPtr[0] = LGTeacher(age: 18, height: 180.0)
tPtr[1] = LGTeacher(age: 18, height: 180.0)
tPtr.deinitialize(count: 5)
tPtr.deallocate()
一种是使用:
struct LGTeacher {
var age: Int
var height: Double
}
var tPtr = UnsafeMutablePointer<LGTeacher>.allocate(capacity: 5)
tPtr.initialize(to: LGTeacher(age: 18, height: 180))
tPtr.advanced(by: MemoryLayout<LGTeacher>.stride).initialize(to: LGTeacher(age: 18, height: 180))
tPtr.deinitialize(count: 2)
tPtr.deallocate()
这里使用完指针后,需要调用deinitialize和deallocate。deinitialize把内存空间全部抹成0,也就是数据清零。deallocate回收内存空间。在实际开发中可以用defer来执行deinitialize和deallocate。
5. 指针读取macho中的属性名称
之前在macho通过地址的操作找到了属性名称,现在通过指针来读取macho中的属性名称。这里先获取到types里面的地址。
class LGTeacher {
var age: Int = 18
var name: String = "hello"
}
var size: UInt = 0
var ptr = getsectdata("__TEXT", "__swift5_types", &size)
print(ptr)
运行后发现这里地址和macho里面的地址是一样的。 接下来需要用到虚拟内存地址等,所以需要去获取。这里先找到machoHeader 的内存地址。然后需要拿到虚拟内存地址,其在__LINKEDIT 里面,需要用到里面的VM Address 和File Offset ,所以通过getsegbyname 拿到__LINKEDIT ,看到这里返回segment_command_64 ,看到segment_command_64的结构,可以看到vmaddr 和fileoff ,使用vmaddr 减去fileoff 才是加载的基地址 。之前的ptr是加过基地址的,所以ptr需要减去linkBaseAddress。
class LGTeacher {
var age: Int = 18
var name: String = "hello"
}
var size: UInt = 0
var ptr = getsectdata("__TEXT", "__swift5_types", &size)
var mhHeaderPtr = _dyld_get_image_header(0)
var secCommondPtr = getsegbyname("__LINKEDIT")
var linkBaseAddress: uint64 = 0
if let vmaddress = secCommondPtr?.pointee.vmaddr, let fileoff = secCommondPtr?.pointee.fileoff {
linkBaseAddress = vmaddress - fileoff
}
var offset: uint64 = 0
if let unwrappedPtr = ptr {
let intRepresentation = uint64(bitPattern: Int64(Int(bitPattern: unwrappedPtr)))
offset = intRepresentation - linkBaseAddress
print(offset)
}
运行后这里得到16164,转换为16进制就得到0X3F24。 那么程序编译之后就得到了swift5_types里面的地址为0X3F24。
接下来要拿到0X3F24地址里面的内容。这里先拿到程序运行的首地址,那么加上offset就得到了0X3F24地址里面的内容存放在内存当中的真实地址。然后将地址转换为指针类型,使用pointee获得里面的内容。
6. 内存绑定
Swift 提供了3种不同的API来绑定/重新绑定指针:
assumingMemoryBound(to:) : 有些时候我们处理代码的过程中,只有原始指针(没有保留指针类型),但此刻对于处理代码的我们来说明确知道指针的类型,这个时候就可以使用assumingMemoryBound来告诉编译器预期的类型(注意:这里只是让编译器绕过类型检查,并没有发生实际类型的转换)。有时候,我们的指针类型是相似的,而我们不想通过一系列操作来转换类型,这个时候就可以使用assumingMemoryBound告诉编译器预期的类型来绕过类型检查。 例如下面的代码是无法运行的,但是他们的指针类型是类似的,本质上是一样的。
func testPoint(_ p: UnsafePointer<Int>) {
print(p)
}
let tuple = (10,20)
withUnsafePointer(to: tuple) { (tuplePtr:UnsafePointer<(Int,Int)>) in
testPoint(tuplePtr)
}
那么如果这里使用assumingMemoryBound来告诉编译器预期的类型是Int,那么这里就可以运行了。
-
bind Memory(to:capacity:) : 用于更改内存绑定的类型,如果当前内存还没有内存绑定,则将首次绑定为该类型,否则就重新绑定为该类型,并且内存中的所有的值都会变成改类型。 不同与assumingMemoryBound的是,这里发生了实际类型的转换。 -
withMemoryRebound(o:capacity:body:) :当我们给外部函数传递参数的时候,不免会有一些数据类型的差距。如果我们进行类型转换,必然要来回复制数据。这个时候可以使用withMemoryRebound来临时更改内存绑定类型。 下面就将uint8临时更改内存绑定类型为Int8类型了,减少了代码的复杂度。
func testPoint(_ p: UnsafePointer<Int8>) {
print(p)
}
let UInt8Ptr = UnsafePointer<uint8>.init(bitPattern: 10)
UInt8Ptr?.withMemoryRebound(to: Int8.self, capacity: 1) { (ptr: UnsafePointer<Int8>) in
testPoint(ptr)
}
7. 内存管理
Swift 中使用自动引用计数(ARC)机制来追踪和管理内存。
class LGTeacher {
var age: Int = 18
var name: String = "ls"
}
var s = LGTeacher()
print(Unmanaged.passUnretained(s as AnyObject).toOpaque())
print("end")
运行后发现是3. 到源码中查看refCount,看到是InlineRefCounts类型。 然后找到InlineRefCounts ,发现是模版类,接受一个泛型参数。 而RefCounts 里面的API都是操作RefCountBits 这个泛型参数,所以RefCounts是对引用计数的一个包装,而引用计数类型是传进来的参数。 之后找到InlineRefCountBits ,这里看到真实操作的类是RefCountBitsT 。 要知道引用计数是什么,就要看RefCountBitsT里面的属性,这里就看到引用计数bits ,他的类型BitsType是由RefCountBitsInt 的type 属性定义的。 查找RefCountBitsInt可以发现type是一个uint64_t的位域信息。
那么创建一个实例的时候,引用计数是多少呢?看到_swift_allocObject_ ,这里使用HeapObject创建实例。 然后看到这里面对refCounts进行了赋值InlineRefCounts::Initialized。 然后进来发现是枚举类型 往下看枚举类型传进去的值是0,1,而RefCountBits就是RefCountBitsT类型。 接下来找RefCountBitsT的初始化函数,那么就看到这里是strongExtraCount是0,unownedCount是1. 那么之前的0x0000000000000003代表的就是PureSwiftDeallocShift为1,UnownedRefCountShift为1了。 这里在代码中添加两个强引用。
class LGTeacher {
var age: Int = 18
var name: String = "ls"
}
var s = LGTeacher()
var s1 = s
var s2 = s
print(Unmanaged.passUnretained(s as AnyObject).toOpaque())
print("end")
运行后打印看到这里就是4了,这是因为2存储在了高33位。 64位位置信息: 验证一下,将s变为可选参数然后后面置位nil,
class LGTeacher {
var age: Int = 18
var name: String = "ls"
}
var s:LGTeacher? = LGTeacher()
print( Unmanaged.passUnretained((s as AnyObject)).toOpaque())
s = nil
print("end")
运行后可以看到这里变成了0x0000000100000003,也就是IsDeinitingShift的位置变为了1.
那么强引用是怎么去添加的呢?这里看到是在swift_retain方法里面调用了increment方法, increment方法里面则是调用了incrementStrongExtraRefCount。 在incrementStrongExtraRefCount就是1左移33位。
8.循环引用
刚才说到了强引用,那么使用强引用就会有一个问题,就是循环引用。 下面就是一个案例,这里t持有subject,subject也持有t,这样就造成了循环引用,导致无法释放。
class LGTeacher{
var age: Int = 18
var name: String = "Kody"
var subject: LGSubject?
}
class LGSubject{
var subjectName: String
var subjectTeacher: LGTeacher
init(_ subjectName: String, _ subjectTeacher: LGTeacher) {
self.subjectName = subjectName
self.subjectTeacher = subjectTeacher
} }
var t = LGTeacher()
var subject = LGSubject.init("Swift ", t)
t.subject = subject
Swift提供了两种方法来解决在使用类的属性时所遇到的循环引用问题:弱引用和无主引用
8.1 弱引用
弱引用不会对其引用的实例保持强引用,因而不会阻止ARC释放被引用的实例,这个特性阻止了引用变为循环引用。声明属性或者变量时,在前面加上weak关键字声明这是一个弱引用。
由于弱引用不会强保持对实例的引用,所以说实例被释放了弱引用依旧引用着这个实例也是有可能的。因此,ARC会在被引用的实例被释放时自动的设置弱引用为nil,由于弱引用需要允许他们的值为nil,所以他们一定的是可选类型。
那么用弱引用的对象的引用计数会有什么变化呢?输入下面代码
class LGTeacher{
var age: Int = 18
var name: String = "Kody"
}
weak var t = LGTeacher()
print( Unmanaged.passUnretained((t as AnyObject)).toOpaque())
print("end")
运行后发现这里对比正常对象多调用了swift_weakInit . 看到swift_weakInit在底层调用的是nativeInit . 而弱引用就是形成一个散列表。 所以formWeakReference本质上就是创建一个散列表。 而在allocateSideTable里面就会判断是否有散列表,有的话就得到散列表并返回,如果在析构就返回nullptr。 如果没有的话,那么往下就会创建散列表,看到这里的类型是HeapObjectSideTableEntry。 找到HeapObjectSideTableEntry,这里发现,swift里面有两种引用计数:InlineRefCountBits和SideTableRefCountBits。在HeapObject里面如果没有弱引用就是存的InlineRefCountBits,如果用弱引用存的就是HeapObjectSideTableEntry这个实例对象,HeapObjectSideTableEntry里面就有弱引用的信息。 找到SideTableRefCounts。 找到SideTableRefCountBits,发现这里也是继承自RefCountBitsT,不过这里多了一个weakBits, 添加弱引用,查看引用计数的变化。
var t = LGTeacher()
print( Unmanaged.passUnretained((t as AnyObject)).toOpaque())
weak var t1 = t
print("end")
运行后看到变成了0xc0000000200e6308,这里62和63位都变成了1. 在看到allocateSideTable里面生成sidetable的方法InlineRefCountBits 。看到这里把散列表位置像右移了3位,然后把62和63位都变为1. 那么把之前的地址0xc0000000200e6308左移三位,得到0x100731840,看到这里0x100731840前八个字节存放的HeapObject的地址,然后0x0000000000000003 和 0x0000000000000002存的是strongCount和weakCount。
8.2 无主引用
无主引用也不会对其引用的实例保持强引用,但是无主引用不是一个可选类型,它假定是永远有值的,所以无主引用相对于弱引用来说不够那么安全。如果强引用的双方,生命周期没有关联,使用weak,比如delegate。如果其中一个对象,另外一个对象也要跟着销毁,就要使用unowned。weak比unowned来说更安全,而unowned则性能更好,一般来说,使用weak就可以了。
9. 闭包循环引用
swift中,闭包会默认捕获外部的变量。这里可以看出来,闭包内部对外部变量的修改会改变外部原始变量的值。 而这里看到,LGTeacher的deinit没有被调用,这是因为t是全局变量。 这个时候,deinit就被调用了。 而如果让LGTeacher持有这个闭包,那么deinit就不会被调用了,因为闭包和对象形成了循环引用。 那么如何解决这个问题呢?这里在捕获列表使用weak修饰t就可以了。 当然,这里也可以使用unowned 当声明了一个捕获列表,编译器会在当前程序运行的上下文中找到与之同名的变量或者常量,然后进行初始化,这里也就是把0赋值给了闭包中的age,而闭包中的age和外面的age是不同的东西,所以外面的age在变化,闭包中的值也不会变化。这里虽然捕获值是在调用closure的时候,但是因为使用了捕获列表,所以对于age的变量,不再捕获age = 10,而是在定义的上下文中用age的值来初始化捕获列表中age的值。 捕获列表中的age是一个常量,所以这里也无法改变age的值。 对于值类型来说,是调用的时候在捕获值,所以这里打印11,13,14. OC中有强弱共舞,那么Swift中有没有呢? 下面就是Swift中的强弱共舞,这里将t可选值做了模式匹配,将值做了解包的操作,然后赋值给了strongSelf。
class LGTeacher{
var age: Int = 18
var name: String = "Kody"
var closure: (() -> ())?
deinit {
print("deinit")
}
}
func testClosure() {
let t = LGTeacher()
t.closure = { [weak t] in
if let strongSelf = t {
print( strongSelf.age)
}
}
}
这里还有另一个写法,就是使用withExtendedLifetime。
class LGTeacher{
var age: Int = 18
var name: String = "Kody"
var closure: (() -> ())?
deinit {
print("deinit")
}
}
func testClosure() {
let t = LGTeacher()
t.closure = { [weak t] in
withExtendedLifetime(t) {
print(t!.age)
}
}
}
|