Manual Memory Management trong Objective-C

Introduction

Quản lý bộ nhớ trong iOS sử dụng khái niệm reference counting để kiểm soát life cycle của các object thông qua việc đếm số reference của chúng. Có 2 kiểu reference counting, đó là:

  1. MRR (Manual Retain Release): Developer phải tự quản lý, thực hiện tất cả công việc liên quan đến reference counting một cách thủ công khi khởi tạo object, retain, hủy object...

  2. ARC (Automatic Reference Counting): Một hệ thống reference counting tự động. Developer chỉ cần khởi tạo và sử dụng object mà không cần quan tâm đến việc reference counting được thực hiện như thế nào cũng như việc giải phóng object khi không sử dụng object được thực hiện tự động. Cách thức hoạt động của ARC tương tự như khái niệm garbage collection trong macOS nhưng về cơ bản vẫn dựa trên các rule của reference counting.

ARC được Apple thêm vào Xcode kể từ phiên bản 4.2. Tuy nhiên chúng ta cũng nên tìm hiểu thêm về Manual Memory Maganement để hiểu được những ưu điểm mà ARC mang lại.

Bài viết này sẽ chủ yếu sẽ viết về MRR - cách quản lý bộ nhớ thủ công trong Objectice-C từ thời xưa. Để tìm hiểu về ARC, các bạn có thể đọc thêm tại Apple's Official Documentation.

Reference Counting

Mọi đối tượng trong Objective-C sử dụng reference counting để quản lý life cycle của chúng. Theo Advanced Memory Management Programming Guide thì reference counting cho phép một object duy nhất có thể có nhiều owner. Object đó sẽ được giữ trong bộ nhớ khi vẫn còn ít nhất một owner. Một khi không còn owner nào thì object đó sẽ tự động được giải phóng khỏi bộ nhớ. Tóm lại:

  • Mọi object đều có một "reference count", được biểu diễn bằng một số nguyên integer.
  • Reference count được set bằng 1 khi object đó được khởi tạo, và ai khởi tạo nó thì người đó sẽ là tự động được coi là owner.
  • Nếu muốn own một object, bạn chỉ cần tăng 1 reference count của object đó.
  • Khi release, xóa bỏ owner, reference count của object đó sẽ giảm 1.
  • Khi reference count của một object bằng 0, nghĩa là nó không còn owner nào, không còn được ai sử dụng, object đó sẽ được giải phóng khỏi bộ nhớ.

Basic Principles

Quản lý bộ nhớ trong Objective-C bao gồm 4 nguyên tắc cơ bản sau. Nếu tuân thủ các nguyên tắc này, chúng ta sẽ tránh được tình trạng leak memory hoặc crash app do bị lỗi null pointer.

Rule 1

Nếu bạn khởi tạo một object sử dụng các method alloc, copy hoặc new thì bạn sẽ own object đó, reference count sẽ tăng lên 1.

Vì vậy khi không sử dụng object nữa, chúng ta phải release một cách thủ công.

Ví dụ:

NSString *strA = [[NSString alloc] initWithString:@"Hello world!"];
NSString *strB = [@"John Lennon" copy];
NSMutableString *strC = [@"The Beatles" mutableCopy];
NSString *strD = [NSString new];

[strA release];
[strB release];
[strC release];
[strD release];

Rule 2

Nếu bạn retain một object, bạn cũng own object đó, reference count cũng tăng lên 1.

Ví dụ:

[donkey retain];
[eagle retain];
[eagle retain];
[eagle retain];

[donkey release];
[eagle release];
[eagle release];
[eagle release];

Ở ví dụ trên, chúng ta đã retain object eagle 3 lần, vì vậy cần phải release nó 3 lần.

Rule 3

Nếu bạn own một object, bạn phải release nó.

