Mục lục
- Các Câu Lệnh Điều Kiện
- Biểu Thức Yoda
- Kiểm Tra nil và BOOL
- Đường Dẫn Vàng (Golden Path)
- Biểu Thức Phức Tạp
- Toán Tử Ba Ngôi
- Xử Lý Lỗi
- Các Câu Lệnh Case
- Quy Tắc Đặt Tên
- Lớp
- Thuộc Tính (Properties)
- Khởi Tạo và Hủy Đối Tượng (trong ngữ cảnh thuộc tính)
- Ký Hiệu Dấu Chấm
- Định Nghĩa Thuộc Tính
- Thuộc Tính Riêng Tư
- Đối Tượng Mutable
- Tải Trì Hoãn (Lazy Loading)
- Phương Thức (tiếp)
- Categories
- Protocols
- NSNotification
- Làm Đẹp Mã
- Tổ Chức Mã
- Giao Tiếp Giữa Các Đối Tượng
- Blocks
- Đi Sâu vào Blocks
- Vòng Lặp Tham Chiếu với
self - Delegate và Datasource
- Kế Thừa (trong Delegate)
- Đa Delegate
- Lập Trình Hướng Khía Cạnh (AOP)
Mục tiêu của tài liệu này là cung cấp một bộ hướng dẫn về cách viết mã Objective-C rõ ràng và hiệu quả. Thay vì đưa ra các quy tắc cứng nhắc, chúng tôi mong muốn tạo ra một nền tảng để các nhà phát triển có thể viết mã một cách nhất quán hơn. Theo thời gian, tài liệu đã phát triển để bao gồm các nguyên tắc về thiết kế và cấu trúc mã nguồn chất lượng cao.
Quan điểm chính là mã nguồn không chỉ cần biên dịch được mà còn phải "hiệu quả". Mã tốt phải có những đặc điểm sau: ngắn gọn, dễ hiểu, tổ chức tốt, được tài liệu hóa kỹ lưỡng, đặt tên hợp lý, thiết kế xuất sắc và có khả năng kiểm thử cao.
Một triết lý cốt lõi của tài liệu này là ưu tiên sự rõ ràng của mã nguồn hơn hiệu suất, đồng thời giải thích lý do tại sao lại như vậy.
Mặc dù tất cả các ví dụ mã đều được viết bằng Objective-C, một số chủ đề mang tính tổng quát và độc lập với ngôn ngữ lập trình.
Các Câu Lệnh Điều Kiện
Thân của các câu lệnh điều kiện (như if, else, while, for) luôn phải được bao quanh bởi dấu ngoặc nhọn. Mặc dù đôi khi bạn có thể bỏ qua dấu ngoặc nhọn (ví dụ, khi thân chỉ có một dòng), việc này tiềm ẩn nguy cơ gây lỗi. Chẳng hạn, khi thêm một dòng mã, bạn có thể nhầm tưởng rằng nó nằm trong khối điều kiện. Nguy hiểm hơn, nếu dòng mã ngay sau if bị comment đi, dòng tiếp theo sẽ vô tình trở thành một phần của câu lệnh if.
Nên dùng:
if (!processingError) {
return operationSuccess;
}
Không nên dùng:
if (!processingError)
return operationSuccess;
Và:
if (!processingError) return operationSuccess;
Một ví dụ nổi tiếng về lỗi goto fail đã được phát hiện trong triển khai SSL/TLS của Apple vào tháng 2 năm 2014. Mã nguồn gây lỗi trông như sau:
static OSStatus
SSLVerifySignedServerKeyExchange(SSLContext *ctx, bool isRsa, SSLBuffer signedParams,
uint8_t *signature, UInt16 signatureLen)
{
OSStatus err;
// ...
if ((err = SSLHashSHA1.update(&hashCtx, &serverRandom)) != 0)
goto fail;
if ((err = SSLHashSHA1.update(&hashCtx, &signedParams)) != 0)
goto fail;
goto fail; // Lỗi: dòng goto fail thừa không được bao trong if
if ((err = SSLHashSHA1.final(&hashCtx, &hashOut)) != 0)
goto fail;
// ...
fail:
SSLFreeBuffer(&signedHashes);
SSLFreeBuffer(&hashCtx);
return err;
}
Rõ ràng, việc thiếu dấu ngoặc nhọn đã dẫn đến việc có hai dòng goto fail; liên tiếp, trong đó một dòng không thuộc về câu lệnh if nào. Chúng ta chắc chắn không muốn tạo ra lỗi tương tự. Hơn nữa, việc giữ phong cách nhất quán cho tất cả các câu lệnh điều kiện cũng giúp dễ dàng kiểm tra hơn.
Biểu Thức Yoda
Không sử dụng biểu thức Yoda. Biểu thức Yoda là khi một hằng số được so sánh với một biến, thay vì biến được so sánh với hằng số. Nó giống như hỏi "Xanh lam có phải là màu của bầu trời không?" thay vì "Bầu trời có màu xanh lam không?".

