Bài này được mình dịch từ đây

Apple đã có một bài báo về strong reference cycles trong class. Không khó hiểu sự rò rỉ bộ nhớ (memory leak) là gì và làm thế nào để tránh trong trường hợp này. Tuy nhiên, đây là một tình huống khá hiếm, và dễ dàng phát hiện được. Tôi sẽ chú ý về closure với nhiều chỗ khó hiểu hơn. Vì vậy, hãy làm rõ điều này một lần và cho sau này nữa.

Vòng tham chiếu với closures

Trước tiên, bạn phải hiểu closure là gì và những gì nó làm. Tôi hình dung nó như một đoạn code, nó như một class tạm thời có chứa một tham chiếu đến tất cả các đối tượng nó cần.

Hãy lấy một ví dụ đơn giản để bắt đầu: một ViewController có một CustomView. CustomView có một closure được gọi khi button được nhấn.

swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class CustomView:UIView{ 
    var onTap:(()->Void)?
    ...
}

class ViewController:UIViewController{ 
    let customView = CustomView() 
    var buttonClicked = false
    
    func setupCustomView(){
        var timesTapped = 0
        customView.onTap = {
            timesTapped += 1 
            print("button tapped \(timesTapped) times")
            self.buttonClicked = true
        }
    }
}

Khi chúng ta đưa giá trị cho closure, nó cần giữ tham chiếu đến một số biến để thực thi. Ở đây, nó cần selftimeTapped. Chúng là những giá trị bên ngoài mà closure cần; và để giữ những giá trị đó, closure sẽ tạo ra một tham chiếu mạnh cho chúng. Điều đó sẽ ngăn không cho các giá trị này được giải phóng, và closure bị crash trong trường hợp chúng được deallocated.

Chờ chút, ViewController có một tham chiếu mạnh đến CustomView, CustomView có tham chiếu mạnh đến closure onTapclosure này tạo ra một tham chiếu mạnh đến self. Vì vậy, đây là những gì chúng ta có:

Như bạn thấy rõ ràng chúng ta có một vòng tuần hoàn. Có nghĩa là, nếu bạn thoát khỏi View Controller, nó không thể bị xoá khỏi bộ nhớ vì nó vẫn được tham chiếu bởi closure.

Ví dụ này khá rõ ràng, viewController có một thuộc tính subview, subview có một thuộc tính onTap mà nắm giữ self. Nhưng không may, nó trở nên phức tạp hơn rất nhiều.

Những ví dụ về sự tuần hoàn tiềm ẩn

Câu hỏi mà bạn phải luôn tự hỏi chính mình: Ai sở hữu closure?

UITableView

Nếu bạn đã từng xây dựng ứng dụng iOS mà bạn phải đối phó với UITableView vào một thời điểm nào đó, và rất có thể là bạn cũng đã phải đối phó với các cell tùy chỉnh với một button tùy chỉnh.

Đây là cách để làm điều đó trong swift, trước tiên bạn có một CustomCell có một action closure cho phép bạn xác định những gì sẽ xảy ra khi button được nhấn.

swift
1
2
3
4
5
6
7
8
9
class CustomCell: UITableViewCell {
  
  @IBOutlet weak var customButton: UIButton!
  var onButtonTap:(()->Void)?
    
  @IBAction func buttonTap(){
      onButtonTap?()
  }
}

Sau đó, trong ViewController, bạn xác định hành động mà bạn muốn có cho button này.

swift
1
2
3
4
5
6
7
8
9
class ViewController: UITableViewController {

  override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
      let cell = tableView.dequeueReusableCell(withIdentifier: "CustomCell", for: indexPath) as! CustomCell
      cell.onButtonTap = {
          self.navigationController?.pushViewController(NewViewController(), animated: true)
      }
  }
}

Ai sở hữu closure? Ở đây, nó khá rõ ràng, vì chúng ta khai báo nó một cách rõ ràng trong CustomCell. Nhưng, cell này thuộc về tableViewtableView thuộc về tableViewController. Giống như trên ở đây có một vòng tuần hoàn, nhưng điều này khó có thể nhận ra nếu bạn chưa từng thấy nó trước đây:

TableViewController retain cycle TableViewController retain cycle

GCD

Bạn chắc chắn đã xử lý Grand Central Dispatch trước đây, bạn có phát hiện ra bất kỳ vòng tuần hoàn nào không?

swift
1
2
3
4
5
6
override func viewDidLoad() {
  super.viewDidLoad()
  DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
    self.navigationController?.pushViewController(NewViewController())
  }
}

Đầu tiên, ai sở hữu closure? ViewController không có bất kỳ thuộc tính nào tham chiếu nó. Nó được gọi là DispatchQueue singleton, trường hợp xấu nhất, trong vòng gọi asyncAfter, singleton giữ một tham chiếu đến nó. Không may chúng ta không thể nhìn thấy việc này. Tuy nhiên, closure đó sẽ được thực hiện chỉ một lần, tại một thời gian định trước, vì vậy nó không có ý nghĩa cho singleton để giữ một tham chiếu đến nó. Trong trường hợp này, khi closure được thực hiện nó sẽ bỏ tham chiếu đến self và vì self không tham chiếu đến closure, không có vòng tuần hoàn.

