美文网首页
Swift 三、指针 & 内存管理

Swift 三、指针 & 内存管理

作者: 常在士心 | 来源:发表于2022-01-12 18:52 被阅读0次
指针&内存管理.png

一、指针

1.1 为什么说指针不安全

  • ⽐如我们在创建⼀个对象的时候,是需要在堆分配内存空间的。但是这个内存空间的生命周期是有限的,也就意味着如果我们使⽤指针指向这块内存空间,如果当前内存空间的⽣命周期已经到了(引⽤计数为0),那么我们当前的指针是不是就变成了未定义的行为了。
  • 我们创建的内存空间是有边界的,⽐如我们创建⼀个⼤⼩为10的数组,这个时候我们通过指针访问 到了 index = 11的位置,这个时候数组是不是就越界了,访问了⼀个未知的内存空间。
  • 指针类型与内存的值类型不⼀致,也是不安全的。

1.2 指针类型

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

指针类型.png

1.2.1 原生指针的使用

我们⼀起来看⼀下如何使⽤ Raw Pointer 来存储 4 个整型的数据,这⾥我们需要选取的是 UnsafeMutableRawPointer

///1、开辟一块内存空间
/// UnsafeMutableRawPointer存储原生指针
/// allocate:开辟空间
///byteCount: 当前总的内存大小 4个Int整型,一个整型是8字节,共32字节
///alignment: 对齐的大小

///2、调用storeBytes方法存储当前的整型数值
///of: 存储值
///as: 值的类型,当前类型是整型

///3、我们打印输出验证一下
///调用load方法加载当前内存当中的数据
///fromByteOffset: 距离首地址的字节的大小,每次移动i * 8字节
///as: 值的类型,当前类型是整型

///4、收回并释放对应的内存空间
/// deallocate

let p = UnsafeMutableRawPointer.allocate(byteCount: 4 * 8, 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)")
}
p.deallocate()

编译运行,我们调用打印数据查看一下

index: 0, value: 3
index: 1, value: 0
index: 2, value: 16
index: 3, value: 0

结果发现,这个打印结果并不能和我们存储的数据一致。那么问题出在哪里哪?
我们在存储后打印一下p的内存地址

省略......
for i in 0 ..< 4 {
    p.storeBytes(of: i, as: Int.self)
}

print(p)

lldb打印结果如下:

0x000000010073c4e0
(lldb) x/8g 0x000000010073c4e0
0x10073c4e0: 0x0000000000000003 0x0000000000000000
0x10073c4f0: 0x0000000000000010 0x0000000000000000
0x10073c500: 0x000000004d55545a 0x000020a000000000
0x10073c510: 0x4d55545a00000000 0x0000000000000000

这⾥很显然是我们对当前数据的存储⽅式不对,按道理来说我们是 8 个字节, 8个字节的排列开来,⽽ 在这个过程中存储的好像不知道每个数据与数据之间的间距,所以这⾥我们需要指定⼀个东⻄,那就是每个数据之间在内存中的间距。

经探究发现,是因为我们存储时storeBytes并没有移动对应的步长信息导致的。那么什么是步长信息哪,我们首先要了解一个枚举类型MemoryLayout

为了便于理解MemoryLayout,我们先来看下下面这个例子:

struct ZGTeacher {
    var age: Int = 18
}

let size = MemoryLayout<ZGTeacher>.size
let stride = MemoryLayout<ZGTeacher>.stride
let alignment = MemoryLayout<ZGTeacher>.alignment

print(size, stride, alignment)

lldb打印结果

8 8 8

我们给ZGTeacher结构体新增一个Bool属性看一下它的变化。

struct ZGTeacher {
    var age: Int = 18
///新增一个Bool属性
    var sex: Bool = true
}
省略......

lldb打印结果如下:

9 16 8

其中 alignment 是不变的,但是 sizestride 都变了。

当前结构体的MemoryLayout.png

MemoryLayout是Swift标准库中定义的一个枚举,顾名思义其是用于获取内存相关信息,MemoryLayout<Int>则是一种泛型的用法,调用其size属性可以获取某种数据类型所占内存空间的字节数,是其在内存中真实占用大小。调用其stride属性可以获取某种数据类型所开辟内存空间的字节数,是系统为其分配的内存大小。调用其alignment属性可以获取某种数据类型所需要的对齐信息,指的是其当前内存对齐方式,是1字节对齐,4字节对齐等。

这⾥我们回到我们的指针操作,此时我们应该明⽩我们在存储 4 个连续整型的数据时候的问题了,那么 就是我们并没有指定当前 Int 数据在排列过程中每个数据和每个数据之间的间隔是多少?
代码修改如下:

///advanced 移动对应的步长,来存放内容
let size = MemoryLayout<Int>.size
let stride = MemoryLayout<Int>.stride
let alignment = MemoryLayout<Int>.alignment

let p = UnsafeMutableRawPointer.allocate(byteCount: 4 * stride, alignment: alignment)