(Ghi chú: Tên gọi này bắt nguồn từ cách nói ngược của Sư phụ Yoda trong Star Wars.)
Nên dùng:
if ([currentValue isEqual:@100]) { /* ... */ }
Không nên dùng:
if ([@100 isEqual:currentValue]) { /* ... */ }
Kiểm Tra nil và BOOL
Tương tự như biểu thức Yoda, cách kiểm tra nil cũng gây tranh cãi. Một số thư viện lớn kiểm tra đối tượng có phải là nil hay không như sau:
if (nil == targetObject) { /* ... */ }
Một số người có thể cho rằng điều này là sai, vì nil là một hằng số, khiến nó giống biểu thức Yoda. Tuy nhiên, một số lập trình viên làm vậy để tránh khó khăn trong việc debug. Hãy xem mã dưới đây:
if (targetObject == nil) { /* ... */ }
Nếu lập trình viên gõ nhầm thành:
if (targetObject = nil) { /* ... */ }
Đây là một câu lệnh hợp lệ, nhưng ngay cả lập trình viên giàu kinh nghiệm cũng khó có thể phát hiện lỗi này chỉ bằng cách nhìn vào mã nhiều lần. Ngược lại, nếu đặt nil ở bên trái, lỗi này sẽ không xảy ra vì nil không thể được gán giá trị. Điều này giúp lập trình viên dễ dàng kiểm tra nguyên nhân tiềm ẩn hơn là rà soát từng dòng mã.
Để tránh những vấn đề kỳ lạ này, hãy sử dụng toán tử phủ định. Vì nil được hiểu là NO, không cần phải so sánh nó với các giá trị khác trong câu lệnh điều kiện. Đồng thời, không nên so sánh trực tiếp với YES, vì YES được định nghĩa là 1, trong khi BOOL là 8 bit, thực chất là kiểu char.
Nên dùng:
if (targetObject) { /* ... */ }
if (![targetObject booleanValue]) { /* ... */ }
if (!targetObject) { /* ... */ }
Không nên dùng:
if (targetObject == YES) { /* ... */ } // Sai
if (numericValue == YES) { /* ... */ } // Không bao giờ làm vậy.
if ([targetObject booleanValue] == NO) { /* ... */ }
Cách này cũng giúp tăng tính nhất quán và cải thiện khả năng đọc.
Đường Dẫn Vàng (Golden Path)
Khi lập trình với các câu lệnh điều kiện, lề trái của mã nên tạo thành một "đường dẫn vàng" hay "đường dẫn hạnh phúc". Điều này có nghĩa là, tránh lồng ghép quá nhiều câu lệnh if. Việc sử dụng nhiều câu lệnh return có thể giúp tránh tăng độ phức tạp của vòng lặp và cải thiện khả năng đọc mã. Vì các phần quan trọng của phương thức không bị lồng trong các nhánh điều kiện, bạn có thể dễ dàng tìm thấy các đoạn mã liên quan.
Nên dùng:
- (void)processData {
if (![self isValidInput]) {
return;
}
// Thực hiện điều gì đó quan trọng
}
Không nên dùng:
- (void)processData {
if ([self isValidInput]) {
// Thực hiện điều gì đó quan trọng
}
}
Biểu Thức Phức Tạp
Khi bạn có một mệnh đề if phức tạp, bạn nên trích xuất chúng thành một biến BOOL. Điều này giúp logic rõ ràng hơn và thể hiện ý nghĩa của từng mệnh đề con.
BOOL eventNameContainsKeyword = [eventName containsString:@"Keynote"];
BOOL isCurrentYear = [eventDetails year] == 2024;
BOOL isTargetEvent = eventNameContainsKeyword && isCurrentYear;
if (isTargetEvent) {
// Thực hiện điều gì đó rất thú vị
}
Toán Tử Ba Ngôi
Toán tử ba ngôi ? : chỉ nên được sử dụng khi nó làm cho mã rõ ràng hơn. Tất cả các biến trong một câu lệnh điều kiện nên đã được đánh giá. Tương tự như câu lệnh if, việc tính toán nhiều mệnh đề điều kiện thường làm cho câu lệnh khó hiểu hơn. Hoặc có thể cấu trúc lại chúng thành các biến thể hiện.
Nên dùng:
result = valueA > valueB ? resultX : resultY;
Không nên dùng:
result = valueA > valueB ? resultX = valueC > valueD ? valueC : valueD : resultY;
Khi tham số thứ hai của toán tử ba ngôi (nhánh if) trả về cùng một đối tượng đã được kiểm tra trong điều kiện, cách diễn đạt sau đây sẽ khéo léo hơn:
Nên dùng:
result = item ?: [self generateItem];
Không nên dùng:
result = item ? item : [self generateItem];
Xử Lý Lỗi
Một số phương thức trả về tham chiếu đến đối tượng NSError thông qua tham số. Khi sử dụng các phương thức như vậy, nên kiểm tra giá trị trả về của phương thức, chứ không phải tham chiếu đến NSError.
Nên dùng:
NSError *opError = nil;
if (![self performOperationWithError:&opError]) {
// Xử lý Lỗi
}
Ngoài ra, một số API của Apple có thể ghi các giá trị rác vào tham số error (nếu nó không phải NULL) trong trường hợp thành công. Do đó, việc kiểm tra giá trị của error có thể dẫn đến lỗi (thậm chí là crash).
Các Câu Lệnh Case
Trừ khi trình biên dịch bắt buộc, dấu ngoặc nhọn không cần thiết trong các câu lệnh case. Tuy nhiên, khi một case chứa nhiều dòng lệnh, cần thêm dấu ngoặc nhọn.
switch (optionType) {
case 1:
// ...
break;
case 2: {
// ...
// Ví dụ nhiều dòng sử dụng dấu ngoặc nhọn
break;
}
case 3:
// ...
break;
default:
// ...
break;
}
Đôi khi, bạn có thể sử dụng "fall-through" để thực thi cùng một đoạn mã trong các case khác nhau. "Fall-through" có nghĩa là loại bỏ break khỏi câu lệnh case và để các case phía dưới tiếp tục thực thi.
switch (optionType) {
case 1:
case 2:
// mã được thực thi cho các giá trị 1 và 2
break;
default:
// ...
break;
}
Khi sử dụng một biến kiểu liệt kê (enum) trong câu lệnh switch, từ khóa default là không cần thiết. Ví dụ:
switch (menuSelection) {
case VMCOptionNone:
// ...
break;
case VMCOptionValue1:
// ...
break;
case VMCOptionValue2:
// ...
break;
}
Hơn nữa, để tránh sử dụng default case, nếu một giá trị mới được thêm vào enum, lập trình viên sẽ ngay lập tức nhận được cảnh báo: Enumeration value 'VMCOptionValue3' not handled in switch.
Kiểu Liệt Kê (Enum)
Khi sử dụng enum, nên sử dụng định nghĩa kiểu cơ bản cố định mới, vì nó có khả năng kiểm tra kiểu mạnh mẽ hơn và hỗ trợ tự động hoàn thành mã. SDK hiện có một macro khuyến khích và thúc đẩy việc sử dụng định nghĩa kiểu cố định - NS_ENUM().
Ví dụ:
typedef NS_ENUM(NSUInteger, VMCDeviceState) {
VMCDeviceStateNone,
VMCDeviceStateIdle,
VMCDeviceStateRunning,
VMCDeviceStatePaused
};
Quy Tắc Đặt Tên
Quy Ước Chung
Cố gắng tuân thủ các quy ước đặt tên của Apple, đặc biệt là các quy tắc liên quan đến quản lý bộ nhớ. Nên sử dụng tên phương thức và biến dài, mang tính mô tả.
Nên dùng:
UIButton *controlPanelButton;
Không nên dùng:
UIButton *cpBut;
Hằng Số
Hằng số nên được đặt tên theo kiểu CamelCase và có tiền tố là tên lớp liên quan.
Nên dùng:
static const NSTimeInterval VMCLoginScreenFadeOutDuration = 0.4;
Không nên dùng:
static const NSTimeInterval fadeOutTime = 0.4;
Nên sử dụng hằng số thay cho các giá trị literal chuỗi và số. Điều này giúp dễ dàng tái sử dụng và nhanh chóng sửa đổi mà không cần tìm kiếm và thay thế. Hằng số nên được khai báo là static, không phải #define, trừ khi nó được sử dụng rõ ràng như một macro.
Nên dùng:
static NSString * const VMCCacheManagerDidClearNotification = @"VMCCacheManagerDidClearNotification";
static const CGFloat VMCImageThumbnailHeight = 50.0f;
Không nên dùng:
#define CompanyName @"Apple Inc."
#define magicNumber 42
Hằng số nên được công khai trong tệp tiêu đề dưới dạng:
extern NSString *const VMCCacheManagerDidClearNotification;
và được gán giá trị trong tệp triển khai.
Chỉ các hằng số công khai mới cần thêm tiền tố namespace. Mặc dù các hằng số riêng tư trong tệp triển khai có thể tuân theo một mẫu khác, bạn vẫn có thể tuân thủ quy tắc này.
Phương Thức
Tên phương thức và kiểu phương thức (ký hiệu -/+) nên được cách nhau bằng một khoảng trắng. Các đoạn phương thức cũng nên được cách nhau bằng khoảng trắng (để phù hợp với phong cách của Apple). Tham số luôn phải có một từ khóa mô tả ở phía trước.
Hạn chế sử dụng từ "and". Nó không nên được dùng để làm rõ có nhiều tham số, ví dụ như ví dụ initWithDimensions:size: dưới đây:
Nên dùng:
- (void)configureContent:(NSString *)text withImage:(UIImage *)image;
- (void)sendAction:(SEL)aSelector toTarget:(id)anObject forAllItems:(BOOL)flag;
- (id)viewWithIdentifier:(NSInteger)tag;
- (instancetype)initWithDimensions:(CGFloat)width size:(CGFloat)height;
Không nên dùng:
- (void)setT:(NSString *)text i:(UIImage *)image;
- (void)sendAction:(SEL)aSelector :(id)anObject :(BOOL)flag;
- (id)identifiedView:(NSInteger)tag;
- (instancetype)initWithWidth:(CGFloat)width andHeight:(CGFloat)height;
- (instancetype)initWith:(int)width and:(int)height; // Không bao giờ làm vậy.
Giá Trị Literal
Sử dụng giá trị literal để tạo các đối tượng NSString, NSDictionary, NSArray và NSNumber bất biến. Lưu ý không truyền nil vào NSArray và NSDictionary, vì điều này sẽ gây crash.
Ví dụ:
NSArray *userNames = @[@"Alice", @"Bob", @"Charlie"];
NSDictionary *departmentHeads = @{@"Sales" : @"David", @"Marketing" : @"Eve"};
NSNumber *shouldUseConstants = @YES;
NSNumber *postalCode = @70000;
Không nên làm vậy:
NSArray *userNames = [NSArray arrayWithObjects:@"Alice", @"Bob", @"Charlie", nil];
NSDictionary *departmentHeads = [NSDictionary dictionaryWithObjectsAndKeys: @"David", @"Sales", @"Eve", @"Marketing", nil];
NSNumber *shouldUseConstants = [NSNumber numberWithBool:YES];
NSNumber *postalCode = [NSNumber numberWithInteger:70000];
Nếu cần dùng các bản sao biến đổi được của các lớp này, chúng tôi khuyến nghị sử dụng các lớp như NSMutableArray, NSMutableString.
Nên tránh cách viết sau:
NSMutableArray *aMutableArray = [@[@"Item 1"] mutableCopy];
Cách viết này có vấn đề về hiệu suất và khả năng đọc.
Về hiệu suất, một đối tượng bất biến không cần thiết được tạo ra rồi bị loại bỏ ngay lập tức. Mặc dù điều này không làm chậm ứng dụng của bạn (trừ khi phương thức này được gọi thường xuyên), nhưng nó không cần thiết để tiết kiệm vài ký tự.
Về khả năng đọc, có hai vấn đề: Thứ nhất, khi duyệt mã và thấy @[], bạn thường liên tưởng đến một thể hiện NSArray, nhưng trong trường hợp này, bạn cần dừng lại và suy nghĩ kỹ; thứ hai, một số người mới học có thể không phân biệt được đây là đối tượng biến đổi được hay bất biến khi nhìn thấy mã của bạn. Họ có thể không quen thuộc với ý nghĩa của phép sao chép biến đổi (mặc dù kiến thức này rất quan trọng). Tất nhiên, không có gì là hoàn toàn sai, chúng ta chỉ đang thảo luận về tính khả dụng của mã (bao gồm khả năng đọc).
Lớp
Tên Lớp
Tên lớp nên bắt đầu bằng tiền tố gồm ba chữ cái viết hoa (tiền tố hai chữ cái được dành riêng cho các lớp của Apple). Mặc dù quy tắc này có vẻ hơi kỳ lạ, nhưng nó giúp giảm thiểu các vấn đề phát sinh do Objective-C không có namespace.
Một số nhà phát triển không tuân thủ quy tắc này khi định nghĩa các đối tượng mô hình (đối với các đối tượng Core Data, chúng ta càng nên tuân thủ quy tắc này). Chúng tôi khuyến nghị tuân thủ nghiêm ngặt quy ước này khi định nghĩa các đối tượng Core Data, vì cuối cùng bạn có thể cần hợp nhất Managed Object Model (MOM) của mình với các MOM khác (của thư viện bên thứ ba).
Bạn có thể nhận thấy, tiền tố của các lớp trong tài liệu này (không chỉ lớp, mà cả các hằng số công khai, Protocol, v.v.) là VMC.
Một quy tắc đặt tên lớp tốt khác: Khi bạn tạo một lớp con, bạn nên đặt phần mô tả vào giữa tiền tố và tên lớp cha.
Ví dụ: Nếu bạn có một lớp VMCNetworkClient, tên lớp con sẽ là VMCTwitterNetworkClient (lưu ý "Twitter" nằm giữa "VMC" và "NetworkClient"); theo quy ước này, một lớp con của UIViewController sẽ là VMCTimelineViewController.
Khởi Tạo và Hủy Đối Tượng
Cách tổ chức mã được khuyến nghị là đặt phương thức dealloc ở đầu tệp triển khai (ngay sau @synthesize và @dynamic), phương thức init nên theo sau dealloc.
Nếu có nhiều phương thức khởi tạo, phương thức khởi tạo được chỉ định (designated initializer) nên đặt ở đầu, các phương thức khởi tạo phụ (secondary initializer) theo sau, điều này sẽ logic hơn. Ngày nay với ARC, phương thức dealloc gần như không cần triển khai, nhưng việc đặt init và dealloc cạnh nhau có thể nhấn mạnh về mặt hình ảnh rằng chúng là một cặp. Thông thường, những gì được thực hiện trong phương thức init cần được hoàn tác trong phương thức dealloc.
Cấu trúc của phương thức init nên như sau:
- (instancetype)init
{
self = [super init]; // gọi phương thức khởi tạo được chỉ định của lớp cha
if (self) {
// Khởi tạo tùy chỉnh
}
return self;
}
Tại sao lại gán self bằng giá trị trả về của [super init], và điều gì đang xảy ra ở giữa? Đây là một chủ đề khá thú vị.
Hãy lùi lại một bước: chúng ta thường viết mã như [[NSObject alloc] init], điều này làm mờ đi sự khác biệt giữa alloc và init. Đặc điểm này của Objective-C được gọi là khởi tạo hai bước.
Điều này có nghĩa là việc cấp phát bộ nhớ và khởi tạo được tách thành hai bước, alloc và init.
allocchịu trách nhiệm tạo đối tượng. Quá trình này bao gồm cấp phát đủ bộ nhớ để lưu trữ đối tượng, ghi con trỏisa, khởi tạo bộ đếm tham chiếu và đặt lại tất cả các biến thể hiện.initchịu trách nhiệm khởi tạo đối tượng, nghĩa là đưa đối tượng vào trạng thái sẵn sàng sử dụng. Điều này thường có nghĩa là gán các giá trị hợp lý và hữu ích cho các biến thể hiện của đối tượng.
Phương thức alloc sẽ trả về một thể hiện đối tượng hợp lệ chưa được khởi tạo. Mỗi tin nhắn gửi đến thể hiện này sẽ được chuyển đổi thành một cuộc gọi hàm objc_msgSend(), trong đó tham số self là con trỏ được trả về bởi alloc. Do đó, self có thể được truy cập trong phạm vi của tất cả các phương thức.
Theo quy ước, để hoàn thành khởi tạo hai bước, phương thức đầu tiên được gọi trên thể hiện mới tạo sẽ là phương thức init. Lưu ý, khi NSObject triển khai init, nó chỉ đơn giản trả về self.
Một phần quan trọng khác của quy ước init là: phương thức này có thể (và nên) thông báo cho người gọi rằng việc khởi tạo đã thất bại bằng cách trả về nil. Việc khởi tạo có thể thất bại vì nhiều lý do, ví dụ như định dạng đầu vào sai hoặc một đối tượng cần thiết khác không khởi tạo được.
Với điều này, chúng ta có thể hiểu tại sao luôn cần gọi self = [super init]. Nếu lớp cha của bạn báo rằng việc tự khởi tạo đã thất bại, bạn phải giả định rằng bạn đang ở trong trạng thái không ổn định. Do đó, trong triển khai của bạn, đừng tiếp tục khởi tạo riêng và cũng trả về nil. Nếu không làm vậy, bạn có thể thao tác trên một đối tượng không khả dụng, hành vi của nó sẽ không thể đoán trước, và cuối cùng có thể khiến chương trình của bạn bị crash.
Phương thức init khi được gọi có thể trả về một thể hiện khác thay vì thể hiện đã gọi bằng cách gán lại giá trị cho self. Ví dụ như Class Cluster, và một số lớp Cocoa trả về cùng một thể hiện cho các đối tượng bằng nhau (bất biến).
Phương Thức Khởi Tạo Chính và Phụ
Objective-C có khái niệm về phương thức khởi tạo được chỉ định (designated initializer) và phương thức khởi tạo phụ (secondary initializer).
Phương thức khởi tạo được chỉ định cung cấp tất cả các tham số, phương thức khởi tạo phụ là một hoặc nhiều phương thức gọi phương thức khởi tạo được chỉ định với một hoặc nhiều tham số mặc định.
@implementation VMCActivity
- (instancetype)initWithDescription:(NSString *)description
time:(NSDate *)time
place:(CLLocation *)place
{
self = [super init];
if (self) {
_description = description;
_time = time;
_place = place;
}
return self;
}
- (instancetype)initWithDescription:(NSString *)description
time:(NSDate *)time
{
return [self initWithDescription:description time:time place:nil];
}
- (instancetype)initWithDescription:(NSString *)description
{
return [self initWithDescription:description time:[NSDate date] place:nil];
}
@end
initWithDescription:time:place: là phương thức khởi tạo được chỉ định, hai phương thức còn lại là phương thức khởi tạo phụ, vì chúng chỉ gọi phương thức khởi tạo được chỉ định của lớp.
Designated Initializer
Một lớp nên có và chỉ có một phương thức khởi tạo được chỉ định. Các phương thức khởi tạo khác nên gọi phương thức khởi tạo được chỉ định này (mặc dù có một ngoại lệ).
Việc kế thừa lớp cho phép gọi bất kỳ phương thức khởi tạo được chỉ định nào, và cần đảm bảo rằng tất cả các phương thức khởi tạo được chỉ định trong chuỗi kế thừa lớp được gọi từ lớp tổ tiên (thường là NSObject) đến lớp hiện tại.
Thực tế, điều này có nghĩa là mã khởi tạo đầu tiên được thực thi là từ lớp tổ tiên xa nhất, sau đó đi xuống chuỗi kế thừa, mỗi lớp có cơ hội thực thi mã khởi tạo riêng của mình. Bằng cách này, trước khi bạn thực hiện công việc khởi tạo cụ thể của mình, tất cả những gì được kế thừa từ lớp cha đã ở trạng thái sẵn sàng. Mặc dù không có quy định rõ ràng, tất cả các framework của Apple đều tuân thủ quy ước này, và lớp của bạn cũng nên làm vậy.
Khi định nghĩa một lớp mới có ba cách khác nhau:
- Không cần ghi đè bất kỳ hàm khởi tạo nào.
- Ghi đè phương thức khởi tạo được chỉ định của lớp cha.
- Định nghĩa một phương thức khởi tạo được chỉ định mới.
Giải pháp đầu tiên đơn giản nhất: bạn không cần thêm bất kỳ logic khởi tạo nào cho lớp, chỉ cần tuân theo phương thức khởi tạo được chỉ định của lớp cha.
Khi bạn muốn cung cấp logic khởi tạo bổ sung, bạn có thể ghi đè phương thức khởi tạo được chỉ định của lớp cha. Bạn chỉ cần ghi đè phương thức khởi tạo được chỉ định của lớp cha trực tiếp và đảm bảo triển khai của bạn gọi phương thức của lớp cha.
Một ví dụ điển hình là khi bạn tạo lớp con UIViewController và ghi đè phương thức initWithNibName:bundle:.
@implementation VMCBaseViewController
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
// gọi phương thức khởi tạo được chỉ định của lớp cha
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
// Khởi tạo tùy chỉnh
}
return self;
}
@end
Trong ví dụ lớp con UIViewController, việc ghi đè init sẽ là một lỗi. Trong trường hợp này, người gọi sẽ cố gắng gọi initWithNib:bundle để khởi tạo lớp của bạn, và triển khai của lớp bạn sẽ không được gọi. Điều này cũng vi phạm quy tắc rằng bất kỳ phương thức khởi tạo được chỉ định nào cũng phải hợp lệ.
Khi bạn muốn cung cấp hàm khởi tạo riêng của mình, bạn nên tuân thủ ba bước này để đảm bảo hành vi chính xác:
- Định nghĩa hàm khởi tạo được chỉ định của bạn, đảm bảo gọi hàm khởi tạo được chỉ định của lớp cha trực tiếp.
- Ghi đè hàm khởi tạo được chỉ định của lớp cha trực tiếp. Gọi hàm khởi tạo được chỉ định mới của bạn.
- Viết tài liệu cho hàm khởi tạo được chỉ định mới.
Nhiều nhà phát triển bỏ qua hai bước sau, đây không chỉ là vấn đề cẩu thả mà còn vi phạm các quy tắc framework, có thể dẫn đến hành vi không xác định và lỗi.
Hãy xem một ví dụ triển khai đúng:
@implementation VMCArticleViewController
- (id)initWithArticle:(VMCArticle *)article
{
// gọi phương thức khởi tạo được chỉ định của lớp cha trực tiếp
self = [super initWithNibName:nil bundle:nil];
if (self) {
_article = article;
}
return self;
}
// Ghi đè phương thức khởi tạo được chỉ định của lớp cha trực tiếp
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
// gọi phương thức khởi tạo được chỉ định mới
return [self initWithArticle:nil];
}
@end
Nếu bạn không ghi đè initWithNibName:bundle:, và người gọi quyết định sử dụng phương thức này để khởi tạo lớp của bạn (điều này hoàn toàn hợp lệ). initWithArticle: sẽ không bao giờ được gọi, dẫn đến quy trình khởi tạo không chính xác, và logic khởi tạo cụ thể của lớp bạn sẽ không được thực thi.
Ngay cả khi có thể suy luận được phương thức nào là phương thức khởi tạo được chỉ định, tốt hơn hết là nên làm rõ nó (bạn của tương lai hoặc các nhà phát triển khác sẽ cảm ơn bạn khi sửa mã).
Bạn nên xem xét sử dụng hai chiến lược này (không loại trừ lẫn nhau): Thứ nhất là bạn làm rõ trong tài liệu phương thức khởi tạo nào là được chỉ định. Bạn có thể sử dụng chỉ thị trình biên dịch __attribute__((objc_designated_initializer)) để đánh dấu ý định của mình.
Khi sử dụng chỉ thị trình biên dịch này, trình biên dịch sẽ giúp bạn. Nếu phương thức khởi tạo được chỉ định mới của bạn không gọi phương thức khởi tạo được chỉ định của lớp cha, trình biên dịch sẽ đưa ra cảnh báo.
Tuy nhiên, khi không gọi phương thức khởi tạo được chỉ định của lớp (và cung cấp các tham số cần thiết), và gọi phương thức khởi tạo được chỉ định trong lớp cha khác, nó sẽ trở thành một trạng thái không khả dụng. Tham khảo ví dụ trước, khi khởi tạo một VMCArticleViewController để hiển thị một bài báo mà bài báo đó không hiển thị thì sẽ vô nghĩa. Trong trường hợp này, bạn nên vô hiệu hóa các phương thức khởi tạo được chỉ định khác để buộc gọi một phương thức khởi tạo được chỉ định rất cụ thể. Bằng cách sử dụng một chỉ thị trình biên dịch khác __attribute__((unavailable("Invoke the designated initializer"))) để sửa đổi một phương thức, thuộc tính này sẽ tạo ra lỗi biên dịch khi bạn cố gắng gọi phương thức đó.
Đây là tệp tiêu đề của ví dụ trước (sử dụng macro để làm cho mã bớt dài dòng):
@interface VMCArticleViewController : UIViewController
- (instancetype)initWithArticle:(VMCArticle *)article VMC_DESIGNATED_INITIALIZER;
- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil VMC_UNAVAILABLE_INSTEAD(initWithArticle:);
- (instancetype)init VMC_UNAVAILABLE_INSTEAD(initWithArticle:);
@end
Một suy luận từ điều trên là: bạn không bao giờ nên gọi một phương thức khởi tạo phụ từ một phương thức khởi tạo được chỉ định (nếu phương thức khởi tạo phụ tuân thủ quy ước, nó sẽ gọi phương thức khởi tạo được chỉ định). Nếu làm vậy, cuộc gọi rất có thể sẽ gọi một phương thức init được ghi đè bởi lớp con và rơi vào vòng lặp vô hạn.
Tuy nhiên, một ngoại lệ là liệu một đối tượng có tuân thủ giao thức NSCoding hay không, và nó được khởi tạo thông qua phương thức initWithCoder:.
Chúng ta nên xem xét liệu lớp cha có tuân thủ giao thức NSCoding hay không để xử lý khác nhau.
Khi tuân thủ, nếu bạn chỉ gọi [super initWithCoder:], bạn có thể cần viết một số mã khởi tạo chung trong phương thức khởi tạo được chỉ định. Một cách tốt để xử lý trường hợp này là đặt các mã đó vào một phương thức riêng tư (ví dụ: p_commonInit).
Khi lớp cha của bạn không tuân thủ giao thức NSCoding, nên coi initWithCoder: như một phương thức khởi tạo phụ và gọi phương thức khởi tạo được chỉ định của self. Lưu ý rằng điều này vi phạm quy định của Apple được viết trong Archives and Serializations Programming Guide:
đối tượng nên gọi phương thức khởi tạo được chỉ định của lớp cha để khởi tạo trạng thái kế thừa trước tiên.
Nếu lớp của bạn không phải là lớp con trực tiếp của NSObject, việc làm như vậy có thể dẫn đến hành vi không thể đoán trước.
Secondary Initializer
Như đã mô tả trước đó, phương thức khởi tạo phụ là một cách cung cấp giá trị mặc định, hành vi cho phương thức khởi tạo được chỉ định. Tức là, trong các phương thức như vậy, bạn không nên thực hiện các thao tác khởi tạo biến thể hiện, và bạn nên luôn giả định rằng phương thức này sẽ không được gọi. Chúng ta đảm bảo rằng phương thức duy nhất được gọi là phương thức khởi tạo được chỉ định.
Điều này có nghĩa là phương thức khởi tạo phụ của bạn luôn phải gọi phương thức khởi tạo được chỉ định hoặc phương thức khởi tạo được chỉ định của self mà bạn tự định nghĩa (trường hợp thứ ba ở trên: tự định nghĩa phương thức khởi tạo được chỉ định). Đôi khi, do nhầm lẫn, có thể gõ thành super, điều này sẽ dẫn đến việc không tuân thủ thứ tự khởi tạo được đề cập ở trên (trong ví dụ đặc biệt này, là bỏ qua khởi tạo của lớp hiện tại).
instancetype
Chúng ta thường bỏ qua việc Cocoa chứa đầy các quy ước, và những quy ước này có thể giúp trình biên dịch trở nên thông minh hơn. Bất kể trình biên dịch gặp phương thức alloc hay init, nó sẽ biết rằng, ngay cả khi kiểu trả về đều là id, các phương thức này luôn trả về một thể hiện của kiểu lớp đã nhận. Do đó, nó cho phép trình biên dịch thực hiện kiểm tra kiểu (ví dụ, kiểm tra xem kiểu được trả về bởi phương thức có hợp lệ hay không). Lợi ích này của Clang đến từ kiểu kết quả liên quan, nghĩa là:
các thông báo được gửi đến một trong các phương thức alloc và init sẽ có cùng kiểu tĩnh với thể hiện của lớp nhận.
Để biết thêm về quy ước định nghĩa kiểu trả về liên quan tự động này, vui lòng xem phần thích hợp trong hướng dẫn Clang Language Extensions.
Một kiểu trả về liên quan có thể được quy định rõ ràng bằng cách sử dụng từ khóa instancetype làm kiểu trả về, và nó có thể rất hữu ích trong một số trường hợp phương thức factory hoặc constructor. Nó có thể gợi ý trình biên dịch kiểm tra kiểu chính xác hơn, và quan trọng hơn, điều này cũng áp dụng cho các lớp con của nó.
@interface VMCUser
+ (instancetype)userWithName:(NSString *)name;
@end
Mặc dù vậy, theo định nghĩa của clang, id có thể được trình biên dịch nâng cấp thành instancetype. Trong alloc hoặc init, chúng tôi khuyến nghị mạnh mẽ sử dụng kiểu instancetype cho tất cả các phương thức lớp và phương thức thể hiện trả về thể hiện của lớp.
Trong API của bạn, hãy tạo thói quen và duy trì tính nhất quán. Ngoài ra, thông qua các điều chỉnh nhỏ trong mã của bạn, bạn có thể cải thiện khả năng đọc: khi duyệt đơn giản, bạn có thể phân biệt được phương thức nào trả về thể hiện của lớp của bạn. Bạn sẽ cảm ơn những chi tiết nhỏ đã được chú ý này sau này.
Các Mẫu Khởi Tạo
Class Cluster
Class cluster được Apple mô tả trong tài liệu của họ như sau:
một kiến trúc nhóm một số lớp con cụ thể, riêng tư dưới một lớp cha trừu tượng, công khai.
Nếu mô tả này nghe quen thuộc, thì trực giác của bạn đúng. Class cluster là tên gọi của Apple cho mẫu thiết kế Abstract Factory.
Ý tưởng của class cluster rất đơn giản: trong quá trình xử lý khởi tạo (lớp), sử dụng thông tin (thường là tham số của phương thức khởi tạo hoặc tham số xác định tính khả dụng của môi trường) để hoàn thành một logic cụ thể hoặc khởi tạo một lớp con cụ thể. Lớp "Public Facing" này phải hiểu rõ các lớp con riêng tư của nó để có khả năng trả về một thể hiện lớp con phù hợp khi đối mặt với các tác vụ cụ thể. Đối với người gọi, chỉ cần biết chức năng của các API khác nhau của đối tượng. Mẫu này ẩn đi logic khởi tạo phức tạp đằng sau nó, và người gọi không cần quan tâm đến chi tiết triển khai.
Class clusters được sử dụng rộng rãi trong Framework của Apple: một số ví dụ rõ ràng như NSNumber có thể trả về các kiểu khác nhau cho lớp con của bạn, tùy thuộc vào cách cung cấp kiểu số (Integer, Float, v.v.) hoặc NSArray trả về các lớp con với chiến lược lưu trữ tối ưu khác nhau.
Điểm tinh tế của mẫu này là người gọi có thể hoàn toàn không quan tâm đến các lớp con. Trên thực tế, điều này có thể được sử dụng trong việc thiết kế một thư viện, có thể được dùng để trao đổi các lớp thực tế được trả về, mà không cần quan tâm đến các chi tiết liên quan, vì tất cả chúng đều tuân thủ các phương thức của lớp cha trừu tượng.
Kinh nghiệm của chúng tôi là sử dụng class cluster có thể giúp loại bỏ nhiều câu lệnh điều kiện.
Một ví dụ điển hình là nếu bạn có một lớp con UIViewController giống nhau cho iPad và iPhone, nhưng có hành vi khác nhau trên các thiết bị khác nhau.
Triển khai cơ bản hơn là sử dụng các câu lệnh điều kiện để kiểm tra thiết bị, sau đó thực hiện logic khác nhau. Mặc dù ban đầu có vẻ tốt, nhưng khi mã phát triển, logic chạy cũng sẽ trở nên phức tạp.
Một thiết kế triển khai tốt hơn là tạo một bộ điều khiển chế độ xem trừu tượng và tổng quát để chứa tất cả logic chia sẻ, và có hai ví dụ con cụ thể cho các thiết bị khác nhau.
Bộ điều khiển chế độ xem tổng quát sẽ kiểm tra thiết bị hiện tại và trả về lớp con thích hợp.
@implementation VMCImageGalleryController
- (id)initWithImages:(NSArray *)images
{
if ([self isMemberOfClass:VMCImageGalleryController.class]) {
self = nil;
if ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad) {
self = [[VMCImageGalleryController_iPad alloc] initWithImages:images];
}
else {
self = [[VMCImageGalleryController_iPhone alloc] initWithImages:images];
}
return self;
}
return [super initWithNibName:nil bundle:nil];
}
@end
Ví dụ này minh họa cách tạo một class cluster.
- Sử dụng
[self isMemberOfClass:VMCImageGalleryController.class]để ngăn chặn việc ghi đè phương thức khởi tạo trong lớp con, tránh vòng lặp vô hạn. Khi[[VMCImageGalleryController alloc] initWithImages:images]được gọi, kết quả của biểu thức điều kiện trên sẽ làTrue. - Mục đích của
self = nillà loại bỏ tất cả các tham chiếu đến thể hiệnVMCImageGalleryController, thể hiện đó (thể hiện của lớp trừu tượng) sẽ được giải phóng (tất nhiên, dù là ARC hay MRC thìdeallocđều xảy ra vào cuối chu kỳ Main Runloop hiện tại). - Logic tiếp theo là quyết định lớp con riêng tư nào sẽ được khởi tạo. Chúng ta giả sử đoạn mã này chạy trên iPhone và
VMCImageGalleryController_iPhonekhông ghi đè phương thứcinitWithImages:. Trong trường hợp này, khi thực thiself = [[VMCImageGalleryController_iPhone alloc] initWithImages:images];,VMCImageGalleryControllersẽ được gọi, và kiểm tra lần đầu tiên sẽ xảy ra ở đây. VìVMCImageGalleryController_iPhonekhông hoàn toàn làVMCImageGalleryController, biểu thức[self isMemberOfClass:VMCImageGalleryController.class]sẽ làFalse, và sau đó sẽ gọi[super initWithNibName:nil bundle:nil], do đó sẽ đi vào quá trình khởi tạo củaVMCImageGalleryController. Lúc này, vì người gọi chính làVMCImageGalleryController, kiểm tra lần này chắc chắn sẽ làTrue, và sau đó quá trình khởi tạo chính xác sẽ diễn ra. (LƯU Ý: Điều này chỉ có hiệu quả khi hoàn toàn tuân thủ các quy tắc thiết kế Designated Initializer và Secondary Initializer! Nếu không hiểu quy tắc này, bạn có thể lùi lại một bước để làm quen với nó rồi quay lại phần giải thích này.)
LƯU Ý: Ở đây có nghĩa là, mã được debug trên iPhone, lập trình viên đã sử dụngself = [[VMCImageGalleryController_iPhone alloc] initWithImages:images];để khởi tạo một đối tượng của view controller. Khi mã chạy trên iPad, quá trình khởi tạo này vẫn chính xác, vì bất kể mã của lập trình viên sử dụngself = [[VMCImageGalleryController_iPhone alloc] initWithImages:images];để khởi tạo viewController (viết trên iPhone, chạy trên iPad), hay sử dụngself = [[VMCImageGalleryController_iPad alloc] initWithImages:images];để khởi tạo viewController (viết trên iPad, chạy trên iPhone), đều sẽ trở nên tổng quát nhờ sự tồn tại của phương thứcinitWithImages:củaVMCImageGalleryController.
Singleton
Nếu có thể, hãy cố gắng tránh sử dụng Singleton mà thay vào đó là dependency injection.
Tuy nhiên, nếu bắt buộc phải dùng, hãy sử dụng một mẫu thread-safe để tạo thể hiện dùng chung. Đối với GCD, chỉ cần dùng hàm dispatch_once().
+ (instancetype)sharedManager
{
static id sharedManager = nil;
static dispatch_once_t onceToken = 0;
dispatch_once(&onceToken, ^{
sharedManager = [[self alloc] init];
});
return sharedManager;
}
Sử dụng dispatch_once() để kiểm soát đồng bộ mã, thay thế cách sử dụng thông thường trước đây:
+ (instancetype)sharedManager
{
static id sharedManager;
@synchronized(self) {
if (sharedManager == nil) {
sharedManager = [[ServiceManager alloc] init];
}
}
return sharedManager;
}
Ưu điểm của dispatch_once() là nhanh hơn và cú pháp sạch hơn, vì dispatch_once() có nghĩa là "thực thi một thứ gì đó một lần", giống như cách chúng ta làm. Điều này cũng giúp tránh các crash có thể xảy ra và đôi khi rất phổ biến.
Các đối tượng singleton kinh điển là: GPS của một thiết bị và cảm biến gia tốc (còn gọi là cảm biến chuyển động) của nó.
Mặc dù đối tượng singleton có thể được kế thừa, nhưng rất hiếm khi cách này hữu ích.
Phải có bằng chứng cho thấy giao diện của một lớp nhất định có xu hướng được sử dụng như một singleton.
Vì vậy, singleton thường chỉ cần công khai một phương thức lớp sharedInstance là đủ, không cần công khai bất kỳ thuộc tính có thể ghi nào.
Cố gắng đặt singleton làm một thùng chứa đối tượng, được chia sẻ ở cấp độ mã hoặc ứng dụng, là một thiết kế tồi và xấu xí.
LƯU Ý: Mẫu singleton nên được áp dụng khi lớp và giao diện của lớp có xu hướng được sử dụng như một singleton. (Người dịch)
Thuộc Tính (Properties)
Thuộc tính nên được đặt tên mô tả nhất có thể, tránh viết tắt, và bắt đầu bằng chữ cái thường theo kiểu CamelCase. Các công cụ của chúng ta có thể dễ dàng giúp chúng ta tự động hoàn thành mọi thứ (gần như mọi thứ, Derived Data của Xcode sẽ lập chỉ mục các tên này). Vì vậy, không có lý do gì để gõ ít ký tự hơn, và tốt nhất là thể hiện càng nhiều thông tin càng tốt trong mã nguồn của bạn.
Ví dụ:
NSString *dataString;
Không nên làm vậy:
NSString* dataString;
NSString * dataString;
(Lưu ý: Thói quen này khác với hằng số, chủ yếu dựa trên thói quen và khả năng đọc. Các nhà phát triển C++ ưa thích tách kiểu khỏi tên biến, với kiểu là NSString* (đối với đối tượng cấp phát từ heap, đối với C++ có thể cấp phát từ stack) định dạng.)
Sử dụng tự động tổng hợp thuộc tính (auto-synthesis) thay vì câu lệnh @synthesize thủ công, trừ khi thuộc tính của bạn là một phần của protocol chứ không phải là một lớp hoàn chỉnh. Nếu Xcode có thể tự động tổng hợp các biến này, hãy để nó làm. Nếu không, bạn chỉ làm mất đi ưu điểm của Xcode và duy trì mã dài dòng hơn.
Bạn nên luôn sử dụng phương thức setter và getter để truy cập thuộc tính, ngoại trừ trong các phương thức init và dealloc. Thông thường, việc sử dụng thuộc tính có thể làm tăng khả năng bạn thêm các khối mã nằm ngoài phạm vi hiện tại, do đó có thể dẫn đến nhiều tác dụng phụ hơn.
Bạn luôn nên sử dụng getter và setter vì:
- Sử dụng setter sẽ tuân thủ ngữ nghĩa quản lý bộ nhớ đã định nghĩa (
strong,weak,copy, v.v.), điều này đã có trước ARC. Ví dụ, thuộc tínhcopyđịnh nghĩa rằng mỗi khi bạn sử dụng setter và truyền dữ liệu, nó sẽ sao chép dữ liệu mà không cần thao tác bổ sung. - Thông báo KVO (
willChangeValueForKey,didChangeValueForKey) sẽ được tự động thực thi. - Dễ dàng debug hơn: bạn có thể đặt breakpoint trên khai báo thuộc tính và breakpoint sẽ thực thi mỗi khi phương thức getter/setter được gọi, hoặc bạn có thể đặt breakpoint trong setter/getter tùy chỉnh của mình.
- Cho phép thêm logic bổ sung để đặt giá trị ở một nơi riêng biệt.
Bạn nên ưu tiên sử dụng getter:
- Nó có khả năng mở rộng cho những thay đổi trong tương lai (ví dụ: thuộc tính được tạo tự động).
- Nó cho phép kế thừa.
- Dễ dàng debug hơn (ví dụ: cho phép đặt breakpoint trong phương thức getter và xem ai đã truy cập getter cụ thể).
- Nó làm cho ý định rõ ràng và minh bạch hơn: bằng cách truy cập ivar
_anIvar, bạn có thể truy cập rõ ràngself->_anIvar. Điều này có thể dẫn đến vấn đề. Trong block, truy cập ivar (bạn nắm giữ và giữ lạiself, ngay cả khi bạn không thấy rõ từ khóaself). - Nó tự động tạo thông báo KVO.
- Chi phí tăng thêm khi gửi tin nhắn là không đáng kể. Để biết thêm về vấn đề hiệu suất, bạn có thể xem Should I Use a Property or an Instance Variable?.
Khởi Tạo và Hủy Đối Tượng (trong ngữ cảnh thuộc tính)
Có một ngoại lệ: không bao giờ sử dụng phương thức getter và setter trong phương thức init (cũng như các phương thức khởi tạo khác). Bạn nên truy cập trực tiếp biến thể hiện. Việc này nhằm ngăn chặn tình huống, khi có lớp con, lớp con đó cuối cùng ghi đè phương thức setter hoặc getter của nó, do đó dẫn đến việc lớp con đó gọi các phương thức khác, truy cập các thuộc tính hoặc ivar đang ở trạng thái không ổn định, hoặc chưa được khởi tạo hoàn chỉnh. Hãy nhớ rằng một đối tượng chỉ được coi là đã hoàn tất khởi tạo khi phương thức init trả về.
Tương tự, trong phương thức dealloc (trong đó một đối tượng có thể ở trạng thái không xác định), điều này cũng cần được chú ý.
- Advanced Memory Management Programming Guide trong phần tự giải thích "Don't Use Accessor Methods in Initializer Methods and dealloc";
- Migrating to Modern Objective-C tại WWDC 2012 ở slide 27;
- trong một pull request từ Dave DeLong.
Ngoài ra, việc sử dụng setter trong init sẽ không thực hiện tốt proxy UIAppearence (xem UIAppearance for Custom Views để biết thêm thông tin).
Ký Hiệu Dấu Chấm
Khi sử dụng phương thức setter getter, cố gắng sử dụng ký hiệu dấu chấm. Nên luôn sử dụng ký hiệu dấu chấm để truy cập và thiết lập thuộc tính.
Ví dụ:
element.backgroundColor = [UIColor orangeColor];
[UIApplication sharedApplication].delegate;
Không nên làm vậy:
[element setBackgroundColor:[UIColor orangeColor]];
UIApplication.sharedApplication.delegate;
Sử dụng ký hiệu dấu chấm giúp diễn đạt rõ ràng hơn và phân biệt giữa truy cập thuộc tính và gọi phương thức.
Định Nghĩa Thuộc Tính
Nên định nghĩa thuộc tính theo định dạng sau:
@property (nonatomic, readwrite, copy) NSString *titleString;
Các tham số của thuộc tính nên được sắp xếp theo thứ tự sau: tính nguyên tử (atomicity), quyền đọc/ghi (readwrite) và quản lý bộ nhớ (memory management). Điều này giúp thuộc tính của bạn dễ sửa đổi chính xác hơn và dễ đọc hơn. (Ghi chú của người dịch: Thông thường, khi sửa đổi một bổ ngữ của thuộc tính, người ta thường tìm kiếm bổ ngữ cần sửa từ phải sang trái của tên thuộc tính. Theo kinh nghiệm, khả năng sửa đổi các bổ ngữ này từ cao xuống thấp nên là: quản lý bộ nhớ > quyền đọc/ghi > thao tác nguyên tử.)
Bạn phải sử dụng nonatomic, trừ khi có trường hợp đặc biệt cần thiết. Trong iOS, khóa do atomic mang lại ảnh hưởng đặc biệt đến hiệu suất.
Thuộc tính có thể lưu trữ một khối mã. Để nó tồn tại cho đến khi kết thúc khối được định nghĩa, phải sử dụng copy (block ban đầu được tạo trên stack, sử dụng copy để sao chép block vào heap).
Để có một getter công khai và một setter riêng tư, bạn nên khai báo thuộc tính công khai là readonly và định nghĩa lại thuộc tính chung là readwrite trong phần mở rộng lớp.
//.h file
@interface MyClass : NSObject
@property (nonatomic, readonly, strong) NSObject *object;
@end
//.m file
@interface MyClass ()
@property (nonatomic, readwrite, strong) NSObject *object;
@end
@implementation MyClass
// Do Something cool
@end
Nếu từ mô tả thuộc tính BOOL là một tính từ, thì setter không nên có tiền tố is, nhưng accessor getter tương ứng nên có tiền tố này, ví dụ:
@property (assign, getter=isEditable) BOOL editable;
Văn bản và ví dụ được trích dẫn từ Cocoa Naming Guidelines.
Trong tệp triển khai, nên tránh sử dụng @synthesize, vì Xcode đã tự động thêm nó cho bạn.
Thuộc Tính Riêng Tư
Thuộc tính riêng tư nên được định nghĩa trong phần mở rộng lớp (anonymous category) của tệp triển khai lớp. Không được phép định nghĩa thuộc tính riêng tư trong category có tên (ví dụ: VMC_Private), trừ khi bạn mở rộng các lớp khác.
Ví dụ:
@interface VMCViewController ()
@property (nonatomic, strong) UIView *headerView;
@end
Đối Tượng Mutable
Bất kỳ thuộc tính nào có thể được thiết lập bằng một đối tượng mutable (ví dụ: NSString, NSArray, NSURLRequest) thì kiểu quản lý bộ nhớ của nó phải là copy.
Điều này nhằm đảm bảo ngăn chặn việc sửa đổi giá trị của đối tượng được đóng gói trong các trường hợp không rõ ràng. (Ghi chú của người dịch: Ví dụ, nếu bạn gán array = mutableArray, trong đó array được định nghĩa là một thể hiện NSArray với thuộc tính copy. Thuộc tính copy sẽ khiến phương thức setter của array thực hiện array = [mutableArray copy]. [mutableArray copy] sẽ trả về một thể hiện NSArray bất biến, đảm bảo tính đúng đắn. Nếu sử dụng các bổ ngữ thuộc tính khác, khi gán trực tiếp, array có thể trỏ đến thể hiện NSMutableArray, cho phép thay đổi giá trị của nó một cách tự do sau đó, dễ dẫn đến lỗi.)
Bạn cũng nên tránh công khai các đối tượng mutable trong giao diện công khai, vì điều này cho phép người sử dụng lớp của bạn thay đổi biểu diễn nội bộ của lớp và phá vỡ tính đóng gói của lớp. Bạn có thể cung cấp các thuộc tính chỉ đọc để trả về bản sao bất biến của đối tượng của bạn.
/* .h */
@property (nonatomic, readonly) NSArray *collectionItems;
/* .m */
- (NSArray *)collectionItems {
return [self.mutableCollectionItems copy];
}
Tải Trì Hoãn (Lazy Loading)
Khi khởi tạo một đối tượng tốn nhiều tài nguyên, hoặc cần gọi nhiều phương thức cấu hình một lần mà bạn không muốn làm rối các phương thức khác, chúng ta cần ghi đè phương thức getter để trì hoãn việc khởi tạo, thay vì cấp phát bộ nhớ cho đối tượng trong phương thức init. Thao tác này thường sử dụng mẫu sau:
- (NSCalendar *)calendarInstance {
if (!_calendarInstance) {
_calendarInstance = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian];
NSLocale *currentLocale = [NSLocale currentLocale];
[_calendarInstance setLocale:currentLocale];
// Thêm các cấu hình khác nếu cần
}
return _calendarInstance;
}
Mặc dù cách này rất tốt trong một số trường hợp, nhưng cần suy nghĩ kỹ trước khi thực hiện. Thực tế, cách làm này có thể tránh được. Dưới đây là những tranh cãi về việc sử dụng khởi tạo trì hoãn:
- Phương thức getter nên tránh tác dụng phụ. Khi nhìn vào một phương thức getter, bạn không mong đợi nó sẽ tạo ra một đối tượng hoặc gây ra tác dụng phụ. Thực tế, nếu gọi phương thức getter mà không sử dụng giá trị trả về, trình biên dịch sẽ đưa ra cảnh báo: "Getter không nên được gọi chỉ vì tác dụng phụ mà nó tạo ra".
Tác dụng phụ (side effect) là khi việc gọi hàm, ngoài việc trả về giá trị hàm, còn tạo ra ảnh hưởng bổ sung đến hàm gọi chính. Ví dụ như sửa đổi biến toàn cục (biến bên ngoài hàm) hoặc sửa đổi tham số. Tác dụng phụ của hàm có thể gây ra những rắc rối không cần thiết trong thiết kế chương trình, dẫn đến những lỗi rất khó tìm và làm giảm khả năng đọc của chương trình. (Người dịch)
- Bạn đã thay đổi chi phí khởi tạo trong lần truy cập đầu tiên, tạo ra tác dụng phụ, điều này làm cho việc tối ưu hiệu suất trở nên khó khăn (cũng như việc kiểm thử).
- Việc khởi tạo này có thể không xác định: ví dụ, bạn mong đợi thuộc tính lần đầu tiên được truy cập bởi một phương thức, nhưng bạn đã thay đổi triển khai của lớp, và accessor được gọi trước thời điểm bạn mong đợi. Điều này có thể dẫn đến vấn đề, đặc biệt là khi logic khởi tạo có thể phụ thuộc vào các trạng thái khác nhau của lớp. Nhìn chung, tốt nhất là làm rõ các mối quan hệ phụ thuộc.
- Hành vi này không thân thiện với KVO. Nếu getter thay đổi tham chiếu, nó nên thông báo thay đổi thông qua một thông báo KVO. Việc nhận được thông báo thay đổi khi truy cập getter là rất kỳ lạ.
Phương Thức (tiếp)
Thẩm Định Tham Số
Phương thức của bạn có thể yêu cầu một số tham số đáp ứng các điều kiện cụ thể (ví dụ: không thể là nil). Trong trường hợp này, tốt nhất nên sử dụng NSParameterAssert() để thẩm định điều kiện có đúng hay không hoặc ném ra một ngoại lệ.
Phương Thức Riêng Tư
Không bao giờ đặt tiền tố _ trước các phương thức riêng tư của bạn. Tiền tố này được Apple dành riêng. Đừng mạo hiểm ghi đè các phương thức riêng tư của Apple.
So Sánh Bằng
Khi bạn muốn triển khai tính bằng nhau, hãy nhớ quy ước này: bạn cần triển khai cả phương thức isEqual và hash. Nếu hai đối tượng được isEqual coi là bằng nhau, thì phương thức hash của chúng cần trả về cùng một giá trị. Nhưng nếu hash trả về cùng một giá trị, điều đó không đảm bảo chúng bằng nhau.
Quy ước này được sử dụng để tìm kiếm các đối tượng khi chúng được lưu trữ trong các tập hợp (ví dụ: NSDictionary và NSSet sử dụng cấu trúc dữ liệu bảng băm ở phía dưới).
@implementation VMCWorker
- (BOOL)isEqual:(id)object {
if (self == object) {
return YES;
}
if (![object isKindOfClass:[VMCWorker class]]) {
return NO;
}
// kiểm tra tính bằng nhau của các thuộc tính đối tượng (tên và ngày sinh)
// ...
return propertiesMatch;
}
- (NSUInteger)hash {
return [self.name hash] ^ [self.employeeID hash];
}
@end
Cần đặc biệt lưu ý rằng phương thức hash không được trả về một hằng số. Đây là một lỗi điển hình và sẽ dẫn đến các vấn đề nghiêm trọng, vì giá trị trả về của phương thức hash thực tế sẽ được sử dụng làm khóa của đối tượng trong bảng băm, điều này sẽ dẫn đến 100% va chạm trong bảng băm.
Bạn nên luôn triển khai một phương thức kiểm tra tính bằng nhau theo định dạng isEqualTo<#class-name-without-prefix#>:. Nếu bạn làm vậy, phương thức này sẽ được ưu tiên gọi để tránh kiểm tra kiểu ở trên.
Một phương thức isEqual hoàn chỉnh nên như sau:
- (BOOL)isEqual:(id)object {
if (self == object) {
return YES;
}
if (![object isKindOfClass:[VMCWorker class]]) {
return NO;
}
return [self isEqualToWorker:(VMCWorker *)object];
}
- (BOOL)isEqualToWorker:(VMCWorker *)worker {
if (!worker) {
return NO;
}
BOOL namesMatch = (!self.name && !worker.name) ||
[self.name isEqualToString:worker.name];
BOOL idsMatch = (!self.employeeID && !worker.employeeID) ||
[self.employeeID isEqualToNumber:worker.employeeID];
return namesMatch && idsMatch;
}
Ghi chú của người dịch:Thông thường, chúng ta sẽ trực tiếp gọi phương thức
isEqualTo__ClassName__:tùy chỉnh để so sánh bằng các thể hiện của lớp. Như đã đề cập ở đầu phần so sánh bằng, ở đây nên ghi đè phương thứcisEqual:, vì phương thứcisEqual:củaNSObjectrõ ràng sẽ không xem xét việc kiểm tra kiểu và tính bằng nhau của các thuộc tính của lớp tùy chỉnh của chúng ta. Khi các đối tượng của lớp tùy chỉnh của chúng ta nằm trong một tập hợp không có thứ tự và được tìm kiếm,isEqual:sẽ được tự động gọi. Tương tự, phương thứchashcủa lớp đó cũng sẽ được sử dụng khi tìm kiếm đối tượng trong tập hợp, chúng ta cũng có thể ghi đè phương thứchashđể đạt được việc xác định xem các đối tượng có băm bằng nhau theo tiêu chuẩn của chúng ta hay không.Phương thức
hashmà chúng ta triển khai nên được xây dựng dựa trên các phương thứchashcủa các đối tượng khác nhau do hệ thống cung cấp (như ví dụ ở đầu phần). Không nên tự mình triển khai một thuật toán băm nào đó để thay thế thuật toán băm do hệ thống cung cấp, điều này thường sẽ ảnh hưởng lớn đến hiệu suất hoặc độ chính xác. Thuật toán băm do hệ thống cung cấp đã trải qua vô số lần cải tiến, đủ để đáp ứng yêu cầu của bạn.
Kết quả tính toán hash của một thể hiện đối tượng phải là xác định. Điều này rất quan trọng khi nó được thêm vào một đối tượng chứa (ví dụ: NSArray, NSSet hoặc NSDictionary), nếu không hành vi sẽ không thể đoán trước (tất cả các đối tượng chứa đều sử dụng băm của đối tượng để tìm kiếm hoặc thực hiện các hành vi đặc biệt, như xác định tính duy nhất). Điều này cũng có nghĩa là, nên sử dụng các thuộc tính bất biến để tính giá trị băm, hoặc, tốt nhất là đảm bảo đối tượng là bất biến.
Categories
Mặc dù chúng tôi biết rằng việc này trông không đẹp, nhưng chúng ta nên thêm tiền tố chữ thường và dấu gạch dưới vào trước tên phương thức category của chúng ta, ví dụ: - (id)vmc_extendedMethod. Thực tiễn này cũng được Apple khuyến nghị.
Điều này là rất cần thiết. Bởi vì nếu trong category được mở rộng hoặc các category khác đã sử dụng cùng một tên phương thức, nó sẽ dẫn đến hậu quả không thể đoán trước. Trên thực tế, phương thức được gọi thực tế là triển khai của phương thức trong category được tải cuối cùng (ghi chú của người dịch: Nếu nhiều category được nhập khẩu có các phương thức trùng tên được nhập vào một lớp, việc gọi phương thức nào cuối cùng được quyết định bởi thứ tự tải tại thời điểm biên dịch, phương thức được tải cuối cùng sẽ ghi đè các phương thức trước đó).
Nếu bạn muốn xác nhận phương thức category của mình không ghi đè các triển khai khác, bạn có thể đặt biến môi trường OBJC_PRINT_REPLACED_METHODS thành YES. Khi đó, các tên phương thức bị thay thế sẽ được in ra Console. Hiện tại LLVM 5.1 sẽ không đưa ra bất kỳ cảnh báo hoặc lỗi nào về điều này, vì vậy hãy tự cẩn thận không ghi đè phương thức trong category.
Một thực tiễn tốt là sử dụng tiền tố trong tên category.
Ví dụ:
@interface NSDate (VMCTimeUtils)
- (NSString *)vmc_relativeTime;
@end
Không nên làm vậy:
@interface NSDate (VMCTimeUtils)
- (NSString *)timeAgoShort;
@end
Categories có thể được sử dụng để định nghĩa một nhóm các phương thức có chức năng tương tự trong tệp tiêu đề. Đây là một thực tiễn rất phổ biến trong Framework của Apple (ví dụ dưới đây được lấy từ tệp tiêu đề NSDate). Chúng tôi cũng khuyến nghị mạnh mẽ việc sử dụng cách này trong mã của riêng bạn.
Kinh nghiệm của chúng tôi là việc tạo một nhóm các category sẽ rất hữu ích cho việc tái cấu trúc sau này. Khi giao diện của một lớp tăng lên, điều đó có thể có nghĩa là lớp của bạn đang làm quá nhiều việc, vi phạm nguyên tắc trách nhiệm đơn nhất của lớp.
Việc nhóm các phương thức đã tạo trước đó có thể được sử dụng để biểu diễn tốt hơn các chức năng khác nhau, và chia lớp thành các thành phần tự chứa hơn.
@interface NSDate : NSObject <NSCopying, NSSecureCoding>
@property (readonly) NSTimeInterval timeIntervalSinceReferenceDate;
@end
@interface NSDate (NSDateCreation)
+ (instancetype)date;
+ (instancetype)dateWithTimeIntervalSinceNow:(NSTimeInterval)secs;
+ (instancetype)dateWithTimeIntervalSinceReferenceDate:(NSTimeInterval)ti;
+ (instancetype)dateWithTimeIntervalSince1970:(NSTimeInterval)secs;
+ (instancetype)dateWithTimeInterval:(NSTimeInterval)secsToBeAdded sinceDate:(NSDate *)date;
// ...
@end
Protocols
Một điều thường bị bỏ qua trong thế giới Objective-C là giao diện trừu tượng. Từ "interface" thường dùng để chỉ tệp `.h` của một lớp, nhưng trong mắt các lập trình viên Java, nó có ý nghĩa khác: một tập hợp các định nghĩa phương thức không phụ thuộc vào triển khai cụ thể. (Ghi chú của người dịch: Trong Objective-C, giao diện của một lớp sẽ có triển khai cụ thể trong tệp `.m`, nhưng trong Java, giao diện gần giống với giao diện trừu tượng hoặc giao thức (protocol) trong Objective-C.)
Trong Objective-C, giao diện trừu tượng được triển khai thông qua protocol. Do các lý do lịch sử, protocol (được sử dụng tương tự như giao diện trong Java) không được sử dụng rộng rãi trong mã Objective-C và không phổ biến trong cộng đồng (ám chỉ cách sử dụng protocol giống như cách các lập trình viên Java sử dụng giao diện). Một lý do chính là hầu hết mã phát triển của Apple không áp dụng cách này, và gần như tất cả các nhà phát triển đều tuân theo các mẫu và hướng dẫn của Apple. Apple hầu như chỉ sử dụng protocol trong mẫu ủy quyền (delegate pattern).
Tuy nhiên, khái niệm giao diện trừu tượng rất mạnh mẽ, có nguồn gốc sâu xa trong lịch sử khoa học máy tính, và không có lý do gì để không sử dụng nó trong Objective-C.
Dưới đây là một ví dụ cụ thể để giải thích sức mạnh của protocol (được sử dụng như giao diện trừu tượng): chuyển đổi một kiến trúc thiết kế rất tồi thành mã nguồn có thể tái sử dụng tốt.
Ví dụ này là triển khai một trình đọc RSS (thường là một bài kiểm tra trong các cuộc phỏng vấn kỹ thuật).
Yêu cầu rất đơn giản: hiển thị một nguồn cấp dữ liệu RSS từ xa trong TableView.
Một cách tiếp cận ngây thơ là tạo một lớp con UITableViewController và đặt tất cả logic truy xuất dữ liệu nguồn cấp dữ liệu, phân tích cú pháp và hiển thị vào cùng một nơi, hay nói cách khác là một MVC (Massive View Controller). Điều này có thể hoạt động, nhưng thiết kế của nó rất tệ, mặc dù nó đủ để vượt qua một số cuộc phỏng vấn không quá khắt khe.
Các bước tối thiểu là tuân thủ nguyên tắc trách nhiệm đơn nhất, tạo ít nhất hai thành phần để hoàn thành nhiệm vụ này:
- Một trình phân tích cú pháp nguồn cấp dữ liệu để phân tích kết quả thu thập được.
- Một trình đọc nguồn cấp dữ liệu để hiển thị kết quả.
Giao diện của các lớp này có thể như sau:
@interface VMCContentParser : NSObject
@property (nonatomic, weak) id <VMCContentParserDelegate> delegate;
@property (nonatomic, strong) NSURL *feedURL;
- (id)initWithFeedURL:(NSURL *)url;
- (BOOL)startParsing;
- (void)stopParsing;
@end
@interface VMCListDisplayController : UITableViewController
- (instancetype)initWithContentParser:(VMCContentParser *)parser;
@end
VMCContentParser được khởi tạo bằng NSURL để lấy nguồn cấp dữ liệu RSS (bên dưới có thể sử dụng NSXMLParser và NSXMLParserDelegate để tạo dữ liệu có ý nghĩa), VMCListDisplayController sẽ được khởi tạo với parser này. Chúng ta muốn nó hiển thị các giá trị mà parser nhận được và chúng ta sử dụng protocol dưới đây để triển khai ủy quyền:
@protocol VMCContentParserDelegate <NSObject>
@optional
- (void)parserDidStart:(VMCContentParser *)parser;
- (void)parser:(VMCContentParser *)parser didParseFeedInfo:(VMCContentInfo *)info;
- (void)parser:(VMCContentParser *)parser didParseFeedItem:(VMCContentItem *)item;
- (void)parserDidFinish:(VMCContentParser *)parser;
- (void)parser:(VMCContentParser *)parser didFailWithError:(NSError *)error;
@end
Tôi phải nói rằng, đây là một protocol hoàn toàn hợp lý và phù hợp để xử lý nghiệp vụ RSS. ViewController này sẽ tuân thủ protocol này trong giao diện Public:
@interface VMCListDisplayController : UITableViewController <VMCContentParserDelegate>
Mã được tạo cuối cùng sẽ như thế này:
NSURL *feedURL = [NSURL URLWithString:@"http://www.example.com/feed.rss"];
VMCContentParser *feedParser = [[VMCContentParser alloc] initWithFeedURL:feedURL];
VMCListDisplayController *listController = [[VMCListDisplayController alloc] initWithContentParser:feedParser];
feedParser.delegate = listController;
Đến đây bạn có thể cảm thấy mã của mình khá tốt, nhưng có bao nhiêu mã có thể tái sử dụng hiệu quả? View controller chỉ có thể xử lý các đối tượng kiểu VMCContentParser: Từ góc độ này, chúng ta chỉ tách mã thành hai thành phần mà không làm bất cứ điều gì có giá trị khác.
Trách nhiệm của view controller phải là "hiển thị nội dung do một cái gì đó cung cấp", nhưng nếu chúng ta chỉ cho phép truyền VMCContentParser, thì không phải như vậy. Điều này thể hiện nhu cầu truyền một đối tượng tổng quát hơn cho view controller.
Chúng ta sử dụng protocol VMCContentParserProtocol (trong tệp VMCContentParserProtocol.h, đồng thời trong tệp đó cũng có VMCContentParserDelegate).
@protocol VMCContentParserProtocol <NSObject>
@property (nonatomic, weak) id <VMCContentParserDelegate> delegate;
@property (nonatomic, strong) NSURL *feedURL;
- (BOOL)startParsing;
- (void)stopParsing;
@end
@protocol VMCContentParserDelegate <NSObject>
@optional
- (void)parserDidStart:(id<VMCContentParserProtocol>)parser;
- (void)parser:(id<VMCContentParserProtocol>)parser didParseFeedInfo:(VMCContentInfo *)info;
- (void)parser:(id<VMCContentParserProtocol>)parser didParseFeedItem:(VMCContentItem *)item;
- (void)parserDidFinish:(id<VMCContentParserProtocol>)parser;
- (void)parser:(id<VMCContentParserProtocol>)parser didFailWithError:(NSError *)error;
@end
Lưu ý rằng protocol ủy quyền này hiện xử lý phản hồi cho protocol mới của chúng ta, và tệp giao diện của VMCContentParser đã tinh gọn hơn:
@interface VMCContentParser : NSObject <VMCContentParserProtocol>
- (id)initWithFeedURL:(NSURL *)url;
@end
Vì VMCContentParser triển khai VMCContentParserProtocol, nó cần triển khai tất cả các phương thức required.
Từ góc độ này, ViewController có thể chấp nhận bất kỳ đối tượng nào tuân thủ protocol đó, miễn là đảm bảo tất cả các đối tượng đều phản hồi các phương thức start và stop và cung cấp thông tin qua thuộc tính delegate (ghi chú của người dịch: vì protocol mặc định tất cả các định nghĩa phương thức là required). Đối với đối tượng được chỉ định, đây là tất cả những gì ViewController cần biết, và không cần biết chi tiết triển khai của nó.
@interface VMCListDisplayController : UITableViewController <VMCContentParserDelegate>
- (instancetype)initWithContentParser:(id<VMCContentParserProtocol>)parser;
@end
Thay đổi trong đoạn mã trên có vẻ không nhiều, nhưng đã có một cải tiến lớn. View controller sẽ hoạt động dựa trên protocol thay vì triển khai cụ thể. Điều này mang lại các ưu điểm sau:
- View controller hiện có thể nhận bất kỳ đối tượng nào cung cấp thông tin qua thuộc tính
delegate: có thể là trình phân tích RSS từ xa, hoặc trình phân tích cục bộ, hoặc một dịch vụ đọc dữ liệu từ xa hoặc cục bộ khác. VMCContentParservàVMCContentParserDelegatecó thể được tái sử dụng bởi các thành phần khác.VMCListDisplayController(phần logic UI) có thể được tái sử dụng.- Kiểm thử đơn giản hơn, vì có thể sử dụng các mock object để đạt được hiệu quả mong muốn của protocol.
Khi triển khai một protocol, bạn nên luôn tuân thủ Nguyên tắc Thay thế Liskov. Nguyên tắc này là: bạn có thể thay thế bất kỳ triển khai giao diện nào (tức là "protocol" trong Objective-C) mà không cần thay đổi client hoặc triển khai liên quan.
Ngoài ra, điều này cũng có nghĩa là protocol không nên quan tâm đến chi tiết triển khai của lớp; việc thiết kế biểu diễn trừu tượng của protocol nên được thực hiện rất cẩn thận, và phải luôn nhớ rằng nó không liên quan đến triển khai đằng sau nó, điều thực sự quan trọng là protocol (biểu diễn trừu tượng được công khai cho người dùng).
Bất kỳ thiết kế nào có thể tái sử dụng trong tương lai, một cách vô hình, có thể cải thiện chất lượng mã, và điều này cũng nên luôn là mục tiêu của lập trình viên. Việc thiết kế mã theo cách này chính là sự khác biệt giữa bậc thầy và người mới.
Mã cuối cùng có thể tìm thấy tại đây.
NSNotification
Khi bạn định nghĩa NSNotification của riêng mình, bạn nên định nghĩa tên thông báo là một hằng số chuỗi, giống như các hằng số chuỗi khác bạn công khai cho các lớp khác. Bạn nên khai báo nó là extern trong tệp giao diện công khai, và định nghĩa trong tệp triển khai tương ứng.
Vì bạn công khai ký hiệu trong tệp tiêu đề, bạn nên tuân thủ quy tắc tiền tố namespace thống nhất, sử dụng tiền tố tên lớp làm tiền tố cho tên thông báo này.
Đồng thời, việc sử dụng các động từ như "Did/Will" và hậu tố "Notification" để đặt tên cho thông báo cũng là một thực tiễn tốt.
// Foo.h
extern NSString * const VMCStatusDidChangeNotification;
// Foo.m
NSString * const VMCStatusDidChangeNotification = @"VMCStatusDidChangeNotification";
Làm Đẹp Mã
Khoảng Trắng
- Thụt lề sử dụng 4 khoảng trắng. Không bao giờ sử dụng tab, hãy đảm bảo bạn đã cài đặt như vậy trong Xcode.
- Dấu ngoặc nhọn của phương thức và các dấu ngoặc nhọn khác (
if/else/switch/while, v.v.) luôn bắt đầu trên cùng một dòng, và kết thúc trên một dòng mới.
Nên dùng:
if (user.isHappy) {
// Thực hiện điều gì đó
}
else {
// Thực hiện điều gì đó khác
}
Không nên dùng:
if (user.isHappy)
{
// Thực hiện điều gì đó
} else {
// Thực hiện điều gì đó khác
}
- Giữa các phương thức nên có một dòng trống để giúp mã trông rõ ràng và có tổ chức. Khoảng trắng bên trong phương thức nên được sử dụng để phân tách chức năng, nhưng thông thường, các chức năng khác nhau nên được định nghĩa bằng các phương thức mới.
- Ưu tiên sử dụng tự động tổng hợp (auto-synthesis). Nhưng nếu cần thiết,
@synthesizevà@dynamic - Khai báo trong tệp triển khai nên bắt đầu trên một dòng mới.
- Nên luôn căn chỉnh dấu hai chấm. Một số chữ ký phương thức có thể có hơn ba dấu hai chấm, việc căn chỉnh dấu hai chấm có thể làm cho mã dễ đọc hơn. Ngay cả khi có khối mã, cũng nên căn chỉnh dấu hai chấm của phương thức.
Nên dùng:
[UIView animateWithDuration:1.0
animations:^{
// ...
}
completion:^(BOOL finished) {
// ...
}];
Không nên dùng:
[UIView animateWithDuration:1.0 animations:^{
// ...
} completion:^(BOOL finished) {
// ...
}];
Nếu việc căn chỉnh tự động làm giảm khả năng đọc, thì nên định nghĩa block là biến trước đó, hoặc xem xét lại thiết kế chữ ký mã của bạn.
Xuống Dòng
Hướng dẫn này tập trung vào hiệu ứng hiển thị mã và khả năng đọc trực tuyến, vì vậy việc xuống dòng là một chủ đề quan trọng.
Ví dụ:
self.productRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:itemIdentifiers];
Một dòng mã dài như trên sẽ được tiếp tục ở dòng thứ hai với một khoảng cách (2 khoảng trắng).
self.productRequest = [[SKProductsRequest alloc]
initWithProductIdentifiers:itemIdentifiers];
Dấu Ngoặc
Sử dụng dấu ngoặc kiểu Ai Cập (Egyptian style brackets) (ghi chú của người dịch: còn gọi là kiểu K&R, là phong cách đặt dấu ngoặc mở của khối mã ở cuối dòng hiện tại, chứ không phải ở một dòng mới. Để biết lý do tại sao gọi là Egyptian Brackets, bạn có thể tham khảo http://blog.codinghorror.com/new-programming-jargon/) ở các nơi sau:
- Các câu lệnh điều khiển (if-else, for, switch).
Dấu ngoặc không theo kiểu Ai Cập có thể được sử dụng ở:
- Triển khai lớp (nếu có).
- Triển khai phương thức.
Tổ Chức Mã
Tổ chức mã là vấn đề vệ sinh.
Chúng tôi hoàn toàn đồng ý với câu nói này. Việc tổ chức mã rõ ràng và định nghĩa quy chuẩn là sự tôn trọng bạn dành cho chính mình và những người khác đọc mã.
Sử Dụng Khối Mã (Code Block)
Một tính năng khá mơ hồ của GCC, và cũng có trong Clang, là khối mã, nếu nằm trong dấu ngoặc tròn đóng, sẽ trả về giá trị của câu lệnh cuối cùng.
NSURL *resourceURL = ({
NSString *fullPath = [NSString stringWithFormat:@"%@/%@", baseAPI, apiPath];
[NSURL URLWithString:fullPath];
});
Pragma
Pragma Mark
#pragma mark - là một cách tốt để tổ chức mã bên trong một lớp và giúp bạn nhóm các triển khai phương thức. Chúng tôi khuyên bạn nên sử dụng #pragma mark - để phân tách:
- Các phương thức của các nhóm chức năng khác nhau.
- Các triển khai của protocol.
- Các phương thức ghi đè từ lớp cha.
- (void)dealloc { /* ... */ }
- (instancetype)init { /* ... */ }
#pragma mark - Vòng đời View
- (void)viewDidLoad { /* ... */ }
- (void)viewWillAppear:(BOOL)animated { /* ... */ }
- (void)didReceiveMemoryWarning { /* ... */ }
#pragma mark - Bộ truy cập tùy chỉnh
- (void)setCustomProperty:(id)value { /* ... */ }
- (id)customProperty { /* ... */ }
#pragma mark - Hành động giao diện (IBActions)
- (IBAction)submitData:(id)sender { /* ... */ }
#pragma mark - Công khai
- (void)publicMethod { /* ... */ }
#pragma mark - Riêng tư
- (void)vmc_internalMethod { /* ... */ }
#pragma mark - Nguồn dữ liệu bảng (UITableViewDataSource)
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { /* ... */ }
#pragma mark - VMCSuperclass
// ... Ghi đè các phương thức từ VMCSuperclass
#pragma mark - NSObject
- (NSString *)description { /* ... */ }
Các đánh dấu trên có thể phân tách và tổ chức mã một cách rõ ràng. Bạn cũng có thể dùng Cmd+Click để nhanh chóng chuyển đến định nghĩa ký hiệu.
Nhưng hãy cẩn thận, ngay cả khi pragma mark là một kỹ thuật, nó không phải là lý do để tăng số lượng phương thức trong lớp của bạn: một lớp có quá nhiều phương thức cho thấy lớp đang làm quá nhiều việc, cần xem xét tái cấu trúc.
Về Pragma
Có một cuộc thảo luận rất hay về pragma tại http://raptureinvenice.com/pragmas-arent-just-for-marks/, ở đây chúng ta sẽ giải thích thêm một phần.
Hầu hết các nhà phát triển iOS thường không làm việc với nhiều tùy chọn trình biên dịch. Một số tùy chọn dùng để kiểm soát việc kiểm tra nghiêm ngặt (hoặc không kiểm tra) mã hoặc lỗi của bạn. Đôi khi, bạn muốn dùng pragma để tạo ra một ngoại lệ trực tiếp, tạm thời ngắt hành vi của trình biên dịch.
Khi bạn sử dụng ARC, trình biên dịch giúp bạn chèn các cuộc gọi liên quan đến quản lý bộ nhớ. Nhưng điều này có thể gây ra một số phiền toái. Ví dụ, khi bạn sử dụng NSSelectorFromString để tạo động một cuộc gọi selector, ARC không biết phương thức này là gì và không biết nên sử dụng phương pháp quản lý bộ nhớ nào. Bạn sẽ được nhắc nhở: performSelector may cause a leak because its selector is unknown.
Nếu bạn biết mã của mình sẽ không gây rò rỉ bộ nhớ, bạn có thể bỏ qua các cảnh báo này bằng cách thêm các mã sau:
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[myObj performSelector:mySelector withObject:name];
#pragma clang diagnostic pop
Lưu ý cách chúng ta tạm thời vô hiệu hóa kiểm tra -Warc-performSelector-leaks trong ngữ cảnh mã liên quan. Điều này đảm bảo chúng ta không vô hiệu hóa toàn cục. Nếu vô hiệu hóa toàn cục, có thể dẫn đến lỗi.
Bạn có thể tìm và nghiên cứu tất cả các tùy chọn trong The Clang User's Manual.
Bỏ Qua Cảnh Báo Biến Không Sử Dụng
Việc trình biên dịch thông báo rằng biến bạn khai báo sẽ không được sử dụng là rất hữu ích. Trong hầu hết các trường hợp, bạn muốn loại bỏ các tham chiếu này để (hơi) cải thiện hiệu suất, nhưng đôi khi bạn muốn giữ chúng. Tại sao? Có lẽ chúng sẽ hữu ích sau này, hoặc một số tính năng chỉ tạm thời bị loại bỏ. Dù sao đi nữa, một cách tốt để loại bỏ các cảnh báo này là chú thích bằng câu lệnh liên quan, sử dụng #pragma unused():
- (NSInteger)calculateValue
{
NSString *unusedVariable;
#pragma unused (unusedVariable)
return 5;
}
Bây giờ mã của bạn sẽ không còn bất kỳ cảnh báo biên dịch nào. Lưu ý rằng pragma của bạn cần được đánh dấu dưới mã có vấn đề.
Đánh Dấu Lỗi và Cảnh Báo Trình Biên Dịch
Trình biên dịch là một robot, nó sẽ đánh dấu những nơi trong mã của bạn được Clang định nghĩa là lỗi. Nhưng, bạn luôn thông minh hơn Clang. Thông thường, bạn sẽ tìm thấy một số mã gây khó chịu dẫn đến vấn đề này, nhưng tạm thời không thể giải quyết được. Bạn có thể đánh dấu lỗi như sau:
- (NSInteger)divide:(NSInteger)dividend by:(NSInteger)divisor
{
#error Whoa, bạn cần kiểm tra chia cho số 0 ở đây!
return (dividend / divisor);
}
Tương tự, bạn có thể đánh dấu cảnh báo như sau:
- (float)divide:(float)dividend by:(float)divisor
{
#warning Này, đừng so sánh số dấu phẩy động như thế này!
if (divisor != 0.0) {
return (dividend / divisor);
}
else {
return NAN;
}
}
Docstring
Tất cả các phương thức, giao diện, category và định nghĩa protocol quan trọng nên có các ghi chú đi kèm để giải thích mục đích và cách sử dụng của chúng. Các ví dụ khác có thể xem trong phần File and Declaration Comments của Google Code Style Guide.
Tóm lại: có hai loại docstring, ngắn và dài.
Docstring ngắn áp dụng cho các tệp một dòng, bao gồm dấu gạch chéo ghi chú. Nó phù hợp cho các hàm ngắn, đặc biệt là (nhưng không chỉ) các API không công khai:
// Trả về dạng thân thiện với người dùng của DataItem, đã thoát HTML.
Văn bản nên sử dụng một động từ ("Return") chứ không phải mô tả như "returns".
Nếu mô tả dài hơn một dòng, nên sử dụng docstring dài:
- Bắt đầu bằng
/**. - Xuống dòng viết một câu tóm tắt, kết thúc bằng
?hoặc!hoặc.. - Để trống một dòng.
- Bắt đầu viết phần ghi chú còn lại ở vị trí căn chỉnh với dòng đầu tiên.
- Cuối cùng kết thúc bằng
*/.
/**
* Bình luận này dùng để minh họa định dạng của một docstring.
* Lưu ý rằng dòng tóm tắt luôn dài tối đa một dòng, và
* sau khối bình luận mở, mỗi dòng văn bản được đặt trước
* bởi một khoảng trắng.
*/
Một hàm phải có một docstring, trừ khi nó đáp ứng tất cả các điều kiện sau:
- Không công khai.
- Rất ngắn.
- Hiển nhiên.
Docstring nên mô tả ký hiệu gọi và ngữ nghĩa của hàm, chứ không phải cách nó được triển khai.
Ghi Chú
Khi cần thiết, ghi chú nên được sử dụng để giải thích những gì một đoạn mã cụ thể đã làm. Tất cả các ghi chú phải được duy trì liên tục hoặc đơn giản là xóa bỏ. Các khối ghi chú nên được tránh, mã nguồn tự nó nên thể hiện ý định càng nhiều càng tốt, chỉ cần rất ít ghi chú gián đoạn. *Ngoại lệ: Điều này không áp dụng cho các ghi chú được sử dụng để tạo tài liệu.*
Tài Liệu Tiêu Đề (Header Doc)
Tài liệu của một lớp chỉ nên được viết trong tệp `.h` bằng cú pháp Doxygen/AppleDoc. Cả phương thức và thuộc tính đều nên được cung cấp tài liệu.
Ví dụ:
/**
* Phương thức khởi tạo được chỉ định.
*
* @param dataManager Trình quản lý dữ liệu cho các thao tác CRUD.
* @param searchService Dịch vụ tìm kiếm được sử dụng để truy vấn dữ liệu.
*
* @return Một đối tượng VMCOperationService.
*/
- (instancetype)initWithDataManager:(id<VMCDataStoreProtocol>)dataManager
searchService:(id<VMCGenericSearchServiceProtocol>)searchService;
Giao Tiếp Giữa Các Đối Tượng
Các đối tượng cần giao tiếp với nhau, đây là nền tảng của mọi phần mềm. Ngay cả phần mềm phi thường nhất cũng cần giao tiếp đối tượng để hoàn thành các mục tiêu phức tạp. Chương này sẽ đi sâu vào một số khái niệm thiết kế, và cách thiết kế kiến trúc tốt dựa trên các khái niệm này.
Blocks
Blocks là phiên bản lambda hoặc closure của Objective-C.
Sử dụng block để định nghĩa giao diện bất đồng bộ:
- (void)fetchResourcesAtPath:(NSString *)path
completion:(void(^)(NSArray *resources, NSError *error))completion;
Khi bạn định nghĩa một giao diện tương tự như trên, hãy cố gắng sử dụng một block riêng biệt làm tham số cuối cùng của giao diện. Việc tích hợp dữ liệu cần cung cấp và thông tin lỗi vào một block riêng biệt sẽ tốt hơn là cung cấp riêng các block thành công và thất bại.
Dưới đây là lý do bạn nên làm như vậy:
- Thông thường, việc xử lý thành công và xử lý thất bại sẽ chia sẻ một số mã (ví dụ: làm mất thanh tiến trình hoặc thông báo);
- Apple cũng làm như vậy, nhất quán với nền tảng có thể mang lại một số lợi ích tiềm năng;
- Block thường có nhiều dòng mã, nếu không đặt làm tham số cuối cùng, nó sẽ làm gián đoạn điểm gọi;
- Sử dụng nhiều block làm tham số có thể làm cho cuộc gọi trông cồng kềnh và tăng độ phức tạp.
Xem phương thức trên, các tham số của block hoàn thành xử lý rất phổ biến: tham số đầu tiên là dữ liệu mà người gọi muốn lấy, tham số thứ hai là thông tin liên quan đến lỗi. Ở đây cần tuân thủ hai điểm sau:
- Nếu
resourceskhông phảinil, thìerrorphải lànil. - Nếu
resourceslànil, thìerrorphải không phảinil.
Vì người gọi quan tâm nhiều hơn đến dữ liệu thực tế, giống như thế này:
- (void)fetchResourcesAtPath:(NSString *)path
completion:(void(^)(NSArray *resources, NSError *error))completion {
// ... logic ...
if (completion) {
if (success) {
completion(fetchedData, nil);
} else {
completion(nil, operationError);
}
}
}
[self fetchResourcesAtPath:@"/data" completion:^(NSArray *resources, NSError *error) {
if (resources) {
// làm gì đó với dữ liệu
} else {
// đã xảy ra lỗi, biến 'error' không nên là nil theo hợp đồng
}
}];
Ngoài ra, một số giao diện đồng bộ do Apple cung cấp có thể ghi các giá trị rác vào tham số error (nếu không phải NULL) trong trường hợp thành công, vì vậy việc kiểm tra giá trị của error có thể gây ra vấn đề.
Đi Sâu vào Blocks
Một số điểm chính:
- Block được tạo trên stack.
- Block có thể được sao chép vào heap.
- Block sẽ nắm giữ các biến (hoặc con trỏ) trên stack, sao chép chúng thành các hằng số riêng tư của chính nó.
- (Nếu sửa đổi các biến và con trỏ trên stack bên ngoài khối Block) thì các biến và con trỏ này phải được khai báo bằng từ khóa
__block(ghi chú của người dịch: nếu không sẽ giống trường hợp trên chỉ nắm giữ giá trị tức thời của chúng).
Nếu block không được giữ ở nơi khác, thì nó sẽ tồn tại cùng với stack và biến mất khi stack frame trả về. Khi chỉ tồn tại trên stack, block không ảnh hưởng đến quản lý bộ nhớ và vòng đời truy cập đối tượng. Nếu block cần tồn tại khi stack frame trả về, chúng cần được sao chép rõ ràng vào heap. Khi đó, block sẽ tăng bộ đếm tham chiếu giống như các đối tượng Cocoa khác. Khi chúng được sao chép, chúng sẽ mang theo phạm vi nắm giữ của mình và giữ lại tất cả các đối tượng mà chúng tham chiếu.
Nếu một block tham chiếu một biến hoặc con trỏ stack, thì khi block khởi tạo, nó sẽ sở hữu một bản sao hằng số của biến hoặc con trỏ này, vì vậy (việc thay đổi giá trị của biến hoặc con trỏ này trên stack sau khi bị nắm giữ) sẽ không có tác dụng. (Ghi chú của người dịch: Do đó, khi chúng ta gán giá trị cho biến này trong block, trình biên dịch sẽ báo lỗi: Variable is not assignable (missing __block type specifier), vì chúng là bản sao và là hằng số. Xem ví dụ dưới đây).
Khi một block được sao chép, tham chiếu đến biến stack được khai báo bằng __block sẽ được sao chép vào heap. Sau khi sao chép hoàn tất, cả block trên stack và bản sao block vừa được tạo trên heap đều sẽ tham chiếu đến bản sao của biến đó trên heap.
(Mã dưới đây được người dịch thêm vào)
// ...
CGFloat blockCounter = 10;
void (^simpleBlock)(void) = ^{
NSLog(@"blockCounter = %zd", blockCounter);
};
blockCounter++;
simpleBlock();
// ...
// Kết quả là: blockCounter = 10
Sử dụng LLDB để hiển thị block như sau:
Điều quan trọng nhất là các biến và con trỏ được khai báo bằng __block trong block được coi là cấu trúc hiển thị giá trị/đối tượng thực sự. Block trong runtime của Objective-C được coi là công dân hạng nhất: chúng có con trỏ isa, một lớp cũng sử dụng con trỏ isa trong runtime của Objective-C để truy cập phương thức và lưu trữ dữ liệu. Trong môi trường không phải ARC chắc chắn sẽ gây ra nhiều vấn đề, và con trỏ treo sẽ dẫn đến crash. __block chỉ có tác dụng với các biến bên trong block, nó chỉ đơn giản nói với block:
Chào, con trỏ này hoặc kiểu nguyên thủy phụ thuộc vào stack mà chúng đang ở. Vui lòng sử dụng một biến mới trên stack để tham chiếu đến nó. Ý tôi là, vui lòng thực hiện giải tham chiếu kép cho nó, đừng giữ lại nó.
Cảm ơn, bạn.
Nếu đối tượng được giải phóng sau khi định nghĩa nhưng trước khi block được gọi, thì việc thực thi block sẽ dẫn đến crash. Biến __block sẽ không được giữ trong block, và cuối cùng... con trỏ, tham chiếu, giải tham chiếu và bộ đếm tham chiếu trở nên lộn xộn.
Vòng Lặp Tham Chiếu với self
Khi sử dụng khối mã và phân phối bất đồng bộ, hãy cẩn thận để tránh các vòng lặp tham chiếu. Luôn sử dụng weak để tham chiếu đối tượng, tránh các vòng lặp tham chiếu. (Ghi chú của người dịch: Cách thanh lịch hơn ở đây là sử dụng biến bóng @weakify/@strongify có giải thích chi tiết hơn tại đây) Ngoài ra, việc đặt thuộc tính giữ block thành nil (ví dụ: self.completionBlock = nil) là một thực tiễn tốt. Nó sẽ phá vỡ vòng lặp tham chiếu do phạm vi nắm giữ của block gây ra.
Ví dụ:
__weak __typeof(self) weakSelf = self;
[self executeBlock:^(NSData *data, NSError *error) {
[weakSelf processData:data];
}];
Không nên làm vậy:
[self executeBlock:^(NSData *data, NSError *error) {
[self processData:data];
}];
Ví dụ nhiều câu lệnh:
__weak __typeof(self) weakSelf = self;
[self executeBlock:^(NSData *data, NSError *error) {
__strong __typeof(weakSelf) strongSelf = weakSelf;
if (strongSelf) {
[strongSelf processData:data];
[strongSelf updateUIWithData:data];
}
}];
Không nên làm vậy:
__weak __typeof(self) weakSelf = self;
[self executeBlock:^(NSData *data, NSError *error) {
[weakSelf processData:data];
[weakSelf updateUIWithData:data];
}];
Bạn nên thêm hai dòng mã này vào Xcode dưới dạng snippet và luôn sử dụng chúng như vậy.
__weak __typeof(self) weakSelf = self;
__strong __typeof(weakSelf) strongSelf = weakSelf;
Ở đây chúng ta sẽ thảo luận về một số điểm tinh tế của các từ khóa __weak và __strong của self trong block. Tóm lại, chúng ta có thể tham khảo ba trường hợp khác nhau của self trong block.
- Sử dụng trực tiếp từ khóa
selftrong block. - Định nghĩa một tham chiếu
__weakđếnselfbên ngoài block, và sử dụng tham chiếu yếu này bên trong block. - Định nghĩa một tham chiếu
__weakđếnselfbên ngoài block, và bên trong block định nghĩa một tham chiếu__strongthông qua tham chiếu yếu này.
Giải pháp 1. Sử dụng trực tiếp từ khóa self trong block
Nếu chúng ta sử dụng trực tiếp từ khóa self trong block, đối tượng sẽ được giữ lại khi block được định nghĩa (thực tế block được sao chép nhưng để đơn giản chúng ta có thể bỏ qua điều này). Một tham chiếu hằng đến self có vị trí riêng trong block và nó sẽ ảnh hưởng đến bộ đếm tham chiếu của đối tượng. Nếu block này được các lớp khác sử dụng và/hoặc được truyền qua lại, chúng ta có thể muốn giữ lại self trong block, giống như các đối tượng khác được sử dụng trong block. Vì chúng là cần thiết cho việc thực thi block.
dispatch_block_t completionBlock = ^{
NSLog(@"%@", self);
};
MyViewController *myController = [[MyViewController alloc] init...];
[self presentViewController:myController
animated:YES
completion:completionBlock];
Không có gì to tát. Nhưng nếu block này được giữ lại bởi self thông qua một thuộc tính (như ví dụ dưới đây), điều gì sẽ xảy ra nếu đối tượng (self) giữ lại block?
self.completionHandler = ^{
NSLog(@"%@", self);
};
MyViewController *myController = [[MyViewController alloc] init...];
[self presentViewController:myController
animated:YES
completion:self.completionHandler];
Đây chính là vòng lặp tham chiếu nổi tiếng, và chúng ta thường nên tránh nó. Trong trường hợp này, chúng ta nhận được cảnh báo của CLANG:
Capturing 'self' strongly in this block is likely to lead to a retain cycle
Vì vậy, __weak sẽ phát huy tác dụng.
Giải pháp 2. Định nghĩa một tham chiếu __weak đến self bên ngoài block, và sử dụng tham chiếu yếu này bên trong block
Điều này sẽ tránh vòng lặp tham chiếu, và thường là điều chúng ta sẽ làm khi block của chúng ta được self giữ lại như một thuộc tính của lớp.
__weak typeof(self) weakSelf = self;
self.completionHandler = ^{
NSLog(@"%@", weakSelf);
};
MyViewController *myController = [[MyViewController alloc] init...];
[self presentViewController:myController
animated:YES
completion:self.completionHandler];
Trong trường hợp này, block không giữ lại đối tượng và đối tượng giữ lại block trong thuộc tính. Vì vậy, chúng ta có thể đảm bảo truy cập self an toàn. Tuy nhiên, điều tồi tệ là nó có thể được đặt thành nil. Vấn đề là: làm thế nào để self được hủy an toàn trong block?
Hãy xem xét trường hợp này: block được sao chép từ một đối tượng sang một đối tượng khác (ví dụ: myController) như là kết quả của việc gán thuộc tính. Trước khi block được sao chép này được thực thi, đối tượng trước đó (tức là đối tượng trước) đã được giải phóng.
Điều sau đây thú vị hơn.
Giải pháp 3. Định nghĩa một tham chiếu __weak đến self bên ngoài block, và bên trong block định nghĩa một tham chiếu __strong thông qua tham chiếu yếu này
Bạn có thể nghĩ, đầu tiên, đây là một mẹo để tránh cảnh báo vòng lặp tham chiếu.
Đây không phải là điểm chính, tham chiếu mạnh đến self được tạo khi block được thực thi, nhưng việc có sử dụng self hay không đã được xác định khi block được định nghĩa, do đó self (khi block được thực thi) sẽ được giữ lại.
Tài liệu của Apple cho biết "đối với các vòng lặp không tầm thường, bạn nên làm như thế này":
MyViewController *myController = [[MyViewController alloc] init...];
// ...
__weak MyViewController *weakMyController = myController;
myController.completionHandler = ^(NSInteger result) {
MyViewController *strongMyController = weakMyController;
if (strongMyController) {
// ...
[strongMyController dismissViewControllerAnimated:YES completion:nil];
// ...
}
else {
// Có lẽ không có gì...
}
};
Đầu tiên, tôi thấy ví dụ này có vẻ sai. Nếu block tự nó được giữ lại trong thuộc tính completionHandler, thì làm thế nào để self được giải phóng và được gán nil bên ngoài block? Thuộc tính completionHandler có thể được khai báo là assign hoặc unsafe_unretained để cho phép đối tượng bị hủy sau khi block được truyền.
Tôi không thể hiểu lý do để làm như vậy, nếu các đối tượng khác cần đối tượng này (self), block khi được truyền nên giữ lại đối tượng, vì vậy block không nên được lưu trữ như một thuộc tính. Trong trường hợp này, không nên sử dụng __weak/__strong.
Tóm lại, trong các trường hợp khác, nếu bạn muốn weakSelf trở thành nil, hãy viết như trong trường hợp thứ hai (định nghĩa một tham chiếu yếu bên ngoài block và sử dụng nó bên trong block).
Hơn nữa, "trivial block" của Apple là gì? Hiểu biết của chúng tôi là trivial block là một block không được truyền đi, nó nằm trong một phạm vi được định nghĩa và kiểm soát tốt, từ khóa weak chỉ để tránh vòng lặp tham chiếu.
Mặc dù có một số tài liệu trực tuyến (từ Kazuki Sakamoto và Tomohiko Furumoto), một số tham khảo khác, và các cuốn sách như Effective Objective-C 2.0 và Pro Multithreading and Memory Management for iOS and OS X của Matt Galloway, hầu hết các nhà phát triển vẫn chưa nắm rõ khái niệm này.
Ưu điểm của việc sử dụng tham chiếu mạnh trong block là tính bền vững khi thực thi preemptive. Khi block đang thực thi, hãy xem lại ba ví dụ trên:
Giải pháp 1: Sử dụng trực tiếp từ khóa self trong block
Nếu block được giữ lại bởi thuộc tính, sẽ có một vòng lặp tham chiếu giữa self và block, và chúng sẽ không bao giờ được giải phóng. Nếu block được truyền đi và được các đối tượng khác sao chép, self sẽ được giữ lại trong mỗi bản sao.
Giải pháp 2: Định nghĩa một tham chiếu __weak đến self bên ngoài block, và sử dụng tham chiếu yếu này bên trong block
Bất kể block có được giữ lại bởi thuộc tính hay không, sẽ không xảy ra vòng lặp tham chiếu ở đây. Nếu block được truyền đi hoặc sao chép, khi thực thi, weakSelf có thể đã trở thành nil.
Việc thực thi block có thể bị preempted, và việc gọi con trỏ weakSelf ở các thời điểm khác nhau có thể dẫn đến các kết quả khác nhau (ví dụ: trong một thời điểm cụ thể, weakSelf có thể trở thành nil).
__weak typeof(self) weakSelf = self;
dispatch_block_t block = ^{
[weakSelf doSomething]; // weakSelf != nil
// preemption, weakSelf đã trở thành nil
[weakSelf doSomethingElse]; // weakSelf == nil
};
Giải pháp 3: Định nghĩa một tham chiếu __weak đến self bên ngoài block, và bên trong block định nghĩa một tham chiếu __strong thông qua tham chiếu yếu này
Bất kể block có được giữ lại bởi thuộc tính hay không, sẽ không xảy ra vòng lặp tham chiếu ở đây. Nếu block được truyền đến các đối tượng khác và được sao chép, khi thực thi, weakSelf có thể là nil, vì khi tham chiếu mạnh được gán và không trở thành nil, chúng ta đảm bảo đối tượng được giữ lại trong toàn bộ chu kỳ gọi block. Nếu preemption xảy ra, việc thực thi tiếp theo của strongSelf sẽ tiếp tục và tạo ra cùng một giá trị. Nếu việc thực thi strongSelf đến nil, thì block đã trả về trước khi có thể thực thi đúng.
__weak typeof(self) weakSelf = self;
myObj.myBlock = ^{
__strong typeof(self) strongSelf = weakSelf;
if (strongSelf) {
[strongSelf doSomething]; // strongSelf != nil
// preemption, strongSelf vẫn không phải nil
[strongSelf doSomethingElse]; // strongSelf != nil
}
else {
// Có lẽ không có gì...
return;
}
};
Trong điều kiện ARC, nếu cố gắng truy cập một biến thể hiện bằng ký hiệu ->, trình biên dịch sẽ đưa ra thông báo lỗi rất rõ ràng:
Dereferencing a __weak pointer is not allowed due to possible null value caused by race condition, assign it to a strong variable first.
Có thể minh họa bằng mã sau:
__weak typeof(self) weakSelf = self;
myObj.myBlock = ^{
id localVal = weakSelf->internalProperty;
};
Tóm lại:
- Giải pháp 1: Chỉ sử dụng khi block không phải là một thuộc tính, nếu không sẽ dẫn đến vòng lặp tham chiếu.
- Giải pháp 2: Sử dụng khi block được khai báo là một thuộc tính.
- Giải pháp 3: Liên quan đến thực thi đồng thời. Khi liên quan đến các dịch vụ bất đồng bộ, block có thể được thực thi sau đó, và sẽ không xảy ra vấn đề về việc
selfcó tồn tại hay không.
Delegate và Datasource
Mẫu ủy quyền (Delegate pattern) là một mẫu được sử dụng rộng rãi trong framework của Apple, đồng thời nó là một mẫu quan trọng trong cuốn sách "Design Patterns" của Gang of Four. Mẫu ủy quyền là một chiều, bên gửi tin nhắn (delegator) cần biết bên nhận (delegate) là ai, ngược lại thì không cần thiết. Các đối tượng được ghép nối lỏng lẻo, bên gửi chỉ cần biết delegate của nó có tuân thủ protocol liên quan hay không.
Về bản chất, mẫu ủy quyền chỉ yêu cầu delegate cung cấp một số phương thức callback, tức là delegate cần triển khai một loạt các phương thức không trả về giá trị.
Thật không may, API của Apple không tuân thủ nguyên tắc này, và các nhà phát triển cũng bắt chước Apple rơi vào một sai lầm. Ví dụ điển hình là protocol UITableViewDelegate.
Một số phương thức của nó trả về kiểu void, giống như các callback chúng ta đã nói:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath;
- (void)tableView:(UITableView *)tableView didHighlightRowAtIndexPath:(NSIndexPath *)indexPath;
Nhưng các phương thức khác thì không như vậy:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath;
- (BOOL)tableView:(UITableView *)tableView canPerformAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender;
Khi delegator hỏi delegate một số thông tin, điều này ngụ ý rằng thông tin đang chảy từ delegate đến delegator chứ không phải ngược lại. Đây là một sự khác biệt về mặt khái niệm, cần một tên mới để mô tả mẫu này: mẫu nguồn dữ liệu (datasource pattern).
Có lẽ có người sẽ nói rằng Apple có một protocol UITableViewDataSource để làm điều này (mặc dù sử dụng tên mẫu ủy quyền), nhưng thực tế các phương thức của nó được sử dụng để cung cấp thông tin về cách dữ liệu thực tế nên được hiển thị.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath;
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView;
Ngoài ra, hai phương thức trên của Apple đã trộn lẫn lớp hiển thị và lớp dữ liệu, điều này trông rất tệ, nhưng rất ít nhà phát triển cảm thấy tệ. Và ở đây chúng ta đều gọi một cách ngây thơ các phương thức không trả về giá trị và các phương thức có trả về giá trị là phương thức ủy quyền.
Để phân tách các khái niệm, chúng ta nên làm như sau:
- Mẫu ủy quyền (delegate pattern): Khi một sự kiện xảy ra, delegator cần thông báo cho delegate.
- Mẫu nguồn dữ liệu (datasource pattern): Delegator cần lấy dữ liệu từ đối tượng nguồn dữ liệu.
Đây là một ví dụ thực tế:
@class VMCRegistrationController;
@protocol VMCRegistrationControllerDelegate <NSObject>
- (void)registrationControllerDidTapSignUpButton:(VMCRegistrationController *)controller;
@end
@protocol VMCRegistrationControllerDataSource <NSObject>
- (VMCUserCredentials *)credentialsForRegistrationController:(VMCRegistrationController *)controller;
@end
@interface VMCRegistrationController : UIViewController
@property (nonatomic, weak) id<VMCRegistrationControllerDelegate> delegate;
@property (nonatomic, weak) id<VMCRegistrationControllerDataSource> dataSource;
@end
Phương thức delegate phải có người gọi (tức là delegator) làm tham số đầu tiên, như ví dụ trên. Nếu không, delegate không thể phân biệt giữa các thể hiện delegator khác nhau. Nói cách khác, nếu người gọi (delegator) không được truyền cho delegate, thì không có cách nào để delegate xử lý hai delegator khác nhau, vì vậy cách viết sau đây sẽ khiến mọi người khó chịu:
- (void)computationDidFinishWithValue:(CGFloat)value;
Mặc định, delegate cần triển khai các phương thức của protocol. Có thể sử dụng các từ khóa @required và @optional để đánh dấu xem phương thức có bắt buộc hay tùy chọn hay không (mặc định là @required: bắt buộc).
@protocol VMCRegistrationControllerDelegate <NSObject>
@required
- (void)registrationController:(VMCRegistrationController *)controller didProvideSignUpInfo:(NSDictionary *)dict;
@optional
- (void)registrationControllerDidTapSignUpButton:(VMCRegistrationController *)controller;
@end
Đối với các phương thức tùy chọn, delegator phải kiểm tra xem delegate có thực sự triển khai phương thức cụ thể đó hay không trước khi gửi tin nhắn (nếu không sẽ crash):
if ([self.delegate respondsToSelector:@selector(registrationControllerDidTapSignUpButton:)]) {
[self.delegate registrationControllerDidTapSignUpButton:self];
}
Kế Thừa (trong Delegate)
Đôi khi bạn có thể cần ghi đè các phương thức delegate. Hãy xem xét trường hợp có hai lớp con UIViewController: UIViewControllerA và UIViewControllerB, với mối quan hệ kế thừa sau:
UIViewControllerB < UIViewControllerA < UIViewController
UIViewControllerA tuân thủ UITableViewDelegate và triển khai - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath.
Bạn có thể muốn cung cấp một triển khai khác trong UIViewControllerB, triển khai này có thể như sau:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
CGFloat retVal = 0;
if ([super respondsToSelector:@selector(tableView:heightForRowAtIndexPath:)]) {
retVal = [super tableView:self.tableView heightForRowAtIndexPath:indexPath];
}
return retVal + 10.0f;
}
Nhưng nếu lớp cha (UIViewControllerA) không triển khai phương thức này thì sao? Lúc này, việc gọi phương thức [super respondsToSelector:@selector(tableView:heightForRowAtIndexPath:)] sẽ sử dụng triển khai của NSObject, tìm sâu vào ngữ cảnh self và xác định rõ self triển khai phương thức này (vì UITableViewControllerA tuân thủ UITableViewDelegate), nhưng ứng dụng sẽ gặp crash ở dòng tiếp theo, với thông báo lỗi như sau:
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[UIViewControllerB tableView:heightForRowAtIndexPath:]: unrecognized selector sent to instance 0x8d82820'
Trong trường hợp này, chúng ta cần hỏi thể hiện lớp cụ thể liệu nó có thể phản hồi selector tương ứng hay không. Đoạn mã dưới đây cung cấp một mẹo nhỏ:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
CGFloat retVal = 0;
if ([[UIViewControllerA class] instancesRespondToSelector:@selector(tableView:heightForRowAtIndexPath:)]) {
retVal = [super tableView:self.tableView heightForRowAtIndexPath:indexPath];
}
return retVal + 10.0f;
}
Giống như mã xấu xí ở trên, thông thường, đây sẽ là một cách thiết kế kiến trúc tốt hơn, vì theo cách này, phương thức delegate không cần phải được ghi đè.
Đa Delegate
Đa ủy quyền là một khái niệm rất cơ bản, nhưng hầu hết các nhà phát triển lại không quen thuộc với nó mà sử dụng NSNotifications. Như bạn có thể nhận thấy, delegate và datasource là các mẫu giao tiếp giữa các đối tượng, nhưng chỉ liên quan đến hai đối tượng: delegator và delegate.
Mẫu datasource buộc một mối quan hệ một-một, khi người gửi yêu cầu thông tin thì chỉ có một đối tượng duy nhất phản hồi. Đối với mẫu delegate thì có thể khác, chúng ta có đủ lý do để triển khai nhiều delegate đang chờ đợi callback (từ một delegator duy nhất). Cách tiếp cận này phù hợp hơn với các hệ thống phân tán và được sử dụng rộng rãi trong các luồng thông tin phức tạp của phần mềm lớn.
Đa ủy quyền có thể được triển khai theo nhiều cách, nhưng người đọc quan tâm nhiều hơn đến việc tìm một triển khai phù hợp với cá nhân mình. Luca Bernardi đã cung cấp một triển khai rất ngắn gọn của mẫu trên trong LBDelegateMatrioska của anh ấy.
Dưới đây là một triển khai cơ bản, giúp bạn hiểu rõ hơn về khái niệm này. Ngay cả trong Cocoa cũng có một số cách để lưu trữ các tham chiếu yếu trong cấu trúc dữ liệu để tránh vòng lặp tham chiếu, ở đây chúng ta sử dụng một lớp để giữ tham chiếu yếu đến đối tượng delegate (giống như delegate đơn):
@interface VMCWeakReference : NSObject
@property (nonatomic, readonly, weak) id object; // Tên thuộc tính đã được chuẩn hóa.
+ (instancetype)weakReferenceWithObject:(id)object;
- (instancetype)initWithObject:(id)object;
@end
@interface VMCWeakReference ()
@property (nonatomic, weak) id object;
@end
@implementation VMCWeakReference
+ (instancetype)weakReferenceWithObject:(id)object {
return [[[self class] alloc] initWithObject:object];
}
- (instancetype)initWithObject:(id)object {
if ((self = [super init])) {
_object = object;
}
return self;
}
- (BOOL)isEqual:(id)object {
if (self == object) {
return YES;
}
if (![object isKindOfClass:[object class]]) { // nên là [VMCWeakReference class]
return NO;
}
return [self isEqualToWeakReference:(VMCWeakReference *)object];
}
- (BOOL)isEqualToWeakReference:(VMCWeakReference *)object {
if (!object) {
return NO;
}
BOOL objectsMatch = [self.object isEqual:object.object];
return objectsMatch;
}
- (NSUInteger)hash {
return [self.object hash];
}
@end
Sử dụng đối tượng yếu để triển khai thành phần đơn giản của đa delegate:
@protocol VMCServiceCallback <NSObject>
@optional
- (void)generalService:(VMCServiceHandler *)service didFetchItems:(NSArray *)items;
@end
@interface VMCServiceHandler : NSObject
- (void)registerDelegate:(id<VMCServiceCallback>)delegate;
- (void)deregisterDelegate:(id<VMCServiceCallback>)delegate;
@end
@interface VMCServiceHandler ()
@property (nonatomic, strong) NSMutableSet *delegates;
@end
@implementation VMCServiceHandler
- (instancetype)init {
self = [super init];
if (self) {
_delegates = [NSMutableSet new];
}
return self;
}
- (void)registerDelegate:(id<VMCServiceCallback>)delegate {
if ([delegate conformsToProtocol:@protocol(VMCServiceCallback)]) {
[self.delegates addObject:[[VMCWeakReference alloc] initWithObject:delegate]];
}
}
- (void)deregisterDelegate:(id<VMCServiceCallback>)delegate {
if ([delegate conformsToProtocol:@protocol(VMCServiceCallback)]) {
[self.delegates removeObject:[[VMCWeakReference alloc] initWithObject:delegate]];
}
}
- (void)_notifyDelegatesWithItems:(NSArray *)items {
// ...
for (VMCWeakReference *weakRef in [self.delegates allObjects]) { // copy set to avoid mutation while enumerating
id<VMCServiceCallback> delegate = weakRef.object;
if (delegate) { // Check if object still exists
if ([delegate respondsToSelector:@selector(generalService:didFetchItems:)]) {
[delegate generalService:self didFetchItems:items];
}
} else {
// Remove nil reference if delegate has been deallocated
[self.delegates removeObject:weakRef];
}
}
}
@end
Với sự giúp đỡ của các phương thức registerDelegate: và deregisterDelegate:, việc kết nối/ngắt kết nối giữa các thành phần rất đơn giản: vào một thời điểm nào đó, nếu delegate không cần nhận callback từ delegator, chỉ cần "unsubscribe".
Điều này rất hữu ích khi các view khác nhau đang chờ cùng một callback để cập nhật hiển thị giao diện: nếu view chỉ tạm thời bị ẩn (nhưng vẫn tồn tại), nó chỉ cần hủy đăng ký callback.
Lập Trình Hướng Khía Cạnh (AOP)
Aspect Oriented Programming (AOP) không nổi tiếng lắm trong cộng đồng Objective-C, nhưng AOP có thể có sức mạnh rất lớn trong runtime. Tuy nhiên, vì không có tiêu chuẩn thực tế, Apple cũng không cung cấp sẵn, và có vẻ không quan trọng, nên các nhà phát triển không mấy quan tâm đến nó.
Trích dẫn từ trang Wikipedia Aspect Oriented Programming:
Một khía cạnh có thể thay đổi hành vi của mã cơ sở (phần không phải khía cạnh của chương trình) bằng cách áp dụng lời khuyên (hành vi bổ sung) tại các điểm nối khác nhau (các điểm trong chương trình) được chỉ định trong một định lượng hoặc truy vấn được gọi là pointcut (phát hiện xem một điểm nối nhất định có khớp hay không).
Trong thế giới Objective-C, điều này có nghĩa là sử dụng các tính năng của runtime để thêm các *khía cạnh* vào các phương thức được chỉ định. Hành vi được thêm vào bởi khía cạnh có thể là:
- Chạy mã cụ thể trước khi gọi một phương thức cụ thể của lớp.
- Chạy mã cụ thể sau khi gọi một phương thức cụ thể của lớp.
- Thêm mã để thay thế triển khai phương thức ban đầu của lớp.
Có nhiều cách để đạt được những mục tiêu này, nhưng chúng ta không đi sâu vào chi tiết, tuy nhiên chúng chủ yếu đều tận dụng runtime. Peter Steinberger đã viết một thư viện, Aspects, hoàn toàn phù hợp với ý tưởng AOP. Chúng tôi thấy nó đáng tin cậy và được thiết kế rất xuất sắc, vì vậy chúng ta sẽ sử dụng nó ở đây làm một ví dụ đơn giản.
Đối với tất cả các thư viện AOP, thư viện này sử dụng runtime để thực hiện một số phép thuật rất hay, có thể thay thế hoặc thêm một số phương thức (tinh vi hơn kỹ thuật method swizzling).
API của Aspects thú vị và rất mạnh mẽ:
+ (id<AspectToken>)aspect_hookSelector:(SEL)selector
withOptions:(AspectOptions)options
usingBlock:(id)block
error:(NSError **)error;
- (id<AspectToken>)aspect_hookSelector:(SEL)selector
withOptions:(AspectOptions)options
usingBlock:(id)block
error:(NSError **)error;
Ví dụ, mã dưới đây sẽ thực thi tham số khối khi phương thức processAction: của lớp BaseComponent (phương thức thể hiện hoặc lớp) được thực thi.
[BaseComponent aspect_hookSelector:@selector(processAction:)
withOptions:AspectPositionAfter
usingBlock:^(id<AspectInfo> aspectInfo) {
// ... mã thực thi sau phương thức
}
error:nil];
Nói cách khác: sau khi phương thức @selector của bất kỳ đối tượng nào thuộc kiểu BaseComponent (hoặc chính kiểu đó khi phương thức @selector là phương thức lớp) được thực thi, khối mã trong tham số sẽ được thực thi.
Chúng ta đã thêm một khía cạnh vào phương thức processAction: của lớp BaseComponent.
Thông thường AOP được sử dụng để triển khai các khía cạnh ngang. Thống kê và ghi nhật ký là một ví dụ hoàn hảo.
Trong ví dụ dưới đây, chúng ta sẽ sử dụng AOP để thực hiện thống kê. Thống kê là một tính năng phổ biến trong các dự án iOS, có nhiều lựa chọn như Google Analytics, Flurry, MixPanel, v.v.
Hầu hết các framework thống kê đều có hướng dẫn về cách theo dõi các giao diện và sự kiện cụ thể, bao gồm việc viết vài dòng mã trong mỗi lớp.
Trên blog của Ray Wenderlich có một bài viết và một số mã ví dụ, thông qua việc thêm Google Analytics vào view controller của bạn để thực hiện thống kê.
- (void)trackButtonInteraction:(UIButton *)button {
id<GAITracker> tracker = [[GAI sharedInstance] defaultTracker];
[tracker send:[[GAIDictionaryBuilder createEventWithCategory:@"UX"
action:@"touch"
label:[button.titleLabel text]
value:nil] build]];
}
Mã trên đã gửi một sự kiện ngữ cảnh cụ thể khi nút được nhấp. Nhưng khi bạn muốn theo dõi màn hình, mọi thứ sẽ trở nên tệ hơn.
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
id<GAITracker> tracker = [[GAI sharedInstance] defaultTracker];
[tracker set:kGAIScreenName value:@"Stopwatch"];
[tracker send:[[GAIDictionaryBuilder createAppView] build]];
}
Đối với hầu hết các kỹ sư iOS có kinh nghiệm, đây không phải là mã tốt. Chúng ta đã làm cho view controller trở nên tệ hơn. Bởi vì chúng ta đã thêm mã sự kiện thống kê, nhưng đó không phải là trách nhiệm của view controller. Bạn có thể phản bác rằng bạn thường có các đối tượng cụ thể chịu trách nhiệm theo dõi thống kê, và bạn đã thêm mã vào view controller, nhưng dù bạn ẩn logic đến đâu, vấn đề vẫn tồn tại: cuối cùng bạn vẫn chèn mã sau viewDidAppear:.
Chúng ta có thể sử dụng AOP trên phương thức viewDidAppear: của lớp để theo dõi màn hình, và chúng ta có thể sử dụng cùng một phương pháp để thêm theo dõi sự kiện trên các phương thức khác mà chúng ta quan tâm. Ví dụ, khi người dùng nhấp vào một nút nào đó (ví dụ: thường gọi IBAction tương ứng).
Phương pháp này rất gọn gàng và không xâm lấn:
- View controller sẽ không bị ô nhiễm bởi mã không thuộc về nó.
- Cung cấp khả năng chỉ định một tệp SPOC (single point of customization) cho tất cả các khía cạnh được thêm vào mã của chúng ta.
- SPOC nên được sử dụng để thêm các khía cạnh khi ứng dụng bắt đầu khởi động.
- Nếu tệp SPOC có lỗi, ít nhất một selector hoặc lớp không được nhận dạng, ứng dụng sẽ crash khi khởi động (điều này rất tốt đối với chúng ta).
- Các nhóm chịu trách nhiệm về thống kê thường cung cấp tài liệu thống kê, liệt kê các sự kiện cần theo dõi. Tài liệu này có thể dễ dàng ánh xạ vào một tệp SPOC.
- Logic theo dõi sau khi được trừu tượng hóa sẽ dễ dàng mở rộng sang nhiều framework thống kê khác.
- Đối với các chế độ xem màn hình, đối với các phương thức cần định nghĩa selector, chỉ cần sửa đổi các lớp liên quan trong tệp SPOC (các khía cạnh liên quan sẽ được thêm vào phương thức
viewDidAppear:). Nếu muốn gửi đồng thời các chế độ xem màn hình và sự kiện, cần (dựa vào nhà cung cấp thống kê) cung cấp một ID theo dõi hoặc có thể cần cung cấp các siêu thông tin khác.
Chúng ta có thể muốn một tệp SPOC trông giống như sau (tương tự, một tệp .plist cũng sẽ phù hợp):
NSDictionary *loadAnalyticsConfig()
{
return @{
@"trackedScreens" : @[
@{
@"class" : @"VMCMainScreenController",
@"label" : @"Main screen"
}
],
@"trackedEvents" : @[
@{
@"class" : @"VMCMainScreenController",
@"selector" : @"loginSuccess:forUser:",
@"label" : @"Login with Facebook"
},
@{
@"class" : @"VMCMainScreenController",
@"selector" : @"logoutTriggeredForUser:",
@"label" : @"Logout with Facebook"
},
@{
@"class" : @"VMCMainScreenController",
@"selector" : @"loginError:withDetails:",
@"label" : @"Login error with Facebook"
},
@{
@"class" : @"VMCMainScreenController",
@"selector" : @"shareContentTapped:",
@"label" : @"Share button"
}
]
};
}
Kiến trúc được đề cập được lưu trữ trên Github của EF Education First.
- (void)setupWithConfiguration:(NSDictionary *)configuration
{
// screen views tracking
for (NSDictionary *trackedScreen in configuration[@"trackedScreens"]) {
Class clazz = NSClassFromString(trackedScreen[@"class"]);
[clazz aspect_hookSelector:@selector(viewDidAppear:)
withOptions:AspectPositionAfter
usingBlock:^(id<AspectInfo> aspectInfo) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),
^{
NSString *viewName = trackedScreen[@"label"];
[tracker trackScreenHitWithName:viewName];
});
}
error:nil];
}
// events tracking
for (NSDictionary *trackedEvent in configuration[@"trackedEvents"]) {
Class clazz = NSClassFromString(trackedEvent[@"class"]);
SEL selektor = NSSelectorFromString(trackedEvent[@"selector"]);
[clazz aspect_hookSelector:selektor
withOptions:AspectPositionAfter
usingBlock:^(id<AspectInfo> aspectInfo) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),
^{
UserActionClickEvent *buttonPressEvent =
[UserActionClickEvent
eventWithLabel:trackedEvent[@"label"]];
[tracker trackEvent:buttonPressEvent];
});
}
error:nil];
}
}