Swift进阶-内存管理

Swift进阶-类与结构体
Swift-函数派发
Swift进阶-属性
Swift进阶-指针
Swift进阶-内存管理
Swift进阶-TargetClassMetadata和TargetStructMetadata数据结构源码分析
Swift进阶-Mirror解析
Swift进阶-闭包
Swift进阶-协议
Swift进阶-泛型
Swift进阶-String源码解析
Swift进阶-Array源码解析

类与结构体章节讲到:通过汇编调试和swift源码知道我们的 纯swift类的对象 Teacher() 的内存分配: __allocating_init -> _swift_allocObject_ -> swift_slowAlloc -> malloc

_swift_allocObject_

其中_swift_allocObject_里面创建一个 HeapObject对象并将其返回了,所以这个HeapObject就是我们实际创建的对象,来看看其初始化函数:

  constexpr HeapObject(HeapMetadata const *newMetadata) 
    : metadata(newMetadata)
    , refCounts(InlineRefCounts::Initialized)
  { }

了解上面内容后,来看看创建一个对象,输出它的地址,格式化输出它的内容:

class Teacher {
    var name: String
    init(name: String) {
        self.name = name
    }
}

class ViewController: UIViewController{
    override func viewDidLoad() {
        super.viewDidLoad()
        let teacher = Teacher(name: "陈老师")
        // 通过Unmanaged指定内存管理,类似于OC与CF的交互方式(所有权的转换 __bridge)
        // passUnretained 不增加引用计数,即不需要获取所有权
        // passRetained 增加引用技术,即需要获取所有权
        // toOpaque 不透明的指针
        let ptr = Unmanaged.passUnretained(teacher as AnyObject).toOpaque()
        print(ptr)
        print("end")  // 断点在这里
    }
}
输出

通过控制台输出后得到上图,而x/8g输出的前16个字节就是matadatarefcounts,这里的refcounts就是本章节研究的内容。

ps: 注意此时要用Unmanaged.passUnretained去输出teacher的指针,而不能在控制台上 po teacher!因为 po teacher 会对其进行引用,就不是上面这个结果了,来看看:

po对象造成引用

本章节围绕一个问题探究引用计数:那为什么一次引用后会变成这个数值0x0000000200000002

引用后打印

1.引用计数的实质

从源码中HeapObject.h的找到refcounts的定义

refcounts 的定义
RefCounts

RefCounts其实就是对我们当前引用计数的包装类,而引用计数的具体类型取决于传递给RefCounts的参数 -> InlineRefCountBits
找到InlineRefCountBits的定义:

typedef RefCountBitsT<RefCountIsInline> InlineRefCountBits;

它同样是一个模板类RefCountBitsT

RefCountIsInline

RefCountIsInline传递的是true/false,再去看看模板类RefCountBitsT

RefCountBitsT

RefCountBitsT就是我们真实操作引用计数的类,它只有一个属性 bits,它的类型定义是 RefCountBitsInt

image.png

引用计数的实质: bits其实就是一个uint64位的位域信息,它存储了就是运行周期相关的引用计数(Swift和OC都是一样的64位的位域信息)。

1.1 提问:当创建一个引用对象的时候,它的引用计数是多少?

来看看源码中的创建对象函数_swift_allocObject_

_swift_allocObject_
HeapObject定义

引用计数的初始化赋值,传递了一个Initialized,点进去看看Initialized定义,在RefCount.h找到它是一个枚举:

可以看到源码的注释:新对象的Refcount为1。
RefCountBits就是模板类RefCountBitsT(它有一个bits属性)

找到RefCountBitsT初始化函数,很快能定位到传递的参数strongExtraCount: 0unownedCount: 1是怎么操作bits的:

RefCountBitsT初始化函数

0左移33位还是0,1左移1位是2,所以refCounts是0x2。
当声明一个引用对象,该对象没有被引用的时候,refCounts是0x0000000000000002:

let teacher = Teacher(name: "陈老师")
let ptr = Unmanaged.passUnretained(teacher as AnyObject).toOpaque()
print(ptr)
print("end")
此时格式化输出ptr的地址

当teacher被引用的时候,其refCounts是0x0000000200000002:

let teacher = Teacher(name: "陈老师")
let ptr = Unmanaged.passUnretained(teacher as AnyObject).toOpaque()
print(ptr)
print("end")
let t = teacher
print("end")

1左移33位,1左移1位是2,所以refCounts是0x0000000200000002。

0x0000000200000002

如果再被引用一次,其refCounts是0x0000000400000002
2左移33位,1左移1位是2,所以refCounts是0x0000000400000002。

0x0000000400000002

结论:强引用计数无主引用计数是通过位移的方式,存储在这64位的信息当中。

引用计数64位存储
1.2 提问:强引用是如何添加的呢?

从swift源码中找到HeapObject.cpp找到_swift_retain_函数实现:

_swift_retain_
increment