for i in 0 ..< 4 {
    p.advanced(by: i * stride).storeBytes(of: i, as: Int.self)
///(p + i * stride).storeBytes(of: i, as: Int.self)
}

print(p)

for i in 0 ..< 4 {
    let value = p.load(fromByteOffset: i * 8, as: Int.self)
    print("index: \(i), value: \(value)")
}
p.deallocate()

lldb打印结果:

0x0000000105058e30
index: 0, value: 0
index: 1, value: 1
index: 2, value: 2
index: 3, value: 3

这才是正确的,符合我们期望的存储内容打印。
其中

p.advanced(by: i * stride).storeBytes(of: i, as: Int.self)

也等价于

///p,基地址,i *stride,移动对应的步长位置来存放i
(p + i * stride).storeBytes(of: i, as: Int.self)

1.2.2 泛型指针的使用

这⾥的泛型指针相⽐较原⽣指针来说,其实就是指定当前指针已经绑定到了具体的类型。同样的,我们 还是通过⼀个例⼦来解释⼀下。

var age = 18
withUnsafePointer(to: &age) { ptr in
    print(ptr)
}

lldb输出打印

0x0000000100008058

我们得到了age这个变量的内存指针。那么如果我们想要修改age这个变量可以怎么办哪?
在进⾏泛型指针访问的过程中,我们并不是使⽤ loadstore ⽅法来进⾏存储操作。这⾥我们使⽤到当前泛型指针内置的变量 pointee。 获取 UnsafePointer 的⽅式有两种。
⼀种⽅式就是通过已有变量获取,如下:

var age = 18
withUnsafePointer(to: &age) { ptr in
    print(ptr)
}

age = withUnsafePointer(to: age) { ptr in
    return ptr.pointee + 20
}
print(age)

发现更改后,它的存储地址和存储的值已经发生改变。

0x0000000100008068
38

注意一点,这里我们无法直接修改ptr.pointee,如果想要修改,我们可以通过可变类型的MutablePointer,代码如下:

var age = 18

withUnsafePointer(to: &age) { ptr in
    print(ptr)
}

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

lldb输出打印结果:

x0000000100008068
68

还有一种方式就是直接分配内存

var age = 18
///1、分配一块Int类型的内存空间,注意这个时候当前内存空间还没有被初始化
let ptr = UnsafeMutablePointer<Int>.allocate(capacity: 1)
///2、为age initialize初始化分配的内存空间
ptr.initialize(to: age)
///3、访问当前内存的值,直接通过pointee属性来进行访问
print(ptr.pointee)

这⾥我们看⼀张图就⽐较清晰了:


泛型指针.png

下面我们操作一个结构体来实践一下。

struct ZGTeacher {
    var age: Int
    var height: Double
}

下面我们来尝试看一下如何用指针来访问这个结构体。代码如下:

struct ZGTeacher {
    var age: Int
    var height: Double
}

///capacity: 比如我们需要开辟5个内存空间的大小
///allocate: 开辟空间
var ptr = UnsafeMutablePointer<ZGTeacher>.allocate(capacity: 5)
///初始化我们的指针内存,并存储对应的值
ptr[0] = ZGTeacher(age: 18, height: 20.0)
ptr[1] = ZGTeacher(age: 22, height: 30.0)

///defer关键字,当前程序运行完成后会执行这块代码
defer {
    ///deinitialize回收5个内存空间
    ptr.deinitialize(count: 5)
    ///销毁对应的内存空间
    ptr.deallocate()
}

我们同样还可以如下面代码这样初始化:

struct ZGTeacher {
    var age: Int = 18
    var height: Double = 1.85
}

let p = UnsafeMutablePointer<ZGTeacher>.allocate(capacity: 2)
p.initialize(to: ZGTeacher())
p.advanced(by: MemoryLayout<ZGTeacher>.stride).initialize(to: ZGTeacher(age: 20, height: 1.75))

defer {
    p.deinitialize(count: 2)
    p.deallocate()
}

1.2.3 内存指针的使用

接下来,我们来尝试用指针读取Mach-o文件中属性的名称,代码如下:

class ZGTeacher {
    var age: Int = 18
    var name: String = "Zhang"
}

var size: UInt = 0
///__swift5_types section 的pFile
var ptr = getsectdata("__TEXT", "__swift5_types", &size)

///pFile地址 0x0000000100007e24
print("ptr: \(ptr!)")

///获取程序当前运行基地址  0x0000000100000000
///mach_header

var excute_header: UnsafePointer<mach_header>?
let count = _dyld_image_count()

for i in 0..<count {
    let ptr = _dyld_get_image_header(i)
    if ptr!.pointee.filetype == MH_EXECUTE {
        excute_header = ptr
    }
}

var mhHeaderPtr = excute_header

print("程序当前运行基地址mhHeaderPtr: \(mhHeaderPtr!)")


