美文网首页swift mvvm
Swift底层进阶--006:内存管理

Swift底层进阶--006:内存管理

作者: 帅驼驼 | 来源:发表于2020-12-30 16:32 被阅读0次
强引用

Swift使用ARC管理内存

  • OC创建实例对象,默认引用计数为0
  • Swift创建实例对象,默认引用计数为1
class LGTeacher{
    var age: Int = 18
    var name: String = "Zang"
}

var t=LGTeacher()
var t1=t
var t2=t

上述代码,通过LLDB指令来查看t的引⽤计数:

查看t的引⽤计数 输出的refCounts为什么是0x0000000600000002

通过源码进行分析,打开HeapObhect.h,看到一个宏

HeapObhect.h

进入SWIFT_HEAPOBJECT_NON_OBJC_MEMBERS宏定义,这里看到refCounts类型是InlineRefCounts

SWIFT_HEAPOBJECT_NON_OBJC_MEMBERS

进入InlineRefCounts定义,它是RefCounts类型的别名

InlineRefCounts

进入RefCounts定义,它是一个模板类。后续逻辑取决于模板参数RefCountBits,也就是上图中传入的InlineRefCountBits的类型

RefCounts

进入InlineRefCountBits定义,它是RefCountBitsT类型的别名

InlineRefCountBits

首先确认RefCountIsInline是什么,进入RefCountIsInline定义,本质上是enum,只有truefalse。这里传入的RefCountIsInline就是true

RefCountIsInline

再进入到RefCountBitsT的定义,里面的成员变量bits,类型为BitsType

RefCountBitsT

bitsRefCountBitsIntType属性取别名,本质上就是uint64_t类型

RefCountBitsInt

明白了bits是什么,下面就来分析HeapObject的初始化方法,重点看第二个参数refCounts

HeapObject初始化方法

进入Initialized定义,它的本质是一个enum,找到对应的refCounts方法,需要分析一下传入的RefCountBits(0, 1)到底在做什么

Initialized

进入RefCountBits,还是模板定义,把代码继续往下拉...

RefCountBits

在下面找到真正的初始化方法RefCountBitsT,传入strongExtraCountunownerCount两个uint32_t类型参数,将这两个参数根据Offsets进行位移操作

RefCountBitsT

通过源码分析,最终我们得出这样⼀个结论

结论
  • isImmortal(0)
  • UnownedRefCount(1-31):无主引用计数
  • isDeinitingMask(32):是否进行释放操作
  • StrongExtraRefCount(33-62):强引用计数
  • UseSlowRC(63)

对照上述结论,使用二进制查看refCounts输出的0x0000000600000002

二进制查看refCounts
  • 1-31位是UnownedRefCount无主引用计数
  • 33-62位是StrongExtraRefCount强引用计数

通过SIL代码,分析t的引用计数,当t赋值给t1t2时,触发了copy_addr

SIL

查看SIL文档,copy_addr内部又触发了strong_retain

copy_addr

回到源码,来到strong_retain的定义,它其实就是swift_retain,其内部是一个宏定义CALL_IMPL,调用的是_swift_retain_,然后在_swift_retain_内部又调用了object->refCounts.increment(1)

strong_retain

进入increment方法,里面的newbits是模板函数,其实就是64位整形。这里我们发现incrementStrongExtraRefCount方法点不进去,因为编译器不知道RefCountBits目前是什么类型

increment方法

我们需要回到HeapObject,从InlineRefCounts进入,找到incrementStrongExtraRefCount方法

image.png 通过BitsType方法将inc类型转换为uint64_t,通过Offsets偏移StrongExtraRefCountShift,等同于1<<33,十进制的1左移33位,再转换为十六进制,得到结果0x200000000。故此上述代码相当于bits += 0x200000000,左移33位后,在33-62位上,强引用计数+1

上述源码分析中,多次看到C++的模板定义,其目是为了更好的抽象,实现代码重用机制的一种工具。它可以实现类型参数化,即把类型定义为参数, 从而实现真正的代码可重用性。模版可以分为两类,一个是函数模版,另外一个是类模版。

通过CFGetRetainCount查看引用计数

class LGTeacher{
    var age: Int = 18
    var name: String = "Zang"
}

var t=LGTeacher()
print(CFGetRetainCount(t))

var t1=t
print(CFGetRetainCount(t))

var t2=t
print(CFGetRetainCount(t))

//输出以下内容:
//2
//3
//4

上述代码中,原本t的引用计数为3,使用CFGetRetainCount方法会导致t的引用计数+1

弱引用
class LGTeacher{
    var age: Int = 18
    var name: String = "Zang"
    var stu: LGStudent?
}

class LGStudent {
    var age = 20
    var teacher: LGTeacher?
}

func test(){
    var t=LGTeacher()
    weak var t1=t
    print(CFGetRetainCount(t))
}

test()

//输出以下内容:
//2

上述代码,t创建实例对象引用计数默认为1,使用CFGetRetainCount查看引用计数+1,打印结果为2。显然将t赋值给使用weak修饰的t1,并没有增加t的强引用计数

通过LLDB指令来查看t的引⽤计数:

查看`t`的引⽤计数 将t赋值给weak修饰的t1,查看refCounts打印出奇怪的地址

通过LLDB指令来查看t1

查看t1 使用weak修饰的t1变成了Optional可选类型,因为当t被销毁时,t1会被置为nil,所以weak修饰的变量必须为可选类型

通过断点查看汇编代码,发现定义weak变量,会调用swift_weakInit函数

