LeakMemory Swift

Trong phần 1, chúng ta đã tạo được leak memory trong closure. Việc tự tạo ra 1 cái leak để nghiên cứu cũng giống như câu nói “đừng đẩy tao tự ngã” 🤣

Đời Đéo Cần Phải Xô Để Bố Mày Tự Ngã - Ảnh | Facebook

For fun tí thôi, nhưng mà thực tế khi bạn hiểu được tại sao lại leak như vậy rồi thì lần sau bạn sẽ không dễ bị leak thế nữa, đúng không nào? Trong bài này, chúng ta lại tự ngã vào 1 leak hoàn toàn mới: Đó là leak trong singleton. Ồ nghe nó có vẻ nguy hiểm nhỉ 😉 Nhưng mình cá khi đọc xong cái bài này các bạn lại bảo “ôi tưởng thế nào, dễ vãi lằn 😋” OK con dê, vậy hãy bóp mông em đồng nghiệp 1 cái rồi vào việc luôn nào.

Đầu tiên là bạn cần có source nha:

https://github.com/codetoanbug/LeakMemorySamples.git

Code nằm ở branch bai2, và nếu bạn không biết tôi nói gì thì vui lòng đọc đoạn sau:

trích dẫn từ bài: https://codetoanbug.com/lap-trinh-ios-trien-khai-mvvm-cho-prject-swiftphan-2/

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 bai2
Sau khi branch bai2 đượ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!
  1. Singleton là gì?

Trước khi tạo được leak cho nó thì chúng ta hãy hiểu Singleton là gì đã nha? Nó là 1 cái khái niệm gì đó bằng tiếng Anh. Nhưng nếu hiểu theo nghĩa thuần Việt singleton là kỹ thuật tạo class mà chỉ khởi tạo được duy nhất 1 lần. Nghĩa là gì? Cùng xem ví dụ sau:

class AlertViewHandleLogic {
}

Tôi tạo 1 class như trên và hoàn toàn không có gì trong đó cả. và bạn mở project lên, vào file SingletonLeakViewController:

thì thấy nó chạy bình thường như hình. Tiếp theo tôi biến class AlertViewHandleLogic thành singleton như sau:

Chuyển class thường thành class Singleton
  1. Dòng 11: Tôi đưa hàm tạo init thành private, nghĩa là không cho phép các lớp khác khởi tạo nó nữa.
  2. Dòng 12: Tôi tạo 1 biến static shared để gọi hàm tạo tại chính nó, ok nó chạy được.

Khi tạo như này, bạn build lại và vào xem ở file SingletonLeakViewController thì kết quả như sau:

Nó báo lỗi đỏ chót, dịch ra nghĩa là hàm init được bảo vệ private – do đó bạn không thể gọi hàm tạo được ở đây. OK vậy tôi sẽ gọi lớp này như nào?

Tôi gọi nó qua biến shared. Lúc này do shared là biến static nên nó chỉ khởi tạo đúng 1 lần và biến này tồn tại mãi mãi trong class AlertViewHandleLogic. Vậy là bạn đã tạo được 1 class Singleton thành công 😻

Vậy trong thực tế, chúng ta có hay gặp singleton không? Ồ rất nhiều luôn bạn nhé. Ví dụ Apple có 1 đoạn code trong UIKit như sau:

UIApplication.shared.keyWindow?.rootViewController

Thì biến shared ở trên chính là singleton nha. Ở ví dụ trên, khi tôi muốn lấy rootViewController thì tôi thông qua biến shared của lớp UIApplication để truy cập. Vì mỗi ứng dụng IOS thì cần 1 biến để lưu rootViewController và duy nhất thôi, cho nên họ tạo singleton cho nó. Quy tắc đặt tên cho singleton thường là shared, hoặc là getInstance() như thời Objective-C bạn nhé. Vậy là bạn biết được tác dụng của Singleton như nào rồi ha. Hãy nhớ cho mình chúng ta chỉ tạo Singleton khi:

  • Quản lý 1 biến hay class duy nhất chạy xuyên suốt ứng dụng
  • Static sẽ không được giải phóng cho đến khi ứng dụng giải phóng