///Segment Name
var setCommond64Ptr = getsegbyname("__LINKEDIT")

///真实基地址
var linkBaseAddress: UInt64 = 0
///vmaddr虚拟基地址
///fileoff当前文件的偏移量
///真实基地址 = 虚拟基地址 - 文件的偏移量
///linkBaseAddress 十进制4294967296 十六进制0x100000000
///十进制转十六进制,返回的是字符串格式String(X,radix:16)

if let vmaddr = setCommond64Ptr?.pointee.vmaddr, let fileoff = setCommond64Ptr?.pointee.fileoff {
    linkBaseAddress = vmaddr - fileoff
//    print("真实基地址linkBaseAddress: \(linkBaseAddress)")
    print("真实基地址linkBaseAddress: " + "0x" + String(linkBaseAddress, radix: 16))
}

///offset = 当前的pFile地址 - 真实基地址
///offset: 十进制32292 十六进制0x7e24
var offset: UInt64 = 0
if let unwrappedPtr = ptr {
    ///把当前的地址信息转换为UInt64类型
    let intRepresentation = UInt64(bitPattern: Int64(Int(bitPattern: unwrappedPtr)))
    offset = intRepresentation - linkBaseAddress
//    print("偏移量: \(offset)")
    print("偏移量: " + "0x" + String(offset, radix: 16))
}

///将首地址ptr转换为UInt64类型的,方便下面的计算
let mhHeaderPtr_IntRepresentation = UInt64(bitPattern: Int64(Int(bitPattern: mhHeaderPtr)))
///存放内容的真实地址
var dataLoAddress = mhHeaderPtr_IntRepresentation + offset
/////将存放内容的真实地址转换为指针类型
//var dataLoAddressPtr = withUnsafePointer(to: &dataLoAddress) {print($0)}

///真实pfile存储的value(内容)地址
var dataLoContent = UnsafePointer<UInt32>.init(bitPattern: Int(exactly: dataLoAddress) ?? 0)?.pointee

///十进制4294966164,十六进制转换一下0xfffffb94
//print("真实pfile存储的value(内容)地址 dataLoContent: \(dataLoContent!)")
print("真实pfile存储的value(内容)地址 dataLoContent: " + "0x" + String(dataLoContent!, radix: 16))

///描述文件pfile在Mach-O文件中的偏移信息
///当前pfile真实地址偏移量 = 当前pfile真实地址 + 偏移量 - 真实基地址
let typeDescOffset = UInt64(dataLoContent!) + offset -  linkBaseAddress

///当前pfile真实地址 = UInt64类型的当前pfile真实地址偏移量 + UInt64类型的真实基地址
var typeDescAdress = typeDescOffset + mhHeaderPtr_IntRepresentation
///十进制4295014840,十六进制转换一下0x10000b9b8
//print("当前pfile真实地址 typeDescAdress: \(typeDescAdress)")
print("当前pfile真实地址 typeDescAdress: " + "0x" + String(typeDescAdress, radix: 16))

///为了打印typeDescAdress内部结构
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
}

///将当前pfile真实地址转换为指向TargetClassDescriptor的指针类型,并获取它的pointee属性
let classDescriptor = UnsafePointer<TargetClassDescriptor>.init(bitPattern: Int(exactly: typeDescAdress) ?? 0)?.pointee

if let name = classDescriptor?.name {
    let nameOffset = Int64(name) + Int64(typeDescOffset) + 8
    
//    print("类名偏移量 nameOffset: \(nameOffset)")
    print("类名偏移量 nameOffset: " + "0x" + String(nameOffset, radix: 16))
    
    let nameAddress = nameOffset + Int64(mhHeaderPtr_IntRepresentation)
    
//    print("类名地址 nameAddress: \(nameAddress)")
    print("类名地址 nameAddress: " + "0x" + String(nameAddress, radix: 16))
    
    if let cChar = UnsafePointer<CChar>.init(bitPattern: Int(nameAddress)) {
        ///类名
        print("类名:" + String(cString: cChar))
    }
}


/// 获取属性
/// 获取属性相关的filedDescriptor 在运行中的内存地址
let filedDescriptorRelaticveAddress = typeDescOffset + 4 * 4 + mhHeaderPtr_IntRepresentation

///十进制4295014856,十六进制转换一下0x10000b9c8
//print("filedDescriptor的地址 filedDescriptorRelaticveAddress: \(filedDescriptorRelaticveAddress)")
print("filedDescriptor的地址 filedDescriptorRelaticveAddress: " + "0x" + String(filedDescriptorRelaticveAddress, radix: 16))

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
}

///获取fieldDescriptor 偏移量offset
let fieldDescriptorOffset = UnsafePointer<UInt32>.init(bitPattern: Int(exactly: filedDescriptorRelaticveAddress) ?? 0)?.pointee

