Swift-自动引用计数(Automatic Reference Counting)(十四)

前言

在iOS5之后apple推出了相对于MRC(Mannul Reference Counting)ARC(Automatic Reference Counting)的内存管理机制,前者是对内存的手动管理,后者是系统对内存的智能化管理。其实我学习iOS的时候已经推出到iOS7了,对于MRC机制我甚至连一知半解都算不上。有时候和iOS老司机交流的时候他们对于各种内存都很熟悉,感觉包括我在内的很多最近两年刚学习iOS开发的同学,都属于填鸭式的学习吧,知其然不知其所以然,甚至都没耐心去读优秀的源码。记得有次去面试,别人问我对于AFNetworingSDWebImage的源码了解有多深,其实我是懵逼的。同时,也正是这种茫然,促使我要系统的读一遍Swift的系统教程,由于英语确实有点渣,并且6月份Swift3.0就要出来了,所以才决定看极客学院翻译的这本教材。并且一段时间以来,写份读书笔记和仅仅是浏览一遍教材,感受和记忆是完全不一样的。我想,读一遍之后,再回头看看自己的笔记,改改错别字,理理不通顺的地方,也许可能效果更好些吧。

  • 自动引用计数的工作机制
  • 自动引用计数实践
  • 类实例之间的循环强引用
  • 解决实例之间的循环强引用
  • 闭包引起的循环强引用
  • 解决闭包引起的循环强引用

Swift使用自动引用计数机制来跟踪和管理应用程序的内存。通常情况下,Swift内存管理机制会一直起作用,我们无需自己来考虑内存的管理。ARC会在类的实例不在被使用时,自动释放其占用的内存。

然而在少数情况下,ARC为了能帮忙我们管理内存,需要更多的关于代码之间关系的信息。所以了解如何启用ARC来管理应用程序的内存还是很有必要的。

需要注意的是,引用计数仅仅应用于类的实例。结构体和枚举都是值类型,不是引用类型,也不是通过引用的方式存储和传递。

