Chủ
đề hôm nay mình muốn nói tới là một vấn đề nền tảng đối với tất cả mọi
người khi mới bắt đầu tiếp cận với lập trình iOS, “Quản lý bộ nhớ” aka
Memory Management.
Ắt
hẳn, những người mới bước chân vào con đường lập trình mobile nói
chung, hoặc iOS nói riêng sẽ liên tục nghe những anh bề trên (hay còn
gọi là senior thần thánh của chúng ta) nói về quản lý bộ nhớ quan trọng
như thế nào khi lập trình trên thiết bị có bộ nhớ hạn chế như điện
thoại. Những từ ngữ được nhắc đi nhắc lại như ARC , non-ARC, Reference
Count,… blah blah. Và bạn sẽ kiểu như: “WTH! Mấy cha này nói cái gì
vậy”. Rồi tiếp đó lại tới những vấn đề sẽ gặp khi quản lý bộ nhớ không
tốt như : leak memory, stack overflow, rồi lại tới crash ( ác mộng của
tất cả developers). Các bạn sẽ dường như là: “Thôi thôi! Dẹp hết… mệt
quá :’( Quan tâm làm gì ba cái này, làm đại cho xong cho rồi…hiu hiu).

Vậy
quản lý bộ nhớ thật sự là gì? Quản lý bộ nhớ có thể được hiểu một cách
đơn giản là quá trình khởi tạo, sử dụng và giải phóng bộ nhớ khi không
còn sử dụng. Trong cách iOS quản lý bộ nhớ, tồn tại một thứ gọi là
“Reference count system” để quản lý rằng có bao nhiêu biến (owners) đang
nắm quyền sở hữu một đối tượng (object) thông quan một biến đếm là reference count hay còn gọi là retain count. Từ lúc chúng ta khởi tạo (alloc) một object thì retain count
của object đó mặc định sẽ là 1. Mỗi khi chúng ta lấy quyền sở hữu tới
một object (bằng cách tham chiếu đến vùng nhớ của đối tượng thông qua
các phương thức/hàm có sẵn) thì retain count của object đó sẽ tăng lên 1 và khi chúng ta không cần dùng nó nữa, chúng ta sẽ bỏ quyền sở hữu tới object (release) thì retain count của object sẽ giảm đi 1. Cứ như vậy cho tới khi retain count
bằng 0 thì object đó sẽ được giải phóng (dealloc) khỏi bộ nhớ. Có thể
lấy một ví dụ cho hành động ở trên là: trong một văn phòng làm việc đang
sáng đèn, tới giờ tan tầm thì mọi người bắt đầu ra về (giảm retain
count) và công việc của bạn (reference count system) là kiểm tra xem còn
ai ở trong phòng không, nếu không thì tắt hết toàn bộ đèn đi...
MMR (Manual Retain-Release) hay còn gọi là Non-ARC
Đây
là một phương thức quản lý bộ nhớ của iOS từ những ngày đầu tiên. Đúng
như tên gọi của nó, bạn sẽ lấy quyền sở hữu object cũng như từ bỏ quyền
sở hữu một cách hoàn toàn thủ công thông quan các phương thức/hàm được
hệ thống cung cấp sẵn. Chúng ta sẽ lấy quyền sở hữu của object bằng các
phương thức như: alloc, new, retain, copy, mutable copy, các hàm khởi tạo đối tượng nói chung mặc định retain count của đối tượng vừa được khởi tạo sẽ bằng 1. Và để bỏ quyền sở hữu tới object chúng ta sẽ phải release hoặc autorelease object đó. Điều cần lưu ý khi sử dụng phương pháp non-ARC đó là chúng ta cần phải release object khi không cần sử dụng nữa để tránh tình trạng memory leak. Bên cạnh đó, sẽ có một vấn đề khác phát sinh khi chúng ta release quá nhiều lần trên một object đó là dangling pointer, dangling pointer sẽ trỏ đến một vùng nhớ không hợp lệ (vùng nhớ được xoá hoặc giải phóng trước đó) có thể gây crash lúc run-time.
ARC (Auto Reference-Counting)
Như
các bạn có thể thấy thì việc sử dụng phương pháp MMR trên hoàn toàn là
thủ công và quá phụ thuộc vào con người nên không tránh được sai sót.


