Swift-指针&内存管理

632 阅读17分钟

之前的几篇文章主要了解了类、结构体以及属性的一些概念,并且通过示例和Mach-O文件知道了他们在内存中是如何存储的,本篇主要介绍一下指针,通过Swift代码利用指针去访问内存中的数据;在开始之前先了解一下指针的含义以及在使用中需要注意哪些问题。

为什么说指针不安全

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

指针类型

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

SwiftObjective-C说明
unsafePointer<T>const T*指针及所指向的内容都不可变
unsafeMutablePointer<T>T*指针及其所指向的内存内容均可变
unsafeRawPointerconst void *指针指向的内存区域未定
unsafeMutableRawPointervoid *同上
unsafeBufferPointer<T>
unsafeRawBufferPointer
unsafeMutableRawBufferPointer

原始指针的使用

我们一起来看一下如何使用Raw Pointer来存储4个整型的数据,这里我们需要选取的是UnsafeMutableRawPointer,下面通过示例来演示一下:

原始指针示例
// 存储原始指针(Int占8字节,总共占4 * 8 = 32,然后8字节对齐)
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)")
}

以上代码就是利用原始指针存储4个Int数据,我们运行一下 01.png 发现value的值和我们预想的不一样,这是由于我们在存储的时候指针也需要移动i * 8个字节,也就是指针在存储的时候也需要移动步长信息,在Swift中可以通过MemoryLayout来计算所内存的大小。可以通过一个简单的例子来打印输出一下,定义个结构体:

struct ATTeacher {
    var age: Int = 18
}
# 分别打印输出一下
MemoryLayout<ATTeacher>.size
MemoryLayout<ATTeacher>.stride
MemoryLayout<ATTeacher>.alignment

02.png 结果3个都是8,在原有的结构体添加一个属性,再打印一下: 03.png 这里得到的结果分别是9168 04.png

  • size: 就是当前结构体的实际内存占用大小
  • stride: 步长信息,要储存连续的实例对象,从实例(ATTeacher)存储的起始位置到下一个实例(ATTeacher)的存储的起始位置为一个步长信息
  • alignment: 字节对齐(结构体默认8字节对齐)

所以上面的示例存储的时候需要移动相应的步长信息,加上移动步长信息代码如下:

// 存(移动步长信息)
for i in 0..<4 {
    p.advanced(by: i * MemoryLayout<Int>.stride).storeBytes(of: i, as: Int.self)
}

再次运行,值就对应上了 05.png 指针使用完以后记得销毁

p.deallocate()

泛型指针的使用

这里的泛型指针相比较原生指针来说,其实就是指定当前指针已经绑定到了具体的类型。同样的,我们还是通过一个例子来解释一下。在进行泛型指针访问的过程中,我们并不是使用loadstore方法来进行存储操作。这里我们使用到当前泛型指针内置的变量pointee

获取UnsafePointer的方式有两种。一种方式就是通过已有变量获取,还有一种方式是直接分配内存。

泛型指针示例
var age = 18
// 可以通过withUnsafePointer来访问当前变量的地址
withUnsafePointer(to: &age){ ptr in
    print(ptr)
}

如果想要修改当前的age,可以返回修改后的值。

var age = 18
age = withUnsafePointer(to: &age){ ptr in
    return ptr.pointee + 12
}
// 这里age输出为30
print(age)

这里没法直接修改ptr.pointee,因为pointee是不可变的,没有Mutable修饰,指针和指针所指向的内容都是不可变的,如果直接修改编译器会报错。 06.png 如果想要直接修改,可以用withUnsafeMutablePointer,下面代码就可以执行。

var b = 20
withUnsafeMutablePointer(to: &b) { ptr in
    ptr.pointee += 10
}
// 执行后b的结果就是30

对于泛型指针主要涉及的API可以通过下面的图例表示。 07.png 下面再通过一个示例,如何用指针去访问结构体。代码如下:

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

// 定义一个可以存储5个ATTeacher结构体的指针
var tPtr = UnsafeMutablePointer<ATTeacher>.allocate(capacity: 5)

