Skip to main content

Quản lý bộ nhớ trong iOS - Basic Memory Management in iOS


Chủ đề hôm nay mình muốn nói tới là một vấn đề nền tảng đối với tất cả mọi người khi mới bắt đầu tiếp cận với lập trình iOS, “Quản lý bộ nhớ” aka Memory Management.
Ắt hẳn, những người mới bước chân vào con đường lập trình mobile nói chung, hoặc iOS nói riêng sẽ liên tục nghe những anh bề trên (hay còn gọi là senior thần thánh của chúng ta) nói về quản lý bộ nhớ quan trọng như thế nào khi lập trình trên thiết bị có bộ nhớ hạn chế như điện thoại. Những từ ngữ được nhắc đi nhắc lại như ARC , non-ARC, Reference Count,… blah blah. Và bạn sẽ kiểu như: “WTH! Mấy cha này nói cái gì vậy”. Rồi tiếp đó lại tới những vấn đề sẽ gặp khi quản lý bộ nhớ không tốt như : leak memory, stack overflow, rồi lại tới crash ( ác mộng của tất cả developers). Các bạn sẽ dường như là: “Thôi thôi! Dẹp hết… mệt quá :’( Quan tâm làm gì ba cái này, làm đại cho xong cho rồi…hiu hiu).

F*CK THAT SHIETT ! I’M OUT

Vậy quản lý bộ nhớ thật sự là gì? Quản lý bộ nhớ có thể được hiểu một cách đơn giản là quá trình khởi tạo, sử dụng và giải phóng bộ nhớ khi không còn sử dụng. Trong cách iOS quản lý bộ nhớ, tồn tại một thứ gọi là “Reference count system” để quản lý rằng có bao nhiêu biến (owners) đang nắm quyền sở hữu một đối tượng (object) thông quan một biến đếm là reference count hay còn gọi là retain count. Từ lúc chúng ta khởi tạo (alloc) một object thì retain count của object đó mặc định sẽ là 1. Mỗi khi chúng ta lấy quyền sở hữu tới một object (bằng cách tham chiếu đến vùng nhớ của đối tượng thông qua các phương thức/hàm có sẵn) thì retain count của object đó sẽ tăng lên 1 và khi chúng ta không cần dùng nó nữa, chúng ta sẽ bỏ quyền sở hữu tới object (release) thì retain count của object sẽ giảm đi 1. Cứ như vậy cho tới khi retain count bằng 0 thì object đó sẽ được giải phóng (dealloc) khỏi bộ nhớ. Có thể lấy một ví dụ cho hành động ở trên là: trong một văn phòng làm việc đang sáng đèn, tới giờ tan tầm thì mọi người bắt đầu ra về (giảm retain count) và công việc của bạn (reference count system) là kiểm tra xem còn ai ở trong phòng không, nếu không thì tắt hết toàn bộ đèn đi...

MMR (Manual Retain-Release) hay còn gọi là Non-ARC

Đây là một phương thức quản lý bộ nhớ của iOS từ những ngày đầu tiên. Đúng như tên gọi của nó, bạn sẽ lấy quyền sở hữu object cũng như từ bỏ quyền sở hữu một cách hoàn toàn thủ công thông quan các phương thức/hàm được hệ thống cung cấp sẵn. Chúng ta sẽ lấy quyền sở hữu của object bằng các phương thức như: alloc, new, retain, copy, mutable copy, các hàm khởi tạo đối tượng nói chung mặc định retain count của đối tượng vừa được khởi tạo sẽ bằng 1. Và để bỏ quyền sở hữu tới object chúng ta sẽ phải release hoặc autorelease object đó. Điều cần lưu ý khi sử dụng phương pháp non-ARC đó là chúng ta cần phải release object khi không cần sử dụng nữa để tránh tình trạng memory leak. Bên cạnh đó, sẽ có một vấn đề khác phát sinh khi chúng ta release quá nhiều lần trên một object đó là dangling pointer, dangling pointer sẽ trỏ đến một vùng nhớ không hợp lệ (vùng nhớ được xoá hoặc giải phóng trước đó) có thể gây crash lúc run-time.

ARC (Auto Reference-Counting)

Như các bạn có thể thấy thì việc sử dụng phương pháp MMR trên hoàn toàn là thủ công và quá phụ thuộc vào con người nên không tránh được sai sót.


