4、Swift指针&内存管理

313 阅读14分钟

1、指针

1.1 指针不安全

  • 指针从内存的角度来说,是不安全的,当我们创建一个对象的时候,系统会在堆分配内存空间,但这个内存生命周期有限,也就是如果我们使用指针指向这个空间,当内存的生命周期结束(引用计数器为0)时,那么这个指针就成了未定义的行为了。
  • 我们创建的内存空间是有边界的,⽐如我们创建⼀个⼤⼩为10的数组,这个时候我们通过指针访问 到了 index = 11 的位置,这个时候是不是就越界了,访问了⼀个未知的内存空间。
  • 当指针类型与内存的值类型不一致时,那么这个指针不安全。

1.2 Swift 指针类型

Swift中指针分为两大类,typed pointer 指定数据类型指针,raw pointer 未指定数据类型的指针(原生指针)

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

1.3 原始指针的使用 UnsafeMutableRowPointer

/*
byteCount:当前总的字节大小
alignment:对齐大小
store 方法存储当前的整形数值
load 方法读取内存中的值
MemoryLayout<Int>.size   //实际大小
MemoryLayout<Int>.stride //步长大小
MemoryLayout<Int>.alignment //对齐大小
*/
let p = UnsafeMutableRawPointer.allocate(byteCount: 32, alignment: 8)

for i in 0..<4 {
    //advanced(by:) 步长
    //storeBytes(of: as:) 存储
    p.advanced(by: i * MemoryLayout<Int>.stride).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)")
}

打印:

index0,value:0
index1,value:1
index2,value:2
index3,value:3

1.4 泛型指针(类型指针)的使用

相比起原始指针,它就是已经绑定到了具体的类型。

1.4.1 withUnsafePointer()与withUnsafeMutablePointer()

var age = 18
/*
 通过withUnsafePointer方法,创建一个指针ptr指向age
 ptr.pointee 访问这个指针引用的实例
 */
withUnsafePointer(to: &age) { ptr in
    print(ptr.pointee)
}

/*
 可以操作指针所指向的实例
 */
age = withUnsafePointer(to: &age) { ptr in
    return ptr.pointee + 12
}
print("age:\(age)")

/*
 withUnsafeMutablePointer 可直接修改指针指向的实例
 */
withUnsafeMutablePointer(to: &age, { ptr in
    ptr.pointee += 10;
})
print("age:\(age)")

1.4.2 主要几个API

.allocate .initialize .assign .deinitialize .deallocate

var age = 10

//分配一块Int类型的内存空间
let tPtr = UnsafeMutablePointer<Int>.allocate(capacity: 1)

tPtr.initialize(to: age)

//通过pointee属性来访问实例
print(tPtr.pointee)
struct CCStruct{
    var age: Int
    var height: Double
}

//capacity: 要分配多少个CCStruct 就写多少
var tPtr = UnsafeMutablePointer<CCStruct>.allocate(capacity: 2)
tPtr[0] = CCStruct(age: 18, height: 180)
tPtr[1] = CCStruct(age: 20, height: 170)

//释放内存 分配与释放要成对出现
tPtr.deinitialize(count: 2)
tPtr.deallocate()

1.5 案例 - 指针读取Macho的属性名称

此案例目的是为了理解指针操作

import Foundation

class CTTeacher {
    var age: Int = 18
    var name:String = "CCT"
}

var size :UInt = 0
//获得maco文件的__swift5_types section 的pFile 也就是程序运行地址 0x0000000100003f24
var ptr = getsectdata("__TEXT", "__swift5_types", &size)
//print(ptr)

//获取当前程序运行地址 0x0000000100000000 macoheader的起始地址
var mhHeaderPtr = _dyld_get_image_header(0)

//计算链接的基地址 = 虚拟内存地址 - 偏移量
//这个里面就存了maco的内容
var setCommond64Ptr = getsegbyname("__LINKEDIT")

var linkBaseAddress: UInt64 = 0
if let vmaddr = setCommond64Ptr?.pointee.vmaddr, let fileOff = setCommond64Ptr?.pointee.fileoff {
    linkBaseAddress = vmaddr - fileOff
}

//把程序运行地址ptr 转换成UInt64类型,然后与链接的基地址相减
var offset:UInt64 = 0
if let unwrappedPtr = ptr{
    
    let intRepresentation = UInt64(bitPattern: Int64(Int(bitPattern: unwrappedPtr)))
    offset = intRepresentation - linkBaseAddress
    print(offset) //16164 -> 16进制0x3F24
}

