Swift 进阶04: 指针&内存管理

775 阅读15分钟

一 、指针

为什么说指针不安全?

  1. ⽐如我们在创建⼀个对象的时候,是需要在堆分配内存空间的。但是这个内存空间的声明周期是有 限的,也就意味着如果我们使⽤指针指向这块内容空间,如果当前内存空间的⽣命周期啊到了(引⽤计数为0),那么我们当前的指针是不是就变成了未定义的⾏为了。

  2. 我们创建的内存空间是有边界的,⽐如我们创建⼀个⼤⼩为10的数组,这个时候我们通过指针访问 到了 index = 11 的位置,这个时候是不是就越界了,访问了⼀个未知的内存空间。

  3. 指针类型与内存的值类型不⼀致,也是不安全的。


1.1 指针类型

Swift中的指针分为两类, typed pointer 指定数据类型指针, raw pointer 未指定数据类型的指针(原⽣指针)。基本上我们接触到的指针类型有一下⼏种:

SwiftObject-C说明
unsafePointer<T>const T *指针及所指向的内容都不可变
unsafeMutablePointer<T>T *指针及其所指向的内存内容均可变
unsafeRawPointerconst void *指针指向的内存区域未定
unsafeMutableRawPointervoid *同上
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 的字节

输出结果:

截屏2022-01-10 下午4.20.34.png

并不是我们想要的值,先从下面案例做出说明:

struct LGTeacher {
    var age: Int = 18
    var sex: Bool = true
}
print(MemoryLayout<LGTeacher>.size)     // 实际大小
print(MemoryLayout<LGTeacher>.stride)   // 步长,理解为对齐之后的大小
print(MemoryLayout<LGTeacher>.alignment)// 当前内存的对齐方式,是1字节对齐,还是 4字节对齐

输出结果:

截屏2022-01-10 下午4.33.15.png

输出结构

截屏2022-01-10 下午7.42.37.png

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()

输出结果:

image.png

这样,原生指针使用就没问题了~


1.3 泛型指针

泛型指针相⽐较原⽣指针来说,其实就是指定当前指针已经绑定到了具体的类型。

在进⾏泛型指针访问的过程中,我们并不是使⽤ loadstore ⽅法来进⾏存储操作。这⾥我们使⽤ 到当前泛型指针内置的变量 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

image.png

直接修改ptr.pointee 编译是不通过的,不可变的。 想要直接修改,可通过 Mutable, 既 withUnsafeMutablePointer, 可变的

var age = 18
withUnsafeMutablePointer(to: &age) { ptr in
    ptr.pointee += 21
}
print(age)

输出结果:

39

总结:

withUnsafePointer: 指针和指针内容是不可变的
withUnsafeMutablePointer: 指针和指针内容是可变的

1.3.1 泛型指针的使用

image.png

使用 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对比:

image.png


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)
}

其中 LGTestImpOC类,为方便调用IMP打印Swift类的方法

@interface LGTestImp : NSObject
+ (void)callFuncWithImp:(IMP)imp;
@end
#import "LGTestImp.h"
@implementation LGTestImp
+ (void)callFuncWithImp:(IMP)imp
{
    imp();
}
@end

输出结果:

截屏2022-01-24 下午4.13.03.png

1.3.4 补充说明

  1. VM Address : Virtual Memory Address, 段的虚拟内存地址,在内存中的位置
  2. VM Size : Virtual Memory Size, 段的虚拟内存大小, 占用多少内存
  3. File Offset : 段在文件中的偏移量
  4. File Size : 段在文件中的大小
  5. Address Space Layout Random ,地址空间布局随机化 , 是一种针对缓冲区溢出的安全保 护技术,通过对堆、栈、共享库映射等线性区布局的随机化,通过增加攻击者预测目的地址 的难度,防止攻击者指针定位攻击代码位置,达到阻止溢出攻击的一种技术

1.4 内存绑定

Swift 提供了三种不同的 API 来绑定/重新绑定指针:

1.4.1 assumingMemoryBound(to:)

image.png

这里会报错~


正确代码输入

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赋值给了t1t2,这个时候是不是就有三个强引用了,也就意味着此时此刻引用计数为3

之前我们在分析对象的内存布局的时候,是不是已经知道了,其中 8字节是⽤来存储当前的引⽤计数 的。

image.png

LLDB调试中,并不能够看出来当前的引用计数是不是3;在Swift源码中找到 HeapObject.h 文件,并在 HeapObject.h 文件中找到 refCounts 的具体定义:

image.png

这里我们知道, refCounts的类型为InlineRefCounts,在RefCount.h文件中找到 InlineRefCounts类型:

typedef RefCounts<InlineRefCountBits> InlineRefCounts;

发现它是一个模板类 RefCounts,接收一个泛型参数,查看 RefCounts 数据结构:

image.png