Để khắc phục sự bất tiện của phương pháp MMR, trong sự kiện WWDC năm 2011, Apple đã giới thiệu một phương pháp quản lý bộ nhớ mới khiến ai nấy đều hào hứng, người người trong giới gần xa đâu đâu cũng nhắc đến đó là ARC ❤.
Vậy thì ARC là gì? Nói trắng ra thì nó cũng chẳng khác gì mấy so với MMR, vẫn là retain count, vẫn là đó “Reference count system”. Vậy thì có gì khác biệt??? Vâng!!! Điều khác biệt duy nhất khi sử dụng ARC là bấy giờ chúng ta không cần phải quá quan tâm đến việc quản lý bộ nhớ nữa, thay vào đó chúng ta sẽ tập trung vào cải thiện các vấn đề về logic của code; các phương thức quản lý bộ nhớ (tăng/giảm retain count như alloc, new,…được đề cập ở trên) sẽ được tự động thêm vào code của chúng ta trong compile-time. In compiler we trust! Như vậy công việc của các Dev sẽ giảm đi đáng kể, thay vì ngồi lần mò, kiểm tra xem chắc chắn là chúng ta đã release object đó hay chưa thì bây giờ ARC đã giúp chúng ta làm điều đó, bên cạnh đó thì số lượng dòng code cũng giảm đi kha khá và code của chúng ta sẽ có vẻ đẹp hơn :3



Haizz! Tới phần này thì cuộc sống của chúng ta đã trở nên dễ dàng hơn khá nhiều. Vấn đề còn lại mà chúng ta cần phải quan tâm đó là retain cycle. Dưới đây là một ví dụ đơn giản về retain cycle và sau đó mình sẽ định nghĩa và đưa hướng giải quyết cho mọi người.
class ObjectA {
   var objectB: ObjectB? = nil
   
