Đa số với mỗi lập trình viên đều đã gặp phải những vấn đề về memory leaks.
Ở bài viết này, mình sẽ đi sâu vào memory Leaks và cách xử lí.
Bài viết này đòi hỏi sự hiểu biết về weak/strong reference, retain cycle và ARC trong swift.
Contents:
- Memory leaks là gì?
- Xử lí memory leaks bằng weak/unowned
- Non-escaping closure vs escaping closure
- Delay Deallocation
- Optional self vs Unwrapped self
- Example
Memory leaks là gì?
- Memory leaks là 1 phần bộ nhớ bị chiếm vĩnh viễn và không được giải phóng mặc dù không cần dùng đến -> Dẫn đến không thể tái sử dụng phần bộ nhớ này.
- Thường xảy ra do retain cycle.
Memory leaks gây ra những gì:
- memory của ứng dụng tăng cao không cần thiết -> dẫn đến memory warning và có thể crash.
- Những object bị leaks sẽ không bị hủy bỏ -> object đó sẽ luôn lắng nghe thông báo và sẽ thực hiện phản ứng mỗi khi nhận thông báo -> Dẫn đến sai lệch kết quả, có thể đặc biệt nghiêm trọng nếu ảnh hưởng đến database.
Memory leaks demo
Khởi tạo 2 View Controller như sau:
- VC1 có 1 button để push sang VC2.
- VC2 có hàm deinit để print ra "VC2 was deallocate from memory" khi VC2 được giải phóng bộ nhớ.
Build thử, tap vào button để push sang VC2, và tap vào nút back để quay lại VC1. Quan sát màn hình console, có thể thấy không có gì được print ra.
Ở đây, VC2 không được giải phóng bộ nhớ bởi VC2 giữ 1 strong reference đến closure completion, closure completion cũng giữ 1 strong reference đến VC2 -> Tạo ra 1 retain cycle -> Memory leaks.
Xử lí memory leaks bằng weak/unowned
Cách giải quyết mà mọi người thường dùng nhất là sử dụng weak/unowned.
Vậy sự khác biệt giữa weak và unowned là gì?
- Giống: Weak và unonwed tạo ra 1 weak reference thay vì 1 strong reference để loại bỏ retain cycle -> Bộ nhớ sẽ dc giải phóng ngay lập tức khi không cần dùng đến.
- Khác:
Weak | Unowned |
– Có thể nil | Không thể nil |
Tuy nhiên, unowned cũng giống như việc force unwrapping self và cố gắng truy cập đến contents của nó ngay cả sau khi self đã được giải phóng -> dẫn đến crash.
Vì vậy ta thường thấy weak self được sử dụng nhiều hơn. Thay đoạn code viewDidLoad ở VC2 bằng:
override func viewDidLoad() {
super.viewDidLoad()
completion = { [weak self] in
self?.view.backgroundColor = .red
}
}
Build và chạy thử. Sau khi pop từ VC2 về VC1, màn hình console đã hiện "VC2 was dellocated from memory" -> Memory leaks đã được giải quyết.
Tuy nhiên câu hỏi ở đây là, liệu có cần sử dụng weak cho mọi closure?
Non-escaping closure vs escaping closure
Để trả lời câu hỏi trên, có 1 vài điều trước hết bạn cần phải biết.
- non-escaping closure (ví dụ higher-order functions như compactMap): Được thực hiện trong 1 phạm vi thân hàm nhất định, được thực hiện ngay lập tức và sau khi thực hiện thì được giải phóng, không được lưu lại.
- escaping closure: Được lưu lại, có thể truyền đi như 1 biến, và có thể được thực hiện lại vào 1 thời điểm khác trong tương lai.
Closure sẽ tạo ra 1 strong reference đối với những thứ được đóng gói bên trong closure, trong ví dụ ở đây là self.
- Đối với non-escaping closure: strong reference này sẽ chỉ tồn tại trong thời gian closure đó dc thực hiện -> Khi closure thực hiện xong, strong reference biến mất -> self không còn strong reference nào trỏ đến nên được giải phóng khỏi bộ nhớ.
- Đối với escaping closure: Nếu escaping closure này đóng gói 1 self bên trong, thì strong reference này sẽ tồn tại mãi mãi -> tạo ra retain cycle. -> self không được giải phóng.
Demo 2: Thay đoạn code viewDidLoad của VC2 thành và run thử.
override func viewDidLoad() {
super.viewDidLoad()
let nonEscapingClosure = {
self.view.backgroundColor = .red
}
}
Ở đây bạn tạo ra 1 non-escaping closure, nó tạo ra 1 strong reference tới self, khi kết thúc thân hàm, strong reference này sẽ biến mất -> Không bị retain cycle. Vì vậy, không cần dùng weak self trong trường hợp này.
Delay Deallocation
Gỉa sử có 1 func download image từ internet như sau:
Ở đây URLSession.shared.dataTask là 1 non-escaping closure cần nhiều thời gian để chạy.
Non-escaping closure không yêu cầu bạn phải dùng weak self để tránh retain cycle, tuy nhiên với những closure cần nhiều thời gian chạy như trên, thì sẽ tạo ra 1 khoảng thời gian delay trước khi VC2 được deallocate rất lớn -> Nếu ng dùng push và pop vào VC2 liên tục thì vẫn tạo ra tăng memory.
Note: Cân nhắc việc sử dụng weak self đối với non-escaping closure!
Optional self vs Unwrapped self
Dùng weak self sẽ làm cho self thành kiểu optional. Khi đó sẽ thường có 2 kiểu giải quyết:
- Dùng optional self: self?
- Dùng unwrapped self
Vậy 2 cách này có khác gì nhau?
- Khi unwrapped self, thì sẽ chỉ kiểm tra xem self có tồn tại 1 lần duy nhất ở đầu thân hàm, nếu self khác nil thì sẽ tạo 1 strong reference tồn tại trong thời gian chạy hàm. Khi hàm kết thúc, strong reference biến mất, khi đó VC2 mới được deallocated mặc dù đã pop về VC1. -> Vẫn tạo ra 1 delay deallocated, không có retain cycle.
- Dùng self? thì sẽ check mỗi lần gọi đến self. Nếu self đã dc giải phóng khỏi bộ nhớ thì trình biên dịch sẽ bỏ qua dòng code đó. -> Không tạo ra delay deallocated, không có retain cycle.
Examples
Grand Central Dispatch
- GCD được khởi tạo và thực hiện ngay lập tức, không được lưu lại nên là non-escaping closure, không cần dùng weak self.
-> Đó là lí do không cần dùng weak self ở main.async
DispatchQueue.main.async {
self.view.backgroundColor = .yellow
}
UIView.Animate
UIView.animate(withDuration: 0, animations: {
self.view.backgroundColor = .yellow
}, completion: nil)
Tương tự như GCD, closure trong UIView.animate cũng là 1 non-escaping closure nên không cần weak self.
Timer
let _ = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { (_) in
self.view.backgroundColor = .yellow
}
Timer sẽ tạo ra 1 strong reference và retain cycle nếu thỏa mãn 2 điều kiện:
- Timer chạy lặp lại liên tục.
- Timer capture lại self trong block closure
-> Nếu thoả mãn 2 điều kiện trên thì phải dùng weak self đối với Timer.