Do vậy, cũng không thể lạm dụng Singleton hay static được đúng không nào? Nếu bạn hiểu đơn giản thì swift hay các ngôn ngữ lập trình khác có 2 loại biến cơ bản là dynamic và static. Dynamic thì khởi tạo runtime, và giải phóng khi lớp chứa nó hủy. Còn static thì khởi tạo luôn khi class đó được gọi init, và tồn tại xuyên suốt ứng dụng. Tuy nhiên nhiều trường hợp bắt buộc vẫn phải dùng static dù muốn hay không.

Trong ví dụ chúng ta làm sau đây, tôi muốn chương trình sẽ sau 1 khoảng thời gian nào đó hiển thị 1 màn hình alert lên view controller, bất kỳ ở đâu nó cũng show lên được ha. Và việc xử lý show đó do class AlertViewHandleLogic xử lý. Nghe hấp dẫn nhỉ? Trong thực tế có những ví dụ như bạn muốn sau 1 khoảng thời gian ứng dụng tự lock màn hình cũng có thể áp dụng, vân vân mây mây ha.

OK đọc đến đây thì cũng mệt rồi, nên hãy đi hát 1 bài rồi ta lại tiếp tục nha 😷

2. Tạo singleton quản lý show alert

Cùng theo dõi đoạn code sau:

Xử lý logic show popup nhờ timer
  • Dòng 12, 13 tôi có 1 biến timer để tính sau 1 khoảng thời gian sẽ show alert ra
  • Dòng 23 đến 27, tôi tạo hàm để cứ sau 1 khoảng 5s nó sẽ gọi vào biến closure needShowAlertView. Mục đích biến này là bắn ra callback cho view controller nào được gán sẽ thực hiện action này.

Vào SingletonLeakViewController và theo dõi đoạn code sau:

  • Dòng 15, tôi khởi tạo biến singleton của class AlertViewHandleLogic
  • Dòng 16 tôi gán closure needShowAlertView và hiển thị 1 dòng chữ need show here
  • Dòng 20 tôi gọi hàm resetShowAlertTimer để timer hoạt động

Sau khi build tôi nhận được kết quả sau:

Vậy là cứ đều đặn 5s nó sẽ bắn ra callback để chúng ta có thể viết logic show cái alert này ra.

Tuy nhiên, vì hàm alertViewHandleLogic.resetShowAlertTimer() gọi rườm rà nên tôi sẽ đưa nó vào hàm init của AlertViewHandleLogic để không phải gọi mỗi khi tạo callback needShowAlertView.

Tuy nhiên có 1 vấn đề nghiêm trọng như sau:

  • Biến closure needShowAlertView thuộc lớp AlertViewHandleLogic, là class Singleton. Do đó khi biến này được gán ở SingletonLeakViewController thì chỉ có view này show alert thôi. Giả sử tôi gán vào SecondViewController thì nó không còn hoạt động ở SingletonLeakViewController. Ồ trong khi tôi muốn là nó phải hoạt động ở tất cả các viewcontroller cơ mà 😡🤬

Khó rồi nha, vì singleton chỉ là 1 và duy nhất, cho nên chúng ta không thể áp dụng kỹ thuật callback bằng closure này được. Giải pháp là gì?

3. Thay thế closure callback bằng observers

quát dờ heo? Ông vừa chỉ tụi tôi cái singleton rồi định chỉ thêm cái observers? Có nhiều quá không vậy? 🥱😲 Ồ nếu bạn vừa ngáp ngủ thì thôi tạm thời làm cốc trà đá, hút điếu thuốc, ngắm em bán nước mông cong 1 lúc rồi lên tiếp tục sau nha.

😍 OK rồi bây giờ ta sẽ tiếp tục nghiên cứu observers là gì nè?

Observers dịch theo nghĩa thuần Việt nhất là 1 cái quan sát viên. Mỗi khi timer tới 5s, thì nó sẽ phải báo cho tất cả những thằng view controller dùng nó phải show alert lên. Tất nhiên là view controller đang ở chế độ hiển thị nha. Kỹ thuật này dùng thế nào?

Đơn giản là bạn tạo 1 cái protocol ở cái class AlertViewHandleLogic, sau đó bạn tạo thêm các hàm add và remove các protocol này để sử dụng hay xóa việc lắng nghe protocol ở các view. Protocol có nhiệm vụ báo cho các view controllers.