// 可以通过这样的方式初始化指针内存
tPtr[0] = ATTeacher(age: 18, height: 20.0)
tPtr[1] = ATTeacher(age: 19, height: 21.0)

// 指针用完后置空然后销毁
tPtr.deinitialize(count: 5)
tPtr.deallocate()
案例-指针读取Mach-O属性名称

接下来通过一个案例,利用指针读取Mach-O中的属性名称,先定义一个类,代码如下:

class ATTeacher {
    var age: Int = 18
    var name: String = "Atom"
}

在之前的篇章中已经了解了类在Mach-O文件存储位置,下面通过SwiftAPI来获取相应的值。

// 获取类的地址信息
var size: UInt = 0
var ptr = getsectdata("__TEXT", "__swift5_types", &size)

// 获取Mach-O文件的起始地址(这里Xcode版本不一致参数可能不一样,Xcode13之前是0,Xcode13传3)
var mhHeaderPtr = _dyld_get_image_header(3)

再获取基地址,这里有2种方式,在Load CommandsLC_SEGMENT_64(__PAGEZERO)LC_SEGMENT_64(__LINKEDIT)都可以拿到。

// 方式一(通过__LINKEDIT获取基地址)
var setCommond64Ptr = getsegbyname("__LINKEDIT")
var linkBaseAddress: UInt64 = 0
if let vmaddr = setCommond64Ptr?.pointee.vmaddr, let fileoff = setCommond64Ptr?.pointee.fileoff {
    linkBaseAddress = vmaddr - fileoff
}

// 方式二(通过__PAGEZERO获取基地址)
var setCommond64Ptr = getsegbyname("__PAGEZERO")
var linkBaseAddress: UInt64 = setCommond64Ptr?.pointee.vmsize ?? 0

选其一即可。再获取类的偏移地址Offset

var offset: UInt64 = 0
if let unwrappedPtr = ptr {
    // 把当前的指针地址转换成UInt64类型
    let intRepresentation = UInt64(bitPattern: Int64(Int(bitPattern: unwrappedPtr)))
    
    // 减去虚拟基地址得到offset
    offset = intRepresentation - linkBaseAddress
}

上面代码计算出来的偏移地址就是下图中的地址。 08.png 再获取Data LO的地址,也就是上图中偏移地址的右边信息,根据偏移地址加上Data LO就知道类的地址信息了。

// 获取Data LO的内存地址

// 将起始地址转换成UInt64
let mhHeaderPtr_IntRepresentation = UInt64(bitPattern: Int64(Int(bitPattern: mhHeaderPtr)))

// offset加上基地址
var dataLoAddress = mhHeaderPtr_IntRepresentation + offset

var dataLoAddressPtr = withUnsafePointer(to: &dataLoAddress){return $0}

// 通过指针获取其值
var dataLoContent = UnsafePointer<UInt32>.init(bitPattern: Int(exactly: dataLoAddress) ?? 0)?.pointee

// 根据偏移地址和Data LO得到TargetClassDescriptor
let typeDescOffset = UInt64(dataLoContent!) + offset - linkBaseAddress
var typeDescAddress = typeDescOffset + mhHeaderPtr_IntRepresentation

之前已经介绍了TargetClassDescriptor的结构,这里再复制过来:

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
}

拿到TargetClassDescriptor的首地址,要知道name的地址,需要偏移8个字节,拿到name的地址后再转换为String类型读取出类的名称。

let classDescriptor = UnsafePointer<TargetClassDescriptor>.init(bitPattern: Int(exactly: typeDescAddress) ?? 0)?.pointee
if let name = classDescriptor?.name {
    let nameOffset = Int64(name) + Int64(typeDescOffset) + 8
    let nameAddress = nameOffset + Int64(mhHeaderPtr_IntRepresentation)
    if let cChar = UnsafePointer<CChar>.init(bitPattern: Int(nameAddress)) {
        print(String(cString: cChar))
    }
}

运行上面的代码,打印输出可以看到类名ATTeacher,打印的offset转换为16进制也就是上面Mach-O文件的偏移地址信息0x7CA809.png 属性是存储在fieldDescriptor里的,也可以根据偏移计算出它的地址。

