Quản lý bộ nhớ trong Swift(Phần 1) - ARC và Hiện tượng Strong Reference Counting


Một trong những vấn đề quan trọng trong code swift và thường được hỏi khi chúng ta đi phỏng vấn. Đó là việc quản lý bộ nhớ trong swift sẽ như thế nào?. Vậy để hiểu rõ hơn vấn đề này chúng ta sẽ tìm hiểu thông qua bài viết sau đây.
Trước khi đi qua bài này chúng ta nên tìm hiểu khái niệm về ARC và cách hoạt động của nó thông qua bài này.
Để bắt đầu cho bài viết này, các bạn nên sử dụng Xcode Playground để thực hành code theo các ví dụ trong bài, nó giúp bạn sẽ dễ hiểu và nhớ lâu hơn.


Swift sử dụng Automatic Reference Counting (ARC) để theo dõi và quản lý bộ nhớ của ứng dụng hay nói chi tiết hơn là dùng nó để quản lý chu kỳ sống của một instance nào đó. Trong nhiều trường hợp việc quản lý bộ nhớ là công việc của  Swift và bạn không cần để ý tới việc quản lý bộ nhớ này, bởi vì ARC sẽ làm điều đó, nó sẽ tự giải phóng bộ nhớ, các instance mà bạn không dùng tới trong một thời gian dài.
Tuy nhiên cũng có một số trường hợp để ARC làm việc hiểu quả bạn cần phải tuân thủ một số nguyên tắc quản lý bộ nhớ. Trong bài này sẽ giúp bạn tìm hiểu một số yêu cầu trên và tìm hiểu chi tiết về ARC(Nó là gì?, hoạt động như thế nào?) trong Swift.


Việc sử dụng ARC trong Swift rất giống khi dùng trong Objective-C. Vậy để hiểu khái niệm cơ bản về ARC bạn có thể tham khảo trước bày này.


Lưu ý:
Trong ARC có một khái niệm là đếm số tham chiếu(Reference counting) và nó chỉ sử dụng cho instance của Class, còn các kiểu như Struct hay enum không phải là kiểu tham chiếu nên sẽ không có cơ chế đếm tham chiếu.


Trong Swift thì ARC hoạt động như thế nào ?


Mỗi khi bạn tạo một instance của class nào đó, ở đây chúng ta gọi instance đó là A. ARC sẽ cấp phát cho A đó một vùng nhớ nào đó trong thiết bị và vùng nhớ đó sẽ giữ các thông tin của A(bao gồm các giá trị của các thuộc tính của A, các liên kết tới A, …v.v.).  
Khi một instance A không được sử dụng trong một thời gian dài, ARC sẽ tự động giải phóng vùng nhớ của instance A để dùng cho các instance mới khởi tạo khác. Nó luôn đảm bảo rằng một vùng nhớ không bị chiếm dụng bởi một instance nào đó mà instance đó không còn được sử dụng.
Vậy thì làm sao ARC biết được khi nào một instance không còn được sử dụng nữa, để nó giải phóng và tránh gây crash xảy ra ?. Nó sẽ sử dụng cơ chế reference counting để kiểm tra xem instance đó còn sử dụng nửa hay không(Để hiểu rõ cơ chế đó bạn nên tìm hiểu bài này trước).  Nếu reference counting khác 0 có nghĩa là instance đó còn dùng, khi nào reference counting bằng 0 thì ARC sẽ tự giải phóng instance đó.


Nhưng sẽ có trường hợp một instance sẽ có chứa các properties với liên kết là strong, nó là liên kết bền vững nên khi đã tạo liên kết rồi thì khó mà cắt đứt do đó, reference coutting của instance đó luôn luôn lớn hơn 0 và ARC sẽ không giải phóng được instance đó. Nếu chúng ta không để ý mà cứ tạo ra các instance có liên kết mạnh như trên sẽ làm cho bộ nhớ bị tràn, Ứng dụng có thể bị lát và crash. Vậy để hiểu và tránh việc tạo ra một liên kết mạnh(strong) trong khi code, chúng ta sẽ tìm hiểu ngay sau đây.


