Quản lý bộ nhớ trong Swift
Tìm hiểu về cách quản lý bộ nhớ trong Swift, sự khác biệt giữa Stack và Heap, Value Types và Reference Types

Tại sao cần phải biết quản lý bộ nhớ?
-
Nhà giàu mà không biết tiêu tiền cũng sạt nghiệp. Mặc dù phần cứng máy tính/điện thoại ngày càng phát triển, nhưng cứ tiêu xài hoan phí bộ nhớ thì dẫn đến app rất chậm, lag. Users chửi, khách hàng chửi
-
Biết để đi phỏng vấn. Mình chưa đi phỏng vấn lần nào nhưng dám chắc mấy câu này rất dễ bị hỏi
-
Học để biết. Kiến thức bao la, biết thêm một kiến thức không bổ bề ngang cũng tràn bề dọc
Rốt cuộc Stack và Heap là gì?
Abstraction, em là ai?
Trước khi bắt đầu vào phần Stack và Heap, mình muốn nói về sự trừu tượng trong học thuật.
Trong cuộc sống, sẽ có rất nhiều thứ bạn xài hằng ngày nhưng không thể giải thích được cách hoạt động của nó. Khi ai đó hỏi bạn "xe ô tô chạy sao mày? ", dù không chạy ô tô bạn vẫn trả lời được "ừ thì cắm chìa khóa rồi khởi động xe rồi chạy thôi". Bạn hiểu là ô tô có động cơ có bánh xe, có phanh, có đèn. Bạn phân biệt được ô tô với xe bò dù không chạy 2 loại này.
Đó chính là sự trừu tượng khóa - Abstraction. Abstraction rất tốt, nó giúp chúng ta hiểu, phân biệt sự vật hiện tượng như xe bò với ô tô ở trên. Trong IT, Abstraction lại càng được sử dụng nhiều hơn.
Tại sao chúng ta cần Abstraction ?
Để hiểu được thực sự 100% Stack, Heap là gì, tại sao thanh Ram lại có 2 cái này, vòng đời, phạm vi của chúng, rồi thằng nào con nào "chi phối" 2 thứ này: Hệ điều hành, complier hay language runtimes, vv là cả một quá trình. Chúng ta phải tự tay viết hệ điều hành, complier, học sâu kiến trúc máy tính mới hiểu được rõ.
Ơi giời, nhưng đã có Abstraction đây rồi, chúng ta không cần hiểu rõ đến mức như vậy. Mục tiêu của chúng ta là lái ô tô chở gấu đi chơi, không phải thiết kế ô tô. Nhưng hiểu một xíu về cách hoạt động sẽ giúp ta lái mượt hơn, ví dụ bẻ cua nên tăng tốc hay giảm tốc.
Stack và Heap?
Stack
-
Vùng nhớ stack được sử dụng cho việc thực thi thread. Khi gọi hàm, các biến cục bộ của hàm được lưu trữ vào block của stack (theo kiểu LIFO). Cho đến khi hàm trả về giá trị, block này sẽ được xóa tự động. Hay nói cách khác, các biến cục bộ được lưu trữ ở vùng nhớ stack và tự động được giải phóng khi kết thúc hàm.
-
Kích thước vùng nhớ stack được fix cố định. Chúng ta không thể tăng hoặc giảm kích thước vùng nhớ stack. Nếu không đủ vùng nhớ stack, gây ra stack overflow. Hiện tượng này xảy ra khi nhiều hàm lồng nhau hoặc đệ quy nhiều lần dẫn đến không đủ vùng nhớ.
Heap
-
Vùng nhớ heap được dùng cho cấp phát bộ nhớ động
-
Vùng nhớ được cấp phát tồn tại đến khi lập trình viên giải phóng vùng nhớ đó
-
Hệ điều hành sẽ có cơ chế tăng kích thước vùng nhớ heap.
Thực sự các anh hùng cũng cái nhau dữ dội, bắt bẻ từng câu chữ về Stack vs Heap trên stackoverflow, mọi người có thể xem thêm tại đây nha:
http://stackoverflow.com/questions/79923/what-and-where-are-the-stack-and-heap
Value Types và Reference types
Biến là một trong những thứ sử dụng bộ nhớ nhiều nhất trong lập trình. Không nói qua khi 50% thời gian lập trình là dùng biến.
Ở các ngôn ngữ lập trình, kiểu dữ liệu được chia làm 2 loại: value type (tham trị) và reference type (tham chiếu).
Ví dụ:
-
Value type: Int, Double, Struct, vv.
-
Reference type: Closure, Class
Điểm khác biệt quan trọng nhất chúng ta cần nhớ giữa 2 loại này:
-
Value type lưu ngay giá trị của biến trên Stack
-
Reference type chỉ lưu địa chỉ đến vùng nhớ trong Heap.
Vẽ data model là gì?
Trước khi đi sâu vào vấn đề cũng như hiểu rõ behind-the-scene tại sao có sự khác nhau như vậy. Chúng ta nên ôn lại cách vẽ data model. Data model mô phỏng lại cách thức lưu trữ của value type và reference type
Vẽ data model cho Value Type
Cho đoạn code sau:
var number1: Int
number1 = 69
var number2: Int
number2 = number1
number1 = 96
Đầu tiên với:
var number1: Int
Khai báo biến number kiểu Int. Tưởng tượng, bạn đang yêu cầu: "Swift, cho tao một chỗ trống để lưu biến kiểu Int, tao đặt tên là number1 nha".
Hình chữ nhật tượng trưng cho một ô nhớ, number1 kế bên là tên biến.
Tiếp theo:
number1 = 69
"Ê Swift, tao muốn bỏ giá trị 69 vô biến number1". Thấy dấu = , Swift sẽ lấy giá trị bên phải dấu = bỏ vô cái hộp lúc trước đã tạo để lưu trữ dữ liệu.
var number2: Int
Tiếp tục tạo một biến number2:
Tiếp theo:
number2 = number1
Theo như chúng ta suy nghĩ, khi thực hiện phép gán =, Swift liệu có lấy giá trị bên phải bỏ vào hộp (ô nhớ) như trước?
Nhưng không, Swift sẽ tạo lấy một bản copy của 69 từ number1 rồi bỏ vào ô nhớ ở number2 như hình bên dưới
Như ta thấy, không có sự liên kết nào giữa number1 và number2.
Và dòng code cuối cùng:
number1 = 96
Do không có sự liên kết nào giữa number1 và number2 nên khi thay đổi giá trị của number1, number2 không bị ảnh hưởng gì hết.
Vẽ data model cho Reference Type
Ta có đoạn code sau:
class User {
var age = 1
init(age: Int) {
self.age = age
}
}
var user1: User
user1 = User(age: 21)
user1.age = 22
var user2 = User(age: 25)
user2 = user1
user1.age = 30
var user1: User
Đầu tiên, khởi tạo biến:
Tiếp theo là khởi tạo đối tượng từ class User:
user1 = User(age: 21)
Lúc này, khác với Value Type, Swift sẽ tạo một instance User ở trong Heap.
Sau khi tạo instance xong, Swift sẽ lấy địa chỉ của instance này bỏ vào ô nhớ của user1. Trong hình trên, instance có địa chỉ là @0x69
Đương nhiên địa chỉ @0x69 là mình bịa ra, để không phải bịa lung tung như vậy nữa. Ta để dấu mũi tên cho dễ nhìn, giống vầy:
Tiếp theo:
user1.age = 22
Swift sẽ:
- Lấy địa chỉ trong ô nhớ user1
- Tìm instance có địa chỉ đó trong Heap
- Thay đổi ô nhớ age trong instance tìm được thành 22.
Hãy nhìn sơ đồ mình họa bên dưới:
Tiếp theo tạo một biến mới là user2 và khởi tạo luôn:
var user2 = User(age: 25)
user2 = user1
Trước khi toán tử gán = thực thi, ô nhớ user2 vẫn đang lưu địa chỉ 0x60. Nhưng sau khi phép gán = thực thi, nó sẽ lưu địa chỉ lấy từ ô nhớ user1 là 0x69. Ta sẽ phải vẽ lại mũi tên như sau:
Đọc nãy giờ vẫn không thấy quản lý bộ nhớ đâu?
Quản lý bộ nhớ chỗ nào???
Từ từ cháo mới nhừ được. Câu hỏi đặt ra là cái instance còn lại có địa chỉ 0x60 trong Heap sẽ được xử lý thế nào, nó tự động được xóa đi, hay ta phải code để xóa nó?
Strong Reference, Reference counting là gì
Mặc định, một mũi tên trỏ đến một instance trong Heap ở những ví dụ trên được xem là một Strong Reference. Còn reference counting thì đếm mũi tên trỏ đến instance.
Automatic Reference Counting (ARC)
ARC là cơ chế quản lý bộ nhớ của Swift. Cơ chế hoạt động của nó rất giống việc chúng ta vẽ data model nãy giờ. Đó là lý do nãy giờ mình muốn bạn ôn lại Stack, Heap, và vẽ vời như vậy.
Nội dung thì dài dòng nhưng đại ý là:
Nếu một instance không có còn strong reference nào hay được hiểu là reference counting = 0 thì cơ chế ARC sẽ xóa và giải phóng bộ nhớ cho instance đó trong Heap
Như ví dụ trên, instance có địa chỉ 0x60 sẽ bị xóa vì có reference counting = 0
Ví dụ ARC
Quick note về vòng đời của object trong Swift:
- Allocation: Giai đoạn cấp phát bộ nhớ. Stack và Heap sẽ đảm nhận việc này.
- Initialization: Khởi tạo đối tượng. Hàm init được chạy
- Usage: Dùng đối tượng đó
- Deinitialization: Hàm deinit chạy
- Deallocation: Giải phóng bộ nhớ. Stack hoặc Heap lấy lại vùng nhớ không xài nữa
Stack và Value Type thì tự động giải phóng rồi, nên ta chỉ quan tâm Heap và Reference Type.
Để mô phỏng lại quá trình hủy object, có 2 cách:
-
Khai báo biến kiểu optional để ta có thể gán nó = nil
-
Cho đoạn code cần test vào một hàm, chạy hàm đó. Hết scope của hàm đó, những biến trong hàm sẽ bị hủy
Lấy luôn ví dụ User từ đầu đến giờ, mình thêm hàm deinit để track xem instance nào trong Heap bị delete nhé:
class User{
var age = 1
init(age: Int){
self.age = age
}
deinit {
print("user has age: \(age) was deallocated")
}
}
var user1: User?
user1 = User(age: 21)
user1?.age = 22
var user2: User? = User(age: 25)
user2 = user1
và kết quả:
user has age: 25 was initialized
Đúng như những gì chúng ta vẽ nãy giờ phải không nào?
Thêm một ví dụ từ Official Guide:
class Person {
let name: String
init(name: String) {
self.name = name
print("\(name) is being initialized")
}
deinit {
print("\(name) is being deinitialized")
}
}
var reference1: Person?
var reference2: Person?
var reference3: Person?
reference1 = Person(name: "John Appleseed")
reference2 = reference1
reference3 = reference1
// instance đang có 3 strong reference, xóa hết mới giải phóng bộ nhớ cho instance đó được, bỏ comment để xóa
//reference1 = nil
//reference2 = nil
//reference3 = nil
Ví dụ trên mình không vẽ data model nữa vì nó tương tự như ví dụ User rồi.
Strong Reference Cycles
Cho đoạn code:
class Person {
let name: String
init(name: String) {
self.name = name
}
var apartment: Apartment?
deinit {
print("\(name) is being deinitialized")
}
}
class Apartment {
let name: String
init(name: String) {
self.name = name
}
var owner: Person?
deinit {
print("Apartment \(name) is being deinitialized")
}
}
```swift
var person: Person? = Person(name: "Khoa dep trai")
var apartment: Apartment? = Apartment(name: "Novaland")
person?.apartment = apartment
apartment?.owner = person
Dựa vào đoạn code ta có thể vẽ data model sau:
Bây giờ ta muốn hủy 2 biến person và apartment:
person = nil
apartment = nil
Một lần nữa vẽ data model để xem chuyện gì xảy ra, và tại sao không xóa được 2 biến trên?
"Hiện tượng" này được gọi là Strong Reference Cycle.
Memory Leak là gì?
Nguyên nhân bị Strong Reference Cycle do cả 2 instance vẫn còn lại strong reference = 1. Mà theo cơ chế ARC thì trong reference = 0 thì instance mới bị hủy được. Trường hợp ta muốn hủy instance nhưng thực tế instance vẫn còn trong Heap như vầy, người ta gọi mà Memory Leak.
Để giải quyết vấn đề này, Swift cung cấp 2 cách đó là Weak Reference và Unowned Reference
Weak Reference
Về chức năng thì Weak Reference giống Strong Reference. Nhưng với cơ chế ARC, thì instance có nhiều weak reference cũng sẽ bị xóa. Đề xài weak reference, ta chỉ cần thêm keyword weak là được:
class Person {
let name: String
init(name: String) {
self.name = name
}
var apartment: Apartment?
deinit {
print("\(name) is being deinitialized")
}
}
class Apartment {
let name: String
init(name: String) {
self.name = name
}
weak var owner: Person?
deinit {
print("Apartment \(name) is being deinitialized")
}
}
var person: Person? = Person(name: "Khoa dep trai")
var apartment: Apartment? = Apartment(name: "Novaland")
person?.apartment = apartment
apartment?.owner = person!
person = nil
apartment = nil
Ta sẽ được kết quả:
Khoa dep trai is being deinitialized
Apartment Novaland is being deinitialized
Để hiểu rõ, ta cứ vẽ data model ra thôi, dùng mũi tên nét đứt ( -------> ) để biểu diễn weak reference:
Như vậy, instance Person sẽ bị xóa trước do strong reference = 0. Tiếp theo do nó bị xóa nên strong reference đến instance Apartment cũng bị xóa luôn. Dẫn đến instance Apartment có strong reference = 0. Đó là lý do ta thấy
Khoa dep trai is being deinitialized
xuất hiện trước:
Apartment Novaland is being deinitialized
Bạn thử thêm weak reference vào class Person, và xóa weak reference ở class Apartment thì output sẽ ngược lại:
weak var apartment: Apartment?
Output lúc này:
Apartment Novaland is being deinitialized
Khoa dep trai is being deinitialized
Tương tự, bạn tự vẽ data model nhé.
Unowned Reference
unowned reference cũng giống như weak reference. ARC chỉ giữ lại instance có strong reference >= 1. Còn instance có unownd reference hay weak reference đều bị xóa
Điểm khác là:
Một instance A unowned reference ( trỏ ) đến một instance B khi mà instance B đó có vòng đời bằng hoặc dài hơn instance A
Là sao? Hãy cùng xem ví dụ sau: Có 2 class và Customer và CreditCard mô phỏng lại ứng dụng ngân hàng:
class Customer {
let name: String
weak var card: CreditCard?
init(name: String) {
self.name = name
}
deinit {
print("\(name) is being deinitialized")
}
}
class CreditCard {
let number: Int
unowned let customer: Customer
init(number: Int, customer: Customer) {
self.number = number
self.customer = customer
}
deinit {
print("Card #\(number) is being deinitialized")
}
}
var john: Customer?
john = Customer(name: "John Appleseed")
john!.card = CreditCard(number: 123456789, customer: john!)
john = nil
Vẽ data model cho dễ nhìn:
Một Customer có thể có CreditCard, tuy nhiên CreditCard chỉ tồn tại khi nó gắn với một Customer.
Instance của CarditCard có vòng đời ngắn hơn instance của Customer là cái chắc. ( A trỏ đến B mà vòng đời B dài hơn hoặc bằng A. A là CreditCard B là Customer )
Thực ra ví dụ này từ Official Guide của Apple. Nó không rõ ràng lắm lại vì trong trường hợp này xài weak cũng được mà. Cùng tìm hiểu thêm về memory managment trong Closure để phân biệt rõ weak với unowned nhé
Memory managment trong Closure
Closure cũng như class là reference types.
Trong một class, nếu một property là closure và trong closure đó lại dùng property/method của class nữa ( xài self.property ) thì sẽ xảy ra "hiện tượng" strong reference cycle như những ví dụ ở trên.
Thôi trăm nghe không bằng một thấy, hãy nhìn qua ví dụ tính Fibonacci dưới đây:
class Fibonacci {
var value: Int
init(value: Int) {
self.value = value
}
// Ví dụ với strong reference cycle
lazy var fibonacci: () -> Int = {
var a = 0
var b = 1
for _ in 0..<self.value {
let temp = a
a = b
b = temp + b
}
return a
}
// Ví dụ với weak reference
lazy var fibonacciWeak: () -> Int = {
[weak self] in
var a = 0
var b = 1
// lúc này self có thể nil, nên phải check optional
guard let max = self?.value else {
return 0 // return 0 nếu self là nil
}
for _ in 0..<max {
let temp = a
a = b
b = temp + b
}
return a
}
// Ví dụ với unowned reference
lazy var fibonacciUnowned: () -> Int = {
[unowned self] in
var a = 0
var b = 1
// xài unowned lúc này self.value không thể nil được vì vòng đời closure bằng với class
for _ in 0..<self.value {
let temp = a
a = b
b = temp + a
}
return a
}
deinit {
print("Fibonacci instance is being deinitialized")
}
}
Từ những ví dụ trên, ta có thể rút ra kết luận sau:
- Unowned thì không thể nil được vì vòng đời cái instance trỏ đi bằng với cái instance nó trỏ đến
- Weak ngược lại có thể nil, suy ra không thể là hằng được.
Nguồn: Raywenderlich
Strong reference cycle trong iOS
Để tránh strong reference cycle, iOS dùng cơ chế ARC này ở nhiều chỗ.
Dễ thấy nhất là @IBOutlet:
@IBOutlet weak var tableView: UITableView!
và delegate:
Ví dụ homeVC có một tableView:
Đa số delegate nên xài weak vì delegate có thể có hoặc không có.
Thêm một trường hợp hay gặp nữa là: Ví dụ a có thuộc tính delegate tới b, b có thuộc tính delegate tới c.
Nếu để strong reference thì nếu muốn hủy b, c sẽ không hủy được b. Vì b còn strong reference tới a.
What's next:
Còn một số cái như unsafe unowned reference mình chưa đề cập. Tuy nhiên với kĩ năng vẽ data model, bạn có thể đọc document trên Apple, vẽ lại data model và hy vọng nó giúp bạn dễ hiểu những gì đang diễn ra hơn.
Một điểm nữa, document của Apple dùng ví dụ khá trực quan và thực tế, không phải kiểu Foo, Bar như những document khác. Bạn nên đọc lại phần này vài lần để củng cố lại kiến thức nhé.
Resources:
Related Posts
Discover more content you might enjoy
![Học Javascript 7: [ES6] Phân biệt var, let và const](/_next/image?url=https%3A%2F%2Fres.cloudinary.com%2Fkhoanguyen1505%2Fimage%2Fupload%2Fv1751211000%2Fkhoa_blog%2Fjavascript_es6_var_let_const.jpg&w=828&q=75)
Học Javascript 7: [ES6] Phân biệt var, let và const
Tìm hiểu về sự khác biệt giữa var, let và const trong JavaScript ES6, cách hoạt động của block scope và các quy tắc sử dụng

Học Javascript 6: Scope Chain là gì?
Tìm hiểu về Scope Chain trong JavaScript, cách hoạt động của Outer Environment và cách biến được tìm kiếm trong các Execution Context

Học Javascript 5: Execution Stack là gì?
Tìm hiểu về Execution Stack trong JavaScript, cách hoạt động của Execution Context và Variable Environment

Time.delta là gì?
Tìm hiểu về Time.delta trong Unity và cách sử dụng Time.deltaTime để tạo chuyển động mượt mà trong game

Sống ảo
Bài viết chia sẻ góc nhìn cá nhân về hiện tượng 'sống ảo' trên mạng xã hội. Tác giả phân tích bốn khía cạnh tích cực của việc chia sẻ cuộc sống trên nền tảng số: lưu giữ kỷ niệm như một dạng nhật ký, kết nối với người có cùng sở thích, tạo ấn tượng với người khác, và mang lại niềm vui. Bài viết cũng đưa ra lời khuyên về cách sống ảo lành mạnh không ảnh hưởng tiêu cực đến cuộc sống thực.

Day 21 - Profitable MVP in 30 Days - Thử nghiệm early adopter
Bài viết ngày 21 của thử thách xây dựng MVP có lợi nhuận, tác giả giải thích khái niệm early adopter và chia sẻ cách thử nghiệm mô hình này cho ứng dụng ReadingPointer. Bài viết mô tả việc tạo trang giới thiệu phiên bản premium với quyền lợi đặc biệt cho người dùng sớm, cách triển khai nút 'Go Pro' trên trang chủ và ứng dụng, nhằm kiểm tra mức độ sẵn sàng chi trả của người dùng.