Retain Cycle trong Swift

retain_cycle_swift

1) ARC nền tảng

  • ARC (Automatic Reference Counting) quản lý vòng đời reference type (class, actor, closure).
  • Mỗi tham chiếu mạnh tăng strong reference count. Khi count về 0 → deinit chạy → giải phóng.
  • Retain cycle: hai hoặc nhiều thực thể giữ mạnh lẫn nhau nên count không về 0 ⇒ rò rỉ bộ nhớ.
final class A { var b: B?; deinit { print("A deinit") } }
final class B { var a: A?; deinit { print("B deinit") } }

var a: A? = A(); var b: B? = B()
a!.b = b; b!.a = a          // strong ↔ strong → retain cycle

a = nil; b = nil            // không deinit

2) Retain cycle: hai mô thức phổ biến

2.1 Object ↔ Object

  • Hai thuộc tính strong tham chiếu lẫn nhau.
  • Cách sửa: Một phía dùng weak (hoặc unowned nếu không bao giờ nil) theo quan hệ sở hữu hợp lý.
final class Parent { var child: Child? }
final class Child  { weak var parent: Parent? }  // tránh cycle

2.2 Closure ↔ Object

  • Closure giữ mạnh self theo mặc định. Nếu object giữ closure như property, tạo vòng lặp.
final class Owner {
    var handler: (() -> Void)?
    func setup() {
        handler = { self.doWork() } // giữ mạnh self → cycle nếu Owner giữ handler
    }
    func doWork() {}
}
  • Cách sửa: dùng capture list [weak self] hoặc [unowned self].
handler = { [weak self] in self?.doWork() }   // an toàn nil
// hoặc
handler = { [unowned self] in self.doWork() } // không nil, crash nếu self đã giải phóng

3) weak vs unowned

Thuộc tínhweakunowned
LoạiOptional tham chiếu yếuTham chiếu yếu không optional
Tự động nilKhông
An toànCao, cần unwrapThấp hơn, crash nếu truy cập sau giải phóng
Dùng khiVòng tham chiếu có thể kết thúc trướcVòng đời đối tượng được đảm bảo dài hơn closure/trỏ tới

Quy tắc thực dụng:

  • Mặc định dùng [weak self] cho closure sống dài hoặc có khả năng outlive self (VD: network callback, timer, animation, Combine subscription).
  • Dùng [unowned self] khi chứng minh được self chắc chắn còn sống trong suốt thời gian closure chạy (VD: trong UIViewController thực thi tức thời trong viewDidLoad không lưu giữ, hoặc mối quan hệ chủ–tớ chặt chẽ).

Ví dụ so sánh:

// weak: an toàn, phải unwrap
service.fetch { [weak self] result in
    guard let self = self else { return }
    self.render(result)
}

// unowned: gọn, nhưng nguy hiểm nếu service giữ closure lâu hơn self
service.sync { [unowned self] data in
    render(data)
}

4) @escaping là gì và liên hệ với retain cycle

  • @escaping áp cho tham số closure của hàm khi closure có thể được gọi sau khi hàm trả về. Khi đó, hàm thường lưu giữ closure (property, queue, completion list).
  • Closure non-escaping chỉ sống trong phạm vi hàm, compiler có thể tối ưu capture.
func loadNow(block: () -> Void) { block() }                // non-escaping
func loadLater(block: @escaping () -> Void) { tasks.append(block) } // escaping
  • Khi một object giữ escaping closure làm property, và closure capture mạnh self, bạn có retain cycle.
final class VC: UIViewController {
    private var completion: (() -> Void)?
    func fetch() {
        completion = { [weak self] in self?.updateUI() } // tránh cycle
        api.request(completion: completion!)
    }
}

Lưu ý: @escaping không tự gây rò rỉ. Nó cho phép vòng đời closure kéo dài, từ đó dễ phát sinh cycle nếu capture sai.


5) Mẫu dùng capture list

// 5.1 Chuẩn an toàn cho callback dời hạn
api.request { [weak self] data in
    guard let self = self else { return }
    self.render(data)
}

// 5.2 Tránh capture biến mạnh ngoài ý muốn
var count = 0
let block = { [count] in print(count) } // capture by value

// 5.3 Kết hợp nhiều mục
cache.load(key) { [weak self, unowned cache] value in
    guard let self = self else { return }
    self.apply(value); cache.touch(key)
}

6) Timer, NotificationCenter, Combine, async/await

Timer

class C {
    var timer: Timer?
    func start() {
        timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in
            self?.tick()
        }
    }
    deinit { timer?.invalidate() }
}

NotificationCenter

class C {
    var token: NSObjectProtocol?
    func observe() {
        token = NotificationCenter.default.addObserver(forName: .init("X"), object: nil, queue: .main) {
            [weak self] _ in self?.handle()
        }
    }
    deinit { if let t = token { NotificationCenter.default.removeObserver(t) } }
}

