[Swift]学习笔记--内存管理

Swift中的内存是自动管理的(ARC),但是在有些情况下,还是会引起内存泄露的,下面就来记录一下哪些情况需要我们注意内存问题;

1. 强引用引起的内存泄露

1.1. weak

下面我们来定义这样一个场景:

People: 拥有姓名,和居住的公寓,但是公寓可有可无
Apartment: 拥有名称,和居住的人,但是人可有可无

代码如下:

class People {
    
    var name: String
    var apartment: Apartment?
    
    init(name: String) {
        
        self.name = name
        print(name,"is initialized")
    }
    
    deinit {
        print("People",name,"is being deinitialized")
    }
}

class Apartment {
    
    let name: String
   var tenant: People?
    
    init(name: String) {
        self.name = name
        print("Apartment",name,"is initialized")
    }
    
    deinit {
        print("Apartment",name,"is being deinitialized")
    }
}

这里的apartment和tenant属性都是可以为nil的,所以定义为可选型;
下面我们进行下面的额操作:
分别初始化两个对象:

var zl: People? = People.init(name: "zl")
var apart: Apartment? = Apartment.init(name: "apart")

然后分别赋值:

zl?.apartment = apart
apart?.tenant = zl

接下来,我们将两个对象置为nil(假设需要释放了)

apart = nil
zl = nil

这时候,我们看控制台的输出:

zl is initialized
Apartment apart is initialized

只有初始化时的输出,没有走deinit方法,也就是对象没有释放掉,产生了内存泄露.
我们都知道这里产生内存泄露的原因就是: 因为强引用,发生了循环引用,互相得不到释放.具体原因,不做过多解释,一般都能理解,这里只给出解决方法.
因为两个对象的相关属性都是强类型,才会导致循环引用,所以只需将其中的一个改为弱引用即可,这就用到了weak关键字

weak关键字的使用条件:

  1. weak关键字修饰的属性必须是变量,即:必须使用var定义
  2. weak关键字修饰的属性必须是可选型

同时满足这两个条件即可使用weak,显然,这里的需求是满足的,因此,只需将People的apartment或者Apartment的tenant,其中之前加上weak关键字即可:

weak var tenant: People?
// 或者
weak var apartment: Apartment?

这时控制台输出:

zl is initialized
Apartment apart is initialized
People zl is being deinitialized
Apartment apart is being deinitialized

两个对象都正常释放掉了.

1.2. unowned

下面我们来看一下这样的场景:

Person: 拥有姓名和信用卡
CreditCard: 拥有卡号和持卡人

注意: 这里的信用卡的卡号和持卡人是必须存在且不可更改的,因此,我们这样定义这两个类:

class Person {
    
    var name: String
    var careditCard: CreditCard?
    
    init(name: String) {
        self.name = name
        
        print(name,"is initialized")
    }
    
    deinit {
        print("Person1",name,"is being deinitialized")
    }
}

class CreditCard {
    
    let number: String
    let customer: Person
    
    init(number: String, customer: Person) {
        
        self.number = number
        self.customer = customer
        
        print("CreditCard",number,"is initialized")
    }
    
    deinit {
        print("CreditCard",number,"is being deinitialized")
    }
}

然后,我们进行如下操作:

var lzz: Person? = Person1.init(name: "lzz")
var goldCard: CreditCard? = CreditCard.init(number: "123444", customer: lzz!)

goldCard = nil
lzz = nil

同样,因为相同的原因,导致循环引用,没有走deinit方法:

lzz is initialized
CreditCard 123444 is initialized

这里的修改,我们可以将Person的属性careditCard使用weak修饰:

weak var careditCard: CreditCard?

这样也能避免内存泄露;
如果想将CreditCard的属性customer改为弱引用,就不能使用weak修饰了,因为他不满足使用weak关键字的条件,这就用到了unowned关键字:

unowned 关键字也是一个弱引用,其可以修饰常量(let定义),也可修饰变量(var定义),但是其修饰的量一定不能是可选型,也就是不能为nil

这里CreditCard的customer属性满足使用unowned关键字的条件,所以可以使用:

unowned let customer: Person

这时也能解决内存泄露问题; 但是unowned关键字有一个缺点:

使用unowned关键字修饰的量,在释放掉后,不能再访问,否则会编译报错,使程序crash掉

如果,把lzz置为nil后,这时goldCard不为nil,如果这时访问其属性customer,就会编译报错:

lzz = nil
goldCard?.customer
goldCard = nil
不能访问

所以,如果使用了unowned关键字修饰的量,一定要注意不要提前释放内存空间;如果下面这样写,是可以的:

goldCard = nil
lzz = nil
goldCard?.customer

这里不会报错的原因是: 在对goldCard进行解包的时候就返回nil了,而不会继续去看customer了,所以,这样使用是安全的.

2. 闭包中的循环引用

看这样一个例子:

class SmartAirConditioner {
    
    var temperature: Int = 26
    var temperatureChange: ((Int) ->())!
    