查看汇编代码

通过源码进行分析,找到swift_weakInit函数,这个函数由WeakReference调用,相当于weak字段在编译器声明过程中自定义了一个WeakReference对象,目的在于管理弱引用。在swift_weakInit函数内部调用了ref->nativeInit(value), 其中value就是HeapObject

swift_weakInit

进入nativeInit方法,判断object不为空,调用formWeakReference

nativeInit

进入formWeakReference方法,首先通过allocateSideTable方法创建SideTable,如果创建成功,调用incrementWeak

formWeakReference

进入allocateSideTable方法,先通过refCounts拿到原有的引用计数,再通过getHeapObject创建SideTable,将地址传入InlineRefCountBits方法

allocateSideTable

进入InlineRefCountBits方法,将参数SideTable的地址,直接进行偏移,然后存储到内存中,相当于将SideTable直接存储到uint64_t的变量中

InlineRefCountBits

之前查看trefCounts,打印出0xc0000000200d1d6e这串奇怪的地址,去掉62位63位保留字段,剩余的就是偏移后的HeapObjectSideTableEntry实例对象的内存地址,即散列表的地址

二进制查看refCounts

回到源码分析,进入HeapObjectSideTableEntry定义,里面有object对象和refCountsrefCounts是一个SideTableRefCounts类型

HeapObjectSideTableEntry

进入SideTableRefCounts定义,它是RefCounts类型的别名,和之前分析的InlineRefCountBits类似,后续逻辑取决于模板参数的传入,这里传入的是SideTableRefCountBits类型

SideTableRefCounts

进入SideTableRefCountBits定义,它继承于RefCountBitsT

SideTableRefCountBits RefCountBitsT存储的是uint64_t类型的64位的信息,用于记录原有引用计数。除此之外SideTableRefCountBits自身还有一个uint32_tweakBits,用于记录弱引用计数

还原散列表地址,查看弱引用refCounts

  • 0xc0000000200d1d6e地址62位63位的保留字段清零,得到地址0x200D1D6E
  • 0x200D1D6E左移3位,还原成HeapObjectSideTableEntry对象地址0x10068EB70,也就是散列表地址
  • 通过x/8g读取地址0x10068EB70
    查看弱引用refCounts
循环引用
案例1:

闭包捕获外部变量

var age = 10

let clourse = {
    age += 1
}

clourse()
print(age)

//输出以下内容:
//11

从输出结果来看, 闭包内部对变量的修改将会改变外部原始变量的值,因为闭包会捕获外部变量,这个与OC中的block一致

案例2:

deinit反初始化器

class LGTeacher{
    
    var age = 18
    
    deinit{
        print("LGTeacher deinit")
    }
}

func test(){
    var t = LGTeacher()
}

test()

//输出以下内容:
//LGTeacher deinit

test函数里的局部变量t被销毁时,会执行反初始化器deinit方法,这个与OC中的dealloc一致

案例3:

闭包修改实例变量的值,闭包能否对t造成强引用?

class LGTeacher{
    
    var age = 18
    
    deinit{
        print("LGTeacher deinit")
    }
}

func test(){

    var t = LGTeacher()

    let closure = {
        t.age += 1
    }

    closure()
    print("age:\(t.age)")
}

test()

//输出以下内容:
//age:19
//LGTeacher deinit

从输出结果来看, 闭包对t并没有造成强引用

案例4

案例3进行修改,在LGTeacher类里定义闭包类型属性completionBlock,在test函数内,调用t.completionBlock闭包,内部修改t.age属性,这样能否对t造成强引用?

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

func test(){

    var t = LGTeacher()

    t.completionBlock = {
        t.age += 1
    }

    print("age:\(t.age)")
}

test()

//输出以下内容:
//age:18

从输出结果来看,这里产生了循环引用,没有执行deinit方法,也没有打印LGTeacher deinit。因为实例变量t的释放,需要等待completionBlock闭包的作用域释放,但闭包又被实例对象强引用,造成循环引用,t对象无法被释放

案例5

案例4中循环引用的两种解决方法

1、使用weak修饰闭包传入的参数,参数的类型是Optional可选类型

func test(){

    var t = LGTeacher()

    t.completionBlock = { [weak t] in
        t?.age += 1
    }

    print("age:\(t.age)")
}

//输出以下内容:
//age:18
//LGTeacher deinit

2、使用unowned修饰闭包参数,与weak的区别在于unowned不允许被设置为nil,在运行期间假定它是有值的,所以使用unowned修饰要注意野指针的情况

func test(){

    var t = LGTeacher()

    t.completionBlock = { [unowned t] in
        t.age += 1
    }

    print("age:\(t.age)")
}

//输出以下内容:
//age:18
//LGTeacher deinit
捕获列表

[unowned t][weak t]Swift中叫做捕获列表

  • 捕获列表的定义在参数列表之前
  • 书写形式:⽤⽅括号括起来的表达式列表
  • 如果使⽤捕获列表,即使省略参数名称、参数类型和返回类型,也必须使⽤in关键字
  • [weak t]就是获取t的弱引用对象,相当于OC中的weakself
var age = 0
var height = 0.0

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

age = 10
height = 1.85

closure()

//输出以下内容:
//0
//1.85

上述代码中,捕获列表的age是常量,并且进行了值拷贝。对于捕获列表中的每个常量,闭包会利⽤周围范围内具有相同名称的常量或变量,来初始化捕获列表中定义的常量。

相关文章

网友评论

    本文标题:Swift底层进阶--006:内存管理

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