///fieldDescriptor的Offset偏移量的地址 十进制656,十六进制转换一下0x290
//print("fieldDescriptor的Offset偏移量的地址 fieldDescriptorOffset: \(fieldDescriptorOffset!)")
print("fieldDescriptor的Offset偏移量的地址 fieldDescriptorOffset: " + "0x" + String(fieldDescriptorOffset!, radix: 16))
///获取 FieldDescriptor 的在运行中的内存地址
let fieldDescriptorAddress = filedDescriptorRelaticveAddress + UInt64(fieldDescriptorOffset!)

///将 FieldDescriptor 的内存地址直接转换成指向 FieldDescriptor 结构体的指针
let fieldDescriptor = UnsafePointer<FieldDescriptor>.init(bitPattern: Int(exactly: fieldDescriptorAddress) ?? 0)?.pointee

///循环遍历属性
for i in 0 ..< fieldDescriptor!.numFields {
    ///FieldRecord 结构体由 3个 4字节组成,并且保持3 * 4 = 12字节对齐
    let stride: UInt64 = UInt64(i * 3 * 4)
    let fieldRecordAddress = fieldDescriptorAddress + stride + 16
//    print(fieldRecordRelactiveAddress)
//    let fieldRecord = UnsafePointer<FieldRecord>.init(bitPattern: Int(exactly: fieldRecordAddress) ?? 0)?.pointee
//    print(fieldRecord)
    let fieldNameRelactiveAddress = UInt64(2 * 4) + fieldRecordAddress - linkBaseAddress + mhHeaderPtr_IntRepresentation
    let offset = UnsafePointer<UInt32>.init(bitPattern: Int(exactly: fieldNameRelactiveAddress) ?? 0)?.pointee
//    print(offset)
    let fieldNameAddress = fieldNameRelactiveAddress + UInt64(offset!) - linkBaseAddress
    if let cChar = UnsafePointer<CChar>.init(bitPattern: Int(fieldNameAddress)){
        print(String(cString: cChar))
    }
}

// 获取v-table
// 函数的结构体
struct TargetMethodDescriptor {
    var kind: UInt32
    var offset: UInt32
}

// 获取方法的数量
if let methods = classDescriptor?.methods {
    for i in 0..<methods {
        // 获取v-table的的首地址
        let VTableRelaticveAddress = typeDescOffset + 4 * 13 + mhHeaderPtr_IntRepresentation
        // 获取当前函数的地址
        let currentMethodAddress = VTableRelaticveAddress + UInt64(i) * UInt64(MemoryLayout<TargetMethodDescriptor>.size)
        // 将 当前函数 的内存地址直接转换成指向 TargetMethodDescriptor 结构体的指针
        let currentMethod = UnsafePointer<TargetMethodDescriptor>.init(bitPattern: Int(exactly: currentMethodAddress) ?? 0)?.pointee
        // 获取到imp的地址
        let impAddress = currentMethodAddress + 4 + UInt64(currentMethod!.offset) - linkBaseAddress
        print(impAddress);
    }
}

1.3 内存绑定

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

  • assumingMemoryBound(to:)
func testPointer(_ p: UnsafePointer<Int>) {
    print(p)
}

let tuple = (10, 20)
withUnsafePointer(to: tuple) { (tuplePtr: UnsafePointer<(Int, Int)>) in
    testPointer(tuplePtr)
}

编译运行,发现报错了,如下图所示

指针访问.png
那么想要顺利访问到元组的值我们可以怎么办哪? 首先,第一点,我们先把我们的指针转换为原生指针UnsafeRawPointer,然后我们调用assumingMemoryBound绑定成对应类型。
func testPointer(_ p: UnsafePointer<Int>) {
    print(p[0], p[1])
}

let tuple = (10, 20)

withUnsafePointer(to: tuple) { (tuplePtr: UnsafePointer<(Int, Int)>) in
    testPointer(UnsafeRawPointer(tuplePtr).assumingMemoryBound(to: Int.self))
}

编译一下,正确输出打印元组的值 1020
那么我们使用assumingMemoryBound的意义是什么?
有时候我们的类型只有这种原生指针UnsafeRawPointer,或者说像UnsafePointer<(Int, Int)>UnsafePointer<Int>这种,两种值类型相似,但是我们又不想经过一系列的转换操作来增加代码复杂度,对我们的指针进行生硬的转换,那么这个时候我们就可以使用这个API assumingMemoryBound来告诉我们的编译器自己预期的类型,不需要编译器再重复检查(注意:这⾥只是让编译器绕过类型检查,并没有发⽣实际类型的转换)。

  • bindMemory(to: , capacity: )
    调用bindMemory绑定成对应类型,这里我们发生了实际类型的转换。如果当前的内存没有绑定类型,那么我们就首次绑定类型,如果有它当前的原有类型,那么调用这个API,我们重新绑定为指定类型。
func testPointer(_ p: UnsafePointer<Int>) {
    print(p[0], p[1])
}

let tuple = (10, 20)