let filedDescriptorRelaticveAddress = typeDescOffset + 16 + mhHeaderPtr_IntRepresentation

再结合FieldDescriptor的结构

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的相对地址转换为UInt32的地址地址
let fieldDescriptorOffset = UnsafePointer<UInt32>.init(bitPattern: Int(exactly: filedDescriptorRelaticveAddress) ?? 0)?.pointee
// 加上偏移量得到FieldDescriptor的真实地址
let fieldDescriptorAddress = filedDescriptorRelaticveAddress + UInt64(fieldDescriptorOffset!)
// 通过指针转换FieldDescriptor结构类型
let fieldDescriptor = UnsafePointer<FieldDescriptor>.init(bitPattern: Int(exactly: fieldDescriptorAddress) ?? 0)?.pointee

// 遍历获取fieldName
for i in 0..<fieldDescriptor!.numFields {
    let stride: UInt64 = UInt64(i * 12)
    let fieldRecordAddress = fieldDescriptorAddress + stride + 16
    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))
    }
}

运行上面代码可以看到输出了类的属性名称。 10.png 以上就是通过指针去读取类和属性的相关信息,详细代码可以查看gitee仓库

内存绑定

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

  • assumingMemoryBound(to:)
  • bindMemory(to: capacity:)
  • withMemoryRebound(to: capacity:)

assumingMemoryBound

有些时候我们处理代码的过程中,只有原始指针(没有保留指针类型),但此刻对于处理代码的我们来说明确知道指针的类型,我们就可以使用assumingMemoryBound(to:)来告诉编译器预期的类型。(注意:这里只是让编译器绕过类型检查,并没有发生实际类型的转换)

举个例子,定义一个方法:

func testPoint(_ p: UnsafePointer<Int>) {
    print(p)
}

再定义一个元组,然后通过withUnsafePointer把一个元组用指针的方式在闭包里调用testPoint方法。 11.png 发现上面调用编译报错,UnsafePointer<(Int, Int)>UnsafePointer<Int>从本质上来说是一样的,都是指向Int类型的首地址,所以这里可以用原生指针,通过assumingMemoryBound(to:)绑定成Int.self类型,代码更新一下: 12.png 编译成功,可以看到通过p[0]p[1]就能取到元组中的值。

bindMemory

用于更改内存绑定的类型,如果当前内存还没有类型绑定,则将首次绑定为该类型;否则重新绑定该类型,并且内存中所有的值都会变成该类型。 13.png 把上面的代码改成bindMemory同样也可以输出tuple的值,不过此时已经被绑定为该类型。

withMemoryRebound

当我们在给外部函数传递参数时,不免会有一些数据类型上的差距。如果我们进行类型转换,必然要来回复制数据;这个时候我们就可以使用withMemoryRebound(to: capacity:)来临时更改内存绑定类型。

func testPoint(_ p: UnsafePointer<Int8>) {
    
}

// 把UInt8类型临时绑定为Int8类型
let uint8Ptr = UnsafePointer<UInt8>.init(bitPattern: 10)
uint8Ptr?.withMemoryRebound(to: Int8.self, capacity: 1) { (int8Ptr: UnsafePointer<Int8>) in
    testPoint(int8Ptr)
}

内存管理

Swift中使用自动引用计数(ARC)机制来追踪和管理内存。这里还是通过一个简单的示例来打印输出引用计数的变化。

示例

class ATTeacher {
    var age: Int = 18
    var name: String = "Atom"
}
var t = ATTeacher()
// 打印当前实例t的指针地址
print(Unmanaged.passUnretained(t as AnyObject).toOpaque())

print("end")

我们在print("end")处打个断点,根据上面打印出t的内存地址,x/8g格式化输出,从之前的篇章中知道,第二个8字节就是refCount15.png 这里输出的0x3,从输出的数据看不出来这个0x3是怎么得来的,接来下通过源码来分析一下。

源码分析

首先搜索找到refCounts的定义,在HeapObject.h

#define SWIFT_HEAPOBJECT_NON_OBJC_MEMBERS \

InlineRefCounts refCounts

