Swift内存管理

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

  • 强引用

class YYTeacher {
    var age : Int = 18
    var name : String = "YY"
}

var t = YYTeacher()
var t1 = t
var t2 = t

通过 lldb 端可知上述代码执行完成后, t 的内存情况如下:

那么为什么其 refCounts0x0000000600000003 呢?

在前面分析swift的类结构时,通过SIL查看源码:

refCountsInlineRefCounts 类型的。

InlineRefCounts 又是 RefCounts 这个模板类的别名,这个模板类又取决于其传入的参数类型 InlineRefCountBits

InlineRefCountBits 又是 RefCountBitsT 这个模板类的别名。

RefCountBitsT 这个类中有一个属性 BitsType 类型的 bits ,通过查看定义可知:

BitsType 其实是 RefCountBitsInt 这个结构体中 Type 属性的别名,所以 bits 其实就是 uint64_t 类型。

分析了 RefCountBitsT 这个类中的属性bits,再来分析一下 swift 中的创建对象的底层方法 _swift_allocObject_

查看 Initialized 的定义可知是一个枚举

refCounts 对应的构造函数:

可知在这里真正做事的是 RefCountBits

点进去可知 RefCountBits 也是一个模板定义,所以真正的初始化操作应该是在 RefCountBitsT 这个类中的构造方法:

根据 offset 进行的一个位移 操作。
分析 RefCountBitsT 的结构,如下图:

  • isImmortal(0)
  • UnownedRefCount(1-31):无主引用计数
  • isDeinitingMask(32):是否释放的标记
  • StrongExtraRefCount(33-62):强引用计数
  • UseSlowRC(63)

这里重点关注 UnownedRefCountStrongExtraRefCount ,将刚刚例子中的 t 的引用计数0x0000000600000003 对比:

可知例子中代码执行完后,t强引用计数变成了3.

通过分析例子中的SIL文件:

通过查看SIL文档

可知 copy_addr 内部又调用了 一次strong_retain,而 strong_retain 其实就是 swift_retain

从上图可知最终调用的就是 __swift_retain_ 这个方法,再往下走:


综合上述可知:__swift_retain_ 其实就是强引用计数增加了1

注意:如果使用 CFGetRetainCount(t) 来获取 t 的强引用计数, t 的强引用计数会在原来的基础上 +1

  • 弱引用

使用关键字weak声明弱引用的变量,

weak var t = YYTeacher()

从上图可知弱引用声明的变量是一个可选值,因为在程序运行过程中是允许将当前变量设置为 nil的。如下面的例子:

class YYTeacher {
    var age : Int = 18
    var name : String = "YY"
    
    deinit {
        print("YYTeacher deinit")
    }
}

var t = YYTeacher()
t = nil

上面的代码在t = nil处会报错,可是如果将变量 t 声明成可选类型,再将 t = nil 则不会报错,所以意味着 weak声明的变量必须是一个可选类型,才能被允许被设置为 nil

通过源码分析 weak 关键字在底层到底做了什么?

class YYTeacher {
    var age : Int = 18
    var name : String = "YY"
}

var t = YYTeacher()
weak var t1 = t

汇编模式下可知调用了 swift_weakInit 这个方法

通过 SIL 中查看源码看一下 swift_weakInit() 这个方法究竟做了什么?

到这里,就引出了 HeapObjectSideTableEntry 这个类

SideTableRefCountBits决定了SideTableRefCounts的真实类型,继承自 RefCountBitsT ,多了一个 uint32_t 的属性 weakBits

class alignas(sizeof(void*) * 2) SideTableRefCountBits : public RefCountBitsT<RefCountNotInline>
{
  uint32_t weakBits;
// ...
}

综上可知:HeapObjectSideTableEntry 其实就是原来的 uint64_t 再加上一个存储弱引用计数的 uint32_t

了解 HeapObjectSideTableEntry 后,接着看 allocateSideTable() 方法中的实现,在新建了一个 SideTable对象后,将其作为参数传入调用了 InlineRefCountBits() 这个构造方法:

HeapObjectSideTableEntry* RefCounts<InlineRefCountBits>::allocateSideTable(bool failIfDeiniting)
{
  auto oldbits = refCounts.load(SWIFT_MEMORY_ORDER_CONSUME);
  // ...
  // FIXME: custom side table allocator
  HeapObjectSideTableEntry *side = new HeapObjectSideTableEntry(getHeapObject());
  
  auto newbits = InlineRefCountBits(side);
// ...

前面学习了 HeapObject 里的 RefCounts 实际上是InlineRefCountBits 的一个模板参数,在这里构造完 Side Table 后,对象中 InlineRefCountBits 就不再是原来的引用计数了,而是一个指向 Side Table 的指针,因为它们都是 uint64_t,所以需要不同的构造函数来区分,这里 InlineRefCountBits 的构造函数如下:

RefCountBitsT(HeapObjectSideTableEntry* side)
    : bits((reinterpret_cast<BitsType>(side) >> Offsets::SideTableUnusedLowBits)
           | (BitsType(1) << Offsets::UseSlowRCShift)
           | (BitsType(1) << Offsets::SideTableMarkShift))
{
   assert(refcountIsInline);
}

其实就是把 Side Table 的地址做了一个偏移 存放到内存中(即把创建的 side地址存放到 uint64_t 这个变量中),将指针地址没用的位置替换成标识位

扩展:如果此时再增加引用计数会怎样呢?

通过前面可知:这里在给 t2 赋值的时候,内部会调用
strong_retain --> swift_retain --> __swift_retain

void incrementStrong(uint32_t inc) {
  refCounts.increment(inc);
}

通过查看源码可知此时的 refCounts 其实就是 SideTableRefCounts ,所以这个时候其实操作的就是SideTable

总结:上面讲了两种 RefCounts

  • InlineRefCounts:用在 HeapObjet 中,其实就是一个uint64_t,没有弱引用时存的是引用计数(strongRefCounts + unownedRefCounts),有弱引用时存的是Side Table 的指针地址

  • HeapObjectSideTableEntry:内部有一个实质为 SideTableRefCountBits类型的属性 refCounts ,该类型中多了一个属性 uint32_t weakBits,继承自 RefCountBitsT ,即原来的 uint64_t 加上一个存储弱引用计数的 uint32_t
    Side Table 的指针地址中存储的是:object + ? + 引用计数(strongRefCounts + unownedRefCounts) + 弱引用计数(weakRefCounts)

  • 循环引用

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

上面例子中,执行完闭包后,age的值为11,说明闭包内部对变量的修改将会改变外部原始变量的值,原因是闭包能默认捕获外部变量,和OC中的Block是一样的。

接下来通过deinit来观察实例对象是否将被释放:
deinit:在swift中叫做反初始化器,实例变量将要被回收时调用deinit,观察实例对象是否被销毁。

class YYTeacher {
    var age = 12
    
    deinit {
        print("YYTeacher deinit")
    }
}

func test() {
    let t = YYTeacher()
    let closure = {
        t.age += 10
    }

    closure()
    print(t.age)
}
test()

在上面例子中,当执行完函数test后,实例对象t调用了deinit即将被销毁,说明闭包内部调用外部变量会对其做强引用操作。

那么怎样才会造成循环引用呢?

class YYTeacher {
    var age = 12
    var completionBack : (()->())?
    
    deinit {
        print("YYTeacher deinit")
    }
}

func test() {
    let t = YYTeacher()
    t.completionBack = {
        t.age += 10
    }
    
    t.completionBack!()
}

test()

执行完上面的代码会发现,并没有调用deinit,说明这里有循环引用,导致实例对象能被销毁
Swift中,通过弱引用(weak)无主引用(unowned)来解决循环引用。
weak可选类型,实例的生命周期内可为nil,实例销毁后实例对象被置为nil;
unowned非可选类型,使用前提要确保实例对象初始化后永能为nil,实例销毁后仍存储着实例对象的内存地址,若再访问则会造成野指针错误。

这里解决循环引用:

t.completionBack = {[unowned t] in
        t.age += 10
}
// 或者
t.completionBack = {[weak t] in
        t?.age += 10
}

/** 这里weak t是可选类型,可能为nil,OC中是允许给nil对象发送消息的,而在Swift中是不允许给nil对象发送消息的,所以要加上?,如果为nil则不会执行后面的.age+=1*/

上面的语法在Swift也叫做捕获列表,定义在参数列表之前,用[ ]括起来,若有多个,中间用,连接,即使省略参数名称、参数类型和返回类型也必须加上关键字in

var i = 0
var arrClosure : [()->()] = []

for _ in 1...3 {
    arrClosure.append {//[i] in
        print(i)
    }
    i += 1
}

arrClosure[0]() //3
arrClosure[1]() //3
arrClosure[2]() //3

上面例子中,三个闭包捕获的都是最后一次i的值,如果想要打印出来的值为1、2、3,三个闭包捕获的就应该是每次i的值的副本(copy),即捕获列表,闭包表达改为:

arrClosure.append {[i] in
   print(i)
}

总结:捕获列表中捕获的是变量的一个副本本地表格;对于捕获列表中的每个常量,闭包会利用周围范围内具有相同名称的常量或变量,来初始化捕获列表中定义的常量。

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

推荐阅读更多精彩内容