   init() {
      print("init A")
   }    deinit {
      print("deinit A")
   }
}class ObjectB {
   var objectA: ObjectA? = nil
   
   init() {
      print("init B")
   }   deinit {
      print("deinit B")
   }
}
var objectA: ObjectA? = ObjectA()
var objectB: ObjectB? = ObjectB() 
objectA?.objectB = objectB
objectB?.objectA = objectA
Như chúng ta có thể thấy thì trong ví dụ trên, instance của class ObjectA sẽ có reference tới instance của class ObjectB và ngược lại. Một điều cần lưu ý đó là: Nếu chúng ta không chỉ định cụ thể thì mọi reference tới một object khác đều là strong reference. Điều đó cũng có nghĩa là chúng ta sẽ lấy quyền sở hữu object, và retain count của object đó sẽ tăng lên 1. Tiếp tục với ví dụ trên:
objectA = nil
objectB = nil
Hãy thử copy đoạn code trên vào Playground và chạy thử. Chúng ta có thể thấy được là phương thức deinit ở trong 2 class sẽ không được thực thi. Vì sao vậy??? Nếu chúng ta nhớ lại những thứ nằm trên bài viết này sẽ có một ý là Object chỉ được giải phóng khi retain count bằng 0, cũng có nghĩa là khi không còn bất cứ strong reference nào tới object. Vậy thì ở ví dụ trên phương thức deinit không thực thi đơn giản vì: objectA đang có retain count = 1 vì có objectB đang strong reference tới nó và ngược lại. Nên cả objectA và objectB sẽ không bao giờ được giải phóng khỏi bộ nhớ, vì chúng đang chờ lẫn nhau để giải phóng. Trường hợp mà các objects strong reference lẫn nhau như vậy gọi là retain cycle, các objects sẽ không bao giờ được giải phóng khỏi bộ nhớ, có thể dẫn tới memory leak.
Để giải quyết vấn đề trên thay vì dùng strong reference thì chúng ta sẽ có giải pháp khác là: weak hoặc unowned.
class ObjectA {
   weak var objectB: ObjectB? = nil
Khi chúng ta weak reference (hoặc unowned) tới một object thì chúng ta không lấy quyền sở hữu của object và retain count sẽ không tăng lên. Thử chạy lại code phía trên chúng ta sẽ thấy deinit được thực thi.
Điểm khác nhau của giữa weakunowned là: khi đối tượng mà chúng ta weak reference tới bị dealloc khỏi bộ nhớ thì weak reference tự động gán về nil, unowned cũng tương đối giống weak nhưng nó sẽ không tự động gán về nil khi object bị dealloc mà nó sẽ vẫn trỏ tới một vùng nhớ rác nào đó, điều này khá nguy hiểm có thể dẫn tới crash app trong run-time khi chúng ta sử dụng một vùng nhớ không hợp lệ.
Tóm lại, chúng ta sử dụng weak reference khi biết rằng đối tượng nó tham chiếu tới có thể trở thành nil, trong khi unowned reference sử dụng khi chúng ta biết chắc chắn rằng đối tượng đó sẽ không bao giờ trở thành nil. Chúng ta sẽ định nghĩa các weak reference bằng các biến kiểu optional, có nghĩa là có giá trị hoặc không giá trị và định nghĩa unowned reference bằng các biến kiểu non-optional.
Tới đâu thì bài viết cũng đã khá dài dòng văn tự nên mình xin được phép kết thúc ở đây. Một lần cảm ơn mọi người đã đọc tới những dòng cuối cùng này. Hẹn mọi người vào những bài viết sau. Nếu có bất cứ sai sót gì trong bài thì mọi người hãy comment để cho mình biết và chỉnh sửa nhé ❤

Comments

Popular posts from this blog

MVVM và VIPER: Con đường trở thành Senior

Trong bài viết trước chúng ta đã tìm hiểu về MVC và MVP để ứng dụng cho một iOS App đơn giản. Bài này chúng ta sẽ tiếp tục ứng dụng 2 mô hình MVVM và VIPER . Nhắc lại là ứng dụng của chúng ta cụ thể khi chạy sẽ như sau: Source code đầy đủ cho tất cả mô hình MVC, MVP, MVVM và VIPER các bạn có thể download tại đây . MVVM MVVM có thể nói là mô hình kiến trúc được rất nhiều các cư dân trong cộng đồng ưa chuộng. Điểm tinh hoa của kiến trúc này là ở ViewModel , mặc dù rất giống với Presenter trong MVP tuy nhiên có 2 điều làm nên tên tuổi của kiến trúc này đó là: ViewModel không hề biết gì về View , một ViewModel có thể được sử dụng cho nhiều View (one-to-many). ViewModel sử dụng Observer design pattern để liên lạc với View (thường được gọi là binding data , có thể là 1 chiều hoặc 2 chiều tùy nhu cầu ứng dụng). Chính đặc điểm này MVVM thường được phối hợp với các thư viện hỗ trợ Reactive Programming hay Event/Data Stream , đây là triết lý lập trình hiện đại và hiệu

Alamofire vs URLSession

Alamofire vs URLSession: a comparison for networking in Swift Alamofire and URLSession both help you to make network requests in Swift. The URLSession API is part of the foundation framework, whereas Alamofire needs to be added as an external dependency. Many  developers  doubt  whether it’s needed to include an extra dependency on something basic like networking in Swift. In the end, it’s perfectly doable to implement a networking layer with the great URLSession API’s which are available nowadays. This blog post is here to compare both frameworks and to find out when to add Alamofire as an external dependency. Build better iOS apps faster Looking for a great mobile CI/CD solution that has tons of iOS-specific tools, smooth code signing, and even real device testing? Learn more about Bitrise’s iOS specific solutions! This shows the real power of Alamofire as the framework makes a lot of things easier. What is Alamofire? Where URLSession can be found within the s

Fileprivate vs private: Giải thích sự khác biệt

Fileprivate vs private in Swift: The differences explained Fileprivate and private are part of the access control modifiers in Swift. These keywords, together with internal, public, and open, make it possible to restrict access to parts of your code from code in other source files and modules. The private access level is the lowest and most restrictive level whereas open access is the highest and least restrictive. The documentation of Swift will explain all access levels in detail to you, but in this blog post, I’m going to explain the differences between two close friends: fileprivate and private. Build better iOS apps faster Looking for a great mobile CI/CD solution that has tons of iOS-specific tools, smooth code signing, and even real device testing? Learn more about Bitrise’s iOS specific solutions! Open access is the highest (least restrictive) access level and private access is the lowest (most restrictive) access level. This will improve readability and mak