//当前程序运行地址 转化为UInt64
let mhHeaderPtr_IntRepresentation = UInt64(bitPattern: Int64(Int(bitPattern: mhHeaderPtr)))

//当前程序运行地址 + 偏移量 = 属性的地址
var dataLoAddress = mhHeaderPtr_IntRepresentation + offset

//拿到属性地址指向的内容
//先把 地址 转换为指针类型
var dataLoAddressPtr = withUnsafePointer(to: &dataLoAddress){return $0}
print(dataLoAddressPtr)
//再通过指针拿到pointee,拿到指针指向的内容
var dataLoContent = UnsafePointer<UInt32>.init(bitPattern: Int(exactly: dataLoAddress) ?? 0)?.pointee
print(dataLoContent)


//拿到描述文件 在maco文件中的偏移信息
let typeDescOffset = UInt64(dataLoContent!) + offset - linkBaseAddress

//描述文件在内存中的真实地址 typeDescOffset + 程序运行地址
var typeDescAddress = typeDescOffset + mhHeaderPtr_IntRepresentation

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

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

let filedDescriptorRelaticveAddress = typeDescOffset + 16 + mhHeaderPtr_IntRepresentation
//print(filedDescriptorAddress)

/*
 // 还原vtable
let numVTables = classDescriptor?.numMethods
//print(numVTables)

struct VTable{
    var kind: UInt32
    var offset: UInt32
}

for i in 0..<numVTables!{
    let vTableOffset = Int(typeDescOffset) + MemoryLayout<TargetClassDescriptor>.size + Int(i) * MemoryLayout<VTable>.size
//    print(vTableOffset)
    //0x3B6C
    //0x3B74
    let vTableAddress = mhHeaderPtr_IntRepresentation + UInt64(vTableOffset)

    let vtable =  UnsafePointer<VTable>.init(bitPattern: Int(exactly: vTableAddress) ?? 0)?.pointee

    let impAddress = vTableAddress + 4 + UInt64(vtable!.offset) - linkBaseAddress

    //方法的名称 -- 符号
    LGTest.callImp(IMP(bitPattern: UInt(impAddress))!)
}

//    let t = LGTest()

 */

/*
 // 还原filedDescriptor
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
}

//print(filedDescriptorAddress)
//let fieldDescriptor = UnsafePointer<FieldDescriptor>.init(bitPattern: Int(exactly: filedDescriptorAddress) ?? 0)?.pointee
//print("end"

let fieldDescriptorOffset = UnsafePointer<UInt32>.init(bitPattern: Int(exactly: filedDescriptorRelaticveAddress) ?? 0)?.pointee
//print(fieldDescriptorOffset)
let fieldDescriptorAddress = filedDescriptorRelaticveAddress + UInt64(fieldDescriptorOffset!)

let fieldDescriptor = UnsafePointer<FieldDescriptor>.init(bitPattern: Int(exactly: fieldDescriptorAddress) ?? 0)?.pointee


for i in 0..<fieldDescriptor!.numFields{
    let stride: UInt64 = UInt64(i * 12)
    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))
    }
}
 
 */

1.6、内存绑定

Swift提供了三种API来绑定、重绑定指针:

1.6.1、assumingMemoryBound(to:)

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

func testPointer(_ p:UnsafePointer<Int>){
    print(p[0])
    print(p[1])
}

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

1.6.2、bindMemory(to: capacity:)

用于更改内存绑定的类型,如果当前内存还没有类型绑定,则将⾸次绑定为该类型;否则重新绑定该类 型,并且内存中所有的值都会变成该类型。(发生了实际类型转换)

func testPointer(_ p:UnsafePointer<Int>){
    print(p[0])
    print(p[1])
}

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

1.6.3、withMemoryRebound(to: capacity: body:)

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

func testPointer(_ p:UnsafePointer<Int8>){

}

let Uint8Ptr = UnsafePointer<UInt8>.init(bitPattern: 10)

Uint8Ptr?.withMemoryRebound(to: Int8.self, capacity: 1){ (int8Ptr: UnsafePointer<Int8>) in
    testPointer(int8Ptr)
}

2、内存管理

2.1 引用计数源码追踪