withUnsafePointer(to: tuple) { (tuplePtr: UnsafePointer<(Int, Int)>) in
    testPointer(UnsafeRawPointer(tuplePtr).bindMemory(to: Int.self, capacity: 1))
}

这里我们将UnsafePointer<(Int, Int)>转换成了UnsafePointer<Int>类型。

  • withMemoryRebound(to: , capacity: )
    withMemoryRebound就是用来临时更改我们的类型,减少代码复杂度。
func testPoint(_ p: UnsafePointer<Int8>) {

}

let UInt8Ptr = UnsafePointer<UInt8>.init(bitPattern: 10)
UInt8Ptr?.withMemoryRebound(to: Int8.self, capacity: 1, { (int8Ptr: UnsafePointer<Int8>) in
    testPoint(int8Ptr)
})

二、内存管理

swift中使⽤⾃动引⽤计数(ARC)机制来追踪和管理内存。

class ZGTeacher {
    var age: Int = 18
    var name: String = "Zhang"
}

var t = ZGTeacher()
///固定写法,打印这个t实例的内存指针
print(Unmanaged.passUnretained(t as AnyObject).toOpaque())
print("end")

打印输出

0x0000000101252a90

我们用x/8g指令输出一下

(lldb) x/8g 0x0000000101252a90
0x101252a90: 0x0000000100008198 0x0000000000000003
0x101252aa0: 0x0000000000000012 0x000000676e61685a
0x101252ab0: 0xe500000000000000 0x0000000000000000
0x101252ac0: 0x00000009a0080001 0x00007ff84be59aa0

我们拿到了这个实例对象t的内存指针地址,我们知道实例对象内存地址其中的前16个字节的后8个字节在这个过程当中本质上是存储我们的refCounts,但我们不知道它存储的到底代表什么意思,现在我们在Swift源码里面看一下。
首先我们先找到refCount的定义,这里我们在HeapObject.h文件中搜索

