Swift 内存管理

  • 跟OC一样,Swift也是采用基于引用计数的ARC内存管理方案(针对堆空间)

  • Swift的ARC中有三种引用

  1. 强应用(strong reference):默认情况下,引用都是强引用
  1. 弱引用(weak reference):通过weak定义弱引用
  • 必须是可选类型的var,因为实例销毁后,ARC会自动将弱引用设置为nil
  • ARC自动给弱引用设置nil时,不会触发属性观察器
  1. 无主引用(unowned reference):通过unowned定义无主引用
  • 不会产生强引用,实例销毁后仍然存储着实例的内存地址(类似OC中的unsafe_unretained
  • 试图在实例销毁后访问无主引用,会产生运行时错误(野指针)

weak/unowned的使用限制

  • weak、unowned只能用在类实例上面

因为一般只有类实例放堆空间,结构体、枚举一般都是不放在堆空间的

class Cat {}
protocol Actions :AnyObject {}

weak var c0: Cat?
weak var c1: AnyObject?
weak var c3: Actions?

unowned var c4: Cat?
unowned var c5: AnyObject?
unowned var c6: Actions?

上面代码编译都是没问题的。AnyObject是可以代表任意类类型,协议Actions也是可以的,因为它后面是Actions :AnyObject,意思就是它的协议只能被类类型遵守。
若协议Actions后面的冒号去掉,c3c6是编译不通过的,因为此协议有可能被结构体、枚举遵守,而weak、unowned只能用在类实例上面,所以编译器提前抛出错误,Swift是强安全语言。


Autoreleasepool

在Swift中,Autoreleasepool是保留的,变成了一个全局的函数:

public func autoreleasepool<Result>(invoking body: () throws -> Result) rethrows -> Result

使用:

class Cat {
    var name: String?
    init(name:String?) {
        self.name = name;
    }
    func eat() {}
}
autoreleasepool {
    let cat = Cat(name: "zhangsan")
    cat.eat()
}

内存开销较大的场景(比如数千对经纬度数据在地图上绘制公交路线轨迹),可以使用自动释放池。


循环引用(Reference Cycle)

  • weak/unowned都能解决循环引用的问题,unowned要比weak少一些性能消耗

weak在实例销毁的时候又设置了一遍weak应用为nil,所以,性能上多了一丢丢消耗

  1. 在生命周期中可能会变为nil的对象,使用weak
  2. 初始化赋值后再也不会改变为nil的对象,使用unowned

闭包的循环引用

  • 闭包表达式默认回对用到的外层对象产生额外的强引用(对外层对象进行了retain操作)
class Person {
    var fn:(() -> ())?
    func run() {
        print("run")
    }
    deinit {
        print("deinit")
    }
}
func test() {
    let p = Person()
    p.fn = {
        p.run()
    }
}
print(1)
test()
print(2)

运行后,打印结果只有1跟2,没有deinit,说明p对象一直没有被销毁,仔细看,问题出在这里:

p.fn = {
        p.run()
    }

对象p里的fn方法强引用了闭包,而闭包里也强引用了对象p,两者形成循环引用,对象p也就无法释放销毁了。
我们在p.fn处打断点,进入汇编,看看是否有强引用(retain):

强应用下引用计数器变化

我们注释掉上面代码里的p.fn = { p.run() },再看汇编:
注释后的引用计数

可以看出,p.fn = { p.run() }里,闭包对p对象进行了强引用也就是retain操作,构成了引用计数始终为1的情况,无法释放对象。

  • 在闭包表达式的捕获列表声明weakunowned引用,解决循环引用问题
func test() {
    let p = Person()
    p.fn = {
        [weak p] in
        p?.run()
    }
}

func test() {
    let p = Person()
    p.fn = {
        [unowned p] in
        p.run()
    }
}

func test() {
    let p:Person? = Person()
    p?.fn = {
        [weak p] in
        p?.run()
    }
}

func test() {
    let p:Person? = Person()
    p?.fn = {
        [unowned p] in
        p?.run()
    }
}

注意:weak弱引用必须是可选类型,所以,对象p后面跟上?
若是unowned修饰pp后面不用跟?,因为p本身就是非可选类型,unowned默认情况下也就是非可选类型,是跟着p走的

class Person {
    var fn:((Int) -> ())?
    func run() {
        print("run")
    }
    deinit {
        print("deinit")
    }
}
func test() {
    let p = Person()
    p.fn = {
        [weak wp = p](age) in
        wp?.run()
    }
}

[weak p]是捕获列表,(age)是参数列表,捕获列表一般是写在参数列表前面的,in后面的就是函数体。

  • 如果想在定义闭包属性的同时引用self,这个闭包必须是lazy的(因为在实例初始化完毕之后才能引用self
class Person {
    lazy var fn:(() -> ()) = {
        self.run()
    }
    func run() {
        print("run")
    }
    deinit {
        print("deinit")
    }
}
func test() {
    let p = Person()
}

这段代码,会打印deinit,说明对象p释放了。
为什么呢?按说对象p有个强引用fn引用了闭包表达式,闭包表达式里也强应用了self,两者形成循环应用,无法释放对象p

因为fnlazy修饰的,也就是说,在未调用p.fn的时候是没有值的,也就说它后面的闭包表达不存在,自然就无法引用self,也就不能造成循环引用。
当第一次调用p.fn()后,才会触发fn的初始化,创建闭包表达式赋值给fn,这里就形成了循环引用。
解决循环引用:

lazy var fn:(() -> ()) = {
        [weak weakSelf = self] in
        weakSelf?.run()
    }

lazy var fn:(() -> ()) = {
        [unowned weakSelf = self] in
        weakSelf.run()
    }

一般用weak,因为weakunowned安全

  • 如果lazy属性是闭包调用的结果,则不用考虑循环引用的问题(因为闭包调用后,闭包的生命周期就结束了)
class Person {
    var age: Int = 10
    lazy var getAge: Int = {
        self.age
    }()
    deinit {
        print("deinit")
    }
}
func test() {
    let p = Person()
    print(p.getAge)
}
test()

打印结果:10 deinit


内存访问冲突(Conflicting Access to Memory)

  • 内存访问冲突会在两个访问满足下面条件时发生:
  1. 至少一个是写入操作
  2. 它们访问的是同一块内存
  3. 它们的访问时间重叠(比如在同一个函数内)
//不存在内存访问冲突
func plus(_ num: inout Int) -> Int {
    num + 1
}
var number = 1
number = plus(&number)

//存在内存访问冲突
var step = 1
func increment(_ num: inout Int) {
    //此处编译没问题,但运行报错
    //Simultaneous accesses to 0x100008178, but modification requires exclusive access
    num += step
}
increment(&step)

increment函数内的num += step产生内存冲突,因为num虽然是形参,但外面传的值还是step的内存地址,+=这里就造成了同一时间对同一份内存进行既读又写的操作,所以造成内存冲突。
上例代码解决内存冲突:

var step = 1
func increment(_ num: inout Int) {
    num += step
}
var temp = step
increment(&temp)
step = temp

下面代码也是存在内存冲突:

func sum(_ x: inout Int, _ y: inout Int) {
    x = x + y
}

struct AA {
    var x: Int = 0
    var y: Int = 0
    mutating func add(a: inout AA) {
        sum(&a.x, &a.y)
    }
}
var aa = AA(x: 1, y: 2)
var bb = AA(x: 1, y: 3)
sum(&aa.x, &aa.y) //编译没问题,但内存冲突,运行报错Simultaneous accesses to 0x100008190, but modification requires exclusive access.

这句语句运行报错,是因为aa.x aa.y虽然是两个不同的变量,内存地址也不一样,但是它们是一个整体,都在结构体实例aa的内存空间内,访问它们两个也就是同时访问同一个结构体内存。
所以上面代码存在内存访问冲突。
元组也一样,元组内的不同变量访问,其实也是访问同一块元组内存,只是它们内部变量的地址不同而已,外部存储变量的元组内存空间还是同一份。

  • 如果下面条件可以满足,说明重叠访问结构体的属性是安全的
  1. 只访问实例存储属性,不是计算属性或者类属性
  2. 结构体是局部变量而非全局变量
  3. 结构体要么没有被闭包捕获要么只被非逃逸闭包捕获
//没问题
func test() {
    var aa = AA(x: 1, y: 2)
    sum(&aa.x, &aa.y)
}

指针

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