Cơ Chế Hoạt Động Của Property Và Quản Lý Bộ Nhớ Trong Objective-C

So sánh truy cập biến instance và property

Trước khi đi sâu vào các thuộc tính memory management, cần phân biệt rõ cách truy cập biến thành viên (instance variable) và property. Xét một lớp TechDocument chỉ chứa một biến instance là revisionCount:

@interface TechDocument : NSObject {
    NSInteger revisionCount;
}
@end

Nếu sử dụng toán tử -> trong C, ta có thể truy cập trực tiếp vào vùng nhớ của biến này mà không cần khai báo property:

TechDocument *doc = [[TechDocument alloc] init];
doc->revisionCount = 10;

Điều này chứng minh biến instance có thể được truy cập trực tiếp. Tuy nhiên, khi chuyển sang cú pháp dấu chấm (dot syntax) thường thấy trong Objective-C:

doc.revisionCount = 10;

Trình biên dịch sẽ báo lỗi. Nguyên nhân là cú pháp chấm thực chất là lời gọi gián tiếp đến phương thức setter và getter. Vì lớp chưa định nghĩa các phương thức này nên lỗi xảy ra.

Giải pháp là khai báo explicit setter/getter hoặc sử dụng @property. Khi dùng @property, trình biên dịch sẽ tự động sinh ra phương thức truy cập và một biến instance ẩn có tên bắt đầu bằng dấu gạch dưới (ví dụ: _revisionCount). Lúc này, việc gán giá trị qua cú pháp chấm sẽ gọi đến setter, còn truy cập trực tiếp biến _revisionCount trong implementation sẽ bypass qua các phương thức này.

Vai trò của @synthesize

Trong các phiên bản Objective-C hiện đại, @property thường tự động đồng bộ (auto-synthesize). Tuy nhiên, nếu lập trình viên tự viết tay phương thức setter/getter, trình biên dịch sẽ ngừng việc tự động tạo biến instance ẩn.

Ví dụ, nếu bạn khai báo property nhưng lại tự implement setter, biến _revisionCount sẽ không tồn tại, dẫn đến lỗi biên dịch khi cố gắng sử dụng nó. Để khắc phục, từ khóa @synthesize được sử dụng để yêu cầu trình biên dịch tạo ra biến instance tương ứng:

@implementation TechDocument
@synthesize revisionCount = _revisionCount;
@end

@synthesize cũng cho phép tùy chỉnh tên biến instance. Nếu không chỉ định tên mới, biến sẽ trùng tên với property. Nếu chỉ định, ví dụ @synthesize revisionCount = customVar;, biến nội bộ sẽ mang tên customVar.

Các thuộc tính quản lý bộ nhớ

Ngoài atomicnonatomic, các thuộc tính quan trọng ảnh hưởng đến vòng đời đối tượng gồm: strong, weak, copy, retain, và assign. Hành vi của chúng thể hiện rõ nhất khi gán giá trị mới cho property.

Strong và Weak

Khi gán một giá trị newValue cho property item:

  • strong: Tương đương với việc tăng retain count. Con trỏ giữ tham chiếu mạnh đến đối tượng.
  • weak: Chỉ giữ tham chiếu yếu. Không tăng retain count, con trỏ sẽ tự động trở thành nil khi đối tượng bị giải phóng.

Assign, Retain và Copy

Cơ chế underlying của các thuộc tính này khi thực hiện lệnh gán self.item = newValue có thể được mô tả như sau:

  • assign: Gán trực tiếp con trỏ. Không thao tác trên retain count. Đây là mặc định cho các kiểu dữ liệu nguyên thủy.
  • retain: Kiểm tra sự khác biệt, release giá trị cũ, retain giá trị mới rồi gán.
  • copy: Tương tự retain, nhưng thay vì retain, nó gọi phương thức copy trên đối tượng mới để tạo một bản sao độc lập.

Xử lý đối tượng Mutable và Immutable

Việc lựa chọn giữa strongcopy phụ thuộc vào tính bất biến của đối tượng. Xét trường hợp sử dụng NSString (immutable) và NSMutableString (mutable).

Nếu khai báo một property kiểu NSString (bất biến) nhưng sử dụng thuộc tính strong và gán vào đó một đối tượng NSMutableString:

@property (nonatomic, strong) NSString *title;
// Gán một mutable string
NSMutableString *mutableTitle = [NSMutableString stringWithString:@"Initial"];
self.title = mutableTitle;
// Sửa đổi mutableTitle
[mutableTitle appendString:@" Modified"];

Lúc này, giá trị của self.title cũng bị thay đổi theo dù nó được khai báo là immutable. Điều này vi phạm nguyên tắc đóng gói. Do đó, **property bất biến nên sử dụng copy**. Khi dùng copy, một vùng nhớ mới được cấp phát cho bản sao, đảm bảo giá trị không bị thay đổi từ bên ngoài.

Ngược lại, với property khả biến (ví dụ NSMutableArray), không nên dùng copy. Vì phương thức copy trên đối tượng mutable thường trả về một đối tượng immutable. Nếu cố gắng gọi các phương thức thay đổi trạng thái lên property này, ứng dụng sẽ crash do NSInvalidArgumentException.

Vì vậy, **property khả biến nên dùng strong**. Tuy nhiên, để tránh rủi ro bị thay đổi giá trị từ bên ngoài, khi gán giá trị, nên sử dụng phương thức mutableCopy để đảm bảo tính độc lập của dữ liệu.

Hành vi của phương thức Getter

Thuộc tính copy chỉ tác động vào quá trình gán giá trị (setter). Khi truy xuất giá trị (getter), hệ thống chỉ trả về con trỏ hiện tại mà không tạo bản sao mới. Điều này có nghĩa là dù property được khai báo là copy, việc lấy giá trị ra vẫn chỉ là tham chiếu đến vùng nhớ đang lưu trữ.

Phân biệt Assign và Weak trên đối tượng

Mặc dù cả assignweak đều không tăng retain count, nhưng chúng khác biệt quan trọng về an toàn bộ nhớ:

  • weak: Khi đối tượng được trỏ đến bị dealloc, con trỏ sẽ tự động được gán về nil. Điều này ngăn chặn việc truy cập vào vùng nhớ đã giải phóng.
  • assign: Con trỏ vẫn giữ địa chỉ cũ sau khi đối tượng bị giải phóng, tạo ra "con trỏ treo" (dangling pointer). Truy cập vào con trỏ này có thể gây crash ngẫu nhiên.

Do đó, quy tắc chuẩn là sử dụng assign cho các kiểu dữ liệu nguyên thủy (int, float, struct) và sử dụng weak cho các đối tượng để đảm bảo an toàn khi quản lý bộ nhớ.

Thẻ: objective-c memory-management property-attributes ios-development arc

Đăng vào ngày 31 tháng 7 lúc 00:13