分条详述

  1. 自动引用计数的工作机制

    当创建一个类的新的实例时,ARC会分配一大块内存来存储实例的信息。内存中会包含实例的类型信息,以及这个实例的所有属性的相关值。

    此外,当实例不再被使用时,ARC释放实例所占的内存,并让释放的内存能挪作他用。这确保了不再被使用的实例,不会一直占用内存空间。

    然而当ARC收回和释放了正在被使用的实例,该实例的属性和方法将不能再被访问和调用。实际上,当我们视图访问一个被释放掉的类实例时,应用程序很可能会崩溃。

    为了确保使用中的实例不会被销毁,ARC会跟踪和计算每一个实例正在被多少属性,常量和变量所引用。哪怕实例的引用数为1,ARC都不会销毁这个实例。

    为了使上述成为可能,无论我们将实例赋值给属性、常量或变量,他们都会创建此实例的强引用。之所称为 引用,因为他们都将实例牢牢的保持住,只要强引用还在,实例是不允许被销毁的。

  2. 自动引用计数实践

    这里通过一个例子介绍自动引用计数在代码中的调用情况,代码的具体作用写在注释里:

    class Person {
    let name: String    //  姓名
    //  构造器,设置名字
    init(personName name: String) {
        self.name = name
        print("\(name) is being initialized")
    }
    //  析构器,实例被注销的时候调用
    deinit {
        print("\(name) is being deinitialized")
    }
    }
    //  创建实例,这里作说明:因为是Person类的可选类型,所以执行下面代码的时候,Person并没有被创建,此时,Person类的引用数为0
    var reference1: Person?
    var reference2: Person?
    var reference3: Person?
    //  下面代码Person才真正的有了内存地址
    reference1 = Person(personName: "Jack")      //  此时第一次输出:Jack is being initialized
    reference2 = reference1      //  Person类有两个常量引用
    reference3 = reference1      //  三个
    //  下面另1、2为空
    reference1 = nil
    reference2 = nil        //  此时,Person类的析构函数不执行,因为reference3还在引用它
    //  令reference3 = nil,此时Person类会被释放
    reference3 = nil       //  此时,输出: Jack is being reinitialized
  3. 类实例之间的循环强引用

    在上面的例子中,ARC会跟踪所创建的Person实例的引用计量,并且会在Person实例不再需要时销毁它。然而,我们可能会写出一个类的强引用数永远不能变为0的代码。如果两个类实例互相持有对方的强引用,因而每个实例都让对方一直存在,这种情况就是所谓循环强引用

    可以通过定义类之间的关系为弱引用或无主引用,以替代强引用,从而解决循环强引用的问题。了解解决方案之前,先看一下强循环引用在代码中是怎样产生的吧。创建一个人类Person类类型和一个公寓Apartment类类型,人可能住在公寓,也可能不在,公寓可能有人,也可能没人,所以它们在代码中都是可选类型的:

    //  住户类
    class Person {
    let name: String       //  姓名
    var apartment: Apartment?    //  可选,如果用 let 修饰或者去掉 ?,那么必须赋初始值
    //  构造器,设置用户的名字
    init(personName name: String) {
        self.name = name
    }
    //  析构器,Person类被销毁的时候调用
    deinit {
        print("\(name) is being deinitalized")
    }
    }
    //  公寓类
    class Apartment {
    let unit: String      //  公寓单元
    var person: Person?       //  住户
    //  构造器,获取单元名
    init(apartmentUnit unit: String) {
        self.unit = unit
    }
    //  析构器
    deinit {
        print("\(unit) is being deinitalized")
    }
    }

    创建实例后,出现下图所示的强引用关系:

    //  创建实例,此时应景产生强引用的关系
    var john = Person(personName: "John")
    var unit4A = Apartment(apartmentUnit: "unit4A")

    Swift-自动引用计数(Automatic Reference Counting)(十四)_第1张图片

    下面两行代码就会产生循环强引用:

    //  下面的相互持有,将会产生循环强引用
    john.apartment = unit4A
    unit4A.person = john

    Swift-自动引用计数(Automatic Reference Counting)(十四)_第2张图片

    此时,即使令johnunit4A都设置为nil,没有任何一个析构函数会被调用,。循环强引用会一直阻止johnunit4A类实例的销毁,这就在引用程序中造成了内存泄漏。

    //  循环强引用不会被断开,并且下面的写法会产生编译错误:Nil cannot be assigned to type 'Person''Aparement'
    john = nil
    unit4A = nil

    Swift-自动引用计数(Automatic Reference Counting)(十四)_第3张图片

  4. 解决实例之间的循环强引用

    Swift提供了两种办法来解决你在使用类的属性时所遇到的循环强引用问题:弱引用weak reference和无主引用unowned reference

    弱引用和无主引用允许循环引用中的一个实例引用另外一个实例而不保持强引用。这样实例能够互相引用而不产生循环强引用。

    对于生命周期中会变为nil的实例使用弱引用。相反的,对于初始化赋值后再也不会被赋值为nil的实例,使用无主引用。

    • 弱引用
      弱引用不会对其引用的实例保持强引用,因为不会阻止ARC销毁被引用的这个实例,这个特性阻止了引用变为循环强引用。声明变量或属性时,前面加上weak关键字表明这是一个弱引用。

      弱引用必须声明为变量,表明其值能在运行时被修改。弱引用不能被声明为常量。因为弱引用可以没有值,必须将每一个弱引用声明为可选类型。在Swift中,推荐使用可选类型描述可能没有值的类型。

      这里只写一个简单地示例代码,即可看到效果,在实际写代码的时候,发现一些自己忽略的问题,写在了注释里:

      //  住户
      class Person {
      let name = "Jack"
      var apartment: Apartment?
      deinit {
          print("住户信息被销毁")
      }
      }
      //  公寓
      class Apartment {
      let unit = "4A"
      weak var person: Person?
      deinit {
          print("公寓信息被销毁")
      }
      }
      //  注意点1:Person、Apartment必须是可选类型,当Person不是可选类型的时候,最后不能销毁循环强引用,即:personOne = nil 、 apartmentOne = nil 这两行代码会抛出异常
      var personOne: Person? = Person()
      var apartmentOne:Apartment? = Apartment()
      //  令它们相互持有
      personOne!.apartment = apartmentOne
      apartmentOne!.person = personOne
      //  注意点2:下面两行代码必须都写,才会执行析构函数,因为它们是相互持有
      personOne = nil     //  此时什么都不输出
      apartmentOne = nil     //  输出:住户信息被销毁           公寓信息被销毁
    • 无主引用
      和弱引用类似,无主引用不会牢牢保持住引用的实例。和弱引用不同的是,无主引用是永远有值的。因此,无主引用总是被定义为非可选类型non-optional type,可以在声明属性或变量时,在前面加上关键字unowned表示这是一个无主引用。

      由于无主引用是非可选类型,所以不需要在使用它时将它展开(即不可要在后面加感叹号‘!’解包)。无主引用总是可以被直接访问。不过ARC无法在实例被销毁后将无主引用设为nil,因为非可选类型的变量不允许赋值为nil。使用无主引用,必须确保引用始终指向一个未销毁的实例。

      下面的例子模拟银行客户Customer和信用卡CreditCard之间的关系。每个用户可能有信用卡,但是每个信用卡必须要有客户,因此信用卡要有一个关于客户的无主引用,以避免循环强引用:

      //  银行客户
      class Customer {
      let name: String
      var card: CreditCard?
      //  构造器
      init(customerName name: String) {
          self.name = name
      }
      //  析构器
      deinit {
          print("\(name) 被销毁")
      }
      }
      //  信用卡
      class CreditCard {
      let number: Int
      //  无主引用,信用卡的用户信息必不为空
      unowned let customer: Customer
      //  构造器
      init(cardNumber: Int, customer: Customer) {
          self.number = cardNumber
          self.customer = customer
      }
      //  析构器
      deinit {
          print("\(number) 号信用卡被销毁")
      }
      }
      //  创建实例,非可选类型不可设置为nil,所以Customer为可选类型
      var john: Customer? = Customer(customerName: "John")
      john!.card = CreditCard(cardNumber: 1234_6789_123, customer: john!)

      此时,客户和信用卡的关系如下图:

      Swift-自动引用计数(Automatic Reference Counting)(十四)_第4张图片

      如果令john = nil,即断开john持有的强引用时,该实例就会被销毁。其后,再也没有指向CreditCard的实例,该实例也随着被销毁了。

      john = nil
      //  此时输出: John 被销毁       12346789123 号信用卡被销毁
    • 无主引用以及隐式解析可选属性

      通过上面的的示例场景,PersonApartment例子展示了两个属性值都允许为nil,并会产生潜在的循环强引用,这种场景最适合使用弱引用weak来解决。

      CustomerCreditCard例子展示了一个属性值允许为nil,另一个属性值不允许为nil,这也可能产生循环强引用。这种场景最适合使用无主引用unowned来解决。

      然而,还有第三种场景。这种场景中,两个属性值都不许许为nil,并且完成初始化之后永远不为nil。此时,需要一个类使用无主属性,而另外一个类使用隐式解析可选属性(即使用感叹号!解包)。

      下面以城市City和国家County为例,二者相互持有并且初始化始终不为空:

      //  国家
      class County {
      let name: String
      var city: City!       //  注意与 银行客户/信用卡 的区别,此时最后面的感叹号 ! 一定不能去掉,表示默认为 nil
      init(name: String, capitalName: String) {
          self.name = name
          self.city = City(name: capitalName, county: self)
      }
      deinit {
          print("\(name) is being deinitialized")
      }
      }
      //  城市
      class City {
      let name: String
      unowned let county: County       //  无主引用
      init(name: String, county: County) {
          self.name = name
          self.county = county
      }
      deinit {
          print("\(name) is being deinitialized")
      }
      }
      //  实例
      var countyOne: County? = County(name: "China", capitalName: "Beijing")
      countyOne = nil     
      //  此时输出:  China is being deinitialized    \   Beijing is being deinitialized ,说明并没有循环强引用。
  5. 闭包引起的循环强引用

    循环强引用还会发生在当你将一个闭包赋值给类实例的某个属性,并且在这个闭包体中又使用了这个实例。这个闭包体中可能访问了实例的某个属性,例如self.someProperty,或者闭包中调用了实例的某个方法,例如self.someMethod。这两种情况都会导致闭包“捕获”self,从而产生循环强引用。

    循环强引用的产生,使用为闭包和类相似,都是引用类型。当把一个闭包赋值给某个属性时,同时也把一个引用赋值给了这个闭包。实质上,这跟之前类之间的循环强引用是一样的:两个强引用让彼此一直有效。但是和两个类实例不同,这次一个是类实例,另一个是闭包。

    Swift提供了一种优雅的方式解决这种问题,称之为闭包捕获列表closure capture list。先了解闭包如何产生循环强引用,然后再用该方法去解决这个问题。

    例子中定义一个文本编辑元素的类HTMLElement的类,用一个简单的模型表示 Element中一个单独的元素:

    //  文本编辑基础类
    class HTMLElement {
    let name: String     //  元素名称
    let text: String?    //  编辑内容
    //  定义一个延迟属性,同时也是一个闭包属性,表示一个没有参数,返回值是 String 的方法
    lazy var asHTML: Void -> String = {
        if let text = self.text {
            return "<\(self.name)>\(self.text)"
        } else {
            return "<\(self.name)/>"
        }
    }
    //  构造器,text默认为nil
    init(name: String, text: String? = nil) {
        self.name = name
        self.text = text
    }
    //  析构器
    deinit {
        print("\(name) is being deinitialized")
    }
    }
    //  创建实例
    var heading = HTMLElement(name: "br")
    let defaultText = "Hello World"
    //  延迟属性,闭包
    heading.asHTML = {
    return heading.name + (heading.text ?? defaultText)      //  小括号必须加上
    }
    print(heading.asHTML())      //  输出: brHello World
    //heading = nil    这句话会报错,因为非可选类型不能赋值为 nil
    var heading2: HTMLElement? = HTMLElement(name: "h2", text: "nihao")
    heading2 = nil        //   输出   h2 is being deinitialized ,说明此时并没有产生循环强引用   

    asHTML声明为lazy属性,因为只有当元素确实需要处理为HTML输出的字符串时,才需要使用anHTML。也就是说,在默认的闭包中使用self,因为只有当初始化完成以及self确实存在时,才能访问lazy属性。

    注意,下面这个实例代码将会产生强引用:

    /****************  下面这个例子将会产生循环强引用  ****************/
    var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello,world")
    print(paragraph!.asHTML())     //  此时产生循环强引用
    paragraph = nil     //  什么也不输出,说明实例没有被销毁

    Swift-自动引用计数(Automatic Reference Counting)(十四)_第5张图片

    虽然闭包内多次使用了self,但是它只捕获了HTMLElement实例的一个强引用。

  6. 解决闭包引起的循环强引用

    在定义闭包时同时定义捕获列表作为闭包的一部分,通过这种方式可以解决闭包和类之间的循环强引用。捕获列表定义了闭包体内捕获一个或多个引用类型的规则。跟解决两个类实例之间的循环强引用一样,声明每个捕获的引用为弱引用或无主引用,而不是强引用。应当根据代码关系决定使用弱引用还是无主引用。

    注意,Swift有如下要求:只要在闭包体内使用self成员,就要使用self.someProperty或者self.someMethod(),而不只是somePropertysomeMethod()。这提醒我们可能一不小心就捕获了self,造成循环强引用。

    • 定义捕获列表
      捕获列表中的每一项都有一对元素组成,一个元素是weakunowned关键字,另一个元素是类实例的引用(如self)或初始化的变量(如 `delegate = self.delegate!)。这些项在方括号中用都好分开。

      如果闭包有参数列表和返回类型,把捕获列表放在他们前面;如果闭包没有致命参数列表或者返回类型,即它们会通过上下文判断,那么可以把捕获列表和关键字 in 放在闭包最开始的地方。两种情况的代码示例如下:

      //  注意,下面的捕获列表输入的时候并不会代码补全,需要手动全部输入
      lazy var someClosure: (Int, String) -> String = {
          [unowned self, weak delegate = self.delegate!](index: Int, stringToProcess: String) -> String in {
              //    closure body goes here  , 闭包体
          }
      }
      //  闭包没有参数列表
      lazy var someClosure2: Void -> String = {
          [unowned self, weak delegate = self.delegate!] in
          //    closure body goes here  , 闭包体
      }
    • 弱引用和无主引用
      在闭包和捕获的 示例总是相互引用时并且总是同时销毁时,将闭包的捕获定义为无主引用。

      相反的,如果在被捕获的引用可能变为 nil 时,将闭包内的捕获定义为弱引用。弱引用总是可选类型,并且当引用的示例被销毁后,弱引用的值会自动置为nil。如果被捕获的引用绝对不会变为nil,应该是无主引用,而不是弱引用。

      上面的示例HTMLElement中的闭包做出下面修改时,可避免循环强引用:

      lazy var asHTML: Void -> String = {
          [unowned self] in
          if let text = self.text {
              return "<\(self.name)>\(self.text)"
          } else {
              return "<\(self.name)/>"
          }
      }

      此时,在创建可引起循环强引用的示例时:

      /****************  下面这个例子将会产生循环强引用  ****************/
      var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello,world")
      print(paragraph!.asHTML())     //  此时产生循环强引用
      paragraph = nil     //  输出: p is being deinitialized

      使用捕获列表后引用关系如下图所示:

      Swift-自动引用计数(Automatic Reference Counting)(十四)_第6张图片

结束语

昨天晚上看了权利的游戏第六季第5集,这集名字是“门”,演绎了holdor的开始和结束。hold the door。

今天和同学聊了会儿这个剧,然后个人预测剧情发展:龙母、瑟曦、囧恩、三傻、小剥皮5股势力,小剥皮这这季结束应该会被囧恩、三傻联手领便当。另外,囧恩、三傻因小指头一句话,分裂flag已立。三傻会成为瑟曦一样的女王,会想办法铲除囧恩。囧恩无意争夺权位,奈何主角光环太重,城外野人、红巫女等都在支持他。预言:冬临城破之日,囧恩出走之时。

其实我就猜测到这儿,同学脑洞打开,说囧恩会找到龙之母,成为龙骑士…想来想去都感觉这脑洞真的大。当然还有很多其他猜测,先说这几点吧。坐等被打脸,啪啪的。

Swift-自动引用计数(Automatic Reference Counting)(十四)_第7张图片

你可能感兴趣的:(Swift)