Lưu ý:
Một instance nào đã bị giải phóng rồi mà vẫn còn được sử dụng thì sẽ xảy ra crash app ngay. Khi ARC giải phóng một instance nào đó thì chúng ta sẽ không thể truy cập vào properties hoặc gọi bất cứ phương thức nào cả.


Hoạt động của ARC.
Ở đây có một ví dụ về ARC, chúng ta sẽ tạo một class Car như ví dụ sau:


Ở đây chúng ta thấy, khi khởi tạo một Car chúng ta sẽ gián tên cho nó và in ra màn hình console tên đó. Khi chúng ta huỷ nó thì sẽ in ra màn hình console tên xe đã huỷ như trong code.
Lưu ý:
Khi huỷ một instance sẽ gọi hàm deinit {} ngược lại khi khởi tạo thì gọi hàm init()...


Chúng ta sẽ tạo ra 3 biến kiểu Car và là optional như bên dưới, biến là optional bởi vì chúng ta có thể gián nil cho biến đó để hệ thống biết biến đó không còn dùng và huỷ nó.
  • var reference1: Car?
  • var reference2: Car?
  • var reference3: Car?
Tiếp theo chúng ta sẽ tạo một Car instance và gián cho reference1 như sau:
  • reference1 = Car(name: "John Appleseed")
  • // Prints "John Appleseed is being initialized"
Khi bạn code trong playground bạn sẽ thấy in ra dòng như trên, có nghĩa là instance đã được tạo.
Khi khởi tạo một car instance và gán cho reference1 có nghĩa là chúng ta đã tạo ra một liên kết mạnh (strong reference), liên kết từ reference1 đến Car instance. Nhờ liên kết đó mà biến refernce1 không bị ARC huỷ.


Chúng ta sẽ lấy biến reference1 gián cho 2 biến còn lại có nghĩa là chúng ta đã tạo thêm 2 liên kết mạnh nửa. Bây giờ thì chúng ta có tổng là 3 liên kết mạnh tới Car instance hay nói cách khác là Car instance có reference counting = 3.
  • reference2 = reference1
  • reference3 = reference1
Vấn đề ở đây là làm thể nào để chúng ta giảm các liên kết mạnh đó và giải phóng Car instance.(Giảm reference counting = 0 để ARC giải phóng Car instance)
Bây giờ chúng ta sẽ gián nil cho các biến như sau(Mỗi khi gián nil cho biến nào đó thì chúng ta sẽ cắt đứt liên kết tới instance của nó và giảm reference counting đi 1) và để ý thấy là Car instance chưa bị huỷ.
  • reference1 = nil
  • reference2 = nil
Đúng là Car instance chưa bị huỷ do chúng ta chỉ gián nil cho 2 biến trên nêu chỉ mới huỷ bỏ 2 liên kết tới car instance thôi, có nghĩa là reference counting = 1, chưa bằng 0 nên ARC chưa hủy Car instance được. Nếu chúng ta huỷ luôn liên kết còn lại, có nghĩa là reference counting = 0 thì ARC sẽ hủy instance đó và sẽ thấy console xuất hiện dòng chữ đã huỷ car tên nào đó như sau:
reference3 = nil
// Prints "John Appleseed is being deinitialized"
Chính xác lúc này thì instance car ta đã tạo lúc đầu mới được huỷ hoàn toàn.


Trên đây là ví dụ cơ bản đầu tiên về cách tạo ra các liên kết mạnh và cách huỷ bỏ liên kết để swift huỷ vùng nhớ của instance đã được tạo.


Tiếp theo chúng ta sẽ đi đến ví dụ về hiện tượng Strong Reference Cycles trong Swift hay còn gọi là Retain Cycles trong Objective-C. Đó là một trong những hiện tượng cực kỳ nguy hiểm với bộ nhớ của máy. Vậy nó là gì và khi nào nó sẽ xảy ra?


Strong Reference Cycles


