美文网首页
Swift 循环引用

Swift 循环引用

作者: sankun | 来源:发表于2019-06-21 14:38 被阅读0次

    简介

    Swift 使用 Automatic Reference Counting (ARC) 管理应用内存的使用,ARC自动释放那些不在使用的对象,然而在一些场景下ARC需要更多的对象之间的引用信息来管理内存.

    ARC 如何工作

    每当你创建一个实例instance对象时,ARC分配一块儿内存用来存储instance对象信息包括对象类型,以及属性的值.
    此外,当instance对象不在使用的时候,ARC释放instance对象所占的内存,以便释放的内存可在利用.然而,
    instance对象被ARC释放后,将不在允许访问该instance对象的属性或者方法,如果你尝试访问,结果就会使APP crash
    为了确保正在使用的instance对象,不被释放. ARC追踪分配给instance对象的属性 property 常量 变量 即 引用计数.只要instance对象被引用着,就不会被释放.

    ARC 的作用
    下面一个Person类对象 有一个name 常量属性 一个初始化方法并赋值给name属性,一个析构方法

    class Person {
        let name: String
        init(name: String) {
            self.name = name
            print("\(name) is being initialized")
        }
        deinit {
            print("\(name) is being deinitialized")
        }
    }
    

    定义三个Person? 类型的变量 reference1 reference2 reference3默认值为nil

    var reference1: Person?
    var reference2: Person?
    var reference3: Person?
    

    创建Person类的实例对象 reference1 强引用Person instance

    reference1 = Person(name: "John Appleseed")
    // Prints "John Appleseed is being initialized"
    

    reference2 reference3 强引用Person instance

    reference2 = reference1
    reference3 = reference1
    

    通过赋值nil给 reference1 reference2 使得Person instance引用变为1,ARC将不会释放 Person instance

    reference1 = nil
    reference2 = nil
    

    当最后一个强引用设置为nil的时候,Person instance执行了析构函数

    reference3 = nil
    // Prints "John Appleseed is being deinitialized"
    

    对象间的循环引用

    在上面的例子🌰中,ARC能过追踪Person instance的引用计数,进行内存管理. 然而,我们很容易写出instance对象不存在强引用情况的代码,发生在两个class instances直接彼此强引用.(各位对方的属性)称为引用循环

    class Person {
        let name: String
        init(name: String) { self.name = name }
        var apartment: Apartment?
        deinit { print("\(name) is being deinitialized") }
    }
    
    class Apartment {
        let unit: String
        init(unit: String) { self.unit = unit }
        var tenant: Person?
        deinit { print("Apartment \(unit) is being deinitialized") }
    }
    

    定义Person Apartment变量 并初始化

    var john: Person?
    var unit4A: Apartment?
    
    john = Person(name: "John Appleseed")
    unit4A = Apartment(unit: "4A")
    
    初始化后 john unit4引用关系图

    接下来给person 入住公寓, Apartment记录person

    // 由于john是可选型, 访问的时候需要解包
    john!.apartment = unit4A
    unit4A!.tenant = john
    
    Person 入住后 Apartment记录后.png
    john = nil
    unit4A = nil
    

    可以看到Person跟Apartment之间的强引用环,因此,当你打破john对Person 跟unit4对Apartment的强引用时,Person和Apartment之间的闭环仍然存在,此时john unit4不会被ARC回收.(造成内存泄漏)

    解决

    Swift提供了两种方式 在属性 类声明前加 weak或者 unowned ,weak或者 unowned引用允许一个instance非强引用令一个instance,来避免出现强循环.
    那什么时候用weak什么时候用unowned呢? weak允许使用在生命周期较短的那一方,unowned稍后再讲. so 在Person 跟Apartment这个场景中, Apartment 的生命周期肯定是比Person要长的.

    class Person {
        let name: String
        init(name: String) { self.name = name }
        var apartment: Apartment?
        deinit { print("\(name) is being deinitialized") }
    }
    
    class Apartment {
        let unit: String
        init(unit: String) { self.unit = unit }
        weak var tenant: Person?
        deinit { print("Apartment \(unit) is being deinitialized") }
    }
    

    一样的初始化 并彼此关联

    var john: Person?
    var unit4A: Apartment?
    
    john = Person(name: "John Appleseed")
    unit4A = Apartment(unit: "4A")
    
    john!.apartment = unit4A
    unit4A!.tenant = john
    
    weak 修饰后的引用关系图.png

    可以看到john 强引用Apartment Apartment弱引用john

    john = nil
    // Prints "John Appleseed is being deinitialized"
    
    john打破强引用后

    同样

    unit4A = nil
    // Prints "Apartment 4A is being deinitialized"
    
    unit4打破强引用后

    In systems that use garbage collection, weak pointers are sometimes used to implement a simple caching mechanism because objects with no strong references are deallocated only when memory pressure triggers garbage collection. However, with ARC, values are deallocated as soon as their last strong reference is removed, making weak references unsuitable for such a purpose.
    在使用垃圾收集的系统中,弱指针有时用于实现简单的缓存机制,因为只有在内存压力触发垃圾收集时才释放没有强引用的对象。然而,使用ARC,值在其最后一个强引用被删除后立即被释放,这使得弱引用不适合用于此目的。

    unowned
    weak一样,unowned也不会对它引用的实例保持强控制。但是,与weak不同的是,当其他实例具有相同的生命周期期或更长的生命周期时,将使用unowned。通过在属性或变量声明前放置unowned关键字,可以指示一个unowned引用。

    一个unowned应该总是有一个值。因此,ARC从不将unowned引用的值设置为nil,这意味着unowned引用是使用非可选类型定义的。

    只有在确定引用始终引用未释放的实例时,才使用unowned。如果在释放实例之后尝试访问一个unowned的值,将会得到一个运行时错误。

    接下来的例子🌰中,Customer客户 CreditCard信用卡 每个人都有可能有一张信用卡,也有可能没有信用卡. 但是一张信用卡必定有一个对应的客户. 那么Customer跟CreditCard之间必定存在一个强引用循环.此时使用unowned避免循环引用

    class Customer {
        let name: String
        var card: CreditCard?
        init(name: String) {
            self.name = name
        }
        deinit { print("\(name) is being deinitialized") }
    }
    
    class CreditCard {
        let number: UInt64
        unowned let customer: Customer
        init(number: UInt64, customer: Customer) {
            self.number = number
            self.customer = customer
        }
        deinit { print("Card #\(number) is being deinitialized") }
    }
    

    创建一个Customer Instance 并设置CreditCard

    john = Customer(name: "John Appleseed")
    john!.card = CreditCard(number: 1234_5678_9012_3456, customer: john!)
    
    john CreditCard创建后引用关系

    因为没有对Customer实例的更强引用,所以john被释放了。在此之后,就不再有对CreditCard实例的强引用,它也被释放

    john = nil
    // Prints "John Appleseed is being deinitialized"
    // Prints "Card #1234567890123456 is being deinitialized"
    

    上面的示例展示了如何使用安全的unowned引用。Swift还为需要禁用运行时安全检查(例如出于性能原因)的情况提供了不安全的unowned引用。与所有不安全的操作一样,您将负责检查代码的安全性。通过编写unowned(不安全)来指示一个不安全的unowned引用。如果您试图在它引用的实例被释放后访问一个不安全的unowned引用,那么您的程序将尝试访问实例曾经所在的内存位置,这是一个不安全的操作。

    Unowned和隐式展开的可选属性
    上面关于weakunowned引用的示例涵盖了两种更常见的场景,需要打破强引用循环。

    Person和Apartment的例子显示了这样一种情况,两个属性都被允许为nil,有可能导致强引用循环。此场景最好使用弱引用来解决。

    Customer和CreditCard示例显示了一种情况,其中一个属性允许为nil,而另一个属性不能为nil,这两种属性都有可能导致强引用循环。此场景最好使用unowned引用来解决。

    然而,还有第三种情况,在这种情况下,两个属性都应该始终有一个值,并且一旦初始化完成,任何一个属性都不应该为nil。在这个场景中,将一个类上的unowned属性与另一个类上的隐式展开的可选属性相结合是很有用的。

    这使得初始化完成后可以直接访问这两个属性(没有可选的展开),同时仍然避免了引用循环。本节将向你展示如何建立这样的关系。

    下面的示例定义了两个类,Country和City,每个类都将另一个类的实例存储为属性。在这个数据模型中,每个国家必须始终有一个首都城市,并且每个城市必须始终属于一个国家。为了表示这一点,Country class有一个capital - City property,而City class有一个Country property:

    class Country {
        let name: String
        var capitalCity: City!
        init(name: String, capitalName: String) {
            self.name = name
            self.capitalCity = City(name: capitalName, country: self)
        }
    }
    
    class City {
        let name: String
        unowned let country: Country
        init(name: String, country: Country) {
            self.name = name
            self.country = country
        }
    }
    

    要设置这两个类之间的相互依赖关系,City的初始化器接受一个Country实例,并将该实例存储在其Country属性中。

    City的初始化器从Country的初始化器中调用。但是,Country的初始化器不能将self传递给City初始化器,直到一个新的Country实例被完全初始化,如两阶段初始化中所述。

    为了满足这一要求,你可以将Country的capitalCity属性声明为一个隐式展开的可选属性,(City!)。这意味着capitalCity属性的默认值为nil,与任何其他可选属性一样,但是不需要像隐式展开Optionals中描述的那样展开它的值就可以访问它。

    因为capitalCity有一个默认的空值,所以只要Country实例在其初始化器中设置了name属性,就会认为新Country实例已经完全初始化。这意味着国家参考和通过隐式初始化器可以开始自我财产一旦该国名称属性设置。因此Country初始化器可以将self作为一个参数传递给City初始化设置City的Country。

    这意味着你可以在一个语句中创建Country和City实例,而不需要创建强引用循环,并且可以直接访问capitalCity属性,而不需要使用感叹号来打开其可选值:

    var country = Country(name: "Canada", capitalName: "Ottawa")
    print("\(country.name)'s capital city is called \(country.capitalCity.name)")
    // Prints "Canada's capital city is called Ottawa"
    

    闭包强引用

    当你将一个闭包作为对象的属性时,同时闭包内又访问了对象内的属性 或者方法时.这时候闭包会捕获对象形成引用闭环.

    class HTMLElement {
    
        let name: String
        let text: String?
    
        lazy var asHTML: () -> String = {
            if let text = self.text {
                return "<\(self.name)>\(text)</\(self.name)>"
            } else {
                return "<\(self.name) />"
            }
        }
    
        init(name: String, text: String? = nil) {
            self.name = name
            self.text = text
        }
    
        deinit {
            print("\(name) is being deinitialized")
        }
    
    }
    

    例如,可以将asHTML属性设置为闭包,如果text属性为nil,则该闭包默认为某些文本,以防止表示返回空HTML

    let heading = HTMLElement(name: "h1")
    let defaultText = "some default text"
    heading.asHTML = {
        return "<\(heading.name)>\(heading.text ?? defaultText)</\(heading.name)>"
    }
    print(heading.asHTML())
    // Prints "<h1>some default text</h1>"
    

    sHTML属性被声明为惰性属性,因为只有当元素实际需要作为某个HTML输出目标的字符串值呈现时才需要它。asHTML是一个惰性属性,这意味着您可以在缺省闭包中引用self,因为在初始化完成且self已知存在之前,惰性属性不会被访问。

    HTMLElement类创建和打印一个新实例

    var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
    print(paragraph!.asHTML())
    // Prints "<p>hello, world</p>"
    
    访问asHTML()闭包后引用关系

    即使闭包内使用self多次,只强引用HTMLElement对象一次

    当你打破paragraph跟HTMLElement对象的强引用后 paragraph = nil ,会发现HTMLElement析构方法并没有执行.(内存泄漏)

    解决

    捕获列表中的每一项都是weak键字或unowned关键字与对类实例(如self)的引用或用某个值初始化的变量的引用的配对(如delegate = self.delegate!)。这些对是在一对方括号中编写的,用逗号分隔

    lazy var someClosure: (Int, String) -> String = {
        [unowned self, weak delegate = self.delegate!] (index: Int, stringToProcess: String) -> String in
        // closure body goes here
    }
    

    如果闭包没有指定参数列表或返回类型,因为它们可以从上下文推断出来,那么将捕获列表放在闭包的最开始,后面跟着in关键字

    lazy var someClosure: () -> String = {
        [unowned self, weak delegate = self.delegate!] in
        // closure body goes here
    }
    

    Weak and Unowned References
    当闭包和它捕获的实例总是相互引用,并且总是同时释放时。此时将闭包中的捕获定义为一个unowned引用

    相反,当捕获的引用可能在将来的某个时刻变为nil时,将捕获定义为weak引用。weak引用始终是可选的类型,当它们引用的实例被释放时,将自动变为nil。这使你能够检查它们是否存在于闭包中

    如果捕获的引用永远不会变为nil,则应该始终将其捕获为unowned引用,而不是weak引用。
    so,HTMLElement将适合使用unowned引用

    class HTMLElement {
    
        let name: String
        let text: String?
    
        lazy var asHTML: () -> String = {
            [unowned self] in
            if let text = self.text {
                return "<\(self.name)>\(text)</\(self.name)>"
            } else {
                return "<\(self.name) />"
            }
        }
    
        init(name: String, text: String? = nil) {
            self.name = name
            self.text = text
        }
    
        deinit {
            print("\(name) is being deinitialized")
        }
    }
    

    创建HTMLElement实例 paragraph

    var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
    print(paragraph!.asHTML())
    // Prints "<p>hello, world</p>"
    
    unowned 修饰后

    paragraph 被释放

    paragraph = nil
    // Prints "p is being deinitialized"
    

    参考文章

    相关文章

      网友评论

          本文标题:Swift 循环引用

          本文链接:https://www.haomeiwen.com/subject/fjtyqctx.html