可以看到refCountsInlineRefCounts定义的,接着在RefCount.h中找到如下定义,它是由RefCounts模板类定义。

typedef RefCounts<InlineRefCountBits> InlineRefCounts;

class RefCounts {
    std::atomic<RefCountBits> refCounts;
    ...
}

从上面的定义可知,RefCounts操作的是外面传进来的泛型参数,其实本质上是对引用计数的包装,而引用计数的具体类型取决于传进来的参数,也就是InlineRefCountBits,接着再找一下InlineRefCountBits的定义。

typedef RefCountBitsT<RefCountIsInline> InlineRefCountBits;

// RefCountIsInline这里是个bool值,要么true,要么false
enum RefCountInlinedness { RefCountNotInline = false, RefCountIsInline = true };

InlineRefCountBits的定义可以看到实际上是RefCountBitsT类型,再找一下RefCountBitsT的定义。

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;
  
  ...
}

所以引用计数实际操作的类就是这个RefCountBitsT,从上面的定义来看,只有bits属性来决定,查看bits的类型就是定义中的RefCountBitsInt<refcountIsInline, sizeof(void*)>::Type,再找到这个Type的定义:

template <RefCountInlinedness refcountIsInline>
struct RefCountBitsInt<refcountIsInline, 8> {
    typedef uint64_t Type;
    typedef int64_t SignedType;
};

可以看到Typeuint64_t类型的位域信息。所以Swift里的引用计数就存在这个64位的位域信息里。知道了引用计数的存储位置,当我们创建一个实例对象时,当前的引用计数是多少呢?我们还是要找到Swift创建对象的方法的定义:

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

  // 初始化方法
  new (object) HeapObject(metadata);

  SWIFT_LEAKS_START_TRACKING_OBJECT(object);

  SWIFT_RT_TRACK_INVOCATION(object, swift_allocObject);

  return object;
}

点进初始化方法,找到了初始化赋值操作。

constexpr HeapObject(HeapMetadata const *newMetadata) 
    : metadata(newMetadata)
    , refCounts(InlineRefCounts::Initialized) // 初始化赋值
{ }

接下来找到了Initialized的定义:

enum Initialized_t { Initialized };

constexpr RefCounts(Initialized_t)
    : refCounts(RefCountBits(0, 1)) {}

这里的RefCountBits就是上面的RefCountBitsT;然后再找一下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))
{ }

根据RefCounts的定义可以知道,这里strongExtraCount0unownedCount1

bits操作流程

  1. strongExtraCount左移StrongExtraRefCountShift
  2. 1左移PureSwiftDeallocShift
  3. unownedCount左移UnownedRefCountShift 这里根据源码的定义,StrongExtraRefCountShift33PureSwiftDeallocShift0UnownedRefCountShift1

其实就是把强引用计数和无主引用计数做左移操作。所以我们上面的示例代码strongCount0左移33位是01左移0位是1unownedCount1左移1位是2,所以结果输出的是3

说明:本篇是基于Swift5.5版本,如果是Swift之前的版本,上面步骤2是没有的,所以在Swift版本不一致的情况可能结果不一致

把上面的代码再改一下,再加几个引用,打个断点运行: 16.png18行断点输出就是之前示例的结果0x3,再过一个断点,到达20行,这时t1 = t赋值操作,引用计数+1,结果是0x200000003strongCount左移33位,通过计算器算一下 17.png 可以看到33位1。同样的再过一个断点,当t2 = t赋值操作后,引用计数又+1,这时候就变成了34位1,也就是上面输出的结果0x400000003,64位存储引用信息可以通过下图来表示。 18.png

  • 0:
  • 1~31:标记的是无主引用
  • 32:标记的析构
  • 33~62:标记的强引用
  • 64: 使用SlowRC 再通过示例来验证一下,把之前的代码实例t设置为可选类型,然后再给它赋值为nil,看看引用计数的变化。 19.png 可以看到,当给t赋值为nil时,输出的结果是0x100000003,根据计算器可以看到32位1

在强引用中实际是调用了swift_retain,看一下源码对这个函数的定义:

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

其实就是做了引用计数+1,然后再找到increment的最终定义

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