Swift中使用自动引用计数ARC机制来追踪和管理内存

对象的内存布局,其中有8个字节是用来存储当前的引用计数的。

来看一下源代码

iShot2022-04-11_15.55.27.png

引用计数refCountsInlineRefCounts类型,点进去看看:

是一个模板类,接受一个泛型参数InlineRefCountBits

typedef RefCounts<InlineRefCountBits> InlineRefCounts;

点击RefCounts进去看看:

iShot2022-04-11_15.59.43.png

这个RefCounts在操作的时候,本质上都是在操作传进来的泛型参数

所以refCounts是对引用计数的一个包装

再回头追踪一下InlineRefCountsiShot2022-04-13_11.01.44.png 它也是个模板函数: iShot2022-04-13_10.50.48.png 也就是我们当前操作的引用计数是在操作RefCountBitsT这个类,而这个类里面只有一个属性,就是BitsType bits,这个属性是由typedef typename RefCountBitsInt<refcountIsInline, sizeof(void*)>::Type BitsType定义的 iShot2022-04-13_11.07.35.png 再接着追踪BitsType bits进去看: iShot2022-04-13_11.13.21.png

看到这里,就能理解其实Swift与OC的引用计数 都是一个64位的位域信息

2.2 创建实例时,引用计数的变化

我们可以通过追踪实例创建的源码,来了解引用计数是如何设置进去的

从头开始: iShot2022-04-13_11.18.15.png 进入HeapObject iShot2022-04-13_11.20.33.png 初始化赋值了一个Initialized,点进去看看是啥: iShot2022-04-13_11.24.12.png 是个枚举类,下面还进行了赋值0、1,这里出现的RefCountBits,正是前面分析过的RefCountBitsT这个类,那么我们就看看RefCountBitsT的初始化函数: iShot2022-04-13_11.30.36.png 能看到,传进来的strongExtraCount是0,unownedCount是1,并且都进行了左移操作(strongExtraCount是左移了33位,unownedCount是左移1位),也就是说通过左移,将“强引用计数”和“无主引用计数”存进了64位当中。

通过一个案例来分析一下:

情况一:仅创建对象

class HTPerson {
    var age  = 10
    var name = "ht"
}

var t = HTPerson()
//打印对象内存地址
print(Unmanaged.passUnretained(t as AnyObject).toOpaque())
print("end")

通过x/4g 对象地址来查看内存内容

**x/4g 0x100737960**
0x100737960: 0x0000000100008180 0x0000000000000002
0x100737970: 0x0000000000000001 0x0000000000544343

得到0x0000000000000002,也就是在十六进制中,unownedCount左移1位

iShot2022-04-14_14.59.22.png

情况二:进行一次强引用

class HTPerson {
    var age  = 10
    var name = "ht"
}

var t = HTPerson()
//打印对象内存地址
var t1 = t
print(Unmanaged.passUnretained(t as AnyObject).toOpaque())

var t2 = t
print(Unmanaged.passUnretained(t as AnyObject).toOpaque())
print("end")

在swift源代码中可以追踪到: swift_retain(heapObject)->refCounts.increment(1)->incrementStrongExtraRefCount(inc)->强引用计数+1并且左移33位 iShot2022-04-13_17.00.51.png iShot2022-04-13_17.01.09.png iShot2022-04-13_17.01.28.png 在汇编层也能看到本质是调用swift_retain进行引用计数操作 iShot2022-04-13_16.57.11.png

在t1处打断点:

t的强引用计数无主引用计数都是1 iShot2022-04-14_15.15.20.png

在t2处打断点:

t的强引用计数为2,无主引用计数1 iShot2022-04-14_15.17.16.png

解释一下最后面为什么是3:

在16进制中第0位是用来标识delloc的,引用计数是从1开始算的,这个跟xcode版本有关,xcode11会初始化为2 iShot2022-04-14_15.24.38.png iShot2022-04-14_15.03.41.png iShot2022-04-14_14.53.30.png

2.3 CGGetRetainCount() 引用计数统计方法

CFGetRetainCount()用于统计对象的引用计数,它在执行前,会对对象进行strong_retain操作,执行后,完成release_value操作。所以在Swift中使用CFGetRetainCount()得到的强引用计数会比实际的引用计数多1

  • 在lldb中使用p和po命令打印对象,,会使引用计数+1,影响CFGetRetainCount结果(p打印一次或多次,x/4g在内存信息中可看到引用计数的明显变化)

