Swift中的内存管理

之前用Swift写了一个App,已经在App Store上架了。前两天更新了一些功能,然后用Instruments检查的时候,发现有内存泄漏问题。有些同学可能觉得奇怪,Swift不是使用ARC自动管理内存的么,怎么也会发生内存泄漏呢。是会的,但几乎都是由于操作不当造成循环引用(strong reference cycle/retain cycle)导致的。

ARC与GC

很多人分不清ARC(Automatic Reference Counting,自动引用计数)跟GC(Garbage Collection,垃圾收集)的区别。其实“引用计数法”也算是一种GC策略,只不过我们现在提到GC的时候一般是指基于“标记-整理”策略的垃圾收集器,譬如主流的JVM(Java虚拟机)几乎都是采用“标记-整理”+“分代收集”的策略来进行自动内存管理的。标记算法一般是从全局对象图的“根”出发进行可达性分析,对象的生死会被批量地标记出来,之后再在某个时间批量地释放死对象。显然,这是一种“全局+延时”的管理策略。

而与之相对的,引用计数是一种“局部+即时”的内存管理策略。它不需要全局的对象信息,一般每个被管理的对象都会跟一个引用计数器关联,这个计数器保存着当前对象被引用的次数,一旦创建一个新的引用指向该对象,引用计数就加1,每当指向该对象的某个引用失效引用计数就减1,直到引用计数为0,就立即释放该对象。使用引用计数法管理内存的语言也不止OC和Swift,还有诸如CPython之类的GC也是基于引用计数的。

早年OC是采用MRC(手动引用计数)的,当然其实现在也有人还在用,它跟ARC的主要区别在于它需要手动管理引用计数器,而ARC是自动管理的。所以其实MRC也不能让你直接释放对象的,只是控制引用罢了。

循环引用

上面解释了一下ARC的运作方式,从中不难看出这种策略的缺陷,就是循环引用问题。看下图:

reference_cycle.png

object1和object2之间形成了循环引用,它们的引用计数始终为1,始终不会被释放,这就造成了内存泄漏。“标记-整理”策略并不会出现这种问题,因为哪怕两个对象相互引用,但只要它们和“根”对象失去了联系,照样会被标记为死对象,然后在合适的时间被释放。

实例分析

接下来看一个稍微复杂一点的实例,分析一下出现循环引用的原因然后给出解决方法。

class SimpleRefreshCtrl: UIRefreshControl {
    typealias Action = () -> ()
    
    var action: Action!
    
    init(action: Action) {
        super.init()
        
        tintColor = UIColor.navigationBarColor()
        self.action = action
        self.addTarget(self, action: "refresh", forControlEvents: UIControlEvents.ValueChanged)
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    func refresh() {
        self.action()
        delay(seconds: 1) {
            self.endRefreshing()
        }
    }
}

这是我自己封装的一个下拉刷新控制器,它继承自UIRefreshControl,可以在UITableViewController中直接使用,如下:

class HouseTableCtrl: UITableViewController {
    //...
    func getPageData() {
        getListFromApi(urlString) { json, nextLink in
            self.houseData = json
            self.page = nextLink
        }
    }
    override func viewDidLoad() {
        super.viewDidLoad()

        let refreshCtrl = SimpleRefreshCtrl(action: getPageData)
        self.refreshControl = refreshCtrl
        //...
    }
}

这样,当你下拉列表的时候,旋转的菊花就会出现旋转1秒,同时执行getPageData方法,刷新页面数据。

但是这里出现了循环引用问题,我们来看看它是怎么发生的。在getPageData方法中我调用了一个全局函数getListFromApi,而这个全局函数需要一个闭包作为参数,而这个闭包又捕获了当前对象的两个属性,也就持有了当前对象的引用。到这里为止并没有什么问题,虽然闭包捕获外部变量从而持有外部对象的引用经常是造成循环引用的一大元凶,但在这里,该闭包是个匿名闭包,我们的HouseTableCtrl对象并没有持有该闭包的引用,所以问题并不是出在这里。

接下来,在初始化SimpleRefreshCtrl对象的时候,getPageData作为参数被传递了过去,并被赋值给SimpleRefreshCtrl的实例属性action。注意,getPageData是在HouseTableCtrl中定义的一个实例方法,是跟当前的HouseTableCtrl对象关联的,作为参数传递过去的实际上是self.getPageData。如此一来,SimpleRefreshCtrl对象就持有了当前HouseTableCtrl对象的引用。然后接下来这一句self.refreshControl = refreshCtrl,持有HouseTableCtrl对象引用的SimpleRefreshCtrl对象被赋值给了HouseTableCtrl的实例属性refreshControl,于是HouseTableCtrl对象也持有了SimpleRefreshCtrl对象的引用。这就造成了循环引用。

要如何打破僵局呢,其实也很简单,使用weak或者unowned就行了:

//refreshCtrl指向的对象只持有当前HouseTableCtrl对象的一个弱引用
let refreshCtrl = SimpleRefreshCtrl { [weak self] in
    self?.getPageData()
}
//这一句强引用
self.refreshControl = refreshCtrl

这样SimpleRefreshCtrl对象就只是持有当前HouseTableCtrl对象的一个弱引用,弱引用是不算在HouseTableCtrl对象的引用计数中的,也就是说当没有其他引用指向HouseTableCtrl对象时,HouseTableCtrl对象能被正常释放,一旦HouseTableCtrl对象被释放了,那SimpleRefreshCtrl对象也就能被正常释放了:

weak_reference.png

至于weakunowned该用哪个么,看情况了,weak修饰的属性或变量是一个optional类型,也就是说是可以为nil的。而unowned则是修饰一个nonoptional,是不能为nil的,一旦这个属性或变量指向的对象被释放了(这是有可能发生的,因为unowned引用也是不算在引用计数中的,如果除了unowned引用外没有其他引用指向那个对象,那它将被释放),而你还想使用该对象的话,将会触发runtime error,程序也就crash了。所以个人来说,我是更推荐使用weak的。

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

推荐阅读更多精彩内容

  • 作为一门现代的高级编程语言,Swift代替我们进行了对象的创建和销毁等相关的内存管理。它使用了一个优雅的技术,叫做...
    Maru阅读 2,036评论 4 17
  • Swift 作为一门现代的高级语言,自然少不了自动内存管理。我们知道,JAVA 中的内存管理是通过垃圾回收完成的。...
    桥下的阿卡迪亚阅读 3,005评论 0 14
  • 29.理解引用计数 Objective-C语言使用引用计数来管理内存,也就是说,每个对象都有个可以递增或递减的计数...
    Code_Ninja阅读 1,426评论 1 3
  • 内存管理是程序在运行时分配内存、使用内存,并在程序完成时释放内存的过程。在Objective-C中,也被看作是在众...
    蹲瓜阅读 2,843评论 1 8
  • 内存管理 简述OC中内存管理机制。与retain配对使用的方法是dealloc还是release,为什么?需要与a...
    丶逐渐阅读 1,870评论 1 16