Để
khắc phục sự bất tiện của phương pháp MMR, trong sự kiện WWDC năm 2011,
Apple đã giới thiệu một phương pháp quản lý bộ nhớ mới khiến ai nấy đều
hào hứng, người người trong giới gần xa đâu đâu cũng nhắc đến đó là ARC
❤.
Vậy
thì ARC là gì? Nói trắng ra thì nó cũng chẳng khác gì mấy so với MMR,
vẫn là retain count, vẫn là đó “Reference count system”. Vậy thì có gì
khác biệt??? Vâng!!! Điều khác biệt duy nhất khi sử dụng ARC là bấy giờ
chúng ta không cần phải quá quan tâm đến việc quản lý bộ nhớ nữa, thay
vào đó chúng ta sẽ tập trung vào cải thiện các vấn đề về logic của code;
các phương thức quản lý bộ nhớ (tăng/giảm retain count như alloc,
new,…được đề cập ở trên) sẽ được tự động thêm vào code của chúng ta
trong compile-time. In compiler we trust! Như vậy công việc của các Dev
sẽ giảm đi đáng kể, thay vì ngồi lần mò, kiểm tra xem chắc chắn là chúng
ta đã release object đó hay chưa thì bây giờ ARC đã giúp chúng ta làm
điều đó, bên cạnh đó thì số lượng dòng code cũng giảm đi kha khá và code
của chúng ta sẽ có vẻ đẹp hơn :3
Haizz!
Tới phần này thì cuộc sống của chúng ta đã trở nên dễ dàng hơn khá
nhiều. Vấn đề còn lại mà chúng ta cần phải quan tâm đó là retain cycle. Dưới đây là một ví dụ đơn giản về retain cycle và sau đó mình sẽ định nghĩa và đưa hướng giải quyết cho mọi người.
class ObjectA { var objectB: ObjectB? = nil init() { print("init A") } deinit { print("deinit A") } }class ObjectB { var objectA: ObjectA? = nil init() { print("init B") } deinit { print("deinit B") } }
var objectA: ObjectA? = ObjectA() var objectB: ObjectB? = ObjectB()
objectA?.objectB = objectB
objectB?.objectA = objectA
Như
chúng ta có thể thấy thì trong ví dụ trên, instance của class ObjectA
sẽ có reference tới instance của class ObjectB và ngược lại. Một điều
cần lưu ý đó là: Nếu chúng ta không chỉ định cụ thể thì mọi reference
tới một object khác đều là strong reference. Điều đó
cũng có nghĩa là chúng ta sẽ lấy quyền sở hữu object, và retain count
của object đó sẽ tăng lên 1. Tiếp tục với ví dụ trên:
objectA = nil
objectB = nil
Hãy thử copy đoạn code trên vào Playground và chạy thử. Chúng ta có thể thấy được là phương thức deinit ở trong 2 class sẽ không được thực thi. Vì sao vậy??? Nếu chúng ta nhớ lại những thứ nằm trên bài viết này sẽ có một ý là “Object chỉ được giải phóng khi retain count bằng 0”, cũng có nghĩa là khi không còn bất cứ strong reference nào tới object. Vậy thì ở ví dụ trên phương thức deinit không thực thi đơn giản vì: objectA đang có retain count = 1 vì có objectB đang strong reference
tới nó và ngược lại. Nên cả objectA và objectB sẽ không bao giờ được
giải phóng khỏi bộ nhớ, vì chúng đang chờ lẫn nhau để giải phóng. Trường
hợp mà các objects strong reference lẫn nhau như vậy gọi là retain cycle, các objects sẽ không bao giờ được giải phóng khỏi bộ nhớ, có thể dẫn tới memory leak.
Để giải quyết vấn đề trên thay vì dùng strong reference thì chúng ta sẽ có giải pháp khác là: weak hoặc unowned.
class ObjectA {
weak var objectB: ObjectB? = nil
Khi chúng ta weak reference (hoặc unowned) tới một object thì chúng ta không lấy quyền sở hữu của object và retain count sẽ không tăng lên. Thử chạy lại code phía trên chúng ta sẽ thấy deinit được thực thi.
Điểm khác nhau của giữa weak và unowned là: khi đối tượng mà chúng ta weak reference tới bị dealloc khỏi bộ nhớ thì weak reference tự động gán về nil, unowned
cũng tương đối giống weak nhưng nó sẽ không tự động gán về nil khi
object bị dealloc mà nó sẽ vẫn trỏ tới một vùng nhớ rác nào đó, điều này
khá nguy hiểm có thể dẫn tới crash app trong run-time khi chúng ta sử
dụng một vùng nhớ không hợp lệ.
Tóm lại, chúng ta sử dụng weak reference khi biết rằng đối tượng nó tham chiếu tới có thể trở thành nil, trong khi unowned reference sử dụng khi chúng ta biết chắc chắn rằng đối tượng đó sẽ không bao giờ trở thành nil. Chúng ta sẽ định nghĩa các weak reference bằng các biến kiểu optional, có nghĩa là có giá trị hoặc không giá trị và định nghĩa unowned reference bằng các biến kiểu non-optional.
Tới đâu thì bài viết cũng đã khá dài dòng văn tự nên mình xin được phép kết thúc ở đây. Một lần cảm ơn mọi người đã đọc tới những dòng cuối cùng này. Hẹn mọi người vào những bài viết sau. Nếu có bất cứ sai sót gì trong bài thì mọi người hãy comment để cho mình biết và chỉnh sửa nhé ❤
Comments
Post a Comment