ARC and Memory Management in Swift Part 1

Link tutorial gốc: https://www.raywenderlich.com/134411/arc-memory-management-swift

Là ngôn ngữ lập trình bậc cao, Swift xử lý việc quản lý bộ nhớ và allocates/deallocates bộ nhớ thay cho bạn bằng ARC - Automatic Reference Counting. Trong tutorial này, chúng ta sẽ nghiên cứu về ARC và quản lý bộ nhớ trong Swift.

Nhắc lại 1 chút về bộ nhớ stack và heap: Stack là vùng nhớ mà lưu trữ các biến tạm thời được tạo bởi mỗi function, mỗi một thread sẽ có 1 stack riêng của mình. Mỗi khi 1 function declare 1 biến mới, nó sẽ được đẩy vào stack, khi function kết thúc, tất cả biến được đẩy vào stack sẽ được free. Như vậy, trong thực tế, những biến lưu trong bộ nhớ stack là những biến local. Heap là bộ nhớ được dùng cho việc cấp phát động (dynamic allocation). Ta có thể allocate hoặc free bất kỳ lúc nào. Thường thì chỉ có 1 heap cho 1 application. Vì vậy ta có thể tác động đến sự kiện khi 1 heap object bị kết thúc. Swift sử dụng ARC 1 cách rất dễ đoán và hiệu quả trong môi trường tài nguyên hạn chế như iOS.

Vì ARC là tự động nên bạn không cần phải tham gia rõ ràng vào reference counting của objects, bạn chỉ cần xem xét mối quan hệ giữa các objects để tránh memory leaks. Trong tutorial này, chúng ta sẽ nghiên cứu:

  • Cách ARC làm việc
  • Reference cycle và cách tránh
  • ví dụ a reference cycle trong thực tế và cách phát hiện với Xcode
  • Đối phó với mixed value và reference types

Getting Started

Tạo 1 Playground mới và add đoạn code sau vào:

 class User {
  var name: String

  init(name: String) {
    self.name = name
    print("User \(name) is initialized")
  }

  deinit {
    print("User \(name) is being deallocated")
  }
}

let user1 = User(name: "John")

Chúng ta vừa định nghĩa 1 class "User" và tạo ra 1 instance của nó. Bạn có thể thấy playground show "User John is initialized\n" trong slidbar do method print bên trong init, nhưng print bên trong deinit không bao giờ đc gọi. Tức là user1 không bao giờ bị deinitialized, ko bao giờ bị deallocated. Điều đó bởi vì scope mà user1 được initialized không bao giờ closed - the playground itself never goes out of scope - đối tượng sẽ ko bị removed khỏi memory.

Wrapping phần initialization lại bằng do:

 do {
  let user1 = User(name: "John")
}

Nó sẽ tạo ra 1 scope bao lấy việc init của user1 và khi kết thúc scope, chúng ta mong muốn nó user1 sẽ được deallocated. Bây h, bạn sẽ thấy cả 2 lệnh print đều hiển thị ở slidebar. Tức là object đã được deinitialized ở cuối scope ngay trước khi nó bị removed khỏi memory. Lifetime của 1 Swift object:

  1. Allocation (memory taken from stack or heap)
  2. Initialization (init code runs)
  3. Usage (the object is used)
  4. Deinitialization (deinit code runs)
  5. Deallocation (memory returned to stack or heap)

Đôi khi, "deallocate" và "deinit" được sử dụng như nhau, nhưng thực sự chúng là 2 trạng thái riêng biệt trong object's lifetime.

Reference counting là 1 cơ chế mà 1 object sẽ bị deallocated khi nó ko còn cần thiết. Câu hỏi đặt ra là "Khi nào thì bạn biết chắc chắn rằng 1 object sẽ ko cần thiết trong tương lại nữa?" Reference counting quản lý điều này bằng veiecj giữ một "usage count" - hay reference count - bên trong mỗi object instance. Số này sẽ đếm số lượng bao nhiêu thứ reference tới object. Khi reference count của 1 object chạm đến 0 thì tức là ko có cái gì reference tới nó cả, thì object sẽ bị deinitializes và deallocates.

Scheme1-480x120.png

Khi bạn initialize User object, nó sẽ bắt đầu với reference count là 1 vì constant user1 references tới object đó. Đến khi kết thúc block do, user1 goes out of scope, count bị giảm xuống, và reference count giảm xuống 0. Và kết qủa, user1 deinitializes và sau đó deallocates.

Reference cycles

Trong hầu hết trường hợp, ARC hoạt động rất "đẹp", bạn sẽ ko máy khi phải lo lắng về việc "Memory leaks". Nhưng nó vẫn có thể xảy ra. Hãy tưởng tượng tình huống 2 objects ko còn cần thiết nữa, nhưng vẫn reference đến nhau. Bởi vì mỗi object có 1 non-zero reference count, thì cả 2 objects sẽ không thể deallocation được:

ReferenceCycle-480x153.png

Cái này được gọi là strong reference cycle. Để thấy rõ, bạn hãy thêm ddoạn code sau vào bên dưới User class nhưng trước do block:

class Phone {
  let model: String
  var owner: User?

  init(model: String) {
    self.model = model
    print("Phone \(model) is initialized")
  }

  deinit {
    print("Phone \(model) is being deallocated")
  }
}

Và thay đổi do block như sau:

do {
  let user1 = User(name: "John")
  let iPhone = Phone(model: "iPhone 6s Plus")
}

Tiếp theo, add thêm đoạn code say vào class User, ngay phía dưới property name:

private(set) var phones: [Phone] = []
func add(phone: Phone) {
  phones.append(phone)
  phone.owner = self
}

ddoanj treen add 1 mảng phones để giữ các phoness sở huwurx bởi user. Setter được đặt private để client buộc phải sử dụng add (phone 😃. Mehtod này đảm bảo rằng chủ sở hữu được thiết lập đúng cách khi bạn thêm nó. Hiện nay, như bạn có thể thấy ở sidebar, cả Phone và User deallocate như mong đợi. Nhưng hãy viết lại do block như sau:

do {
  let user1 = User(name: "John")
  let iPhone = Phone(model: "iPhone 6s Plus")
  user1.add(phone: iPhone)
}

Ở đây, bạn add iPhone cho user1. Nó sẽ tự động set owner của iPhone cho user1. 1 strong reference cycle giữa 2 objects ngăn ARC khỏi việc giải phóng chúng. Và kết quả là cả user1 và iPhone đều không được deallocate.

UserIphoneCycle-480x202.png