Trong ví dụ trên thì chúng ta đã thấy được ARC đã theo dõi số reference count giảm bằng 0 và giải phóng Car instance.
Tuy nhiên, sẽ có một trường hợp mà reference count không thể giảm xuống bằng không được do đó mà instance đó sẽ không được giải phòng và gây ra hiện tượng tràn bộ nhớ. Chúng ta sẽ có trường hợp là instance A liên kết mạnh với instance B và instance B cũng liên kiết mạnh với instance A, khi đó 2 thằng này sẽ luôn giữ liên kết của nhau và không thằng nào bị phá vỡ ví nó là liên kết mạnh. Và đó được gọi là strong reference cycle.
Vậy làm sao để tránh trường hợp này, trước tiên chúng ta phải hiểu khi nào nó xảy ra hiện tượng trên bằng ví dụ minh hoạ sau đây.
Chúng ta sẽ có 2 Class như sau:

Chúng ta để ý kỹ và thấy rằng trong class Person có một properties kiểu optional là apartment với kiểu dữ liệu là Apartment cũng như vậy trong class Apartment có một properties kiểu optional là tenant với kiểu dữ liệu là Person.
Chúng ta sẽ khai báo 2 biến như sau:
  • var john: Person?
  • var unit4A: Apartment?
và khởi tạo 2 biến đó như sau:
  • john = Person(name: "John Appleseed")
  • unit4A = Apartment(unit: "4A")
Tương tự ví dụ trên khi khởi tạo xong 2 biến trên chúng ta sẽ có 2 strong reference như sau:
Sau đó chúng ta sẽ gán các giá trị cho 2 instance vừa tạo như sau:
  • john!.apartment = unit4A
  • unit4A!.tenant = john
Khi đó chúng ta sẽ tạo thêm 2 strong reference nửa như sau:
Chính 2 strong reference mới tạo thêm của chúng ta đã làm nên một strong reference cycle cho 2 instance john và unitA4.
Giờ chúng ta sẽ bẽ gãy 2 liên kết của john và unit4A bằng cách gán nil cho 2 biến đó, xem sẽ như thế nào.
  • john = nil
  • unit4A = nil
Kết quả là vẫn tồn tại 2 liên kết không thể bị phá vỡ đó cũng chính là 2 liên kết tạo nên strong reference cycle. Để hiểu rõ hơn chúng ta xem hình bên dưới.
Lưu ý:
Khi gán nil cho 2 biến trên nhưng 2 biến trên sẽ không gọi hàm deinit để giải phóng bộ nhớ. Bởi vì nó vẫn còn 2 strong reference không bị phá vỡ, có nghĩa là mỗi instance Person và Aparment hiện đang có reference counting = 1, nên 2 insatnce đó chưa bị hủy. Cứ vậy nếu chúng ta cứ tạo ra thêm nhiều instance như vậy sẽ làm cho tràn bộ nhớ và crash app.
Vậy để giải quyết vấn đề trên thì chúng ta cần phải làm gì?, tiếp theo chúng ta sẽ đi tìm hiểu các cách để giải quyết vấn đề trên thông qua bài viết tiếp theo…

Tóm lại, qua bài này chúng ta đã hiểu được cách mà ARC quản lý bộ nhớ thông qua với các chu kỳ sống của instance trong swift và biết được khi nào xảy ra hiện tượng  strong reference cycle, là một hiện tượng khá nguy hiểm với bộ nhớ của máy. Bài tiếp theo chúng ta sẽ tìm hiểu tất tần tật về cách tránh và giải quyết hiện tượng strong reference cycle cũng như tìm hiểu thêm một số hiện tượng khác gây nguy hiểm cho bộ nhớ máy.

Hy vọng các bạn thích và học được nhiều kiến thức từ bài viết này. Mong các bạn chia sẽ nó để mọi người cùng học và cùng trao đổi. Mọi thắc mắc hay trao đổi về bài viết, các bạn có thể để lại bình luận bên dưới mình sẽ hỗ trợ sớm nhất.

Post a Comment

0 Comments