Giới thiệu
Nhớ những ngày mà các lập trình viên iOS phải dành một nửa thời gian để đếm các lệnh retain và release? Nếu bạn không nhớ, hãy tự chúc mừng mình. Hôm nay, chúng ta sẽ thảo luận về ARC (Automatic Reference Counting) – công nghệ đã giải phóng chúng ta khỏi cơn ác mộng quản lý bộ nhớ thủ công và thay đổi hoàn toàn cách viết ứng dụng iOS.
Thời Kỳ Tối Tăm: Trước ARC
Hãy tưởng tượng: Năm 2010. Bạn đang xây dựng một ứng dụng iOS, và mã của bạn trông như thế này:
objc
- (void)doSomething {
NSString *myString = [[NSString alloc] initWithString:@"Xin chào"];
[self processString:myString];
[myString release]; // Phải cân bằng mỗi alloc với release
// Quên release này? Rò rỉ bộ nhớ.
// Release thừa? Crash.
}
Một lệnh release bị thiếu? Rò rỉ bộ nhớ. Một lệnh release thừa? Crash. Thú vị đúng không?
Quản lý tham chiếu thủ công (MRC) giống như việc tung hứng với cưa máy – có thể làm được, nhưng chỉ cần một sai sót là bạn gặp rắc rối. Các lập trình viên đã dành vô số giờ để gỡ lỗi các vòng giữ, phân tích cảnh báo của trình phân tích tĩnh và tranh luận về việc autorelease có phải là một phước lành hay một lời nguyền.
Giới thiệu ARC: Cuộc Cách Mạng
Apple đã giới thiệu ARC với iOS 5 và Xcode 4.2 vào tháng 10 năm 2011, và đó không chỉ là một cải tiến nhỏ – đó là một sự thay đổi mô hình. Ý tưởng rất đơn giản: "Điều gì sẽ xảy ra nếu trình biên dịch có thể xử lý tất cả các lệnh retain và release cho bạn?"
Nhưng đây là điều mà hầu hết mọi người không nhận ra: ARC không phải là thu gom rác. Không có chi phí thời gian chạy, không có luồng thu gom rác chạy trong nền. Nó vẫn là đếm tham chiếu, chỉ là tự động hóa tại thời điểm biên dịch. Trình biên dịch phân tích mã của bạn và chèn chính xác các lệnh retain và release mà bạn sẽ viết thủ công (nhưng không có sai sót).
Hãy nghĩ về ARC như có một đồng nghiệp rất tỉ mỉ theo sát bạn, dọn dẹp mã quản lý bộ nhớ của bạn. Chỉ khác là đồng nghiệp này không bao giờ mệt mỏi, không bao giờ mắc sai lầm và làm việc tại thời điểm biên dịch nên không có chi phí thời gian chạy.
Cách ARC Hoạt Động
Hãy phân tích điều này với một ví dụ thực tế:
swift
func createUser() {
let user = User(name: "John") // ARC: retain count = 1
let sameUser = user // ARC: retain count = 2
processUser(user) // Truyền vào hàm, vẫn được giữ lại
} // Kết thúc phạm vi: ARC phát hành cả hai tham chiếu, retain count = 0, đối tượng bị giải phóng
Dưới đây là những gì đang diễn ra:
- Tạo đối tượng: Khi bạn tạo một đối tượng, ARC gán cho nó một giá trị đếm giữ là 1.
- Gán giá trị: Khi bạn gán đối tượng đó cho một biến khác, giá trị đếm giữ tăng lên.
- Kết thúc phạm vi: Khi các biến ra khỏi phạm vi, giá trị đếm giữ giảm xuống.
- Giải phóng: Khi giá trị đếm giữ đạt 0, đối tượng ngay lập tức bị giải phóng.
Nghe có vẻ đơn giản đúng không? Chà... hầu như như vậy.
Những Điều Có Thể Khiến Bạn Gặp Rắc Rối
1. Vòng Giữ Cổ Điển
Đây là vấn đề lớn. Rắc rối đến sự nghiệp. Lỗi mà có thể xuất hiện trong sản phẩm.
swift
class ViewController: UIViewController {
var closure: (() -> Void)?
override func viewDidLoad() {
super.viewDidLoad()
// ĐIỀU NÀY TẠO RA MỘT VÒNG GIỮ
closure = {
self.view.backgroundColor = .red
}
}
}
Điều gì đang xảy ra ở đây? ViewController sở hữu closure (tham chiếu mạnh), và closure giữ lại self (một tham chiếu mạnh khác). Chúng đang giữ nhau sống mãi mãi. Giống như hai người trong một bể bơi, mỗi người giữ đầu người kia trên mặt nước – không ai chết đuối, nhưng cũng không ai có thể rời đi cả.
Cách khắc phục:
swift
closure = { [weak self] in
self?.view.backgroundColor = .red
}
Tham chiếu [weak self] là lối thoát của bạn. Nó nói với closure: "Này, bạn có thể tham chiếu đến ViewController này, nhưng đừng giữ nó sống chỉ vì bạn."
2. Cạm Bẫy Unowned
"Unowned giống như weak nhưng không có tùy chọn!" Không. Dừng lại. Cách nghĩ này sẽ làm bạn gặp rắc rối.
swift
class Child {
unowned let parent: Parent // Tôi 100% chắc rằng parent sẽ sống lâu hơn child
func doSomething() {
parent.update() // Nếu parent đã biến mất, điều này sẽ gây crash
}
}
Unowned là một lời hứa với trình biên dịch: "Tham chiếu này sẽ KHÔNG BAO GIỜ là nil khi tôi sử dụng nó." Nếu bạn phá vỡ lời hứa đó, ứng dụng của bạn sẽ bị crash. Sử dụng unowned chỉ khi bạn có một mối quan hệ cha-con mà child thực sự không thể sống lâu hơn parent.
Ví dụ an toàn trong thực tế:
swift
class HTMLElement {
let name: String
let text: String?
lazy var asHTML: () -> String = { [unowned self] in
// An toàn vì asHTML không thể tồn tại mà không có HTMLElement
if let text = self.text {
return "<\(self.name)>\(text)</\(self.name)>"
} else {
return "<\(self.name) />"
}
}
init(name: String, text: String? = nil) {
self.name = name
self.text = text
}
}
3. Nhầm Lẫn với Collection
swift
class Node {
var children: [Node] = [] // Các tham chiếu mạnh đến tất cả các con
weak var parent: Node? // Tham chiếu yếu đến cha
func addChild(_ child: Node) {
children.append(child)
child.parent = self // Đúng: cha-con không có vòng giữ
}
}
Các Collection (mảng, từ điển, tập hợp) giữ tham chiếu mạnh theo mặc định khi các phần tử đó là các kiểu tham chiếu (class). Nếu bạn đang xây dựng cây hoặc đồ thị, bạn cần suy nghĩ cẩn thận về những tham chiếu nào nên là yếu để tránh vòng giữ.
4. Cạm Bẫy NotificationCenter/Timer
swift
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// SAI: Tạo ra một vòng giữ với Timer
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
self.updateUI() // Tham chiếu mạnh đến self!
}
// ĐÚNG: Sử dụng weak self
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] timer in
self?.updateUI()
}
// Hoặc thậm chí tốt hơn trong iOS 10+: Không có vòng giữ!
NotificationCenter.default.addObserver(
self,
selector: #selector(handleNotification),
name: .someNotification,
object: nil
)
// Điều này tự động được dọn dẹp khi self bị giải phóng
}
}
Mẹo Thực Tế Để Tránh Rò Rỉ Bộ Nhớ
1. Mẫu Khiêu Vũ Yếu-Mạnh
Đây là điều bạn cần:
swift
someAsyncOperation { [weak self] in
guard let self = self else { return }
// Sử dụng self ở khắp nơi trong closure này - bây giờ được giữ mạnh
self.doThis()
self.doThat()
}
2. Sử Dụng Instruments, Không Đoán
Ngừng đoán nơi có vòng giữ của bạn. Mở Instruments, sử dụng công cụ Leaks, và xem Debugger Đồ Thị Bộ Nhớ (biểu tượng mũi tên nhỏ trong thanh gỡ lỗi của Xcode). Những công cụ này sẽ cho bạn biết chính xác cái gì đang giữ cái gì sống.
3. Nghĩ Trong Các Đồ Thị Đối Tượng
Trước khi bạn viết mã, hãy phác thảo mối quan hệ giữa các đối tượng. Hãy hỏi bản thân:
- Ai sở hữu ai?
- Điều này có thể tạo ra một vòng giữ không?
- Điều gì sẽ xảy ra khi người dùng điều hướng đi nơi khác?
4. Delegates Nên Hầu Như Luôn Là Weak
swift
protocol MyDelegate: AnyObject { // AnyObject = giao thức chỉ cho class
func didSomething()
}
class MyClass {
weak var delegate: MyDelegate? // YẾU!
}
5. Biết Ngữ Cảnh Closure Của Bạn
Không phải mọi closure đều cần [weak self]. Nếu một closure là không thoát hoặc bạn muốn giữ self sống cho đến khi hoàn thành, thì các tham chiếu mạnh là tốt:
swift
// Closure không thoát - không cần weak self
UIView.animate(withDuration: 0.3) {
self.view.alpha = 0 // Điều này là tốt!
}
// Các phép toán mảng - không thoát
let names = users.map { user in
return self.formatName(for: user) // Cũng tốt!
}
// Nhưng cẩn thận với các closure lưu trữ hoặc các phép toán bất đồng bộ
networkManager.onCompletion = { [weak self] result in
self?.handleResult(result) // Điều này cần yếu!
}
Tham Khảo Nhanh: Khi Nào Sử Dụng Gì
Sử dụng weak khi:
- Tạo delegates
- Self được giữ trong một closure mà self sở hữu
- Bạn không chắc chắn về thời gian sống của đối tượng
- Phá vỡ vòng giữ
Sử dụng unowned khi:
- Bạn 100% chắc chắn rằng tham chiếu sẽ không bao giờ là nil
- Ví dụ điển hình: Một closure không thể sống lâu hơn chủ sở hữu của nó
- Bạn đã phân tích và cần tăng hiệu suất nhỏ đó
Sử dụng mạnh (mặc định) khi:
- Bạn muốn giữ một cái gì đó sống
- Cha sở hữu con
- Tham chiếu tạm thời trong các hàm
- Các closure không thoát
Tư Duy Hiện Đại Về ARC
Điều quan trọng là: ARC không phải là thứ bạn chống lại hay phải làm việc quanh. Đó là một công cụ mà, khi được hiểu, làm cho mã của bạn vừa an toàn vừa sạch sẽ hơn. Chìa khóa là thay đổi mô hình tư duy của bạn từ "quản lý bộ nhớ" thành "quản lý mối quan hệ."
Mỗi tham chiếu mạnh là một mối quan hệ nói rằng "Tôi cần bạn tồn tại." Mỗi tham chiếu yếu nói rằng "Tôi muốn nói chuyện với bạn nếu bạn có mặt." Mỗi tham chiếu unowned nói rằng "Bạn phải ở đó khi tôi gọi."
Những Hiểu Lầm Thường Gặp Về ARC
- "ARC có nghĩa là không có rò rỉ bộ nhớ" - Sai. ARC ngăn bạn quên giải phóng đối tượng, nhưng các vòng giữ vẫn có thể gây ra rò rỉ.
- "Tham chiếu yếu thì chậm" - Không đáng kể trong 99% trường hợp. Chúng sử dụng tra cứu bảng bên, nhưng trừ khi bạn đang truy cập chúng hàng nghìn lần mỗi giây, bạn sẽ không nhận thấy.
- "Unowned là nguy hiểm, đừng bao giờ sử dụng" - Nó có chỗ đứng của nó. Khi được sử dụng đúng cách, nó hoàn toàn an toàn.