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关键字的使用条件:
- weak关键字修饰的属性必须是变量,即:必须使用var定义
- 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 修饰吧.
这时候会发现, 报错了:
这是因为weak只能修饰类, 只需要将我们的协议继承自NSObjectProtocol即可:
// 修改之后的 PeopleProtocol
protocol PeopleProtocol: NSObjectProtocol {
func people(_ people: People, didEatSomeThing thing: String)
}
这样, 就可以正常编译, 并正常释放内存了