    init() {
        
        temperatureChange = { newTemperature in
            
            if abs(newTemperature - self.temperature) >= 10 {
                
                print("It's not healthy to do it")
            } else {
                self.temperature = newTemperature
                print("New temperature \(self.temperature) is set")
            }
        }
    }
    
    deinit {
        print("Smart Air-conditioner is being deinitialized")
    }
}

然后进行使用:

var airCon: SmartAirConditioner? = SmartAirConditioner.init()
airCon?.temperature
airCon?.temperatureChange(100)
airCon?.temperatureChange(24)

airCon = nil

会发现,控制台输出:

It's not healthy to do it
New temperature 24 is set

并没有走deinit方法,产生了内存泄露:

这是因为temperatureChange是类的一个属性,也就是类持有temperatureChange,而在temperatureChange内又使用了self,即temperatureChange引用了self(类),这就形成了强引用循环.

因为闭包捕获了外部引用self,这样我们可以声明一个捕获列表,在这里对self进行处理,声明捕获列表的方法如下:

在闭包参数列表之前,用方括号([ ])括起来即可:

将init方法修改如下:

init() {
        
        temperatureChange = { [unowned self] newTemperature in
            
            if abs(newTemperature - self.temperature) >= 10 {
                
                print("It's not healthy to do it")
            } else {
                self.temperature = newTemperature
                print("New temperature \(self.temperature) is set")
            }
        }
    }

这里使用的是unowned关键字的弱引用,当然也可以使用weak关键字进行修饰,需要注意的是: 如果使用weak修饰,那么self就是一个可选型,在使用的时候需要解包:

init() {
        
        temperatureChange = { [weak self] newTemperature in
            
            if abs(newTemperature - self!.temperature) >= 10 {
                
                print("It's not healthy to do it")
            } else {
                self!.temperature = newTemperature
                print("New temperature \(self!.temperature) is set")
            }
        }
    }

这里使用的是强制解包,也可以使用? 解包;当然,也可以使用if let进行解包:

init() {
        
        temperatureChange = { [weak self] newTemperature in
            
            if let weakSelf = self {
                
                if abs(newTemperature - weakSelf.temperature) >= 10 {
                    
                    print("It's not healthy to do it")
                } else {
                    weakSelf.temperature = newTemperature
                    print("New temperature \(weakSelf.temperature) is set")
                }
            }
        }
    }

3. 代理中的循环引用

在使用代理的时候, 如果把含有代理的对象设置为某个类的属性, 再设置其代理为当前类的时候, 就有可能引起循环引用.

3.1 模拟循环引用

这里我设置了一个这样的场景: 实例化一个People类, 设置一个协议 PeopleProtocol, 为People类添加一个遵循该协议的代理属性:
People 类 :

// People 类 
import UIKit

class People: NSObject {

    var name = ""
    var age = 0
    var delegate: PeopleProtocol?
    
    func eat() {
        
        print("eat action")
    }
}

PeopleProtocol 协议 :

// PeopleProtocol 协议
import UIKit

protocol PeopleProtocol {
    
    func people(_ people: People, didEatSomeThing thing: String)
}

然后新建一个控制器PeopTestViewController, 设置一个People属性, 实例化, 并设置其代理为当前控制器:

class PeopTestViewController: UIViewController, PeopleProtocol {

    var op: People!
    
    override func viewDidLoad() {
        super.viewDidLoad()

        // Do any additional setup after loading the view.
        
        self.view.backgroundColor = UIColor.green

        op = People()
        op.name = "张三"
        op.age = 19
// 设置代理
        op.delegate = self
        
        let btn = UIButton(type: .custom)
        btn.backgroundColor = UIColor.red
        btn.frame = CGRect(x: 100, y: 100, width: 100, height: 30)
        btn.addTarget(self, action: #selector(buttonClick), for: .touchUpInside)
        self.view.addSubview(btn)
    }
// 实现协议方法
    func people(_ people: People, didEatSomeThing thing: String) {
        
        print("People \(people.name) eat \(thing)")
    }
    
    func buttonClick() {
        
        self.dismiss(animated: true, completion: nil)
    }
    
    deinit {
        print("deinit")
    }

这里的代理没有做任何操作, 实现协议方法及设置协议代理, 都是模拟使用.
最后, 我们在ViewController内实例化一个PeopTestViewController, 并模态出来, 在返回的时候, 会发现并没有释放实例化的PeopTestViewController实例, 即没有走 deinit 方法:

PeopTestViewController实例对象--持有-->People实例对象
People 实例对象的代理---持有---> PeopTestViewController实例对象

这就引起了循环引用.

3.2 避免循环引用

避免循环引用的方法很简单, 只需要在People类中的协议代理设置为弱引用即可, 好, 加上 weak 修饰吧.
这时候会发现, 报错了:

g

这是因为weak只能修饰类, 只需要将我们的协议继承自NSObjectProtocol即可:

// 修改之后的 PeopleProtocol 
protocol PeopleProtocol: NSObjectProtocol {
    
    func people(_ people: People, didEatSomeThing thing: String)
}

这样, 就可以正常编译, 并正常释放内存了

你可能感兴趣的:([Swift]学习笔记--内存管理)