一 、指针
为什么说指针不安全?
-
⽐如我们在创建⼀个对象的时候,是需要在堆分配内存空间的。但是这个内存空间的声明周期是有 限的,也就意味着如果我们使⽤指针指向这块内容空间,如果当前内存空间的⽣命周期啊到了(引⽤计数为
0
),那么我们当前的指针是不是就变成了未定义的⾏为了。 -
我们创建的内存空间是有边界的,⽐如我们创建⼀个⼤⼩为
10
的数组,这个时候我们通过指针访问 到了index
=11
的位置,这个时候是不是就越界了,访问了⼀个未知的内存空间。 -
指针类型与内存的值类型不⼀致,也是不安全的。
1.1 指针类型
Swift
中的指针分为两类, typed pointer
指定数据类型指针, raw pointer
未指定数据类型的指针(原⽣指针)。基本上我们接触到的指针类型有一下⼏种:
Swift | Object-C | 说明 |
---|---|---|
unsafePointer<T> | const T * | 指针及所指向的内容都不可变 |
unsafeMutablePointer<T> | T * | 指针及其所指向的内存内容均可变 |
unsafeRawPointer | const void * | 指针指向的内存区域未定 |
unsafeMutableRawPointer | void * | 同上 |
unsafeBufferPointer<T> | ||
unsafeMutableBufferPointer<T> | ||
unsafeRawBufferPointer | ||
unsafeMutableRawBufferPointer |
下面就了解下这几种类型指针的使用!
1.2 原生指针的使⽤
对于如何使⽤ Raw Pointer
来存储4
个整形的数据,这⾥我们需要选取的是UnsafeMutableRawPointer
.
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)")
}
- 先开辟一块内存空间
byteCount
: 当前总的字节大小alignment
: 对齐的大小 - 调用
storeBytes
方法存储当前的整数形值 - 调用
load
方法加载当前内存当中的,这里fromByteOffset
是距离首地址的字节大小,每次移动i * 8
的字节
输出结果:
并不是我们想要的值,先从下面案例做出说明:
struct LGTeacher {
var age: Int = 18
var sex: Bool = true
}
print(MemoryLayout<LGTeacher>.size) // 实际大小
print(MemoryLayout<LGTeacher>.stride) // 步长,理解为对齐之后的大小
print(MemoryLayout<LGTeacher>.alignment)// 当前内存的对齐方式,是1字节对齐,还是 4字节对齐
输出结果:
输出结构
size
是指当前类型的实际大小,stride
步长信息,由于要8字节对齐,所以当前的步长信息是16
字节。就是在内存中连续存储LGTeacher
的实例,就是从第一个LGTeacher
实例到下一个LGTeacher实例所跨越的长度信息,称为步长信息。其中 alignment
就是以当前结构体中最⼤的元素的内存对⻬⼤⼩来作为整个结构体的内存对⻬⼤⼩, 也就是 8
。
回到上面的指针操作,p
是当前的起始地址, 在for
循环中,p.storeBytes(of: i, as: Int.self)
是不断往p
指针 8
字节里面地址存储i
,所以需要移动p
let p = UnsafeMutableRawPointer.allocate(byteCount: 32, alignment: 8)
for i in 0..<4 {
// advanced: 移动步长信息 i * MemoryLayout<Int>.size(Int 类型步长)
p.advanced(by: i * MemoryLayout<Int>.size).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)")
}
p.deallocate()
输出结果:
这样,原生指针使用就没问题了~
1.3 泛型指针
泛型指针相⽐较原⽣指针来说,其实就是指定当前指针已经绑定到了具体的类型。
在进⾏泛型指针访问的过程中,我们并不是使⽤ load
和 store
⽅法来进⾏存储操作。这⾥我们使⽤ 到当前泛型指针内置的变量 pointee
获取 UnsafePointer
的⽅式有两种。⼀种⽅式就是通过已有变量获取
- 通过
withUnsafePointer
来访问到当前变量的地址
var age = 18
withUnsafePointer(to: &age) { ptr in
print(ptr)
}
输出结果:
0x00000001000081a8
Program ended with exit code: 0
- 如果想要修改当前
age
,可以返回当前的修改,没法直接修改ptr.pointee
var age = 18
age = withUnsafePointer(to: &age) { ptr in
return ptr.pointee + 21
}
print(age)
输出结果:
39
直接修改ptr.pointee
编译是不通过的,不可变的。 想要直接修改,可通过 Mutable
, 既 withUnsafeMutablePointer
, 可变的
var age = 18
withUnsafeMutablePointer(to: &age) { ptr in
ptr.pointee += 21
}
print(age)
输出结果:
39
总结:
withUnsafePointer
: 指针和指针内容是不可变的
withUnsafeMutablePointer
: 指针和指针内容是可变的
1.3.1 泛型指针的使用
使用 UnsafeMutablePointer
创建结构体内存
struct LGStruct {
var age: Int
var height: Double
}
var tptr = UnsafeMutablePointer<LGStruct>.allocate(capacity: 5)
// 和C函数的数组一样,使用tPtr[0]来存值
tptr[0] = LGStruct(age: 18, height: 20.0)
tptr[0] = LGStruct(age: 19, height: 21.0)
defer {
// 把当前的内存空间数据清零
tptr.deinitialize(count: 5)
// 回收内存空间
tptr.deallocate()
}
同时还可以使用advanced
这种方式来初始化值
tptr.advanced(by: MemoryLayout<LGStruct>.stride).initialize(to: LGStruct(age: 19, height: 21.0))
1.3.2 指针读取Macho中的属性名称
- 指针获取属性名称源码
class LGTeacher {
var age : Int = 18
var name: String = "Joker"
}
var size: UInt = 0
// 获取__swift5_types在内存中的地址
var ptr = getsectdata("__TEXT", "__swift5_types", &size)
// 获取macho header的起始地址
var mhHeaderPtr = _dyld_get_image_header(0)
// setCommond 的地址
var setCommond64Ptr = getsegbyname("__LINKEDIT")
// 计算链接的基地址
var linkBaseAddress: UInt64 = 0
if let vmaddr = setCommond64Ptr?.pointee.vmaddr, let fileOff = setCommond64Ptr?.pointee.fileoff{
linkBaseAddress = vmaddr - fileOff
}
// 获取__swift5_types的偏移量
var offset: UInt64 = 0
if let unwrappedPtr = ptr{
let intRepresentation = UInt64(bitPattern: Int64(Int(bitPattern: unwrappedPtr)))
// 需要减去 基地址
offset = intRepresentation - linkBaseAddress
}
//print(offset)
//DataLo的内存地址
let mhHeaderPtr_IntRepresentation = UInt64(bitPattern: Int64(Int(bitPattern: mhHeaderPtr)))
var dataLoAddress = mhHeaderPtr_IntRepresentation + offset
// 转换为指针类型
var dataLoAddressPtr = withUnsafePointer(to: &dataLoAddress){return $0}
//获取dataLo内容
var dataLoContent = UnsafePointer<UInt32>.init(bitPattern: Int(exactly: dataLoAddress) ?? 0)?.pointee
//获取描述文件的偏移量 dataLo + 文件偏移量 - 基地址
let typeDescOffset = UInt64(dataLoContent!) + offset - linkBaseAddress
//获取描述文件的地址(TargetClassDescriptor) 偏移量 + macho的起始地址
var typeDescAddress = typeDescOffset + mhHeaderPtr_IntRepresentation
//print(typeDescAddress)
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
}
// 将typeDescAddress地址按照TargetClassDescriptor读取获取相关结构
let classDescriptor = UnsafePointer<TargetClassDescriptor>.init(bitPattern: Int(exactly: typeDescAddress) ?? 0)?.pointee
// 获取 TargetClassDescriptor 中的 name
if let name = classDescriptor?.name{
let nameOffset = Int64(name) + Int64(typeDescOffset) + 8
print(nameOffset)
let nameAddress = nameOffset + Int64(mhHeaderPtr_IntRepresentation)
print(nameAddress)
if let cChar = UnsafePointer<CChar>.init(bitPattern: Int(nameAddress)){
print(String(cString: cChar))
}
}
// 获取fieldDescriptor 的相对地址 typeDescOffset(描述文件的偏移量)
let filedDescriptorRelaticveAddress = typeDescOffset + 16 + mhHeaderPtr_IntRepresentation
//print(filedDescriptorAddress)
struct FieldDescriptor {
var mangledTypeName: Int32
var superclass: Int32
var Kind: UInt16
var fieldRecordSize: UInt16
var numFields: UInt32
// var fieldRecords: [FieldRecord]
}
struct FieldRecord{
var Flags: UInt32
var mangledTypeName: Int32
var fieldName: UInt32
}
// 指针类型
let fieldDescriptorOffset = UnsafePointer<UInt32>.init(bitPattern: Int(exactly: filedDescriptorRelaticveAddress) ?? 0)?.pointee
// 真实的内存地址
//print(fieldDescriptorOffset)
let fieldDescriptorAddress = filedDescriptorRelaticveAddress + UInt64(fieldDescriptorOffset!)
// 拿到 fieldDescriptor 类型指针
let fieldDescriptor = UnsafePointer<FieldDescriptor>.init(bitPattern: Int(exactly: fieldDescriptorAddress) ?? 0)?.pointee
// 读取属性
for i in 0..<fieldDescriptor!.numFields{
// 计算偏移量 Flags(4字节) + mangledTypeName(4字节)+ fieldName(四字节)= 12
let stride: UInt64 = UInt64(i * 12)
// 获取 fieldRecord 的地址
let fieldRecordAddress = fieldDescriptorAddress + stride + 16
// 获取fieldName的相对地址
let fieldNameRelactiveAddress = UInt64(2 * 4) + fieldRecordAddress - linkBaseAddress + mhHeaderPtr_IntRepresentation
let offset = UnsafePointer<UInt32>.init(bitPattern: Int(exactly: fieldNameRelactiveAddress) ?? 0)?.pointee
// 读取内存地址
let fieldNameAddress = fieldNameRelactiveAddress + UInt64(offset!) - linkBaseAddress
// 类型读取
if let cChar = UnsafePointer<CChar>.init(bitPattern: Int(fieldNameAddress)){
print(String(cString: cChar))
}
}
- 指针获取属性名称源码分析: 获取__swift5_types在内存中的地址
class LGTeacher {
var age : Int = 18
var name: String = "Joker"
}
var size: UInt = 0
// 获取__swift5_types在内存中的地址
var ptr = getsectdata("__TEXT", "__swift5_types", &size)
print(ptr)
输出结果与Macho对比:
1.3.3 指针读取Macho中的方法名称V-Table
定义一个类,添加三个方法
class LGTeacher{
func teach() {
print("teach")
}
func teach1(){
print("teach1")
}
func teach2(){
print("teach2")
}
}
准备v-table
结构
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
}
// V-Table 的结构
struct TargetMethodDescriptor {
// 占 4 字节,Flags 标识是什么方法。
var Flags: UInt32
// 不是真正的 imp,这里存储的是相对指针,offset。
var Impl: UInt32
}
获取 V-Table
方法信息代码
var size: UInt = 0
// 获取__swift5_types在内存中的地址
var ptr = getsectdata("__TEXT", "__swift5_types", &size)
// 获取macho header的起始地址
var mhHeaderPtr = _dyld_get_image_header(0)
// setCommond 的地址
var setCommond64Ptr = getsegbyname("__LINKEDIT")
// 计算链接的基地址
var linkBaseAddress: UInt64 = 0
if let vmaddr = setCommond64Ptr?.pointee.vmaddr, let fileOff = setCommond64Ptr?.pointee.fileoff{
linkBaseAddress = vmaddr - fileOff
}
// 获取__swift5_types的偏移量
var offset: UInt64 = 0
if let unwrappedPtr = ptr{
let intRepresentation = UInt64(bitPattern: Int64(Int(bitPattern: unwrappedPtr)))
// 需要减去 基地址
offset = intRepresentation - linkBaseAddress
}
//print(offset)
//DataLo的内存地址
let mhHeaderPtr_IntRepresentation = UInt64(bitPattern: Int64(Int(bitPattern: mhHeaderPtr)))
var dataLoAddress = mhHeaderPtr_IntRepresentation + offset
// 转换为指针类型
var dataLoAddressPtr = withUnsafePointer(to: &dataLoAddress){return $0}
//获取dataLo内容
var dataLoContent = UnsafePointer<UInt32>.init(bitPattern: Int(exactly: dataLoAddress) ?? 0)?.pointee
//获取描述文件的偏移量 dataLo + 文件偏移量 - 基地址
let typeDescOffset = UInt64(dataLoContent!) + offset - linkBaseAddress
//获取描述文件的地址(TargetClassDescriptor) 偏移量 + macho的起始地址
var typeDescAddress = typeDescOffset + mhHeaderPtr_IntRepresentation
print(typeDescAddress)
// 将typeDescAddress地址按照TargetClassDescriptor读取获取相关结构
let classDescriptor = UnsafePointer<TargetClassDescriptor>.init(bitPattern: Int(exactly: typeDescAddress) ?? 0)?.pointee
for i in 0..<classDescriptor!.size {
// VTable offset
let vTable_offset = Int(typeDescOffset) + MemoryLayout<TargetClassDescriptor>.size + MemoryLayout<TargetMethodDescriptor>.size * Int(i)
// 获取 VTable 的地址
let vTable_address = Int(mhHeaderPtr_IntRepresentation) + 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(linkBaseAddress)
// 转成 IMP
let imp: IMP = IMP(bitPattern: UInt(imp_address))!
// 通过 OC 的类和语法调用 IMP,打印方法名
LGTestImp.callFunc(imp: imp)
}
其中 LGTestImp
为OC
类,为方便调用IMP打印Swift
类的方法
@interface LGTestImp : NSObject
+ (void)callFuncWithImp:(IMP)imp;
@end
#import "LGTestImp.h"
@implementation LGTestImp
+ (void)callFuncWithImp:(IMP)imp
{
imp();
}
@end
输出结果:
1.3.4 补充说明
VM Address
:Virtual Memory Address
, 段的虚拟内存地址,在内存中的位置VM Size
:Virtual Memory Size
, 段的虚拟内存大小, 占用多少内存File Offset
: 段在文件中的偏移量File Size
: 段在文件中的大小Address Space Layout Random
,地址空间布局随机化 , 是一种针对缓冲区溢出的安全保 护技术,通过对堆、栈、共享库映射等线性区布局的随机化,通过增加攻击者预测目的地址 的难度,防止攻击者指针定位攻击代码位置,达到阻止溢出攻击的一种技术
1.4 内存绑定
Swift
提供了三种不同的 API
来绑定/重新绑定指针:
1.4.1 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(UnsafeRawPointer(tuplePtr).assumingMemoryBound(to: Int.self))
}
输出结果:
10
20
这样就可以了
有些时候我们处理代码的过程中,只有原始指针(没有保留指针类型),但此刻对于处理代码的我们来 说明确知道指针的类型,我们就可以使⽤ assumingMemoryBound(to:)
来告诉编译器预期的类型。 (注意:这⾥只是让编译器绕过类型检查,并没有发⽣实际类型的转换)
1.4.2 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
testPoint(UnsafeRawPointer(tuplePtr).bindMemory(to: Int.self, capacity: 1))
}
输出结果:
10
20
⽤于更改内存绑定的类型,如果当前内存还没有类型绑定,则将⾸次绑定为该类型;否则重新绑定该类 型,并且内存中所有的值都会变成该类型。
1.4.3. withMemoryRebound(to: capacity: body:)
func testPoint(_ p: UnsafePointer<Int8>) {
print(p[0])
print(p[1])
}
let Uint8Ptr = UnsafePointer<UInt8>.init(bitPattern: 10)
Uint8Ptr?.withMemoryRebound(to: Int8.self, capacity: 1){ (int8Ptr: UnsafePointer<Int8>) in
testPoint(int8Ptr)
}
当我们在给外部函数传递参数时,不免会有⼀些数据类型上的差距。如果我们进⾏类型转换,必然要来 回复制数据;这个时候我们就可以使⽤ withMemoryRebound(to: capacity: body:)
来临时更 改内存绑定类型。
二、内存管理
2.1 强引用
Swift
中使⽤⾃动引⽤计数(ARC)
机制来追踪和管理内存。
class LGTeacher{
var age: Int = 18
var name: String = "Joker"
}
// 初始化变量
var t = LGTeacher()
// 接下来把 t 赋值给了 t1、t2
var t1 = t
var t2 = t
第一步初始化了一个变量,接下来把t
赋值给了t1
、t2
,这个时候是不是就有三个强引用了,也就意味着此时此刻引用计数为3
之前我们在分析对象的内存布局的时候,是不是已经知道了,其中 8字节是⽤来存储当前的引⽤计数 的。
LLDB调试中,并不能够看出来当前的引用计数是不是3
;在Swift
源码中找到 HeapObject.h
文件,并在 HeapObject.h
文件中找到 refCounts
的具体定义:
这里我们知道, refCounts
的类型为InlineRefCounts
,在RefCount.h
文件中找到 InlineRefCounts
类型:
typedef RefCounts<InlineRefCountBits> InlineRefCounts;
发现它是一个模板类 RefCounts
,接收一个泛型参数,查看 RefCounts
数据结构:
RefCounts
是什么呢,RefCounts
其实是对引用计数的一个包装,而引用计数的具体类型取决于外部传进来的泛型参数InlineRefCountBits
;那么这个泛型参数 InlineRefCountBits
是什么呢?查看它的定义:
typedef RefCountBitsT<RefCountIsInline> InlineRefCountBits;
同样的,它也是一个模板函数,并且参数为RefCountIsInline
,参数传的值是 true
或者 false
,接下来看 RefCountBitsT
的结构:
RefCountBitsT
只有一个属性信息bits
,bits的类型为 BitsType
,由 RefCountBitsInt
中的Type
属性来定义,
可以看到,Type
是一个 uint64_t
位域信息,在这个 uint64_t
存储了运行生命周期的相关引用计数。
到这我们仍然不知道是如何设置的,我们先来看⼀下,当我们创建⼀个实例对象的时候,当前的引⽤计数是多少?找到 HeapObject
定义,在HeapObject
初始化方法找到了 refCounts
初始化赋值。
全局搜索 Initialized_t
,找到Initialized
为Initialized_t
枚举的一个值。
看到 constexpr RefCounts(Initialized_t) : refCounts(RefCountBits(0, 1)) {}
中,RefCounts
函数参数 就是前面找的 RefCountBits
类。RefCountBits
类的初始化方法如下:
如图可得出:调用
RefCountBitsT
初始化方法,其中的三个参数的值怎么来的呢?
接下来看 RefCountBitOffsets
在 64 位中的表现
PureSwiftDeallocShift
: 0
UnownedRefCountShift
和 StrongExtraRefCountShift
都调用的同一个函数 shiftAfterField
,函数的实现方式为:
# define shiftAfterField(name) (name##Shift + name##BitCount)
UnownedRefCountShift
传入的参数为PureSwiftDealloc
,具体内部实现:
PureSwiftDeallocShift + PureSwiftDeallocBitCount = 0 + 1 = 1
StrongExtraRefCountShift
传入的参数为IsDeiniting
IsDeinitingShift + IsDeinitingBitCount
IsDeinitingShift
传入的是UnownedRefCount
, 所以具体实现为:
(UnownedRefCountShift + UnownedRefCountBitCount) + IsDeinitingBitCount = 1 + 31 + 1 = 33
知道三个参数的由来后,开始计算 RefCountBitsT
初始化调用 bits
的值,最终计算出来的结果为 3
代码验证结果:
通过 refCounts
了解它是一个引用计数相关的数据,在创建对象后,对这个对象进行多个引用,refCounts
变化如下:
对一个实例对象进行引用的时候,是一个位移的运算。 以下图是64位位域信息下的 refCounts
的存储。
验证一下,先声明一个可选类型,如果没有对实例进行一个强引用,这个实例会被释放掉,也就是 32 位会变成 1。
强引用是怎么添加的呢?可通过Swift源码看下,全局是是是_swift_retain_
,在 HeapObject.cpp文件中找到他的实现
在进行强引用的时候,本质上就是调用object
的refCounts
, 区别就是increment
+ 1;
increment
中调用了 incrementStrongExtraRefCount
, incrementStrongExtraRefCount
的实现为
前面已知道 StrongExtraRefCountShift = 33
,并且inc = 1
,所以左移33
位, 1 << 33 = 0x2
这⾥我们就了解了我们的强引⽤,使⽤强引⽤就会造成⼀个问题:循环引⽤。我们来看⼀个经典的循环 引⽤案例:
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 提供了两种办法⽤来解决你在使 ⽤类的属性时所遇到的循环强引⽤问题:弱引⽤( weak reference )和⽆主引⽤( unowned reference )。
2.2 弱引⽤ weak reference
弱引⽤不会对其引⽤的实例保持强引⽤,因⽽不会阻⽌ ARC
释放被引⽤的实例。这个特性阻⽌了引⽤ 变为循环强引⽤。声明属性或者变量时,在前⾯加上 weak
关键字表明这是⼀个弱引⽤。
由于弱引⽤不会强保持对实例的引⽤,所以说实例被释放了弱引⽤仍旧引⽤着这个实例也是有可能的。 因此,ARC
会在被引⽤的实例被释放是⾃动地设置弱引⽤为 nil
。由于弱引⽤需要允许它们的值为 nil
, 它们⼀定得是可选类型 var 定义。
class LGTeacher{
var age: Int = 18
var name: String = "Joker"
}
weak var t = LGTeacher()
print(Unmanaged.passUnretained(t as AnyObject).toOpaque())
print("end")
在 weak
出打一个符号断点,进行汇编调试
通过汇编可以看出,通过weak
修饰后,t
变成了一个可选项,之后调用了一个符号信息swift_weakInit
,Swift
源码分析
声明⼀个 weak
变量相当于定义了⼀个 WeakRefrence
对象,在其中调用了nativeInit
,进入nativeInit
函数内部
void nativeInit(HeapObject *object) {
auto side = object ? object->refCounts.formWeakReference() : nullptr;
nativeValue.store(WeakReferenceBits(side), std::memory_order_relaxed);
}
在里面,调用了object->refCounts.formWeakReference()
,形成了一个弱引用;进入 formWeakReference
内部函数
// SideTableRefCountBits specialization intentionally does not exist.
template <>
HeapObjectSideTableEntry* RefCounts<InlineRefCountBits>::formWeakReference()
{
auto side = allocateSideTable(true);
if (side)
return side->incrementWeak();
else
return nullptr;
}
它本质上就是创建了一个散列表,进入allocateSideTable
函数
// Return an object's side table, allocating it if necessary.
// Returns null if the object is deiniting.
// SideTableRefCountBits specialization intentionally does not exist.
template <>
HeapObjectSideTableEntry* RefCounts<InlineRefCountBits>::allocateSideTable(bool failIfDeiniting)
{
// 第一步,先拿到原有的引用计数
auto oldbits = refCounts.load(SWIFT_MEMORY_ORDER_CONSUME);
// Preflight failures before allocating a new side table.
// 判断原先是否有引用计数
if (oldbits.hasSideTable()) {
// Already have a side table. Return it.
// 有就获取到原有的引用计数
return oldbits.getSideTable();
}
else if (failIfDeiniting && oldbits.getIsDeiniting()) {
// Already past the start of deinit. Do nothing.
// 没有直接返回nil
return nullptr;
}
// Preflight passed. Allocate a side table.
// FIXME: custom side table allocator
// 接下来创建一个side table.
HeapObjectSideTableEntry *side = new HeapObjectSideTableEntry(getHeapObject());
// 用创建出来的side table的地址来进行初始化
auto newbits = InlineRefCountBits(side);
// 对原来的side table做处理
do {
if (oldbits.hasSideTable()) {
// Already have a side table. Return it and delete ours.
// Read before delete to streamline barriers.
auto result = oldbits.getSideTable();
delete side;
return result;
}
else if (failIfDeiniting && oldbits.getIsDeiniting()) {
// Already past the start of deinit. Do nothing.
return nullptr;
}
// 将原有的引用计数保存
side->initRefCounts(oldbits);
} while (! refCounts.compare_exchange_weak(oldbits, newbits,
std::memory_order_release,
std::memory_order_relaxed));
return side;
}
散列表创建可以得出以下几步:
取出原有的引用计数
判断原先的引用计数是否有side table,如果有直接返回,没有并且这种析构返回nil
创建一个side table
对原来的side table做处理
接下来看下 HeapObjectSideTableEntry
是什么?在Swift 源码中全局搜索 HeapObjectSideTableEntry
如图已指出强引用和弱引用内部实现的区别了,进入 HeapObjectSideTableEntry
类结构
HeapObjectSideTableEntry
有 RefCounts
成员,是内部是 SideTableRefCountBits
,而它又继承于 RefCountBitsT
模板类
验证是否存储了对象的指针
class LGTeacher{
var age: Int = 18
var name: String = "Joker"
}
var t = LGTeacher()
print(Unmanaged.passUnretained(t as AnyObject).toOpaque())
weak var t1 = t
print("end")
weak var t1 = t
与 print("end")
处分别打上断点调试
LLDB调试:
weak
修饰后,refCounts
从原来的 0x0000000000000003
变成 0xc0000000200e5e1c
,打开计算器进行还原处理
在62
位和63
位进行归0处理,得到结果为 0x200E5E1C
找到 RefCountBitsT
模板类的初始方法
SWIFT_ALWAYS_INLINE
RefCountBitsT(HeapObjectSideTableEntry* side)
: bits((reinterpret_cast<BitsType>(side) >> Offsets::SideTableUnusedLowBits)
| (BitsType(1) << Offsets::UseSlowRCShift)
| (BitsType(1) << Offsets::SideTableMarkShift))
{
assert(refcountIsInline);
}
SideTableUnusedLowBits
= 3
,在这个过程中是往右移动了 3
位,所以我们需要把它往左移3
位进行还原处理得到散列表地址.
散列表地址:
0x200E5E1C << 3 = 0x10072F0E0
LLDB调试:
LLDB调试的结果与分析一致。所以,当用 weak
修饰的时候,本质上是创建了一个散列表。
2.3 ⽆主引⽤ unowned reference
和弱引⽤类似,⽆主引⽤不会牢牢保持住引⽤的实例。但是不像弱引⽤,总之,⽆主引⽤假定是永远有 值的.
class LGTeacher{
var age: Int = 18
var name: String = "Joker"
}
var t: LGTeacher? = LGTeacher()
print(Unmanaged.passUnretained(t as AnyObject).toOpaque())
unowned var t1 = t
t = nil
print(t1)
print("end")
LLDB调试结果:
0x00000001005b4340
Fatal error: Attempted to read an unowned reference but object 0x1005b4340 was already deallocated2022-01-25 17:37:06.520466+0800 LGSwiftTest[58643:3045039] Fatal error: Attempted to read an unowned reference but object 0x1005b4340 was already deallocated
Fatal error: Attempted to read an unowned reference but object 0x1005b4340 was already deallocated
根据苹果的官⽅⽂档的建议。当我们知道两个对象的⽣命周期并不相关,那么我们必须使⽤ weak。相 反,⾮强引⽤对象拥有和强引⽤对象同样或者更⻓的⽣命周期的话,则应该使⽤ unowned
。
Weak VS unowned
如果两个对象的⽣命周期完全和对⽅没关系(其中⼀⽅什么时候赋值为
nil
,对对⽅都没影响),请⽤weak
如果你的代码能确保:其中⼀个对象销毁,另⼀个对象也要跟着销毁,这时候,可以(谨慎)⽤unowned
2.4 闭包循环引⽤闭包循环引⽤
⾸先我们的闭包会⼀般默认捕获我们外部的变量
闭包内部对变量的修改将会改变外部原始变量的值
那同样就会有⼀个问题,如果我们在 class 的内部定义⼀个闭包,当前闭包访问属性的过程中,就会对 我们当前的实例对象进⾏捕获:
当前的
t
和 closure
并没有形成相互引用的关系,所以他们之间没有强引用
此时此刻,对于当前的闭包来说和当前的对象形成了循环引用,所以未打印
deinit
?怎么解决呢?
在闭包表达式的捕获列表声明 weak
或者 unowned
引用,可以解决循环引用的问题
t.testClosure = { [weak t] in
t!.age += 1
}
t.testClosure = { [unowned t] in
t.age += 1
}
输出结果:
什么是捕获列表
-
默认情况下,闭包表达式从其周围的范围捕获常量和变量,并强引⽤这些值。您可以使⽤捕获列表来显 式控制如何在闭包中捕获值。
-
在参数列表之前,捕获列表被写为⽤逗号括起来的表达式列表,并⽤⽅括号括起来。如果使⽤捕获列 表,则即使省略参数名称,参数类型和返回类型,也必须使⽤in关键字。
创建闭包时,将初始化捕获列表中的条⽬。对于捕获列表中的每个条⽬,将常量初始化为在周围范围内
具有相同名称的常量或变量的值。例如,在下⾯的代码中,捕获列表中包含age
,但捕获列表中未包含height
,这使它们具有不同的⾏为。
var age = 0
var height = 0.0
let closure = { [age] in
print(age)
print(height)
}
age = 10
height = 1.85
closure()
输出结果:
age: 0
height: 1.85
创建闭包时,内部作⽤域中的
age
会⽤外部作⽤域中的age
的值进⾏初始化,但它们的值未以任何特殊⽅式连接。
这意味着更改外部作⽤域中的
age
的值不会影响内部作⽤域中的age
的值,也不会更改封闭内部的值,也不会影响封闭外部的值。
相⽐之下,只有⼀个名为
height
的变量-外部作⽤域中的height
因此,在闭包内部或外部进⾏的更改在两个地⽅均可⻅.
闭包表达式 [age] 默认是let
修饰,不可变的.