可以看到就是对引用计数累加做左移33位操作。这就验证了上面示例的运行结果引用计数的过程。以上就是Swift中强引用关于引用计数的过程分析。

循环引用

在使用强引用在某些情况就会遇到循环引用的问题,我们看一个经典的循环引用的例子:

class ATTeacher {
    var age: Int = 18
    var name: String = "Atom"
    var subject: ATSubject?
}
class ATSubject {
    var subjectName: String
    var subjectTeacher: ATTeacher
    init(_ subjectName: String, _ subjectTeacher: ATTeacher) {
        self.subjectName = subjectName
        self.subjectTeacher = subjectTeacher
  } 
}

var t = ATTeacher()
var subject = ATSubject.init("Swift ", t) 
t.subject = subject

以上代码实例t强引用了subject,而subject又强引用了t,就造成了循环引用,为了解决循环引用的问题,Swift提供了2种方法:弱引用(weak reference)无主引用(unowned reference)

弱引用

弱引用不会对其引用的实例保持强引用,因而不会阻止ARC释放被引用的实例。这个特性阻止了引用变为循环强引用。声明属性或者变量时,在前面加上weak关键字表明这是一个弱引用。

由于弱引用不会强保持对实例的引用,所以说实例被释放了弱引用仍旧引用着这个实例也是有可能的。因此,ARC会在被引用的实例被释放时自动地设置弱引用为nil。由于弱引用需要允许它们的值为nil,它们一定得是可选类型

示例

把之前的实例化对象的代码用户weak来修饰。

class ATTeacher {
    var age: Int = 18
    var name: String = "Atom"
}

weak var t = ATTeacher()

然后在初始化这一行打个断点,以汇编的方式查看。运行 20.png 可以看到用weak修饰的Swift底层调用了swift_weakInit方法。

源码分析

我们通过源码找一下方法的定义:

WeakReference *swift::swift_weakInit(WeakReference *ref, HeapObject *value) {
  ref->nativeInit(value);
  return ref;
}

调用swift_weakInit方法其实创建了一个实例对象WeakReference,我们声明了一个weak变量相当于定义了一个WeakReference对象,再看一下nativeInit的定义:

void nativeInit(HeapObject *object) {
    auto side = object ? object->refCounts.formWeakReference() : nullptr;
    nativeValue.store(WeakReferenceBits(side), std::memory_order_relaxed);
}

nativeInit方法中调用了formWeakReference

template <>
HeapObjectSideTableEntry* RefCounts<InlineRefCountBits>::formWeakReference()
{
  auto side = allocateSideTable(true);
  if (side)
    return side->incrementWeak();
  else
    return nullptr;
}

而从定义中可以知道这里创建了弱引用表。再看一下弱引用表的实际创建过程:

template <>
HeapObjectSideTableEntry* RefCounts<InlineRefCountBits>::allocateSideTable(bool failIfDeiniting)
{
  // 去除原来的refCounts
  auto oldbits = refCounts.load(SWIFT_MEMORY_ORDER_CONSUME);
  // 如果原来的是否有当前的引用计数
  if (oldbits.hasSideTable()) {
    // 如果有返回原来的引用计数
    return oldbits.getSideTable();
  } 
  else if (failIfDeiniting && oldbits.getIsDeiniting()) {
    // 如果没有就返回空
    return nullptr;
  }

  // 创建HeapObjectSideTableEntry
  HeapObjectSideTableEntry *side = new HeapObjectSideTableEntry(getHeapObject());
  auto newbits = InlineRefCountBits(side); // 传入InlineRefCountBits
  ...
  
  return side;
}

HeapObjectSideTableEntry的定义:

class HeapObjectSideTableEntry {
    std::atomic<HeapObject*> object;
    SideTableRefCounts refCounts;
}

// SideTableRefCounts的定义
typedef RefCounts<SideTableRefCountBits> SideTableRefCounts;

// SideTableRefCountBits共用了RefCountBitsT模板类
class alignas(sizeof(void*) * 2) SideTableRefCountBits : public RefCountBitsT<RefCountNotInline>
{
  uint32_t weakBits; // 存储弱引用信息

