Hello guys! Chúng ta code ios mỗi ngày, và chắc hẳn nhiều thanh niên giống mình ít khi check memory của app khi sử dụng để tìm hiểu những vấn đề bất thường của app. Trong ví dụ hôm nay chúng ta sẽ tự tay mình tạo ra leak để hiểu bản chất nó là gì, thay vì nghe các ông chém gió suông đọc xong rồi không hiểu gì cả.
Hình ảnh demo:
Đầu tiên bạn tải source code ở đây:
https://github.com/codetoanbug/LeakMemorySamples.git
Code ở branch bai1 nên bạn cần chuyển source về nhánh này nha. Cách chuyển xem phần mình pink sau:
Tuy nhiên tôi khuyên bạn chơi với terminal của MacBook cho nó pro nhé. Vì nếu bạn tải chay về bằng trình duyệt thì không thấy source code ở đâu đâu :v
Chơi với terminal đơn giản như sau:
Bạn gõ lệnh cd vào source code hôm trước bạn tải về:cd LeakMemorySample
2. Tiếp theo là gõ fetch để lấy source code mới nhất của tôi về:git fetch
3. Tiếp theo là bạn show toàn bộ branch trên repo của tôi bằng lệnh:git branch -a
Ở đây bạn sẽ thấy các branch sau:bai1 * bai2 master
Ví dụ bài này, tôi để hết source vào branch tên là bai2. Bạn chuyển sang source code bài 2 như sau:git checkout bai
1
Sau khi branch bai1 được bôi đậm chuyển màu nghĩa là bạn đã thành công rồi đó. Còn nếu nó báo không thấy thì chứng tỏ bạn làm sai, hãy làm lại!
Tôi sẽ giải thích từng phần 1.
- Màn hình thứ nhất: ViewController.
Tôi lười nên tôi lấy mặc định file do Xcode tạo luôn. Bạn vào file Main.storyboard để xem cách tôi layout kéo thả các thứ 😂
Màn hình này chả có gì cả, ngoài cái ảnh logo web, 1 cái nút để mở sang màn thứ 2, nơi chứa leak memory.
2. Màn hình thứ 2: SecondViewController.
Màn này code như sau:
Tôi giải thích đoạn code trên:
Dòng 19 – tôi gọi hàm bindViewModel để kết nối viewmodel với view. Tôi thích làm MVVM cho code clean. Nên nếu bạn không hiểu về MVVM là cái gì, thì vui lòng tìm tag MVVM ở web này để hiểu thêm.
Từ dòng 23:
Tôi tạo viewModel ở đây. Ở dòng 27, tôi có 1 hàm closure. Hàm này làm cái gì? Nó chứa đoạn code 28(hoặc 29) set lại màu của background. Như ảnh thì tôi đang comment cái dòng 27 đằng sau, và dòng 28. Nếu bạn không biết gì về closure, thì hiểu nôm na nó là cái hàm chạy vào 1 thời điểm nào đó từ view model phát ra. Nó không chạy luôn ngay lúc khai báo đâu nha. Giống như bạn tạo 1 cái lịch biểu, rằng nếu như người yêu mà hôn bạn thì bạn làm cái gì đó 😘 Hôn ở đây là hàm needClosure, còn làm gì đó là dòng 28(hoặc 29).
Dòng 33, tôi tạo 1 hàm fake request API, có thời gian delay, và cuối cùng giả sử khi có kết quả thì nó sẽ call vào closure tôi nói ở trên.
Rồi vậy viewModel tôi có gì nào? Theo dõi code sau:
Ở dòng 11, tôi tạo 1 biến closure để bắn sự kiện ra khi API có kết quả trả về.
Dòng 12 tôi tạo 1 timer fake timeout của API.
Dòng 13 tôi tạo 1 biến lưu mảng số nguyên data.
Dòng 18 tới 21, tôi cố gắng tạo ra nhiều dữ liệu chiếm memory để cho bạn thấy sẽ tai hại thế nào khi ta không giải phóng nó. Tôi đẩy nó vào background thread để không block main thread khi bạn cố gắng mở SecondViewController.
Dòng 25 tới 30, tôi tạo fake 1 hàm call API. Hàm này sau 1 giây sẽ trả về kết quả. Khi có kết quả, nó sẽ gọi vào biến closure để bắn ra cho thằng SecondViewController xử lý. SecondViewController sẽ thực hiện việc đổi màu.
OK vậy là bạn đã hiểu luồng của app rồi. Vậy việc tiếp theo là theo dõi nó đã leak memory như nào.
Đầu tiên bạn chạy app và xem memory sử dụng như hình:
Bạn bấm vào nút Open Second screen, sau đó đợi 1 lát cho SecondViewController tạo data rồi bấm back ra màn 1. Cứ làm như vậy dăm bảy lần, bạn sẽ thấy 1 điều là bộ nhớ chỉ có tăng mà không có giảm.
Chúc mừng bạn đã tạo leak memory thành công.
Thế tại sao nó lại leak memory?
Quay lại với đoạn code closure dưới đây:
Ở đây tôi sử dụng liên kết mạnh từ viewModel sang view controller, bằng việc lấy self.view. Nghĩa là thằng viewModel này chỉ thực sự bị hủy khi thằng SecondViewController bị hủy. Tuy nhiên, thằng ViewModel này là con của thằng SecondViewController. Mà thằng SecondViewController muốn hủy thì nghĩa là con nó phải hủy trước. Ồ, vậy là bố bảo là con hủy thì bố mới hủy, con thì nói bố ơi bố hủy đi thì con mới hủy nè. Cuối cùng 2 thằng chả ai chịu ai, bộ nhớ vẫn cứ ở đấy, chả bao giờ giải phóng được.
Rất simple đúng không?
Vậy cách khắc phục thế nào? Chúng ta tiến hành sửa lại đoạn code trên như sau:
Tôi tiến hành capture cho hàm closure trên. Ồ thế capture là gì? Đơn giản là thằng con nói bố ơi bố khi con có closure trả về, nếu như bố vẫn sống ở đó thì con sẽ set màu cho bố nhé! Mà bố ngỏm rồi(đã bị giải phóng khi bấm back) thì thôi con không gán nữa. Tôi tạo capture [weak self] để tạo liên kết yếu cho điều đó. Khi đó đoạn code ở closure sẽ là self?. thay vì self. Thật là đơn giản đúng không nào 😍
Khi đó hàm sau sẽ in ra dòng chữ “free memory SecondViewController”
deinit {
NSLog("free memory SecondViewController")
}
Từ nay về sau mỗi khi bạn tạo 1 view controller mới, bạn hãy thêm hàm deinit vào và đặt debug vào đó, xem nó đã được giải phóng chưa? Nếu như bạn đã hủy view controller rồi thì không có leak memory ở nó. Còn mà không in ra thì chắc chắn leak memory rồi bạn nhé.
Vậy là tôi đã trình bày xong trường hợp leak memory ở closure. Qua ví dụ hi vọng bạn sẽ cẩn thận hơn trong việc code của mình. App chạy được nhưng phải chạy mượt.
Hope you enjoy it!
One thought on “Lập trình IOS: Tạo Leak memory trong closure (phần 1)”