Lưu ý logic này cũng có thể áp dụng cho closure animation của UIView.

Alamofire

Giả sử bạn có một ứng dụng với LoginViewController và bạn đang sử dụng Alamofire để nhận response từ backend của bạn:

swift
1
2
3
4
5
6
7
Alamofire.request("https://yourapi.com/login", method: .post, parameters: ["email":"[email protected]","password":"1234"]).responseJSON { (response:DataResponse<Any>) in
    if response.response?.statusCode == 200 {
        self.navigationController?.pushViewController(NewViewController(), animated: true)
    }else{
        //Show alert
    }
}

Ai sở hữu closure? Closure được khai báo như một tham số của một chức năng của request. Nhưng bạn thực sự không biết những gì Alamofire sẽ làm với closure đó, cũng như khi nó được giải phóng.

Nếu bạn xem xét việc triển khai, bạn có thể thấy rằng request có một OperationQueue: queue. Khi gọi hàm response() chúng ta truyền closure mà sau đó được thêm vào queue. Khi closure thực hiện xong, nó chỉ đơn giản là được gỡ bỏ khỏi queue. Vì vậy, không có vòng tuần hoàn ở đây bởi vì chỉ có queue giữ lại closure, nhưng nó được thực hiện một lần.

Lưu ý rằng, ngay cả khi bạn giữ một tham chiếu đến request hoặc giữ lại một SessionManager, closure cũng sẽ được giải phóng và bạn sẽ không có bất kỳ vòng tuần hoàn nào.

RxSwift

Trong ví dụ này, bạn có một UISearchBar, và bất cứ khi nào bạn thay đổi văn bản trong SearchBar bạn muốn cập nhật một label.

swift
1
2
3
4
5
6
7
8
9
10
11
class ViewController: UIViewController {
  
  @IBOutlet weak var searchBar: UISearchBar!
  @IBOutlet weak var label: UILabel!
  
  override func viewDidLoad() {
    searchBar.rx.text.throttle(0.2, scheduler: MainScheduler.instance).subscribe(onNext: {(searchText) in
      self.label.text = "new value: \(searchText)"
    }).addDisposableTo(bag)
  }
}

Ai sở hữu closure? Closure có thể được gọi là nhiều lần và chúng ta không biết khi nào, do đó, RxSwift cần phải giữ một tham chiếu đến nó. Trong trường hợp này closure thực sự là sở hữu trực tiếp bởi searchBar. Nó có ý nghĩa bởi vì closure được phát hành khi có searchBar. Nhưng đợi đã, searchBar là thuộc sở hữu của self khi nó được thêm vào hierarchy, và closure tham chiếu self. Vì vậy, trong trường hợp này chúng ta có một vòng tuần hoàn, và chúng ta cần phải phá vỡ nó để tránh bị rò rỉ bộ nhớ.

Phá vỡ vòng tuần hoàn

Để phá vỡ vòng tuần hoàn, bạn chỉ cần phá vỡ một tham chiếu, và bạn sẽ muốn phá vỡ cái dễ nhất. Khi giải quyết với closure, bạn sẽ luôn luôn muốn phá vỡ liên kết cuối cùng, đó là những gì closure tham chiếu.

Để làm như vậy, bạn cần chỉ định một biến mà bạn không muốn có một liên kết mạnh. Hai lựa chọn mà bạn có là: weak hoặc unowned và bạn khai báo nó ngay khi bắt đầu closure.

Trong ví dụ UITableView nó sẽ giống như sau:

swift
1
2
3
cell.onButtonTap = { [unowned self] in
    self.navigationController?.pushViewController(NewViewController(), animated: true)
}

Cho dù sử dụng weak hoặc unowned, nói chung bạn muốn sử dụng unowned nếu closure không thể tồn tại lâu hơn các đối tượng nó nắm bắt. Trong kịch bản này, cellclosure không thể sống lâu hơn tableViewController vì vậy chúng tôi có thể sử dụng unowned. Nếu bạn muốn biết thêm về weakunowned tôi sẽ khuyên bạn nên đọc bài báo tuyệt vời này.

Debug rò rỉ bộ nhớ

Đôi khi bạn khó có thể biết được closure của bạn được giữ như một tham chiếu, đặc biệt khi bạn đang sử dụng các thư viện của bên thứ ba hoặc thực hiện riêng, bạn cần phải gỡ lỗi để tìm các vòng tuần hoàn. Xcode cung cấp một công cụ mới rất hữu ích để giúp bạn tìm ra những rò rỉ. Điều hướng qua ứng dụng của bạn và nhấp vào biểu tượng biểu đồ nhỏ ở dưới Xcode để kiểm tra xem có gì trong bộ nhớ.

Trong ví dụ TableView, nếu bạn không đặt weak hoặc unowned trong closure onButtonTap bạn sẽ thấy một cái gì đó giống như:

Dấu chấm than bên phải cho thấy một sự rò rỉ. Nhưng Xcode đôi khi gặp rắc rối khi phát hiện ra những rò rỉ. Hoàn toàn có thể, bạn có một rò rỉ nhưng không có dấu chấm than. Trong trường hợp đó, bạn chỉ cần chú ý đến những gì có trong bộ nhớ và nếu bạn thấy cái gì đó bất thường ở đó chắc chắn bộ nhớ bị rò rỉ.