Automatic Reference Counting (ARC) trong 10 phút
Code . iOS & SwiftContents
Chào mừng bạn đến với Fx Studio. Chủ đề bài viết lần này là Automatic Reference Counting (ARC). Đây là một trong những điểm lý thuyết cơ bản mà bạn cần phải nắm được. Có thể lúc bắt đầu học Swift hay iOS, thì người học chưa cần phải hiểu. Nhưng khi bạn muốn tiến xa hơn, thì bắt buộc bạn phải nắm vững kiến thức này.
Còn nếu mọi việc đã ổn rồi, thì …
Bắt đầu thôi!
Chuẩn bị
Với chủ đề này, bạn không cần chuẩn bị gì về mặt code demo. Nó mang tính chất lý thuyết thuần túy. Trước hết, bạn cần tìm hiểu:
Khái niệm ARC trong Swift
Automatic Reference Counting (ARC) là một cơ chế quản lý bộ nhớ trong ngôn ngữ lập trình Swift của Apple. ARC tự động giải phóng bộ nhớ mà một đối tượng chiếm giữ khi không còn có tham chiếu nào đến đối tượng đó. Điều này giúp giảm thiểu rủi ro của các lỗi bộ nhớ như rò rỉ bộ nhớ (memory leaks) và trỏ treo (dangling pointers).
ARC hoạt động bằng cách theo dõi số lượng “tham chiếu mạnh” (strong references) đến một đối tượng. Nếu không còn tham chiếu mạnh nào, đối tượng sẽ được giải phóng.
Một tham chiếu mạnh là một tham chiếu mà không cho phép đối tượng được giải phóng miễn là tham chiếu đó vẫn tồn tại.
Tuy ARC tự động quản lý bộ nhớ. Nhưng lập trình viên vẫn cần hiểu rõ về nó. Để tránh các vấn đề như vòng tham chiếu mạnh (strong reference cycles). Nơi mà hai đối tượng hoặc nhiều hơn tham chiếu lẫn nhau mà không giải phóng. Dẫn đến rò rỉ bộ nhớ.
Để giải quyết vấn đề này, Swift cung cấp hai loại tham chiếu khác là:
Tham chiếu yếu (weak references) và Tham chiếu không sở hữu (unowned references).
Non-ARC thì thế nào?
Non-ARC trong Swift không tồn tại.
Swift sử dụng ARC như một phần không thể tách rời của ngôn ngữ để quản lý bộ nhớ. ARC tự động giải phóng bộ nhớ mà một đối tượng chiếm giữ khi không còn có tham chiếu nào đến đối tượng đó.
Trong Objective-C, bạn có thể chọn không sử dụng ARC (non-ARC). Và thay vào đó quản lý bộ nhớ một cách thủ công bằng cách sử dụng các từ khóa như: retain
, release
, và autorelease
. Tuy nhiên, việc này không khả thi trong Swift.
Swift vs. Objective-C
ARC trong Swift và Objective-C hoạt động theo cùng một nguyên tắc cơ bản. Theo dõi số lượng tham chiếu đến một đối tượng. Và giải phóng bộ nhớ của đối tượng khi không còn tham chiếu nào. Tuy nhiên, có một số khác biệt quan trọng:
- Sự rõ ràng: Trong Swift, ARC hoạt động tự động và không cần sự can thiệp của lập trình viên. Trong khi đó, trong Objective-C, lập trình viên cần phải sử dụng các từ khóa như
retain
,release
, vàautorelease
để quản lý bộ nhớ. - Tham chiếu yếu và không sở hữu: Swift cung cấp hai loại tham chiếu khác là “tham chiếu yếu” (weak references) và “tham chiếu không sở hữu” (unowned references) để giải quyết vấn đề vòng tham chiếu mạnh. Trong Objective-C, chỉ có tham chiếu yếu.
- Closures: Swift cung cấp danh sách bắt (capture list) trong closures. Để giải quyết vấn đề vòng tham chiếu mạnh giữa đối tượng và closure. Objective-C không có cơ chế tương tự.
- Optional và Unwrapping: Swift có cơ chế optional và unwrapping để xử lý trường hợp tham chiếu yếu có thể trở thành nil. Trong Objective-C, việc tham chiếu yếu trở thành nil có thể gây ra lỗi runtime.
- ARC là mặc định: Trong Swift, ARC được bật mặc định cho tất cả các đối tượng. Trong Objective-C, ARC không phải lúc nào cũng được bật và lập trình viên cần phải bật nó.
Object lifetime vs. ARC
- Tạo Đối tượng: Khi một thể hiện của một lớp được tạo, một phần bộ nhớ được cấp phát cho nó. Đây là bắt đầu của thời gian tồn tại của đối tượng.
- Sở hữu và Tham chiếu: Swift sử dụng ARC để theo dõi và quản lý việc sử dụng bộ nhớ của ứng dụng. Mỗi lần bạn tạo một tham chiếu đến một đối tượng, ARC tăng số lượng tham chiếu cho đối tượng đó lên 1.
- Giải phóng Tham chiếu: Khi tham chiếu đến một đối tượng không còn cần thiết nữa (ví dụ, nếu tham chiếu ra khỏi phạm vi hoặc được đặt thành
nil
), ARC giảm số lượng tham chiếu cho đối tượng đó đi 1. - Hủy Đối tượng: Khi số lượng tham chiếu của một đối tượng đạt đến 0. ARC giải phóng bộ nhớ được sử dụng bởi đối tượng, đây là kết thúc của thời gian tồn tại của đối tượng. Trước khi bộ nhớ được giải phóng, phương thức hủy của đối tượng được gọi để dọn dẹp bất kỳ tài nguyên nào mà đối tượng đang quản lý.
Điều quan trọng cần lưu ý là ARC chỉ áp dụng cho các thể hiện của class. Struct & Enum là các kiểu giá trị. Và không được lưu trữ và tham chiếu theo cùng một cách như các thể hiện của lớp.
Các vấn đề cần quan tâm
Khi tìm hiểu về Automatic Reference Counting (ARC) trong Swift, bạn nên tìm hiểu về các vấn đề sau:
- Cách hoạt động của ARC
- Tham chiếu mạnh (Strong References)
- Tham chiếu yếu (Weak References)
- Tham chiếu không sở hữu (Unowned References)
- Chu trình tham chiếu mạnh (Strong Reference Cycles)
- Chu trình tham chiếu mạnh giữa đối tượng và closure (Strong Reference Cycles Between Class Instances and Closures)
Cách hoạt động của ARC
ARC hoạt động bằng cách theo dõi số lượng tham chiếu mạnh đến một đối tượng.
- Mỗi khi bạn tạo một tham chiếu mạnh mới đến một đối tượng, ARC tăng số lượng tham chiếu của đối tượng đó lên.
- Khi tham chiếu mạnh đến một đối tượng bị giải phóng, ARC giảm số lượng tham chiếu của đối tượng đó đi.
- Khi số lượng tham chiếu của một đối tượng đạt đến 0, nghĩa là không còn tham chiếu mạnh nào đến đối tượng đó. ARC giải phóng bộ nhớ mà đối tượng đó đang chiếm giữ.
Điều này có nghĩa là:
“Bạn không cần phải nghĩ về việc giải phóng bộ nhớ mỗi khi bạn tạo một đối tượng”.
Thay vào đó, bạn chỉ cần quản lý số lượng tham chiếu mạnh đến đối tượng. Dưới đây là một ví dụ về cách ARC hoạt động trong Swift:
class MyClass { var name: String init(name: String) { self.name = name print("\(name) is being initialized") } deinit { print("\(name) is being deinitialized") } } var reference1: MyClass? = MyClass(name: "My Class Instance") var reference2 = reference1 var reference3 = reference1 reference1 = nil reference2 = nil // Prints "My Class
Trong ví dụ trên,
- Chúng ta tạo một lớp
MyClass
với một thuộc tínhname
và hai phương thứcinit
vàdeinit
. - Phương thức
init
được gọi khi một thể hiện của lớp được khởi tạo. -
deinit
được gọi khi thể hiện đó được giải phóng.
Chúng ta sau đó tạo:
- ba tham chiếu đến một thể hiện của
MyClass
:reference1
,reference2
, vàreference3
. - Khi chúng ta gán
nil
choreference1
vàreference2
, thể hiện củaMyClass
vẫn không được giải phóng, vìreference3
vẫn đang tham chiếu đến nó. - Chỉ khi chúng ta gán
nil
choreference3
, thể hiện củaMyClass
mới được giải phóng, và phương thứcdeinit
được gọi.
Strong References
Trong ngữ cảnh của ARC trong Swift, một tham chiếu mạnh (Strong Reference) là một tham chiếu đến một đối tượng. Mà không cho phép đối tượng đó bị giải phóng bởi hệ thống quản lý bộ nhớ tự động (ARC) miễn là tham chiếu mạnh đó vẫn tồn tại.
Tuy nhiên, tham chiếu mạnh có thể dẫn đến vấn đề vòng tham chiếu mạnh, nơi mà hai đối tượng hoặc nhiều hơn tham chiếu lẫn nhau mà không giải phóng, dẫn đến rò rỉ bộ nhớ.
Dưới đây là một ví dụ về việc sử dụng tham chiếu yếu (weak reference) và tham chiếu không sở hữu (unowned reference) để giải quyết vấn đề vòng tham chiếu mạnh trong Swift:
class Employee { let name: String var department: Department? init(name: String) { self.name = name } deinit { print("\(name) is being deinitialized") } } class Department { let name: String unowned var manager: Employee init(name: String, manager: Employee) { self.name = name self.manager = manager } deinit { print("Department \(name) is being deinitialized") } } var john: Employee? = Employee(name: "John Doe") var engineering: Department? = Department(name: "Engineering", manager: john!) john?.department = engineering john = nil engineering = nil
Trong ví dụ trên, chúng ta tạo hai lớp Employee
và Department
. Mỗi Employee
có thể làm việc cho một Department
, và mỗi Department
có một manager
là một Employee
. Điều này tạo ra một chu trình tham chiếu mạnh giữa hai đối tượng Employee
và Department
.
Điều này được gọi là “Strong Reference Cycle” hoặc “Retain Cycle”.
Để giải quyết vấn đề này, chúng ta sử dụng một tham chiếu không sở hữu (unowned reference) cho thuộc tính manager
của Department
. Điều này có nghĩa là khi Department
bị giải phóng, nó không giữ lại tham chiếu mạnh đến manager
của nó, do đó không ngăn manager
đó được giải phóng.
Khi chúng ta gán nil
cho john
và engineering
, cả hai đối tượng đều được giải phóng, và không có rò rỉ bộ nhớ nào xảy ra.
Weak References
Một tham chiếu yếu (Weak Reference) là một loại tham chiếu không tăng số lượng tham chiếu của một đối tượng. Điều này có nghĩa là:
“Nó không ngăn cản ARC giải phóng đối tượng mà nó tham chiếu khi đối tượng đó không còn tham chiếu mạnh nào khác.”
Tham chiếu yếu thường được sử dụng để tránh vòng tham chiếu mạnh (strong reference cycles). Khi hai đối tượng tham chiếu lẫn nhau.
Một tham chiếu yếu phải được khai báo như một biến optional. Vì nó có thể trở thành nil
bất cứ lúc nào. Khi đối tượng mà nó tham chiếu không còn tồn tại, tham chiếu yếu sẽ tự động trở thành nil
.
Dưới đây là một ví dụ khác về việc sử dụng tham chiếu yếu để tránh vòng tham chiếu mạnh trong Swift:
class Person { let name: String weak var friend: Person? init(name: String) { self.name = name } deinit { print("\(name) is being deinitialized") } } var alice: Person? = Person(name: "Alice") var bob: Person? = Person(name: "Bob") alice?.friend = bob bob?.friend = alice alice = nil bob = nil
Trong ví dụ trên:
- Chúng ta tạo một lớp
Person
với một thuộc tínhfriend
là một tham chiếu yếu đến một đối tượngPerson
khác. - Điều này cho phép chúng ta tạo một mối quan hệ bạn bè giữa hai đối tượng
Person
mà không tạo ra một vòng tham chiếu mạnh.
Khi chúng ta gán nil
cho alice
và bob
, cả hai đối tượng đều được giải phóng, và không có rò rỉ bộ nhớ nào xảy ra.
Unowned References
Tham chiếu không sở hữu không tăng số lượng tham chiếu của một đối tượng. Tuy nhiên, khác với tham chiếu yếu,
- Tham chiếu không sở hữu không phải là optional.
- Không tự động trở thành
ni
l khi đối tượng mà nó tham chiếu bị giải phóng.
Do đó, bạn nên chỉ sử dụng tham chiếu không sở hữu khi bạn chắc chắn rằng đối tượng mà nó tham chiếu sẽ luôn tồn tại trong thời gian mà tham chiếu không sở hữu tồn tại.
Dưới đây là một ví dụ về việc sử dụng tham chiếu không sở hữu (unowned reference) trong Swift:
class Customer { let name: String var card: CreditCard? init(name: String) { self.name = name } deinit { print("\(name) is being deinitialized") } } class CreditCard { let number: Int64 unowned let customer: Customer init(number: Int64, customer: Customer) { self.number = number self.customer = customer } deinit { print("Card #\(number) is being deinitialized") } } var john: Customer? = Customer(name: "John Appleseed") john?.card = CreditCard(number: 1234567890123456, customer: john!) john = nil
Trong ví dụ trên,
- Chúng ta tạo hai lớp
Customer
vàCreditCard
. - Mỗi
Customer
có thể sở hữu mộtCreditCard
, - Mỗi
CreditCard
có một tham chiếu không sở hữu đếnCustomer
của nó.
Khi chúng ta gán nil
cho john
, cả hai đối tượng Customer
và CreditCard
đều được giải phóng, và không có rò rỉ bộ nhớ nào xảy ra.
Strong Reference Cycles (Retain Cycles)
“Strong Reference Cycles” hoặc “Retain Cycles” trong Swift là tình huống mà hai đối tượng hoặc nhiều hơn tham chiếu lẫn nhau thông qua tham chiếu mạnh, tạo ra một chuỗi tham chiếu không bao giờ giải phóng.
Điều này dẫn đến rò rỉ bộ nhớ.
Vì ARC không thể giải phóng bất kỳ đối tượng nào trong chuỗi tham chiếu, ngay cả khi chúng không còn được sử dụng nữa. Dưới đây là một ví dụ về Strong Reference Cycles trong Swift:
class ClassA { var b: ClassB? deinit { print("ClassA is being deinitialized") } } class ClassB { var a: ClassA? deinit { print("ClassB is being deinitialized") } } var instanceA: ClassA? = ClassA() var instanceB: ClassB? = ClassB() instanceA?.b = instanceB instanceB?.a = instanceA instanceA = nil instanceB = nil
Trong ví dụ trên,
ClassA
có một tham chiếu mạnh đếnClassB
vàClassB
có một tham chiếu mạnh đếnClassA
.- Tạo ra một vòng tham chiếu mạnh.
- Khi chúng ta gán
nil
choinstanceA
vàinstanceB
, cả hai đối tượng không được giải phóng, vì mỗi đối tượng vẫn còn được tham chiếu mạnh từ đối tượng kia. Điều này dẫn đến rò rỉ bộ nhớ.
Retain Cycles và ảnh hưởng tới hệ thống
Hậu quả của việc này là:
- Bộ nhớ không được giải phóng: Khi một đối tượng không còn được sử dụng, nó nên được giải phóng để giải phóng bộ nhớ. Tuy nhiên, trong một Retain Cycle, các đối tượng không thể được giải phóng. Dẫn đến việc bộ nhớ không được giải phóng.
- Tăng tiêu thụ bộ nhớ: Khi các đối tượng không được giải phóng. Chúng tiếp tục chiếm bộ nhớ, dẫn đến việc tiêu thụ bộ nhớ tăng lên.
- Hiệu suất ứng dụng giảm: Khi bộ nhớ bị chiếm dụng bởi các đối tượng không còn được sử dụng. Có ít bộ nhớ hơn cho các hoạt động khác, có thể làm giảm hiệu suất của ứng dụng.
- Ứng dụng có thể bị crash: Nếu ứng dụng tiếp tục tạo ra các đối tượng mà không giải phóng chúng. Ứng dụng có thể sử dụng hết bộ nhớ. Và cuối cùng bị hệ điều hành tắt để giải phóng bộ nhớ.
Strong Reference Cycles Between Class Instances and Closures
Xảy ra khi một closure nắm giữ một tham chiếu mạnh đến một đối tượng. Và đối tượng đó cũng nắm giữ một tham chiếu mạnh đến closure đó …
Tạo ra một chu trình tham chiếu mạnh không thể giải phóng.
Điều này thường xảy ra khi bạn gán một closure cho một thuộc tính của đối tượng. Và closure đó truy cập đến self
(đối tượng chứa nó). Khi đó, closure sẽ nắm giữ một tham chiếu mạnh đến self
, tạo ra một chu trình tham chiếu mạnh.
Nổ cái não!
Để giải quyết vấn đề này, bạn có thể sử dụng [weak self]
hoặc [unowned self]
trong danh sách nắm giữ (capture list) của closure để tạo một tham chiếu yếu hoặc không sở hữu đến self
. Giúp ngăn chặn việc tạo ra chu trình tham chiếu mạnh.
Dưới đây là một ví dụ về chu trình tham chiếu mạnh giữa đối tượng và closure trong Swift:
class MyClass { var value = 0 lazy var closure: () -> () = { self.value += 1 } deinit { print("MyClass instance is being deinitialized") } } var instance: MyClass? = MyClass() instance?.closure() instance = nil
Trong ví dụ trên,
closure
nắm giữ một tham chiếu mạnh đếnself
(đối tượngMyClass
).- Vì nó truy cập vào thuộc tính
value
củaself
. - Đồng thời,
self
cũng nắm giữ một tham chiếu mạnh đếnclosure
, vìclosure
là một thuộc tính củaself
.
Điều này tạo ra một chu trình tham chiếu mạnh.
Khi chúng ta gán nil
cho instance
, đối tượng MyClass
không được giải phóng. Vì closure
vẫn nắm giữ một tham chiếu mạnh đến nó.
Điều này dẫn đến rò rỉ bộ nhớ.
Để giải quyết vấn đề này, chúng ta có thể sử dụng [weak self]
hoặc [unowned self]
trong danh sách nắm giữ (capture list) của closure:
class MyClass { var value = 0 lazy var closure: () -> () = { [weak self] in self?.value += 1 } deinit { print("MyClass instance is being deinitialized") } } var instance: MyClass? = MyClass() instance?.closure() instance = nil
Bây giờ, khi chúng ta gán nil
cho instance
, đối tượng MyClass
được giải phóng, và không có rò rỉ bộ nhớ nào xảy ra.
Tạm kết
Automatic Reference Counting (ARC) là một cơ chế quản lý bộ nhớ tự động trong Swift. ARC hoạt động bằng cách theo dõi và quản lý số lượng tham chiếu mạnh đến một đối tượng.
Điểm chú ý:
- ARC chỉ áp dụng cho các đối tượng của lớp. Không áp dụng cho các kiểu dữ liệu giá trị như struct hay enum.
- ARC tự động giải phóng một đối tượng khi không còn tham chiếu mạnh nào đến đối tượng đó.
- ARC không thể giải phóng một đối tượng, nếu có một chu trình tham chiếu mạnh.
Hậu quả:
- Rò rỉ bộ nhớ: Khi có một chu trình tham chiếu mạnh, các đối tượng trong chu trình không thể được giải phóng. Dẫn đến rò rỉ bộ nhớ.
- Tiêu thụ bộ nhớ tăng lên: Khi các đối tượng không được giải phóng. Chúng tiếp tục chiếm bộ nhớ, dẫn đến việc tiêu thụ bộ nhớ tăng lên.
- Hiệu suất ứng dụng giảm: Khi bộ nhớ bị chiếm dụng bởi các đối tượng không còn được sử dụng. Thì sẽ có ít bộ nhớ hơn cho các hoạt động khác. Và có thể làm giảm hiệu suất của ứng dụng.
Cách khắc phục:
- Sử dụng tham chiếu yếu (weak reference) hoặc tham chiếu không sở hữu (unowned reference). Để ngăn chặn chu trình tham chiếu mạnh.
- Trong các closure, sử dụng danh sách nắm giữ (capture list) với
[weak self]
hoặc[unowned self]
để ngăn chặn chu trình tham chiếu mạnh giữa đối tượng và closure.
Okay! Tới đây, mình xin kết thúc bài viết giới thiệu về Automatic Reference Counting. Nếu có gì thắc mắc hay góp ý cho mình. Thì bạn có thể để lại bình luận hoặc gửi email theo trang Contact.
Cảm ơn bạn đã đọc bài viết này!
Related Posts:
Written by chuotfx
Hãy ngồi xuống, uống miếng bánh và ăn miếng trà. Chúng ta cùng nhau đàm đạo về đời, về code nhóe!
Leave a Reply Cancel reply
Fan page
Tags
Recent Posts
- Prompt Engineering trong 10 phút
- Một số ví dụ sử dụng Prompt cơ bản khi làm việc với AI
- Prompt trong 10 phút
- Charles Proxy – Phần 1 : Giới thiệu, cài đặt và cấu hình
- Complete Concurrency với Swift 6
- 300 Bài code thiếu nhi bằng Python – Ebook
- Builder Pattern trong 10 phút
- Observer Pattern trong 10 phút
- Memento Pattern trong 10 phút
- Strategy Pattern trong 10 phút
You may also like:
Archives
- December 2024 (3)
- September 2024 (1)
- July 2024 (1)
- June 2024 (1)
- May 2024 (4)
- April 2024 (2)
- March 2024 (5)
- January 2024 (4)
- February 2023 (1)
- January 2023 (2)
- November 2022 (2)
- October 2022 (1)
- September 2022 (5)
- August 2022 (6)
- July 2022 (7)
- June 2022 (8)
- May 2022 (5)
- April 2022 (1)
- March 2022 (3)
- February 2022 (5)
- January 2022 (4)
- December 2021 (6)
- November 2021 (8)
- October 2021 (8)
- September 2021 (8)
- August 2021 (8)
- July 2021 (9)
- June 2021 (8)
- May 2021 (7)
- April 2021 (11)
- March 2021 (12)
- February 2021 (3)
- January 2021 (3)
- December 2020 (3)
- November 2020 (9)
- October 2020 (7)
- September 2020 (17)
- August 2020 (1)
- July 2020 (3)
- June 2020 (1)
- May 2020 (2)
- April 2020 (3)
- March 2020 (20)
- February 2020 (5)
- January 2020 (2)
- December 2019 (12)
- November 2019 (12)
- October 2019 (19)
- September 2019 (17)
- August 2019 (10)