// The members of the HeapObject header that are not shared by a
 // standard Objective-C instance
 #define SWIFT_HEAPOBJECT_NON_OBJC_MEMBERS       \
   InlineRefCounts refCounts

 /// The Swift heap-object header.
 /// This must match RefCountedStructTy in IRGen.
 struct HeapObject {
   /// This is always a valid pointer to a metadata object.
   HeapMetadata const *__ptrauth_objc_isa_pointer metadata;

   SWIFT_HEAPOBJECT_NON_OBJC_MEMBERS;

 #ifndef __swift__
   HeapObject() = default;

   // Initialize a HeapObject header as appropriate for a newly-allocated object.
   constexpr HeapObject(HeapMetadata const *newMetadata)
     : metadata(newMetadata)
     , refCounts(InlineRefCounts::Initialized)
   { }

我们看到refCounts是由InlineRefCounts定义的,接下来,我们沿着这个InlineRefCounts定义点击进去。

typedef RefCounts<InlineRefCountBits> InlineRefCounts;
 typedef RefCounts<SideTableRefCountBits> SideTableRefCounts;

可以看到InlineRefCounts其实是一个模版类,接受一个泛型参数InlineRefCountBits

template <typename RefCountBits>
 class RefCounts {
   std::atomic<RefCountBits> refCounts;

   // Out-of-line slow paths.

   SWIFT_NOINLINE
   void incrementSlow(RefCountBits oldbits, uint32_t inc) SWIFT_CC(PreserveMost);

   SWIFT_NOINLINE
   void incrementNonAtomicSlow(RefCountBits oldbits, uint32_t inc);

   SWIFT_NOINLINE
   bool tryIncrementSlow(RefCountBits oldbits);

   ......
   }

可以看到,本质上它在操作我们的API的时候都操作的是我们的泛型参数RefCountBitsInlineRefCountBits它其实是一个模版类,RefCounts本质上是对我们当前引用计数的一个包装,我们引用计数的具体类型取决于传进来的参数RefCountBits

// Basic encoding of refcount and flag data into the object's header.
 template <RefCountInlinedness refcountIsInline>
 class RefCountBitsT {

   friend class RefCountBitsT<RefCountIsInline>;
   friend class RefCountBitsT<RefCountNotInline>;
   
   static const RefCountInlinedness Inlinedness = refcountIsInline;

   typedef typename RefCountBitsInt<refcountIsInline, sizeof(void*)>::Type
     BitsType;
   typedef typename RefCountBitsInt<refcountIsInline, sizeof(void*)>::SignedType
     SignedBitsType;
   typedef RefCountBitOffsets<sizeof(BitsType)>
     Offsets;

   BitsType bits;

点击Type看一下类型,它是一个uint64_t类型的位域信息

template <>
 struct RefCountBitsInt<RefCountNotInline, 4> {
   typedef uint64_t Type;
   typedef int64_t SignedType;
 };

看到这里我们可以得出结论,我们的引用计数它是一个64位的位域信息。在这个位域信息里存储了和我们当前这个运行生命周期相关的引用计数。
当我们创建一个实例对象的时候,当前的引用计数是多少?
我们先从源代码中找一下它的初始化方法new (object) HeapObject(metadata)

static HeapObject *_swift_allocObject_(HeapMetadata const *metadata,
                                       size_t requiredSize,
                                       size_t requiredAlignmentMask) {
  assert(isAlignmentMask(requiredAlignmentMask));
  auto object = reinterpret_cast<HeapObject *>(
      swift_slowAlloc(requiredSize, requiredAlignmentMask));

  // NOTE: this relies on the C++17 guaranteed semantics of no null-pointer
  // check on the placement new allocator which we have observed on Windows,
  // Linux, and macOS.
  new (object) HeapObject(metadata);

  // If leak tracking is enabled, start tracking this object.
  SWIFT_LEAKS_START_TRACKING_OBJECT(object);

  SWIFT_RT_TRACK_INVOCATION(object, swift_allocObject);

  return object;
}

点击HeapObject方法,我们看到了它的初始化赋值方法refCounts(InlineRefCounts::Initialized)

// Initialize a HeapObject header as appropriate for a newly-allocated object.
  constexpr HeapObject(HeapMetadata const *newMetadata) 
    : metadata(newMetadata)
    , refCounts(InlineRefCounts::Initialized)
  { }

点击或者搜索Initialized,可以看到以下代码:

public:
  enum Initialized_t { Initialized };
  enum Immortal_t { Immortal };

  // RefCounts must be trivially constructible to avoid ObjC++
  // destruction overhead at runtime. Use RefCounts(Initialized)
  // to produce an initialized instance.
  RefCounts() = default;
  
  // Refcount of a new object is 1.
  constexpr RefCounts(Initialized_t)
    : refCounts(RefCountBits(0, 1)) {}

  // Refcount of an immortal object has top and bottom bits set
  constexpr RefCounts(Immortal_t)
  : refCounts(RefCountBits(RefCountBits::Immortal)) {}
  

RefCountBits就是我们刚才讲的模版类。想要了解这个类那么我们就去搜索一下RefCountBitsT

SWIFT_ALWAYS_INLINE
  constexpr
  RefCountBitsT(uint32_t strongExtraCount, uint32_t unownedCount)
    : bits((BitsType(strongExtraCount) << Offsets::StrongExtraRefCountShift) |
           (BitsType(1)                << Offsets::PureSwiftDeallocShift) |
           (BitsType(unownedCount)     << Offsets::UnownedRefCountShift))
  { }

strongExtraCount 传入的0,StrongExtraRefCountShift 在33位,unownedCount 传入的是1,UnownedRefCountShift 1位,
0左移33位是0,1左移1位是2。
接下来我们来看一下当我们做一个强引用的时候它的引用计数是怎么变化的哪?

class ZGTeacher {
    var age: Int = 18
    var name: String = "Zhang"
}

var t = ZGTeacher()
///固定写法,打印这个t实例的内存指针
print(Unmanaged.passUnretained(t as AnyObject).toOpaque())

var t1 = t
var t2 = t
print("end")
image.png

我们逐步放开断点,并用x/8g指令打印输出,看到它的强引用计数变为了2。通过位移运算左移33位,高34位,每次加2
我们来看一下位域布局图

位域.png
  • 1 ~31位存储的是无主引用
  • 32位存储的是当前的类是否正在析构
  • 33 ~ 62位存储的是强引用
    我们简单用代码来验证一下第32位信息
class ZGTeacher {
    var age: Int = 18
    var name: String = "Zhang"
}

var t: ZGTeacher? = ZGTeacher()
///固定写法,打印这个t实例的内存指针
print(Unmanaged.passUnretained(t as AnyObject).toOpaque())

t = nil

print("end")
析构.png

32位为1,表示正在析构。
那么引用计数是如何操作强引用的哪,我们也可以到源码里看一下。

SWIFT_ALWAYS_INLINE
 static HeapObject *_swift_retain_(HeapObject *object) {
   SWIFT_RT_TRACK_INVOCATION(object, swift_retain);
   if (isValidPointerForNativeRetain(object))
     object->refCounts.increment(1);
   return object;
 }
SWIFT_ALWAYS_INLINE
   void increment(uint32_t inc = 1) {
     auto oldbits = refCounts.load(SWIFT_MEMORY_ORDER_CONSUME);
     
     // constant propagation will remove this in swift_retain, it should only
     // be present in swift_retain_n
     if (inc != 1 && oldbits.isImmortal(true)) {
       return;
     }
     
     RefCountBits newbits;
     do {
       newbits = oldbits;
       bool fast = newbits.incrementStrongExtraRefCount(inc);
       if (SWIFT_UNLIKELY(!fast)) {
         if (oldbits.isImmortal(false))
           return;
         return incrementSlow(oldbits, inc);
       }
     } while (!refCounts.compare_exchange_weak(oldbits, newbits,
                                               std::memory_order_relaxed));
   }
SWIFT_NODISCARD SWIFT_ALWAYS_INLINE bool
   incrementStrongExtraRefCount(uint32_t inc) {
     // This deliberately overflows into the UseSlowRC field.
     bits += BitsType(inc) << Offsets::StrongExtraRefCountShift;
     return (SignedBitsType(bits) >= 0);
   }

这里也是通过位域运算得来的。使用强引用就会造成一个问题:循环引用。我们来看一下以下案例:

class ZGTeacher {
    var age: Int = 18
    var name: String = "zhang"
    var subject: ZGTeacher?
}

class ZGSubject {
    var subjectName: String
    var subjectTeacher: ZGTeacher
    init(_ subjectName: String, _ subjectTeacher: ZGTeacher) {
        self.subjectName = subjectName
        self.subjectTeacher = subjectTeacher
    }
}

var t = ZGTeacher()

var subject = ZGSubject.init("swift", t)

t.subject = subject

这里实例对象相互持有,造成对象无法释放,产生了循环引用。那么我们怎么解决这一问题哪?在swit中有两种方式,第一种是我们的弱引用,第二种方式是无主引用Unowned。

2.1 弱引用

弱引⽤不会对其引⽤的实例保持强引⽤,因⽽不会阻⽌ ARC 释放被引⽤的实例。这个特性阻⽌了引⽤变为循环强引⽤。声明属性或者变量时,在前⾯加上 weak 关键字表明这是⼀个弱引⽤。 由于弱引⽤不会强保持对实例的引⽤,所以说实例被释放了弱引⽤仍旧引⽤着这个实例也是有可能的。 因此,ARC 会在被引⽤的实例被释放时⾃动地设置弱引⽤为 nil 。由于弱引⽤需要允许它们的值为 nil , 它们⼀定得是可选类型。

class ZGTeacher {
    var age: Int = 18
    var name: String = "zhang"
}

weak var t = ZGTeacher()

print(Unmanaged.passUnretained(t as AnyObject).toOpaque())

print("end")

weak var t = ZGTeacher()加上断点,并打开Xcode的汇编调试,编译运行一下

image.png
发现它明显执行了一个swift_weakInit。我们到swift源码里来搜索一下这个方法。
WeakReference *swift::swift_weakInit(WeakReference *ref, HeapObject *value) {
  ref->nativeInit(value);
  return ref;
}

声明一个weak变量相当于定义了一个WeakReference对象。

void nativeInit(HeapObject *object) {
    auto side = object ? object->refCounts.formWeakReference() : nullptr;
    nativeValue.store(WeakReferenceBits(side), std::memory_order_relaxed);
  }
template <>
HeapObjectSideTableEntry* RefCounts<InlineRefCountBits>::formWeakReference()
{
  auto side = allocateSideTable(true);
  if (side)
    return side->incrementWeak();
  else
    return nullptr;
}
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.
    return nullptr;
  }

  // Preflight passed. Allocate a side table.
  
  // FIXME: custom side table allocator
  HeapObjectSideTableEntry *side = new HeapObjectSideTableEntry(getHeapObject());
  
  auto newbits = InlineRefCountBits(side);
  
  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;
}

