blog icon indicating copy to clipboard operation
blog copied to clipboard

Swift 中使用weak避免对象循环引用

Open waltcow opened this issue 6 years ago • 0 comments

项目中经常会在Closure使用unownedweak 修饰 self 对象避免循环引用所带来的坑

Closure里面会用到self的地方,一般都是这样处理

{[unowned self] in }

但是在一个网络请求的Closure回调里面,这样后来发现在这个Closure执行之前, self 已经被释放了

{[weak self] in 
  if let weSelf = self{
      执行代码
}
}

swift 会使用ARC(Automatic Reference Counting)帮我们打理内存,但是在有些时候,尤其是对象间存在互相引用的时候,就会由于reference cycle导致内存无法释放,而我们也需要时刻保持清醒,来避免内存泄露

ARC是如何工作的呢?

Swift 使用“引用计数(reference count)”来管理类对象的生命周期,避免类对象在“仍被使用”的时候被意外释放。当一个对象的reference count为0时,Swift会立即删除该对象。

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

当执行以下的流程代码时:

var ref1: Person?
var ref2: Person?

ref1 = Person(name: "Mars") // count = 1
// Mars is being initialized.

ref2 = ref1 // count = 2

ref2 = nil // count = 1

ref1 = nil // count = 0
// Mars is being deinitialized.

Reference cycle(循环引用)又是怎么发生的呢?

对于Person这样的单个对象,ARC 可以很好的默默为我们工作。但是,当不同类对象之间存在相互引用时,指向彼此的是强引用,就会导致reference cycle, ARC无法释放它们之中的任何一个。

再看看下面的例子:

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

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

执行以下的代码


var mars: Person? = Person(name: "Mars") // count = 1
// Mars is being initialized

var apt: Apartment? = Apartment(unit: "11") // count = 1
// Apartment 11 is being initialized

mars!.apartment = apt11
// mars.count = 2
apt!.tenant = mars

// Set mars and apartment to nil
mars = nil
apt = nil

尽管我们把marsapt设置为nilPersonApartmetndeinit也不会被调用了。因为它们的两个memberapartmenttenant)是一个强引用,指向了彼此,让对象仍旧“存活”在内存里。但是,marsapt已经被设置成nil,我们也已经无能为力了。这就是类对象之间的reference cycle

Reference cycle的解决方案

  1. 当class member允许为nil时

ApartmentPerson 中引起reference cycle数据成员都允许为nil,对于这种情况,我们可以像下面这样使用weak来解决 reference cycle

weak var name: Type

class Apartment { 
  let unit: String
  weak var tenant: Person? 
// omit for simplicity...
}

var mars: Person? = Person(name: "Mars")
var apt: Apartment? = Apartment(unit: "11", owner: mars!)
mars!.apartment = apt
apt!.tenant = mars
mars = nil //由于已经没有strong reference指向**mars**,于是**mars**就被**ARC**释放了。
apt = nil // 这时,也没有任何strong reference指向apt了,它也会被ARC释放。

那么,我们是用weak还是unowned呢

在闭包和捕获的实例总是相互引用并且同时销毁时,将闭包内的捕获定义为unowned(无主引用)。 相反的,在被捕获的引用可能会变为nil时,将闭包内的捕获定义为 weak(弱引用)。

那什么情况下需要用到weak呢?

class ViewController: UIViewController {
    var workItem = DispatchWorkItem(block: { [weak self] in
        guard let strongSelf = self else { return }
        strongSelf.updateUI()
    })
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        DispatchQueue.main.asyncAfter(deadline: .now() + 2.33, execute: workItem)
    }
    
    func updateUI() {
        // ...
    }
}

上面是个延时操作的例子,如果声明为unowned(无主引用),在还没有到达延迟的时间的时候,我们 pop 掉这个 ViewController 的话,程序就会 crash。因为无主引用是非可选类型,pop 掉这个 ViewController 后,闭包中捕获的 self (也就是ViewController的实例)已经被销毁了,这时候再访问被销毁的实例,程序肯定会奔溃的。

所以这里应该声明为weak(弱引用),弱引用总是可选类型,当引用的实例被销毁后,弱引用的值会自动置为 nil,并且我们可以在闭包内检查它们是否存在。

waltcow avatar Apr 26 '18 07:04 waltcow