代码试验:

class HTPerson {
    var age  = 10
    var name = "ht"
}
var t = HTPerson()
print("end")

看一下SIL: 没有retain、release iShot2022-04-14_15.36.57.png

然后来个CGGetRetainCount()打印

class HTPerson {
    var age  = 10
    var name = "ht"
}
var t = HTPerson()
print("end")

再看下SIL:CGGetRetainCount()执行之前 调用了strong_retain,执行结束了调用release_value iShot2022-04-14_15.40.42.png

3、循环引用

来个循环引用的例子:

class CTTeacher{
    var age: Int = 18
    var name: String = "Kody"
    var subject: LGSubject?
    
}

class CTSubject{
    var subjectName: String
    var subjectTeacher: LGTeacher
    init(_ subjectName: String, _ subjectTeacher: LGTeacher) {
        self.subjectName = subjectName
        self.subjectTeacher = subjectTeacher
        
    }
}
var t = CTTeacher()
var subject = CTSubject.init("XXXXX", t)
t.subject = subject

在Swift中有两种方式解决循环引用的问题,一种是弱引用weak,一种是无主引用unowned

3.1 weak 弱引用

  • 声明属性或者变量时,由于弱引⽤不会强保持对实例的引⽤,所以实例被释放之后弱引⽤有可能仍旧引⽤着这个实例,因此ARC会在实例被释放后⾃动地设置弱引⽤为nil

  • 由于弱引⽤需要允许它们的值为 nil ,所以它们⼀定得是可选类型

  • weak修饰的对象,不影响强引用计数,不影响对象的释放。

  • 允许对象为nil,当对象为nil时,后面的语句不执行

swift中的弱引用与OC不同的是:

  • OC:
    弱引用计数是存放在全局维护散列表中,isa中会记录是否使用了散列表
    引用计数0时,自动触发dealloc,会检查清空当前对象散列表计数

  • swift:
    弱引用计数也是存放在散列表中,但这个散列表不是全局的。

    • 如果对象没有使用weak弱引用,就是单纯的HeapObject对象,没有散列表
    • 如果使用weak弱引用,会变为WeakReference对象。这是一个Optionl(可空对象)。其结构中自带散列表计数区域。
      swift散列表refCount无关联。当强引用计数0时,不会触发散列表的清空。而是在下次访问发现当前对象不存在(为nil)时,会清空散列表计数

看一段代码:

class CTTeacher{
    var age: Int = 18
    var name: String = "Kody"
}

weak var t = CTTeacher()

从汇编的来看,调用了swift_weakInit符号 iShot2022-04-14_16.47.40.png

在Swift源码中搜索swift_weakInit

可以看出,声明一个weak变量,相当于定义了一个weakRefrence对象,本质上就是创建了一个散列表 iShot2022-04-14_16.50.49.png iShot2022-04-14_16.52.10.png iShot2022-04-14_16.54.16.png

来看一下散列表的初始化allocateSideTable() iShot2022-04-15_11.50.44.png
初始化的是 HeapObjectSideTableEntry 这个玩意儿

那就继续看一下HeapObjectSideTableEntry这是个啥玩意儿

全局搜一下,可以找到这个: iShot2022-04-15_10.29.15.png

在Swift中,存在着两种引用计数,第一种就是一般的引用计数,也就是上面分析过的InlineRefCounts,第二种是弱引用,也就是HeapObjectSideTableEntry

如果是一般的引用计数(强引用)InlineRefCounts里边儿就是 strong RC + unowned RC + flags

如果是弱引用 那么InlineRefCounts里边儿就是HeapObjectSideTableEntry这个sidetable

里面存在着 strong RC + unowned RC + weak RC + flags

接着看HeapObjectSideTableEntryiShot2022-04-15_14.17.41.png object是引用的对象,refCounts是引用计数

refCounts是个SideTableRefCounts类型,接着看: iShot2022-04-15_10.47.50.png 可以看到SideTableRefCountsSideTableRefCountBits类型,而它跟InlineRefCountBits公用一个模板类RefCountBitsT iShot2022-04-15_14.22.48.png 那既然SideTableRefCountBits继承自RefCountBitsT,那么RefCountBitsT的64位信息也就被继承下来了。

  • 常规对象与弱引用对象的区别:

12857030-7a7de1b2feb4d6d2.webp

除了继承下来的信息,它还有一个存储弱引用计数weakBits,所以当我们声明了weak变量之后,它会存储一下内容,存的啥呢?

来个例子:

class CTTeacher{
  var age: Int = 18
  var name: String = "Kody"
}
var t = CTTeacher()
print(Unmanaged.passUnretained(t as AnyObject).toOpaque())
//weak修饰
weak var t1 = t 
print("end")

iShot2022-04-19_15.08.46.png 可以看到,weak修饰的t1,变成了optional可选值

  • weak修饰的对象,不会改变原来对象的引用计数,只是一层可空的状态

断点到t和t1,观察x/4g打印的地址变化 iShot2022-04-15_11.07.10.png t是正常的引用,引用计数是0x3,而t1的断点是weak,引用计数就变了

那我们来研究一下这个内存地址,把它放到计算器里边儿: iShot2022-04-15_11.10.02.png 回到sidetable生成的代码: iShot2022-04-15_11.13.41.pngInlineRefCountBits(side)初始化方法: iShot2022-04-15_11.18.45.png iShot2022-04-15_11.23.33.png side的地址向右移动了3位,然后UseSlowRCShiftSideTableMarkShift都标记成1,也就是第62位和63位会标识成1,所以我们手动把62位63位改为0,然后左移3位iShot2022-04-15_11.25.44.png 最终得到的这个地址,用x/8g打一下: iShot2022-04-15_11.27.38.png 它的前8个字节,存储了heapObject,第三个8字节和第四个8字节,分别存储了strongCountweakCount,所以这就是经过weak修饰之后,引用计数器的变化过程,本质上就是创建了一个SideTable

3.2 Unowned 无主引用

无主引用假定永远有值,不能nil。

必须确保捕获对象的生命周期可控(当对象被释放时,会存在产生野指针有crash风险

weak和unowned选择:

  • 如果两个对象的⽣命周期完全和对⽅没关系(其中⼀⽅什么时候赋值为nil,对对⽅都没影响),请⽤ weak

  • 如果你的代码能确保:其中⼀个对象销毁,另⼀个对象也要跟着销毁,这时候,可以(谨慎)⽤ unowned

iShot2022-04-15_15.11.56.png

3.3 闭包循环引用

1、闭包内部自动捕获变量,修改它会改变闭包外变量的值

var age = 18

let closure = {
    age += 1
}
closure()

print(age)

打印: iShot2022-04-18_17.19.02.png 自动捕获了外部变量age,闭包内修改age值,影响了外部age的值

2、手动捕获外界变量、值类型对象,会copy一份,不可修改,也不影响外部值 iShot2022-04-18_17.26.42.png

3、如果闭包直接持有外部对象,容易造成循环引用 例子:

class CTTeacher{
    var age = 18
    var complateCallBack:(()->())?
    deinit{
        print("deinit")
    }
}

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

test()
  • 打印结果:

iShot2022-04-19_14.50.37.png

t对象持有complateCallBack属性complateCallBack闭包捕获t这个外部变量,并且强引用计数+1,造成循环引用

捕获列表

  • 闭包表达式从其周围的范围捕获常量和变量,并强引⽤这些值。

  • 创建闭包时,将初始化捕获列表中的条⽬。对于捕获列表中的每个条⽬,将常量初始化为在周围范围内具有相同名称的常量或变量的值。

var age = 0 
var height = 0.0 
let closure = { [age] in //手动捕获,这是个常量age,初始化为外部同名age的值
  print(age) 
  print(height) //自动捕获 外部变量 内外修改都会产生影响
}
age = 10 
height = 1.85 
closure() //输出结果为0, 1.85

打破循环引用:

【方式一】weak弱引用

iShot2022-04-19_14.11.17.png

【方式二】unowned无主引用

iShot2022-04-19_14.19.45.png

总结

  1. Swift闭包会自动捕获外界变量,并影响引用计数,有循环引用风险

  2. 手动捕获变量(在[]内写相同的变量名):

    值类型(var let):会copy一份数据,为只读属性,不可修改,与原始值互不影响

    引用类型(对象):会操作对象地址,会影响引用计数,也会影响该对象的内容,有循环引用的风险。

  3. 打破循环引用的方式

    1、weak弱引用

    2、unowned无主引用