  ...
}

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

再运行上面的示例,在weak修饰实例处打个断点,在16行断点出打印当前refCount的值,再过一个断点再打印refCount的值。 21.png 可以看到由0x0000000000000003变成了0xc000000020c00cbe,把它复制到计算器里 22.png 可以看到使用了weak之后会把62位63位标记为1,我们把62位63位都置为0得到0x20c00cbe,通过上面HeapObjectSideTableEntry创建可以找到它的初始化方法:

SWIFT_ALWAYS_INLINE
RefCountBitsT(HeapObjectSideTableEntry* side)
  : bits((reinterpret_cast<BitsType>(side) >> Offsets::SideTableUnusedLowBits)
         | (BitsType(1) << Offsets::UseSlowRCShift) // 63位标记为1
         | (BitsType(1) << Offsets::SideTableMarkShift)) // 62位标记为1
{
  assert(refcountIsInline);
}

也就是把散列表内存地址往右移了3位,所以我们把上面的0x20c00cbe再左移3位得到0x1060065f0,这个内存地址就是当前的散列表的内存地址,再把这个地址格式化输出一下。 23.png 可以看到前8字节存储的是之前的HeapObject,第3个8字节存储的是strongCount,而第4个8字节存储的就是weakCount。以上就是通过weak关键字修饰之后,创建散列表的过程。

unowned

和弱引用类似,无主引用不会牢牢保持住引用的实例。但是不像弱引用,无主引用假定是永远有值的。根据苹果的官方文档的建议,当我们知道两个对象的生命周期并不相关,那么我们必须使用 weak。相反,非强引用对象拥有和强引用对象同样或者更长的生命周期的话,则应该使用 unowned

因此把上面循环引用的代码改一下用unowned修饰就可以。

class ATSubject {
    var subjectName: String
    // 这里用unowned修饰
    unowned var subjectTeacher: ATTeacher
    init(_ subjectName: String, _ subjectTeacher: ATTeacher) {
        self.subjectName = subjectName
        self.subjectTeacher = subjectTeacher
  }
}

weak vs unowned

  • 如果两个对象的生命周期完全和对方没关系(其中一方什么时候赋值为nil,对对方都没影响),用weak
  • 如果你的代码能确保其中一个对象销毀,另一个对象也要跟着销毁,这时候可以(谨慎)unowned

闭包循环引用

可以通过一个例子来说明一下,代码如下:

class ATTeacher {
    var age: Int = 18
    
    var testClosure:(() -> ())?
    
    deinit {
        print("ATTeacher deinit")
    }
}

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

test()

上面代码运行就发现deinit不执行了,此时就是闭包和当前的对象形成了循环引用。解决闭包的循环引用就要通过捕获列表。可以把上面调用闭包的地方通过捕获列表的形式来获取实例。

func test() {
    let t = ATTeacher()

    // 同样也可以用unowned
    t.testClosure = { [weak t] in
        t!.age += 1
    }

    print("end")
}

上面示例通过捕获列表解决了闭包的循环引用问题,那捕获列表的定义是什么?

捕获列表

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

在参数列表之前,捕获列表被写为用逗号括起来的表达式列表,并用方括号括起来。如果使用捕获列表,则即使省略参数名称、参数类型和返回类型,也必须使用in关键字。

下面通过示例来看一下。

var age = 0
var height = 0.0

let closure = { [age] in
    print(age)
    print(height)
}
age = 10
height = 1.85

closure()

运行,输出的结果age的值是0height的值是1.8524.png 创建闭包时,将初始化捕获列表中的条目。对于捕获列表中的每个条目,将常量初始化为在周围范围内具有相同名称的常量或变量的值。例如,在上面的代码中,捕获列表中包含age,但捕获列表中未包含height,编译器在编译时捕获到age的初始值0,把捕获到的值赋值age变量,后面不管怎么改变age的值都不会影响闭包里面的值,这就使ageheight具有不同的行为。

如果将上面的age改成a,捕获列表根据上下文捕获的时候没有找到有a的定义,因此编译就报错了。 25.png 以上就是对闭包循环引用和捕获列表的的分析。