Quản lý bộ nhớ trong lập trình iOS

Cập nhật ngày: 21/10/2021 - Đã có 1479 lượt xem bài viết này!
Quản lý bộ nhớ trong lập trình iOS
Bài viết này dành cho những người chưa có hiểu biết về khái niệm Quản lý bộ nhớ / Memory Management. Nhiều giáo trình hay cuốn sách về iOS thường bỏ qua phần này vì cho rằng nó phức tạp đối với những người mới học. Ví dụ như khi bạn kéo thả một outlet, bạn sẽ nhìn thấy những từ như weak, strong. Nhưng bạn sẽ không để ý nó và làm tiếp, vì cơ bản là trong lúc đấy nó không ảnh hưởng gì đến công việc bạn đang làm.

Quản lý bộ nhớ trong lập trình iOS

Lời nói đầu

Bài viết này dành cho những người chưa có hiểu biết về khái niệm Quản lý bộ nhớ / Memory Management. Nhiều giáo trình hay cuốn sách về iOS thường bỏ qua phần này vì cho rằng nó phức tạp đối với những người mới học. Ví dụ như khi bạn kéo thả một outlet, bạn sẽ nhìn thấy những từ như weak, strong. Nhưng bạn sẽ không để ý nó và làm tiếp, vì cơ bản là trong lúc đấy nó không ảnh hưởng gì đến công việc bạn đang làm.

Trước khi chúng ta nói về Swift thì hãy cùng nhau nhìn qua một cách cơ bản nhất khái niệm bộ nhớ/memory, và tại sao chúng ta lại cần nó.

Khái niệm memory management mô tả quá trình hệ điều hành, ví dụ như iOS xử lý việc lưu và xuất dữ liệu. Như các bạn đã biết thì chúng ta có 2 địa điểm chính để lưu trữ dữ liệu. Đó là ổ cứng và RAM.

Vai trò của RAM

Tưởng tượng bạn đang chơi một game bắn súng trên điện thoại và nó cần lưu trữ một lượng lớn hình ảnh và các xử lý đồ họa mà khi bạn ấn vào nút setting, bạn vẫn có thể chơi tiếp.

Nhưng khi bạn tắt máy, tất cả những hình ảnh trên đều biến mất. Bởi vì chúng được lưu tại RAM. RAM là nơi lưu trữ tạm thời trên điện thoại của bạn, tốc độ lưu trữ của RAM cũng nhanh hơn ổ cứng bình thường nhiều lần, khoảng 15,000 MB/s so với 1,000MB/s. Nói cách khác, RAM chính là một vùng nhớ ngắn hạn, những hình ảnh đó không được lưu trong ổ cứng của máy mà được lưu trong RAM. Mỗi khi trò chơi kết thúc hay điện thoại được tắt đi, lập tức RAM sẽ xóa những hình ảnh đó để giải phóng bớt dung lượng của mình. 

iPhone của tôi có 4GB RAM và ổ cứng 128GB và khi toi chạy một ứng dụng nào đó, phần lớn dữ liệu sẽ được lưu tại RAM trừ khi tôi sử dụng UserDefaults hay CoreData để lưu dữ liệu lại ổ cứng.

Giới hạn lưu trữ của RAM

Trong một câu chuyện khác, bạn đang vuốt qua lại ứng dụng Instagram hay xem Facebook feed. Nhưng, làm thế nào mà điện thoại của bạn có thể duy trì một độ mượt chuẩn 60 khung hình/giây như vậy?. Bởi vì những đối tượng và dữ liệu đó được lưu tạm thời trên RAM. Tuy nhiên bạn không thể lưu chúng mãi mãi.

Khi nói đến memory management, đặc biệt là trong iOS , chúng ta liên tưởng đến việc quản lý những vùng trống trong RAM. Mặc dù ngày nay bạn sẽ hiếm khi thấy bộ nhớ RAM bị quá tải vì nó đã mạnh mẽ hơn ngày xưa rất nhiều. Tuy nhiên, một nguyên tắc vàng cho những lập trình viên iOS đó là tạo một ứng dụng tối ưu tốt nhất với bộ nhớ máy.

RAM giông như một chiếc tủ lạnh, bạn có thể thêm thức ăn, đồ uống hoặc thậm chí là cả quần áo (???). Tương tự như vậy, trong iOS, bạn có thể thêm một album ảnh, hay những đối tượng lớn như UIView. Tuy nhiên, ngăn tủ lạnh chỉ có dung lượng nhất định và bạn không thể nhét tất cả những gì bạn muốn vào đó. Khi đó bạn phải lấy bớt một vài thứ ra trước khi muốn thêm cái gì đó vào.

May mắn là trong iOS 10, việc thu dọn và làm trống bộ nhớ RAM đã được hoàn thành một cách tự động nhờ một thư viện được thiết kế bởi các kỹ sư của Apple. Họ gọi đó là cơ chế Automatic Reference Counting / ARC để phân biệt những đối tượng còn đang được sử dụng hay không sử dụng nữa. Mọi việc đã trở nên tự động hóa, thay vì làm một cách thủ công trước đây.