Nói thì hay lắm, code xem nào 😅 Theo dõi đoạn code sau:

  • Dòng 11 đến 14, Ở đây tôi tạo protocol AlertViewHandleLogicDelegate có nhiệm vụ là bắn hàm needShowAlertView để show alert. Một số quy tắc viết protocol nếu bạn chưa nắm vững có thể xem tại đây ha.
  • Dòng 18 tôi làm 1 biến observers kiểu AlertViewHandleLogicDelegate nhằm lưu trữ các observer(quan sát viên) của các view controller muốn listen nó.

Tiếp tục xem đoạn code:

  • Dòng 43 đến 46, tôi tạo hàm add observer. Tôi có check nếu nó chưa được add thì mới thêm, có rồi thì bỏ qua không thêm nữa.
  • Dòng 51 đến 54 tôi viết hàm remove observer. Vì không thêm lặp cho nên tôi chỉ check phần tử đầu tiên nếu là thằng cần remove thì xóa ra khỏi list thôi.

Trong hàm bạn sửa lại 1 xíu như sau:

Mục đích là chạy 1 vòng for rồi notification cho tất cả những thằng đã add observer nha. Tôi comment cái callback bằng closure.

OK vậy là bạn đã chuẩn bị để chơi với observer rồi nha. Bây giờ bạn vào SingletonLeakViewController và code như sau:

  • Dòng 21 tôi add observer cho SingletonLeakViewController. Khi add như này thì nó sẽ lắng nghe được sự kiện sau 5s sẽ show alert.
  • Dòng 26 đến 29, vì tôi add cho nên class SingletonLeakViewController phải extend các hàm từ protocol AlertViewHandleLogicDelegate. Tôi show print để biết nó hoạt động.
  • Ở các view controller khác bạn làm tương tự.

Và kết quả chạy như sau:

Good job. Vậy là nó đã hoạt động 🥰

Nhưng khoan đã, ông vừa bảo leak cái mẽ gì trong singleton rồi cơ mà, sao giờ lại lan man ra đây? Ồ bây giờ tôi mới chốt issue nè. Leak memory đã xảy ra rồi đó!

Bây giờ bạn thêm dòng code sau để check SingletonLeakViewController giải phóng khi bấm back ra không:

Và nó hoàn toàn không nhảy vào! OMG tôi đã làm gì sai mà leak vậy 😩🥺

Rồi quay lại vấn đề, việc ta add 1 observer vào SingletonLeakViewController như sau:

AlertViewHandleLogic.shared.addObserver(self)

Thì view controller này đã tham chiếu mạnh tới class AlertViewHandleLogic. Nghĩa là lớp SingletonLeakViewController chỉ bị giải phóng khi observer trong AlertViewHandleLogic được giải phóng. Tuy nhiên AlertViewHandleLogic làm sao mà giải phóng được khi nó có hàm remove ở đâu đâu. Có bạn bảo bạn ơi sao bạn không đưa hàm remove như này:

deinit {
        NSLog("free memory SingletonLeakViewController")
        AlertViewHandleLogic.shared.removeObserver(self)
    }

Rất tiếc là nó đã không giải phóng thì không nhảy vào deinit, mà không vào thì không chạy remove observer được. Câu chuyện trở thành bế tắc rồi 😢

Giải pháp của tôi như sau:

  • Tôi trick khi nhấn nút back ở UIBarButtonItem thì sẽ gọi vào hàm viewWillDisappear, tôi sẽ xóa ở đó:

Kết quả như sau:

Vậy là SingletonLeakViewController đã call được vào deinit, nghĩa là nó được giải phóng 😍

Tuy nhiên tôi thấy thật sự đây cũng chưa phải là giải pháp thông minh nhất, vì bạn sẽ phải biết trick tùy trường hợp mà giải phóng chứ không phải lúc nào cũng là như tôi làm ở trên.

OK vậy là bài này cũng khá dài rồi, nếu bạn có giải pháp nào hay hơn của tôi vui lòng hãy comment cho tôi biết ở dưới bài này nhé.

Cảm ơn vì đã theo dõi tới đây và hi vọng bạn sẽ học được thêm nhiều kiến thức hay ho, thực tế qua bài này. Thanks all!

Code Toàn Bug

Code nhiều bug nhiều!

Leave a Reply

Your email address will not be published. Required fields are marked *