[译]Swift中的weak self和unowned self

在找关于weak和unowned方面的知识,看到的一篇文章。

原文来自Weak self and unowned self explained in Swift

对于我们大多数人来说,Swift中的weak self和unowned self是很难理解的。尽管自动引用计数(ARC)已经为我们解决了很多问题,但是我们还是需要处理引用当我们不使用值类型的时候。

在大多数情况下,使用可选类型的时候会默认添加weak,但其实并不需要这样做

什么是ARC,retain和release

我们需要这些基础知识来完全理解weak self和unowned self是做什么的。理解内存管理,最好的办法就是读Swift的官方文档Automatic Reference Counting in Swift documentation.

在没有ARC的时候,我们必须手动管理内存和引用。这会造成许多难以解决的bug和各种头痛的问题。当一个新的实例retain一个对象时引用计数会增加,当引用被released时引用计数会减少。一旦一个对象的引用不存在时,内存会被释放,这意味着这个对象再也不被需要了。

在Swift中,我们需要在代码中必要的时候用到weak self和unowned self。不用weak和unowned基本上意味着需要对某个对象保持强引用(strong),并且不然其引用计数变为0。不正确的使用这些关键词可能会导致内存泄漏或者循环引用。

引用计数仅仅对类的实例有效,对于结构体和枚举这些值类型来说,并不需要通过引用来存储和传递

什么时候用weak self ?

首先,当可选类型的引用被释放时,其会被ARC自动置为nil,这种情况下通常会使用弱引用。下面两个类会帮助我们理解弱引用。

class Blog {
    let name: String
    let url: URL
    var owner: Blogger?

    init(name: String, url: URL) { self.name = name; self.url = url }

    deinit {
        print("Blog \(name) is being deinitialized")
    }
}

class Blogger {
    let name: String
    var blog: Blog?

    init(name: String) { self.name = name }

    deinit {
        print("Blogger \(name) is being deinitialized")
    }
}

一旦这些类的实例被释放,会打印出一条信息。在随后的例子中,我们定义了两个可选类型,并把他们置为nil,一些人可能会认为会打印出信息,但实际上并不会发生:

var blog: Blog? = Blog(name: "SwiftLee", url: URL(string: "www.avanderlee.com")!)
var blogger: Blogger? = Blogger(name: "Antoine van der Lee")

blog!.owner = blogger
blogger!.blog = blog

blog = nil
blogger = nil

// Nothing is printed

这是因为循环引用,blog强引用了blogger,blogger强引用了blog,互相释放不了,造成了无限循环。

所以,需要引入弱引用,在这个例子中,只需要一个弱引用来打破这个循环,例如我们可以设置blog对blogger为弱引用:

class Blog {
    let name: String
    let url: URL
    weak var owner: Blogger?

    init(name: String, url: URL) { self.name = name; self.url = url }

    deinit {
        print("Blog \(name) is being deinitialized")
    }
}

class Blogger {
    let name: String
    var blog: Blog?

    init(name: String) { self.name = name }

    deinit {
        print("Blogger \(name) is being deinitialized")
    }
}

var blog: Blog? = Blog(name: "SwiftLee", url: URL(string: "www.avanderlee.com")!)
var blogger: Blogger? = Blogger(name: "Antoine van der Lee")

blog!.owner = blogger
blogger!.blog = blog

blog = nil
blogger = nil

// Blogger Antoine van der Lee is being deinitialized
// Blog SwiftLee is being deinitialized
那weak self呢?

我知道,这不是一个关于weak self的例子,但是它可以解释。

我们很多人,为了避免闭包中的循环引用问题会一直加上weak self。然而,只有在self和闭包相互引用的时候才需要weak self。很多情况下,其实并不需要weak self。

为了说明这个问题,我们为Blog引入了publish方法,通过手动添加delay来模拟网络请求。

struct Post {
    let title: String
    var isPublished: Bool = false

    init(title: String) { self.title = title }
}

class Blog {
    let name: String
    let url: URL
    weak var owner: Blogger?

    var publishedPosts: [Post] = []

    init(name: String, url: URL) { self.name = name; self.url = url }

    deinit {
        print("Blog \(name) is being deinitialized")
    }

    func publish(post: Post) {
        /// Faking a network request with this delay:
        DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
            self.publishedPosts.append(post)
            print("Published post count is now: \(self.publishedPosts.count)")
        }
    }
}

