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

强引用

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是常量,并且进行了值拷贝。对于捕获列表中的每个常量,闭包会利⽤周围范围内具有相同名称的常量或变量,来初始化捕获列表中定义的常量。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342

推荐阅读更多精彩内容