RefCounts是什么呢,RefCounts其实是对引用计数的一个包装,而引用计数的具体类型取决于外部传进来的泛型参数InlineRefCountBits;那么这个泛型参数 InlineRefCountBits 是什么呢?查看它的定义:

typedef RefCountBitsT<RefCountIsInline> InlineRefCountBits;

同样的,它也是一个模板函数,并且参数为RefCountIsInline,参数传的值是 true 或者 false,接下来看 RefCountBitsT的结构:

image.png

RefCountBitsT 只有一个属性信息bits,bits的类型为 BitsType,由 RefCountBitsInt中的Type属性来定义,

image.png

可以看到,Type 是一个 uint64_t 位域信息,在这个 uint64_t存储了运行生命周期的相关引用计数。


到这我们仍然不知道是如何设置的,我们先来看⼀下,当我们创建⼀个实例对象的时候,当前的引⽤计数是多少?找到 HeapObject定义,在HeapObject初始化方法找到了 refCounts初始化赋值。

image.png

全局搜索 Initialized_t,找到InitializedInitialized_t枚举的一个值。

image.png

看到 constexpr RefCounts(Initialized_t) : refCounts(RefCountBits(0, 1)) {}中,RefCounts 函数参数 就是前面找的 RefCountBits类。RefCountBits类的初始化方法如下:

截屏2022-01-23 17.48.03.png 如图可得出:调用RefCountBitsT初始化方法,其中的三个参数的值怎么来的呢?

接下来看 RefCountBitOffsets在 64 位中的表现

截屏2022-01-23 17.58.25.png

  • PureSwiftDeallocShift: 0

UnownedRefCountShiftStrongExtraRefCountShift 都调用的同一个函数 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

代码验证结果:

image.png

通过 refCounts 了解它是一个引用计数相关的数据,在创建对象后,对这个对象进行多个引用,refCounts变化如下:

image.png

对一个实例对象进行引用的时候,是一个位移的运算。 以下图是64位位域信息下的 refCounts的存储。

截屏2022-01-23 18.55.59.png

验证一下,先声明一个可选类型,如果没有对实例进行一个强引用,这个实例会被释放掉,也就是 32 位会变成 1。

image.png

强引用是怎么添加的呢?可通过Swift源码看下,全局是是是_swift_retain_,在 HeapObject.cpp文件中找到他的实现

截屏2022-01-24 下午4.23.44.png

在进行强引用的时候,本质上就是调用objectrefCounts, 区别就是increment + 1;

increment 中调用了 incrementStrongExtraRefCount, incrementStrongExtraRefCount的实现为

image.png

image.png

前面已知道 StrongExtraRefCountShift = 33,并且inc = 1,所以左移33位, 1 << 33 = 0x2

image.png

这⾥我们就了解了我们的强引⽤,使⽤强引⽤就会造成⼀个问题:循环引⽤。我们来看⼀个经典的循环 引⽤案例:

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出打一个符号断点,进行汇编调试

image.png

通过汇编可以看出,通过weak 修饰后,t 变成了一个可选项,之后调用了一个符号信息swift_weakInitSwift源码分析

image.png

声明⼀个 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

screenshot-20220125-142904.png

如图已指出强引用和弱引用内部实现的区别了,进入 HeapObjectSideTableEntry 类结构

image.png

HeapObjectSideTableEntryRefCounts 成员,是内部是 SideTableRefCountBits,而它又继承于 RefCountBitsT模板类

image.png

验证是否存储了对象的指针

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 = tprint("end") 处分别打上断点调试 LLDB调试:

image.png

weak 修饰后,refCounts 从原来的 0x0000000000000003 变成 0xc0000000200e5e1c,打开计算器进行还原处理

image.png

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调试:

screenshot-20220125-152442.png

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 闭包循环引⽤闭包循环引⽤

⾸先我们的闭包会⼀般默认捕获我们外部的变量

image.png

闭包内部对变量的修改将会改变外部原始变量的值


那同样就会有⼀个问题,如果我们在 class 的内部定义⼀个闭包,当前闭包访问属性的过程中,就会对 我们当前的实例对象进⾏捕获:

image.png 当前的tclosure并没有形成相互引用的关系,所以他们之间没有强引用

image.png 此时此刻,对于当前的闭包来说和当前的对象形成了循环引用,所以未打印 deinit?怎么解决呢?

在闭包表达式的捕获列表声明 weak 或者 unowned 引用,可以解决循环引用的问题

t.testClosure = { [weak t] in
   t!.age += 1
}
t.testClosure = { [unowned t] in
   t.age += 1
}

输出结果:

image.png

什么是捕获列表

  • 默认情况下,闭包表达式从其周围的范围捕获常量和变量,并强引⽤这些值。您可以使⽤捕获列表来显 式控制如何在闭包中捕获值。

  • 在参数列表之前,捕获列表被写为⽤逗号括起来的表达式列表,并⽤⽅括号括起来。如果使⽤捕获列 表,则即使省略参数名称,参数类型和返回类型,也必须使⽤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修饰,不可变的.