Combine

final class VM {
    private var bag = Set<AnyCancellable>()
    func bind() {
        publisher
            .receive(on: DispatchQueue.main)
            .sink { [weak self] value in self?.apply(value) }
            .store(in: &bag) // giải phóng khi VM deinit
    }
}

async/await

final class VC: UIViewController {
    func load() {
        Task { [weak self] in
            guard let self else { return }
            let data = try await api.fetch()
            self.render(data)
        }
    }
}

7) Debug và đo rò rỉ

  • Instruments → Leaks / Allocations để phát hiện object không deinit.
  • Thêm deinit { print("ClassName deinit") } trong debug để kiểm chứng.
  • Dùng Memory Graph Debugger (Xcode) để thấy chuỗi tham chiếu.

Checklist nhanh:

  1. Controller không deinit khi dismiss? Kiểm tra Timer, Notification, Combine, Task.
  2. Closure property có [weak self] chưa? Có self → closure → self không?
  3. Quan hệ cha–con đặt phía con weak hay unowned đúng chưa?

8) Tình huống phỏng vấn

Q1. Phân biệt weakunowned?

  • weak: optional, tự nil, an toàn hơn. Dùng khi đối tượng có thể biến mất trước.
  • unowned: non-optional, không nil. Dùng khi đảm bảo vòng đời.

Q2. Khi nào cần @escaping?

  • Khi callback chạy sau khi hàm trả về. Ví dụ network, lưu closure vào property.

Q3. Vì sao [weak self] thường dùng với network/animation?

  • Vì chúng thực thi muộn, có thể vượt qua vòng đời self. Tránh giữ mạnh self.

Q4. Có nên luôn dùng [weak self]?

  • Không. Với closure nội bộ, thực thi tức thời, hoặc không lưu giữ, không cần. Lạm dụng gây rườm rà và che bug logic.

Q5. Cho ví dụ cycle và cách phá?

  • Owner giữ closure property, closure capture mạnh self. Phá bằng [weak self] hoặc không giữ closure nữa.

Q6. Dùng unowned khi nào an toàn?

  • Khi chắc chắn self còn sống trọn vòng đời closure. Nếu sai sẽ crash.

9) Mẫu anti-pattern và sửa

Anti-pattern:

class VC: UIViewController {
    var onDone: (() -> Void)?
    override func viewDidLoad() {
        super.viewDidLoad()
        onDone = { self.dismiss(animated: true) } // giữ self mạnh
    }
}

Sửa:

onDone = { [weak self] in self?.dismiss(animated: true) }

Anti-pattern:

class VM { var cb: ((Int) -> Void)? }
class View { let vm = VM()
    init() {
        vm.cb = { value in self.render(value) } // self ↔ vm nếu View giữ vm
    }
}

Sửa:

vm.cb = { [weak self] value in self?.render(value) }

10) Kết luận nhanh

  • @escaping cho phép callback sống lâu → dễ tạo cycle.
  • Dùng [weak self] theo mặc định cho closure dời hạn. Chỉ dùng unowned khi chắc chắn vòng đời.
  • Kiểm tra bằng deinit, Memory Graph, Leaks.
  • Thiết kế quan hệ sở hữu rõ ràng: cha sở hữu con mạnh, con tham chiếu cha yếu.
Tôi là một lập trình viên IOS. Code chính là IOS nhưng thỉnnh thoảng vẫn đá sang Android hoặc web. Mặc dù không quá thông thạo nhưng tôi sẽ chia sẻ những kiến thức mà mình đã tìm hiểu, áp dụng qua.

Bài viết liên quan

Swift Class vs Struct

Struct và Class trong Swift

1) Bức tranh tổng quát Gợi ý: Swift ưu tiên value semantics để dễ suy luận và an toàn luồng. Chỉ dùng class khi cần chia sẻ trạng thái sống…

Xem thêm

Unit Test cho thư viện CocoaPods bằng Podspec

Trong bài viết này, bạn sẽ học cách tạo một thư viện CocoaPods, cấu hình Unit Test sử dụng podspec, cách tích hợp vào dự án chính, và cách chạy…

Xem thêm

Unit Test trong Swift với Quick và Nimble

Trong phát triển phần mềm, đảm bảo chất lượng mã nguồn thông qua kiểm thử tự động là một bước quan trọng. Bài viết này sẽ hướng dẫn bạn cách…

Xem thêm
0 0 đánh giá
Article Rating
Theo dõi
Thông báo của
guest
0 Comments
Cũ nhất
Mới nhất Được bỏ phiếu nhiều nhất
Phản hồi nội tuyến
Xem tất cả bình luận