引用计数+1:

incrementStrongExtraRefCount

引用计数-1:

decrementStrongExtraRefCount

2.循环引用问题

经典案例:

class WJTeacher { 
    var age: Int = 18 
    var name: String = "林老师" 
    var subject: WJSubject?
}
class WJSubject { 
    var subjectName: String 
    var subjectTeacher: WJTeacher 
    init(_ subjectName: String, _ subjectTeacher: WJTeacher) {
        self.subjectName = subjectName 
        self.subjectTeacher = subjectTeacher 
    } 
}

class ViewController: UIViewController{
    override func viewDidLoad() {
        super.viewDidLoad()
        let teacher = WJTeacher()
        let subject = WJSubject.init("Swift进阶", teacher)
        teacher.subject = subject
    }
}

上面这段代码两个对象相互引用导致无法释放。
Swift 提供了两种办法⽤来解决你在使 ⽤类的属性时所遇到的循环强引⽤问题:弱引⽤( weak reference )⽆主引⽤( unowned reference )

weak弱引用解决上面的问题:

class WJTeacher {
    var age: Int = 18
    var name: String = "Steven"
    weak var subject: WJSubject?  // 弱引用subject
}
class WJSubject {
    var subjectName: String
    var subjectTeacher: WJTeacher
    init(_ subjectName: String, _ subjectTeacher: WJTeacher) {
        self.subjectName = subjectName
        self.subjectTeacher = subjectTeacher
    }
}

或者使用unowned无主引用解决上面的问题

class WJTeacher {
    var age: Int = 18
    var name: String = "Steven"
    var subject: WJSubject?
}
class WJSubject {
    var subjectName: String
    unowned var subjectTeacher: WJTeacher  // 无主引用subjectTeacher
    init(_ subjectName: String, _ subjectTeacher: WJTeacher) {
        self.subjectName = subjectName
        self.subjectTeacher = subjectTeacher
    }
}

弱引用和无主引用有什么区别呢?

2.1 weak弱引用

Alaways Show Disassembly打开编调试,看看weak修饰对象在创建时调用流程

weak var t = WJTeacher()
print(t)  // 断点打在这里
汇编调试weak修饰的WJTeacher对象创建

类与结构体章节看过__allocating_init()的调用过程,没有看到相关弱引用的内容;接下来又调用了swift_weakInit函数;找到swift源码:

swift_weakInit
nativeInit
formWeakReference

来看看allocateSideTable做了啥事儿

allocateSideTable

来找到side: HeapObjectSideTableEntry到底是啥玩意儿,全局搜索了一下找到了源码中有这么一段注释,直接给出了结论:

HeapObjectSideTableEntry说明

在swift里面存在着两种引用计数,一种是强引用包含的是strong RC + unowned RC + flags;一种是弱引用包含的是strong RC + unowned RC + weak RC + flags

来一下HeapObjectSideTableEntry的源码:

HeapObjectSideTableEntry
SideTableRefCounts
image.png

可以发现弱引用与强引用共用一个模板类RefCounts 其实就是它:RefCountBitsT
来看看引用计数操作模板类RefCountBitsT通过弱引用的方式创建的源码:

weak的RefCountBitsT初始化

weak方式创建RefCountBitsT,首先是将散列表右移3位,然后将62位和63位标记为1。

通过逆反操作验证上面源码分析的的准确性:

weak引用后refCounts那8位返回的是一个内存地址(而不是引用计数64位域)

image.png

1.我们要还原,先将62位63位的标志位设置为0得到的结果:

还原62位63位

2.还要将这个结果向左移动3位:

左移动3位还原操作

将这个结果在控制台上格式化输出:

weak引用一个对象本质上就是创建一个散列表

2.2 unownedweak

上面介绍了强引用计数和无主引用计数是通过位移的方式,存储在这64位的信息当中的。

  • unowned不允许被引用的对象有nil的可能,无需要新建/维护散列表,直接可以通过64位的位域操作即可进行引用计数。
  • weak它所引用的对象允许为nil,它需要新建/维护散列表

共同点:

  • 引用对象的自动引用计数都不会加1,不会造成对引用对象的强引用。

解决block引用计数问题:

class WJTeacher {
    var age: Int = 18
    var name: String = "Steven"
    
    var closure: (()->())?
    
    deinit {
        print("WJTeacher 消失")
    }
}

func test() {
    let t = WJTeacher()
    var closure = {
        t.age = 19
    }
    t.closure = closure
    closure()
}

此时循环引用:t->closure->t
修改后:

class WJTeacher {
    var age: Int = 18
    var name: String = "Steven"
    
    var closure: (()->())?
    
    deinit {
        print("WJTeacher 消失")
    }
}

func test() {
    let t = WJTeacher()
    var closure = { [weak t] in
        t?.age = 19
    }
    t.closure = closure
    closure()
}

// 或者
/**

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

推荐阅读更多精彩内容