HeapObject {
    isa
    InlineRefCounts {
      atomic<InlineRefCountBits> {
        strong RC + unowned RC + flags
        OR
        HeapObjectSideTableEntry*
      }
    }
  }

  HeapObjectSideTableEntry {
    SideTableRefCounts {
      object pointer
      atomic<SideTableRefCountBits> {
        strong RC + unowned RC + weak RC + flags
      }
    }   
  }

在我们的swift里面,本质上存在着两种引用计数,一种是InlineRefCounts,里面就是strong RC + unowned RC + flags,如果我们当前是有我们的引用计数,此时它就存储了HeapObjectSideTableEntry,里面包含了strong RC + unowned RC + weak RC + flags

截屏2022-01-12 17.52.16.png 截屏2022-01-12 17.52.28.png 截屏2022-01-12 17.52.43.png
截屏2022-01-12 17.55.34.png 截屏2022-01-12 17.55.41.png

可以看到这里的本质是用64位的指针,把当前的side存储到64位位域中,并设置一些标记位。

Side Table 是⼀种类名为 HeapObjectSideTableEntry 的结构,⾥⾯也有 RefCounts 成员,内部是 SideTableRefCountBits,其实就是原来的 uint64_t 加上⼀个存储弱引⽤数的 uint32_t

截屏2022-01-12 17.57.43.png

2.2 Unowned

和弱引⽤类似,⽆主引⽤不会牢牢保持住引⽤的实例。但是不像弱引⽤,总之,⽆主引⽤假定是永远有值。

class ZGTeacher {
    var age: Int = 18
    var name: String = "zhang"
}

var t: ZGTeacher?
t = ZGTeacher()