Automatic Reference Counting

Đầu tiên hãy tạo một đối tượng trước. Tôi đã tạo một class có tên là Passport chứa property quốc tịch và một optional property là human

class Passport {
  var human: Human?
  let citizenship: String
 init(citizenship: String) {
  self.citizenship = citizenship
  print("You've made a passport object")
 }
 deinit {
  print("I, paper, am gone")
 }
}

Nếu bạn còn đang băn khoăn deinit là gì thì nó là khái niệm trái ngược với init. Khi bạn gọi hàm init, bạn sẽ tạo được một object và đưa nó vào trong bộ nhớ. deinit xảy ra khi vị trí vùng nhớ của đối tượng đó trong bộ nhớ đã bị xóa bỏ. Ở đây tôi để một đối tượng tự khởi tạo nó mà không cần dùng đến var ,let:

Passport(citizenship: "Republic of Korea")
// "You've made a passport object"
// "I, paper, am gone"

Tại sao đối tượng này lại bị xóa ngay sau khi bạn khởi tạo nó?. Đó là bởi vì ARC.

Để duy trì một đối tượng trong bộ nhớ, bạn phải có tham chiếu tới một thứ gì đó :

var myPassPort: Passport? = Passport(citizenship: "Republic of Korea")

Khi bạn để Passport tự tạo một đối tượng cho nó, sẽ` không có một tham chiếu/liên kết/bộ đếm tham chiếu/reference count nào cả. Nhưng với đoạn code trên thì chúng ta đã tạo một liên kết giữa đối tượng myPassport và class Passport, và reference count là 1.

Bạn sẽ băn khoăn không hiểu strong là gì. Đó là một liên kết mặc định, một liên kết sẽ tăng reference count thêm 1 đơn vị, và tôi sẽ giải thichs tại sao chúng ta nên dùng weak ở một vài trường hợp. 

Bây giờ tôi sẽ tạo một class có tên là Human và có một optional property thuộc kiểu Passport. 

class Human {
 var passport: Passport?
 let name: String
 init(name: String) {
  self.name = name
 }
 
 deinit {
  print("I'm gone, friends")
 }
}

Biến passport là một kiểu optional type, bạn sẽ không phải set nó khi tạo một đối tượng Human.

var bob: Human? = Human(name: "Bob Lee")

Nếu bạn để bob và myPassport là nil thì:

myPassport = nil // "I, paper, am gone"
bob = nil // "I'm gone, friends"

Khi bạn set nil cho từng object, mối liên kết sẽ không còn, và reference count sẽ trở về 0 bởi vì cả 2 object đều đã được giải phóng khỏi bộ nhớ. 

Tuy nhiên, kể cả khi bạn set vài thứ trở về nil thì chưa chắc chúng sẽ được giải phóng do mối liên kết với các đối tượng khác, khiến cho reference count không trở về 0.

Class Human có một optional property thuộc kiểu Passport. Ngược lại, Passport cũng có một optional property tương tự thuộc kiểu Human.

var newPassport: Passport? = Passport(citizenship: "South Korea")
var bobby: Human? = Human(name: "Bob the Developer")
bobby?.passport = newPassport
newPassport?.human = bobby

Tiếp theo chúng ta set các object về nil

newPassport = nil
bobby = nil
// Nothing happens 🤔

Không có gì xảy ra, cả 2 đối tượng vẫn còn.Bởi vì vẫn còn liên kết giữa bobby và newPassport

Ở đây chúng ta gọi hiện tượng này là reference cycle hay memory leak. Kể cả những đối tượng này không được sử dụng và bạn nghĩ rằng chúng đã được giải phóng, thì chúng vẫn còn nằm lại trong bộ nhớ và chiếm dần dung lượng, dẫn tới hiện tượng tràn bộ nhớ hay còn gọi là memory leak. Một trải nghiệm cực kỳ khó chịu.

Weak

Để giải quyết sự cố trên, chúng ta cần phải sử dụng tới weak. Đói tượng weak sẽ không làm tăng reference count. 

class Passport {
 weak var human: Human?
 let citizenship: String
 init(citizenship: String) {
  self.citizenship = citizenship
  print("You've created an object")
 }
 deinit {
  print("I, papepr, am gone")
 }
}

Bây giờ chúng ta set lại 2 đối tượng về nil

newPassport = nil
bobby = nil
// "I, papepr, am gone"
// "I'm gone, friends" 👋

Với weak không làm tăng reference count, chúng ta sẽ chỉ có một reference count trước khi bạn set bobby về nil. Do vậy chúng ta sẽ không gặp phải trường hợp memory leak nữa.

Source Code

Đến đây thì các bạn đã hiểu vai trò và độ quan trọng của strong, weak cũng như việc quản lý bộ nhớ trong lập trình iOS

Xem khóa đào tạo nhân sự theo danh mục!

Xem các khóa đào tạo nhân sự