Bài này chúng ta sẽ tìm hiểu về cách quản lý tất cả bộ nhớ của hệ thống. Vậy để giảm bộ nhớ của toàn hệ thống thì hệ thống phải quản lý bộ nhớ của từng chương trình hay nói chi tiết hơn là hệ thông sẽ quản lý tới từng thời gian sống của các đối tượng. Để thực hiện điều này trong các ứng dụng iOS hoặc OSX sẽ quản lý thông qua các tham chiếu của các đối tượng để biết được các đối tượng đó có còn được sử dụng hay không. Để hiểu rõ hơn điều này cafedev và các bạn sẽ tìm hiểu chi tiết hơn qua bài sau đây.
Cơ chế quản lý các tham chiếu(reference) được thực hiện thông qua việc đếm các tham chiếu(reference counting), cái này sẽ theo dõi được các tham chiếu tới đối tượng đó. Khi bạn khởi tạo một đối tượng nào đó hay gián một đối tượng nào đó thì reference counting của object đó sẽ tăng lên và cho tới khi bạn làm việc xong với object đó thì reference couting sẽ giảm xuống. Khi mà một đối tượng có số reference counting lớn hơn 0 thì đối tượng đó sẽ được đảm bảo tồn tại cho tới khi số reference couting giảm xuống bằng 0 thì khi đó hệ thống sẽ cho phép hủy nó.
Cơ chế thì như vậy còn đối với developer thì sẽ phải quản lý các reference counting bằng thủ công thông qua các hàm được hỗ trợ trong NSObject protocol. Cách này được gọi là Quản lý bộ nhớ thủ công(Manual Retain Release (MRR)). Tuy nhiên, Kể từ Xcode 4.2 trở đi, chúng ta sẽ không còn phải quản lý bộ nhớ nữa vì Apple đã giới thiệu cơ chế tự quản lý bộ nhớ(Automatic Reference Couting (ARC)). Với Cơ chế này thay vì bạn phải gọi thủ công các hàm cấp phát bộ nhớ và hủy bộ nhớ như trước thì mọi chuyện được Apple tự động gọi các hàm đó để quản lý bộ nhớ cho chúng ta. Apple muốn chúng ta tập trung code các tính năng của ứng dụng hơn là việc phải quản lý bộ nhớ của ứng dụng.
Nhưng để hiểu được ARC nó hoạt động thế nào thì chúng ta phải biết MRR nó hoạt động ra sao, Vì vậy chúng ta sẽ đi tìm hiểu về cách làm việc của MRR trước.
Nội dung chính
Manual Retain Release(MRR)
Với cơ chế này, bạn phải tự tạo tham chiếu và tự hủy các tham chiếu đó cho tất cả các đối tượng trong chương trình. Để làm điều đó bạn sẽ gọi một số hàm đặc biệt như sau:
- Alloc – Tạo một object, xin hệ thống một vùng nhớ và tham chiếu tới vùng nhớ đó.
- Retain – Nói với hệ thống sẽ luôn giữ liên kết tới vùng nhớ của đối tượng nào đó.
- Release – Nói với hệ thống mình sẽ hủy bỏ tham chiếu tới đối tượng đó và hủy nó ngay lập tức để giải phóng vùng nhớ cho đối tượng khác dùng.
- Autorelease – Nói với hệ thống mình sẽ hủy bỏ tham chiếu tới đối tượng đó nhưng trì hoãn việc hủy vùng nhớ của nó.
Việc quản lý bộ nhớ thủ công thế này thực sự là nhiệm vụ khó khăn nhưng cách thực hiện thì dễ. Bạn chỉ cần nhớ là khi nào bạn yêu cầu một bộ vùng nhớ nào đó(Alloc) thì bạn phải giải phóng nó(Release) để khỏi bị tràng bộ nhớ.
Nếu bạn quên thì release bộ nhớ thì sao, Khi đó sẽ xảy ra hiện tượng tràn bộ nhớ(memory leak) và sau đó ứng dụng có thể bị đơ, lác, nguy hiểm hơn là bị crash. Còn trường hợp bạn release một vùng nhớ quá nhiều lần, khi đó bạn truy cập vào vùng nhớ bị release nhiều lần vậy sẽ xảy ra hiện tượng truy cập vào vùng nhớ không tồn tại ( BAD ACCESS) và cũng carsh ứng dụng.
Bật cơ chế MRR
Do cơ chế này cũ quá rồi, nên muốn dùng nó thì chúng ta phải bất nó lên bằng cách tắt cơ chế ARC. Vào Build Setting trong project và sau đó search từ khóa “automatic reference ” sau đó thay đổi Thuộc tính Objective-C Automatic Reference couting thành No như sau:
Hàm alloc
Chúng ta sẽ dùng hàm alloc để tạo một đối tượng, nhưng ngoài việc tạo đối tượng thì hàm alloc tăng reference couting lên 1.
Ví dụ:
// main.m
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSMutableArray *inventory = [[NSMutableArray alloc] init];
[inventory addObject:@"Honda Civic"];
NSLog(@"%@", inventory);
}
return 0;
}
Trên đoạn code trên chúng ta có sử dụng alloc và autorelease để hủy đối tượng đó tại một thời điểm nào đó chưa biết. Do đó nên khi chạy xong hàm main thì đối tượng inventory vẫn chưa được giải phóng và làm memory leak, vì đối tượng quá nhỏ nên chúng ta sẽ không thấy được hiện tượng memory leak, nhưng nếu ta tạo tạo đối tượng cực lớn thì chúng ta sẽ thấy văng bug hiện tượng memory leak. Vì thế nếu một object thực sự không dùng nửa thì chúng ta nên hủy nó ngay lập tức bằng cách dùng hàm release.
Hàm release
Hàm release dùng để cắt đứt tham chiếu và hủy vùng nhớ của đối tượng đó. Vậy để giảm hiện tượng memoray leak ở code trên thì chúng ta làm như sau:
// main.m
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSMutableArray *inventory = [[NSMutableArray alloc] init];
[inventory addObject:@"Hoc Objective-C"];
NSLog(@"%@", inventory);
[inventory release];
}
return 0;
}
Theo đúng logic thì cứ alloc thì chúng ta phải release đối tượng. Nhưng chúng ta thử để hàm release đối tượng như sau:
// main.m
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSMutableArray *inventory = [[NSMutableArray alloc] init];
[inventory addObject:@"Honda Civic"];
[inventory release];
NSLog(@"%@", inventory);
}
return 0;
}
Hiện tượng gì sẽ xảy ra, Chính xác là chương trình sẽ crash với bug là EXC_BAD_ACCESS, hiểu đơn giản là chúng ta đã truy cập vào một vùng nhớ không tồn tại vì nó đã giải phóng. Vậy nên chúng ta phải luôn nhớ là dùng xong rồi mới giải phóng.
Hàm retain
Hàm này sẽ nói với hệ thống rằng chúng ta muốn giữ đối tượng đó lại vì có thể chúng ta muốn sử dụng nó nửa. Để hiểu rõ hơn chúng ta sẽ đi vào ví dụ sau: Bây giờ chúng ta sẽ tạo một lớp CarStore như sau:
// CarStore.h
#import <Foundation/Foundation.h>
@interface CarStore : NSObject
- (NSMutableArray *)inventory;
- (void)setInventory:(NSMutableArray *)newInventory;
@end
// CarStore.m
#import "CarStore.h"
@implementation CarStore {
NSMutableArray *_inventory;
}
- (NSMutableArray *)inventory {
return _inventory;
}
- (void)setInventory:(NSMutableArray *)newInventory {
_inventory = newInventory;
}
@end
Và tại file main.m chúng ta sẽ code như sau:
// main.m
#import <Foundation/Foundation.h>
#import "CarStore.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSMutableArray *inventory = [[NSMutableArray alloc] init];
[inventory addObject:@"Honda Civic"];
CarStore *superstore = [[CarStore alloc] init];
[superstore setInventory:inventory];
[inventory release];
// Do some other stuff...
// Try to access the property later on (error!)
NSLog(@"%@", [superstore inventory]);
}
return 0;
}
Với code bên trên chúng ta sẽ thấy biến inventory đã bị release nhưng vẫn còn dùng ở dưới, kết quả là sẽ bị crash ứng dụng. Vậy để để giữ lại đối tượng đó để dùng về sau thì chúng ta sẽ code thêm như sau:
// CarStore.m
- (void)setInventory:(NSMutableArray *)newInventory {
if (_inventory == newInventory) {
return;
}
NSMutableArray *oldValue = _inventory;
_inventory = [newInventory retain];
[oldValue release];
}
Và sau đây chúng ta sẽ có một sơ đồ về việc tăng giảm reference counting như sau:
Chúng ta có thể hiểu như sau: Khi alloc thì reference count tăng lên 1, sau đó chúng ta set inventory và retain nó thì reference count sẽ tăng lên 2, tiếp theo ta release inventory thì reference count sẽ giảm đi 1, tiếp theo khi chạy xong hàm main thì autorelease sẽ giảm reference xuống còn lại 0, khi đó đối tượng sẽ được hủy hoàn toàn.
Thêm ví dụ:
NSString* s = [[NSString alloc] init]; // Ref count bằng 1
[s retain]; // Ref count bằng 2
[s release]; // Ref count giảm đi 1
[s release]; // Ref count giảm còn lại bằng 0, object được giải phóng
Bây giờ chắc các bạn cũng hiểu được bản chất của việc quản lý bộ nhớ của một đối tượng là như thế nào. Việc quản lý bằng cơ chế MRR khá khó khăn phải nhớ mỗi khi cấp phát thì phải hủy bộ nhớ, nếu hủy nhiếu quá thì cũng gây lỗi mà khồn hủy thì cũng gây lỗi, tóm lại là quá phức tạp. Cũng vì thế mà Apple đã tạo ra cơ chế tự động quản lý bộ nhớ ARC(Automatic Reference Couting) để đảm bảo không xảy ra các lỗi như ở trên chúng ta đã tìm hiểu.
Automatic Reference Counting(ARC)
Nó là một cơ chế tự động quản lý bộ nhớ, quản lý tuổi thọ của từng đối tượng thông qua số reference counting. Có nghĩa là khi một đối tượng có reference counting bằng 0 thì hệ thống sẽ tự động gọi hàm release cho đối tượng đó để hủy nó. Chúng ta chỉ cần hiểu là khi nào thì reference counting sẽ tăng lên và khi nào giảm xuống mà biết được đối tượng đó còn tồn tại hay đã bị hệ thống hủy rồi.
Sau khi chúng ta dùng cơ chế ARC thì các hàm như retain, release, autorelease sẽ không được dùng nửamà thay vào đó chúng ta sẽ dùng từ khóa @properties với các từ khóa strong(tương tự như retain bên MRR), weak(Tạo liên kết yếu thường dùng cho trường hợp xảy ra hiện tượng retain cycle).
Retain cycle là một hiện tượng 2 đối tượng tham chiếu qua lại lẫn nhau, với 2 tham chiếu đó kiểu strong nên ARC không thể tự phá vỡ được nó và gây ra hiện tượng vùng nhớ không thể được giải phóng(deadlock). Vì thế này dùng từ khoá weak để ARC có thể tự động huỷ tham chiếu đó và giải phóng bộ nhớ cho 2 đối tượng.
Có thể hiểu đơn giản như sau: thay vì mình phải gọi thủ công các hàm giải phóng bộ nhớ(release, autorelease) của các object bên cơ chế MRR thì bây giờ hệ thống sẽ tự động gọi các hàm đó nhờ vào việc kiểm tra số reference counting để huỷ đối tượng.
Lưu ý:
Chúng ta cần phải nắm rõ khi nào đối tượng còn tồn tại và đã bị tự động hủy bởi hệ thống, từ đó mới biết mà sử dụng đối tượng hợp lý, sẽ có trường hợp đối tượng bị hệ thống hủy mà chúng ta không biết và sử dụng nó, kết quả là gây ra BAD_ACCESS. Nếu một properties hay biến nào đó sử dụng strong thì nó sẽ được hệ thống giữ lại không bị huỷ bởi ARC cho tới khi ta gián nil cho nó.
Nếu một properties hay biến nào đó sử dụng weak thì nó sẽ dễ dàng bị ARC huỷ và sau khi huỷ nó sẽ được gián nil.
Qua bài này chúng ta đã hiểu rõ hơn bản chất bên trong của việc quản lý tự động bộ nhớ(ARC) thông qua cơ chế MRR và một số lưu ý nhỏ khi dùng ARC để tránh gây lỗi. Ngoài ra các bạn có thể tham khảo thêm tài liệu Apple để biết thêm về ARC
Hy vọng các bạn thích và học được nhiều kiến thức từ bài viết này. Mong các bạn chia sẽ nó để mọi người cùng học và cùng trao đổi. Mọi thắc mắc hay trao đổi về bài viết, các bạn có thể để lại bình luận bên dưới mình sẽ hỗ trợ sớm nhất.
Chân thành cảm ơn các bạn đã theo dõi.