之前的几篇文章主要了解了类、结构体以及属性的一些概念,并且通过示例和Mach-O
文件知道了他们在内存中是如何存储的,本篇主要介绍一下指针,通过Swift
代码利用指针去访问内存中的数据;在开始之前先了解一下指针的含义以及在使用中需要注意哪些问题。
为什么说指针不安全
- 比如我们在创建一个对象的时候,是需要在堆分配内存空间的。但是这个内存空间的声明周期是有限的,也就意味着如果我们使用指针指向这块内存空间,如果当前内存空间的生命周期结束了(引用计数为0),那么我们当前的指针是不是就变成了未定义的行为了。
- 我们创建的内存空间是有边界的,比如创建一个大小为
10
的数组,这个时候通过指针访问到了index = 11
的位置,这个时候是不是就越界了,访问了一个未知的内存空间。 - 指针类型与内存的值类型不一致,也是不安全的。
指针类型
Swift
中的指针分为两类:typed pointer
指定数据类型指针,raw pointer
未指定数据类型的指针(原生指针),基本上我们接触到的指针类型有以下几种:
Swift | Objective-C | 说明 |
---|---|---|
unsafePointer<T> | const T* | 指针及所指向的内容都不可变 |
unsafeMutablePointer<T> | T* | 指针及其所指向的内存内容均可变 |
unsafeRawPointer | const void * | 指针指向的内存区域未定 |
unsafeMutableRawPointer | void * | 同上 |
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数据,我们运行一下
发现
value
的值和我们预想的不一样,这是由于我们在存储的时候指针也需要移动i * 8
个字节,也就是指针在存储的时候也需要移动步长信息
,在Swift
中可以通过MemoryLayout
来计算所内存的大小。可以通过一个简单的例子来打印输出一下,定义个结构体:
struct ATTeacher {
var age: Int = 18
}
# 分别打印输出一下
MemoryLayout<ATTeacher>.size
MemoryLayout<ATTeacher>.stride
MemoryLayout<ATTeacher>.alignment
结果3个都是
8
,在原有的结构体添加一个属性,再打印一下:
这里得到的结果分别是
9
、16
和8
size
: 就是当前结构体的实际内存占用大小stride
: 步长信息,要储存连续的实例对象,从实例(ATTeacher)
存储的起始位置到下一个实例(ATTeacher)
的存储的起始位置为一个步长信息alignment
: 字节对齐(结构体默认8字节对齐)
所以上面的示例存储的时候需要移动相应的步长信息,加上移动步长信息代码如下:
// 存(移动步长信息)
for i in 0..<4 {
p.advanced(by: i * MemoryLayout<Int>.stride).storeBytes(of: i, as: Int.self)
}
再次运行,值就对应上了
指针使用完以后记得销毁
p.deallocate()
泛型指针的使用
这里的泛型指针
相比较原生指针来说,其实就是指定当前指针已经绑定到了具体的类型。同样的,我们还是通过一个例子来解释一下。在进行泛型指针访问的过程中,我们并不是使用load
和store
方法来进行存储操作。这里我们使用到当前泛型指针内置的变量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
修饰,指针和指针所指向的内容都是不可变的,如果直接修改编译器会报错。
如果想要直接修改,可以用
withUnsafeMutablePointer
,下面代码就可以执行。
var b = 20
withUnsafeMutablePointer(to: &b) { ptr in
ptr.pointee += 10
}
// 执行后b的结果就是30
对于泛型指针主要涉及的API可以通过下面的图例表示。
下面再通过一个示例,如何用指针去访问结构体。代码如下:
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
文件存储位置,下面通过Swift
的API
来获取相应的值。
// 获取类的地址信息
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 Commands
的LC_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
}
上面代码计算出来的偏移地址就是下图中的地址。
再获取
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
文件的偏移地址信息0x7CA8
。
属性是存储在
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))
}
}
运行上面代码可以看到输出了类的属性名称。
以上就是通过指针去读取类和属性的相关信息,详细代码可以查看gitee仓库。
内存绑定
Swift
提供了三种不同的API来绑定/重新绑定指针:
assumingMemoryBound(to:)
bindMemory(to: capacity:)
withMemoryRebound(to: capacity:)
assumingMemoryBound
有些时候我们处理代码的过程中,只有原始指针(没有保留指针类型),但此刻对于处理代码的我们来说明确知道指针的类型,我们就可以使用assumingMemoryBound(to:)
来告诉编译器预期的类型。(注意:这里只是让编译器绕过类型检查,并没有发生实际类型的转换)
举个例子,定义一个方法:
func testPoint(_ p: UnsafePointer<Int>) {
print(p)
}
再定义一个元组,然后通过withUnsafePointer
把一个元组用指针的方式在闭包里调用testPoint
方法。
发现上面调用编译报错,
UnsafePointer<(Int, Int)>
和UnsafePointer<Int>
从本质上来说是一样的,都是指向Int
类型的首地址,所以这里可以用原生指针,通过assumingMemoryBound(to:)
绑定成Int.self
类型,代码更新一下:
编译成功,可以看到通过
p[0]
和p[1]
就能取到元组中的值。
bindMemory
用于更改内存绑定的类型,如果当前内存还没有类型绑定,则将首次绑定为该类型;否则重新绑定该类型,并且内存中所有的值都会变成该类型。
把上面的代码改成
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字节
就是refCount
。
这里输出的
0x3
,从输出的数据看不出来这个0x3
是怎么得来的,接来下通过源码来分析一下。
源码分析
首先搜索找到refCounts
的定义,在HeapObject.h
中
#define SWIFT_HEAPOBJECT_NON_OBJC_MEMBERS \
InlineRefCounts refCounts
可以看到refCounts
由InlineRefCounts
定义的,接着在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;
};
可以看到Type
是uint64_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
的定义可以知道,这里strongExtraCount
是0
,unownedCount
是1
。
bits
操作流程
- 把
strongExtraCount
左移StrongExtraRefCountShift
- 把
1
左移PureSwiftDeallocShift
- 把
unownedCount
左移UnownedRefCountShift
这里根据源码的定义,StrongExtraRefCountShift
是33
,PureSwiftDeallocShift
是0
,UnownedRefCountShift
是1
其实就是把强引用计数和无主引用计数做左移操作。所以我们上面的示例代码strongCount
是0
左移33
位是0
,1
左移0
位是1
,unownedCount
是1
左移1
位是2
,所以结果输出的是3
。
说明:本篇是基于Swift5.5版本,如果是Swift之前的版本,上面步骤2是没有的,所以在Swift版本不一致的情况可能结果不一致。
把上面的代码再改一下,再加几个引用,打个断点运行:
在
18行
断点输出就是之前示例的结果0x3
,再过一个断点,到达20行
,这时t1 = t
赋值操作,引用计数+1
,结果是0x200000003
,strongCount
左移33位,通过计算器算一下
可以看到
33位
为1
。同样的再过一个断点,当t2 = t
赋值操作后,引用计数又+1
,这时候就变成了34位
为1
,也就是上面输出的结果0x400000003
,64位存储引用信息可以通过下图来表示。
- 0:
- 1~31:标记的是无主引用
- 32:标记的析构
- 33~62:标记的强引用
- 64: 使用SlowRC 再通过示例来验证一下,把之前的代码实例
t
设置为可选类型,然后再给它赋值为nil
,看看引用计数的变化。可以看到,当给
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()
然后在初始化这一行打个断点,以汇编的方式查看。运行
可以看到用
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
的值。
可以看到由
0x0000000000000003
变成了0xc000000020c00cbe
,把它复制到计算器里
可以看到使用了
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
,这个内存地址就是当前的散列表的内存地址,再把这个地址格式化输出一下。
可以看到前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
的值是0
,height
的值是1.85
。
创建闭包时,将初始化捕获列表中的条目。对于捕获列表中的每个条目,将常量初始化为在周围范围内具有相同名称的常量或变量的值。例如,在上面的代码中,捕获列表中包含
age
,但捕获列表中未包含height
,编译器在编译时捕获到age
的初始值0
,把捕获到的值赋值age
变量,后面不管怎么改变age
的值都不会影响闭包里面的值,这就使age
和height
具有不同的行为。
如果将上面的age
改成a
,捕获列表根据上下文捕获的时候没有找到有a
的定义,因此编译就报错了。
以上就是对闭包循环引用和捕获列表的的分析。