var blog: Blog? = Blog(name: "SwiftLee", url: URL(string: "www.avanderlee.com")!)
var blogger: Blogger? = Blogger(name: "Antoine van der Lee")

blog!.owner = blogger
blogger!.blog = blog

blog!.publish(post: Post(title: "Explaining weak and unowned self"))
blog = nil
blogger = nil

会打印出以下信息:

// Blogger Antoine van der Lee is being deinitialized
// Published post count is now: 1
// Blog SwiftLee is being deinitialized

可以看出在blog释放前,完成了网络请求。强引用让我们完成了publish并且把这个post存储在published post中。

因此,如果在执行闭包后立即对引用实例进行处理,请确保不要使用弱自我。

弱引用和循环引用

当self引用闭包,并且闭包会捕获self时会发生循环引用。如果我们有一个闭包变量onPublish,就会发生:

class Blog {
    let name: String
    let url: URL
    weak var owner: Blogger?

    var publishedPosts: [Post] = []
    var onPublish: ((_ post: Post) -> Void)?

    init(name: String, url: URL) {
        self.name = name
        self.url = url

        // Adding a closure instead to handle published posts
        onPublish = { post in
            self.publishedPosts.append(post)
            print("Published post count is now: \(self.publishedPosts.count)")
        }
    }

    deinit {
        print("Blog \(name) is being deinitialized")
    }

    func publish(post: Post) {
        /// Faking a network request with this delay:
        DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
            self.onPublish?(post)
        }
    }
}

var blog: Blog? = Blog(name: "SwiftLee", url: URL(string: "www.avanderlee.com")!)
var blogger: Blogger? = Blogger(name: "Antoine van der Lee")

blog!.owner = blogger
blogger!.blog = blog

blog!.publish(post: Post(title: "Explaining weak and unowned self"))
blog = nil
blogger = nil

结果会打印出以下信息:

// Blogger Antoine van der Lee is being deinitialized
// Published post count is now: 1

虽然我们看到网络请求发送成功了,但是并没有看到blog被释放。这是因为存在循环引用,内存并没有被释放。

通过在obPublish闭包中添加blog的弱引用,可以解决循环引用的问题:

onPublish = { [weak self] post in
    self?.publishedPosts.append(post)
    print("Published post count is now: \(self?.publishedPosts.count)")
}

结果会打印以下信息:

// Blogger Antoine van der Lee is being deinitialized
// Published post count is now: Optional(1)
// Blog SwiftLee is being deinitialized

数据存储在本地,并且所有的实例都被释放,不存在循环引用了!

最后,总结本小节,很容易知道:

当ARC将弱引用设置为nil时,不会调用属性观察器

什么时候用unowned self ?

不像弱引用,当使用unowned时,引用不会变成可选的。但是weak和unowned都不会建立强引用。

引用Apple官方文档:

Use a weak reference whenever it is valid for that reference to become nil at some point during its lifetime. Conversely, use an unowned reference when you know that the reference will never be nil once it has been set during initialization.
当其他的实例有更短的生命周期时,使用弱引用,也就是说,当其他实例析构在先时。相比之下,当其他实例有相同的或者更长生命周期时,请使用无主引用。

总之,当使用unowned时要非常小心。这可能会让你使用一个已经不存在的实例,从而造成crash。相比weak,用unowned唯一的好处就是可以不用处理可选类型。所以在很多情况下使用weak总是安全的。

我们为什么不需要在值类型(例如结构体)中使用这些?

在Swift中,有值类型和引用类型。这已经说得够清楚了,引用类型需要你考虑其引用。这意味这你需要管理这些关系,例如:strong,weak,unowned。而值类型是保持了一个data的copy,一个单独的实例。这意味着在多线程环境下完全不需要用弱引用,因为没有引用,而是我们正在使用的唯一copy。

在闭包中,weak和unowned仅仅用于修饰self吗?

不,当然不是。只要是引用类型,都可以。所以,这样也是可行的:

download(imageURL, completion: { [weak imageViewController] result in
    // ...
})

并且,你甚至可以引用多个实例,因为它基本上是一个数组:

download(imageURL, completion: { [weak imageViewController, weak imageFinalizer] result in
    // ...
})

讨论

总的来说,这是一个让人头晕的问题。最好通过阅读Swift官方文档来开始,这会讨论的更加深入。另外,如果你不确定,优先使用weak,它可以避免烦人的bug。此外,如果需要在闭包内部完成工作,请不要使用weak并确保您的代码正在执行。

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

推荐阅读更多精彩内容