Rule này chỉ ra khi nào bạn cần phải release một object để giảm retain count của nó đi. Cho đến khi retain count của object đó bằng 0 thì nó sẽ tự động gọi method dealloc để tự hủy một cách tự động.

Điều này có nghĩa là bạn không bao giờ được gọi trực tiếp method dealloc. Thay vào đó chỉ cần release object đủ và đúng thời điểm, phần việc giải phóng bộ nhớ sẽ tự động được giải quyết.

Ví dụ:

NSString *iMadeThis = [[NSString alloc] init]; // Rule 1
[iMadeThis release];

[imSharingThis retain]; // Rule 2
[imSharingThis release];

// Có thể own một object nhiều lần
Pidgeon *pidgeon = [[Pidgeon alloc] init]; // Rule 1
[pidgeon retain]; // Rule 2
[pidgeon release];
[pidgeon release];

Rule 4

Nếu bạn trỏ đến một object thì bạn phải own object đó.

Nếu bạn own một object thì bạn có thể sử dụng pointer để truy cập và sử dụng object đó thoải mái. Nhưng nếu bạn không own nó, trong một số trường hợp đặc biệt, bạn vẫn có thể truy cập nó trong bộ nhớ một cách tạm thời. Tuy nhiên với các object kiểu ivar hoặc các biến global, để sử dụng chúng, bạn phải retain để có được ownership. Nếu không, những object này rất có thể đã bị giải phóng khỏi bộ nhớ và bạn sẽ bị lỗi null pointer và dẫn đến crash app.

Có một trường hợp ngoại lệ, đó các việc sử dụng các object kiểu string. Các string không bao giờ bị deallocate. Các thao tác retainrelease không có tác động gì với các string.hen you are trying to avoid retain cycles, which we will look at later.

Dưới đây là một số ví dụ về việc sử dụng biến đúng và sai cách:

// Đúng, vì không cần retain đối với object kiểu string
NSString* g_globalDefaultName = @"Balram";

@interface Tiger {
    NSString *name;
    NSImage *picture;
}
// Sai, property này phải là retain hoặc copy.
@property (assign) NSImage *picture;

- (id)initWithName:(NSString*)nameArg;
+ (void)setDefaultName:(NSString*)defaultName;
@end

@implementation

@synthesize picture;

- (id)initWithName:(NSString*)nameArg
{
    if (self = [super init]) {
        // Sai
        name = nameArg;
        
        // Đúng ra phải là
        // name = [nameArg copy]; // Đúng
        
        // Hoặc
        // name = [nameArg retain]; // Đúng
    }
    return self;
}

- (void)dealloc
{
    // Đúng, vì luôn phải relase các biến ivar mà bạn own trong method `dealloc`
    [name release];
    [image release];
    [super dealloc];
}

+ (void)setDefaultName:(NSString*)defaultName;
{
    // Sai
    g_globalDefaultName = defaultName;
    
    // Đúng ra phải là:
    // if (g_globalDefaultName != defaultName) {
    //     [g_globalDefaultName release];
    //     g_globalDefaultName = [defaultName copy];
    // }
}
@end

NSAutoreleasePool and autorelease

Giả sử chúng ta khởi tạo một object string như sau:

NSString *greeting = [NSString stringWithFormat:@"%@ %@!", @"Hello", @"everyone"];

Theo như Rule 1, chúng ta sẽ không own object greeting string trên bởi vì method stringWithFormat: không phải 1 trong 3 method gây tăng reference count alloc, copy, new. Vậy thì không own object này thì chúng ta có thể sử dụng nó không? Có cần release object này không?

Trong Objective-C, tại một thời điểm sẽ luôn có một global NSAutorealsePool hoạt động. Khi bạn gọi method autorelease trên một object thì object đó sẽ được thêm vào global pool hiện tại. Khi một pool được "drain" thì tất cả các object có trong pool đó sẽ được release. Vì vậy, autorelease có tác dụng khiến object sẽ được tự động release trong tương lai.