print(Unmanaged.passUnretained(t as AnyObject).toOpaque())

unowned var t1 = t

t = nil

print(t1)

这里产生了崩溃,因为unowned修饰的t1必须是假定有值的,t或者t1为nil就产生了崩溃。
当我们知道两个对象的⽣命周期并不相关,那么我们必须使⽤ weak。相反,⾮强引⽤对象拥有和强引⽤对象同样或者更⻓的⽣命周期的话,则应该使⽤ unowned
我们来看一下这个例子:

class ZGTeacher {
    var age: Int = 18
    var name: String = "zhang"
    var subject: ZGSubject?
}

class ZGSubject {
    var subjectName: String
    var subjectTeacher: ZGTeacher
    init(_ subjectName: String, _ subjectTeacher: ZGTeacher) {
        self.subjectName = subjectName
        self.subjectTeacher = subjectTeacher
    }
}

var t = ZGTeacher()

var subject = ZGSubject.init("swift", t)

t.subject = subject

这个过程中我们的Teacher和SubjectName,如果老师不在了,那么这个过程中所讲授的课程也就不在了。
这个过程中我们可以通过unowned来解决这个循环引用,也就意味着当前的生命周期ZGTeacher更长。

class ZGTeacher {
    var age: Int = 18
    var name: String = "zhang"
    var subject: ZGSubject?
}

class ZGSubject {
    var subjectName: String
    ///这里添加 unowned
    unowned  var subjectTeacher: ZGTeacher
    init(_ subjectName: String, _ subjectTeacher: ZGTeacher) {
        self.subjectName = subjectName
        self.subjectTeacher = subjectTeacher
    }
}

var t = ZGTeacher()

var subject = ZGSubject.init("swift", t)

t.subject = subject

2.3 Weak VS Unowned比较

注意weak相对于unowned更安全,但是unowned相对于weak性能更好。因为weak需要重新创建一个sideTable散列表,还要对我们当前的sideTable散列表进行操作,而unowned直接操作了我们64的信息,但是在使用过程中要确保unowned是有值的。

2.4 闭包循环引用

var age = 18
let closure = {
    age += 1
}

closure()
print(age)

lldb输出打印结果

19

我们的闭包会⼀般默认捕获我们外部的变量,闭包内部对变量的修改将会改变外部原始变量的值
下面的案例就是闭包循环引用的经典案例,对象无法释放。

class ZGTeacher {
    var age: Int = 18
    var name: String = "zhang"
    var testClosure: (() -> ())?
    deinit {
        print("ZGTeacher deinit")
    }
}

func test() {
    let t = ZGTeacher()
    t.testClosure = {
        t.age += 1
    }
    
    print("end")
}

test()

那么我们如何解决这一问题哪?可以使用我们当前的捕获列表。

class ZGTeacher {
    var age: Int = 18
    var name: String = "zhang"
    var testClosure: (() -> ())?
    deinit {
        print("ZGTeacher deinit")
    }
}

func test() {
    let t = ZGTeacher()
    t.testClosure = { [weak t] in
        t!.age += 1
    }
    
    print("end")
}

test()

相关文章

  • Swift 三、指针 & 内存管理

    一、指针 1.1 为什么说指针不安全 ⽐如我们在创建⼀个对象的时候,是需要在堆分配内存空间的。但是这个内存空间的生...

  • C 指针内存管理

    // C 指针的内存管理 // C 指针在 Swift中被冠以 unsafe 的另一个是无法对其进行自动的内存管理...

  • Swift 指针&内存管理

    指针 为什么说指针不安全 比如我们在创建一个对象的时候,是需要在堆分配内存空间的。但是这个内存空间的声明周期是有限...

  • Swift指针&内存管理

    一、指针    1、指针类型   Swift中的指针分为两类:指定数据类型的指针(typed pointer);未...

  • Swift指针|内存管理

    一、Swift指针 1.Swift指针简介 swift中的指针分为两类 typed pointer 指定数据类型指...

  • swift指针操作

    swift官方不建议使用指针,为了安全起见,而且使用比较麻烦,内存必须自己管理 1、直接创建指针 2、获取指针 可...

  • swift指针&内存管理-内存绑定

    swift提供了3种不同的API来绑定/重新绑定指针 assumingMemoryBound(to:) bindM...

  • swift指针&内存管理-闭包的循环引用

    swift指针&内存管理-引用[https://www.jianshu.com/p/841c726e0036] 无...

  • Swift-内存管理,指针

    内存管理 Swift采用引用计数的ARC内存管理方案(堆空间) Swift的ARC中有3钟引用强引用弱引用(wea...

  • Swift-指针&内存管理

    石墨文档编写 《Swift-指针&内存管理》[https://shimo.im/docs/c3x9rWcR8YQj...

网友评论

      本文标题:Swift 三、指针 & 内存管理

      本文链接:https://www.haomeiwen.com/subject/snkmcrtx.html