Đây cũng chính là lý do vì sao chúng ta không own và không cần release thủ công object greeting string. Bởi vì khi nó được khởi tạo bằng stringWithFormat: đã tự động gọi thêm method autorelease , có nghĩa là nó đã được thêm vào global pool hiện tại và sẽ được release ngay khi pool được drain. Object sau khi pool drain sẽ không thể truy cập vì bộ nhớ của nó đã được giải phóng. Và một pool sẽ được drain sau khi tất cả các NSEvent được gửi.

Ngoài các global pool sẵn có mặc định, chúng ta cũng có thể tự tạo các autorelease pool và gọi drain một cách thủ công:

// Object `outside` sẽ được đưa vào global pool sẵn có hiện tại
Tiger *outside = [[[Tiger alloc] init] autorelease];

// Tự tạo một global pool mới `newPool` và pool này sẽ là pool hiện tại hoạt động
NSAutoreleasePool *newPool = [[NSAutoreleasePool alloc] init];

// Object `inside` sẽ được vào pool mới tạo `newPool`
Tiger *inside = [[[Tiger alloc] init] autorelease];

// Drain `newPool` sẽ khiến tất cả các object có trong pool này bị release, bao gồm object `inside`
[newPool drain];

// Sai, sẽ gây ra crash app. Vì object `inside` đã bị dealloc do nằm trong `newPool` đã bị drain trước đó
// Object này sẽ không thể truy cập được nữa
[inside speak];

// Đúng. Vì object `outside` được đưa vào autorelase pool khác trước đó
// Nên nó sẽ không bị release khi `newPool` bị drain
[outside speak];

Tóm lại, chúng ta vẫn có thể truy cập và sử dụng một object không own trước khi thoát khỏi một method. Nếu muốn một object được giữ lại trong bộ nhớ khi return method, hãy retain để own nó.

Common mistakes

Releasing an object you don't own

Release một object mà bạn không hề own nó.

Không phải lúc nào bạn cũng cần phải release một object sau khi khởi tạo. Chúng ta chỉ cần release với các object mà chúng ta own.

Ví dụ: Khởi tạo object string bằng stringWithFormat: thì không cần release.

Keeping and using an object you don't own

Truy cập và sử dụng một object mà bạn không hề own nó.

@interface Tiger {
    NSString *voice;
}
- (void)speak;
@end

@implementation Tiger
- (id)init;
{
    if (self = [super init]) {
        // Sai, vì không own object `voice`, object này sẽ bị dealloc ngay khi method init kết thúc
        voice = [NSString stringWithFormat:@"%@, I'm a %@", "ROAR", "tiger"];
    }
}

- (void)speak;
{
    // Sẽ gây ra crash app vì null pointer, object ```voice``` đã bị dealloc
    NSLog(@"%@", voice);
}

- (void)dealloc;
{
    // Ở đây cũng sẽ gây ra crash vì null pointer
    [voice release];
    [super dealloc];
}

@end

Theo Rule 1, stringWithFormat: sẽ không giúp bạn own object voice ngay cả khi bạn khởi tạo nó. Vì vậy nếu truy cập và sử dụng object này trong method speakdealloc sẽ gây ra crash vì voice đã bị giải phóng bộ nhớ.

Calling dealloc directly

dealloc trực tiếp một object.

Một số người lầm tưởng quản lý bộ nhớ trong Objective-C cũng giống hệt như C++. Tuy nhiên thì không phải như vậy.

Chúng ta chỉ nên release chứ không được dealloc trực tiếp một object. Khi release, reference count của object đó sẽ giảm 1. Khi giảm xuống 0, object này sẽ tự động được dealloc.

// Sai
Tiger *pet = [Tiger alloc];
[pet speak];
[pet dealloc];

// Đúng
Tiger *pet = [[Tiger alloc] init];
[pet speak];
[pet release];

References:


All Rights Reserved