2012-05-03 29 views
13

Khi viết một phương thức chấp nhận một khối làm đối số, tôi có cần thực hiện bất kỳ điều gì đặc biệt như sao chép khối đó vào heap trước khi thực hiện nó không? Ví dụ: nếu tôi có phương pháp sau:Truyền khối trong Objective-C

- (void)testWithBlock:(void (^)(NSString *))block { 
    NSString *testString = @"Test"; 
    block(testString); 
} 

Tôi có nên làm bất cứ điều gì với block trước khi gọi hoặc khi nhập phương pháp? Hoặc là ở trên đúng cách sử dụng khối được truyền vào? Ngoài ra, là cách sau đây gọi phương thức chính xác, hoặc tôi nên làm gì đó với khối trước khi vượt qua nó?

[object testWithBlock:^(NSString *test){ 
    NSLog(@"[%@]", test); 
}]; 

Nơi Tôi cần phải sao chép khối? Và điều này sẽ khác thế nào nếu tôi không sử dụng ARC?

+0

Bạn đang sử dụng ARC? –

+0

@pcperini, vâng. – rid

Trả lời

18

Khi bạn nhận được khối dưới dạng tham số phương pháp, khối đó có thể là bản gốc đã được tạo trên ngăn xếp hoặc nó có thể được sao chép (một khối trên heap). Theo như tôi biết, không có cách nào để nói. Vì vậy, quy tắc chung của ngón tay cái là nếu bạn định thực thi khối trong phương thức nhận nó, bạn không cần phải sao chép nó. Nếu bạn dự định chuyển khối đó sang phương thức khác (có thể hoặc không thể thực thi nó ngay lập tức) thì bạn cũng không cần sao chép nó (phương thức nhận sẽ sao chép nó nếu nó có ý định giữ nó). Tuy nhiên, nếu bạn dự định lưu trữ khối theo bất kỳ cách nào cho một số nơi để thực hiện sau này, bạn cần phải sao chép nó.Ví dụ chính nhiều người sử dụng là một số loại khối hoàn thành tổ chức như là một biến Ví dụ:

typedef void (^IDBlock) (id); 
@implementation MyClass{ 
    IDBlock _completionBlock; 
} 

Tuy nhiên, bạn cũng cần phải sao chép nó nếu bạn đang đi để thêm nó vào bất kỳ loại lớp bộ sưu tập, giống như một NSArray hoặc NSDictionary. Nếu không, bạn sẽ gặp lỗi (rất có thể là EXC_BAD_ACCESS) hoặc có thể là hỏng dữ liệu khi bạn cố gắng thực thi khối sau đó.

Khi bạn thực hiện một khối, điều quan trọng là phải kiểm tra trước tiên nếu khối là nil. Mục tiêu-c sẽ cho phép bạn vượt qua nil để tham số phương thức chặn. Nếu khối đó là 0, bạn sẽ nhận được EXC_BAD_ACCESS khi bạn cố thực hiện nó. May mắn thay điều này là dễ dàng để làm. Trong ví dụ của bạn, bạn sẽ viết:

- (void)testWithBlock:(void (^)(NSString *))block { 
    NSString *testString = @"Test"; 
    if (block) block(testString); 
} 

Có những cân nhắc về hiệu suất trong các khối sao chép. So với việc tạo một khối trên ngăn xếp, sao chép một khối vào heap không phải là tầm thường. Nó không phải là một thỏa thuận lớn nói chung, nhưng nếu bạn đang sử dụng một khối lặp đi lặp lại hoặc sử dụng một loạt các khối lặp đi lặp lại và sao chép chúng trên mỗi thực thi, nó sẽ tạo ra một hit hiệu suất. Vì vậy, nếu phương pháp của bạn - (void)testWithBlock:(void (^)(NSString *))block; là trong một số loại vòng lặp, sao chép khối đó có thể làm hỏng hiệu suất của bạn nếu bạn không cần phải sao chép nó.

Một địa điểm khác mà bạn cần sao chép một khối là nếu bạn có ý định gọi khối đó trong chính nó (chặn đệ quy). Đây không phải là tất cả những gì phổ biến, nhưng bạn phải sao chép các khối nếu bạn có ý định làm điều này. Xem câu hỏi/câu trả lời của tôi trên SO tại đây: Recursive Blocks In Objective-C.

Cuối cùng, nếu bạn định lưu trữ một khối, bạn cần phải thực sự cẩn thận về việc tạo các chu kỳ lưu giữ. Các khối sẽ giữ lại bất kỳ đối tượng nào được truyền vào nó, và nếu đối tượng đó là một biến cá thể, nó sẽ giữ lại lớp của biến cá thể (self). Cá nhân tôi yêu thích các khối và sử dụng chúng mọi lúc. Nhưng có một lý do mà Apple không sử dụng/lưu trữ các khối cho các lớp UIKit của họ và thay vào đó gắn bó với một mục tiêu/hành động hoặc mẫu đại biểu. Nếu bạn (lớp tạo khối) đang giữ lại lớp đang nhận/sao chép/lưu trữ khối, và trong khối đó bạn tham chiếu hoặc biến thể cá thể của bất kỳ lớp nào, bạn đã tạo một chu trình giữ lại (classA -> classB - > block -> classA). Điều này rất dễ làm, và đó là điều tôi đã làm quá nhiều lần. Hơn nữa, "Rò rỉ" trong dụng cụ không bắt được nó. Phương pháp để giải quyết vấn đề này thật dễ dàng: chỉ cần tạo biến số __weak tạm thời (đối với ARC) hoặc biến số __block (không phải ARC) và khối sẽ không giữ lại biến đó. Vì vậy, ví dụ, sau đây sẽ là một giữ lại chu kỳ nếu 'đối tượng' copies/cửa hàng khối:

[object testWithBlock:^(NSString *test){ 
    _iVar = test; 
    NSLog(@"[%@]", test); 
}]; 

Tuy nhiên, để khắc phục điều này (sử dụng ARC):

__weak IVarClass *iVar = _iVar; 
[object testWithBlock:^(NSString *test){ 
    iVar = test; 
    NSLog(@"[%@]", test); 
}]; 

Bạn cũng có thể làm một cái gì đó như thế này:

__weak ClassOfSelf _self = self; 
[object testWithBlock:^(NSString *test){ 
    _self->_iVar = test; 
    NSLog(@"[%@]", test); 
}]; 

Lưu ý rằng nhiều người không thích ở trên, vì họ coi đó là mong manh, nhưng nó là một cách hợp lệ truy cập vào các biến. Cập nhật - Trình biên dịch hiện tại cảnh báo nếu bạn cố gắng truy cập trực tiếp biến bằng cách sử dụng '->'. Vì lý do này (cũng như lý do an toàn), tốt nhất bạn nên tạo thuộc tính cho các biến mà bạn muốn truy cập. Vì vậy, thay vì _self->_iVar = test; bạn sẽ sử dụng: _self.iVar = test;.

UPDATE (thêm thông tin)

Nói chung, nó là tốt nhất để xem xét các phương pháp tiếp nhận khối như chịu trách nhiệm về việc xác định khối cần phải được sao chép chứ không phải là người gọi. Điều này là do phương thức tiếp nhận có thể là phương thức duy nhất biết được khối cần lưu giữ trong bao lâu hoặc nếu nó cần được sao chép. Bạn (với tư cách là lập trình viên) rõ ràng sẽ biết thông tin này khi bạn viết cuộc gọi, nhưng nếu bạn xem xét người gọi và người nhận ở các đối tượng riêng biệt, người gọi sẽ cung cấp cho người nhận khối và được thực hiện với nó. Vì vậy, nó không cần phải biết những gì được thực hiện với khối sau khi nó đã biến mất. Mặt khác, có thể người gọi đã sao chép khối (có thể nó đã lưu trữ khối và hiện đang chuyển nó sang phương thức khác) nhưng người nhận (người cũng dự định lưu trữ khối) vẫn nên sao chép khối (mặc dù khối đã được sao chép). Người nhận không thể biết rằng khối đã được sao chép, và một số khối nó nhận được có thể được sao chép trong khi những người khác có thể không được. Vì vậy, người nhận nên luôn luôn sao chép một khối nó dự định giữ xung quanh? Có lý? Điều này về cơ bản là thực hành thiết kế hướng đối tượng tốt. Về cơ bản, bất cứ ai có thông tin đều chịu trách nhiệm xử lý nó.

Các khối được sử dụng rộng rãi trong GCD của Apple (Grand Central Dispatch) để dễ dàng bật đa luồng. Nói chung, bạn không cần phải sao chép một khối khi bạn gửi nó trên GCD. Thật kỳ lạ, điều này hơi phản trực giác (nếu bạn nghĩ về nó) bởi vì nếu bạn gửi một khối không đồng bộ, thường là phương thức mà khối được tạo ra sẽ trở lại trước khi khối đã thực hiện, điều này có nghĩa là khối sẽ hết hạn vì nó một đối tượng ngăn xếp. Tôi không nghĩ rằng GCD sao chép khối vào ngăn xếp (tôi đọc ở đâu đó nhưng không thể tìm thấy nó một lần nữa), thay vào đó tôi nghĩ rằng cuộc sống của các chủ đề được mở rộng bằng cách được đặt trên một sợi khác.

Mike Ash cũng có bài viết sâu rộng về khối, GCD và ARC mà bạn có thể tìm thấy hữu ích:

+0

Làm thế nào về một phương thức chấp nhận một trình xử lý hoàn thành mà nó thực thi sau này? Người gọi hoặc phương pháp sao chép khối? – rid

+0

Phương pháp nên sao chép khối. Người gọi không nên biết khối đang được xử lý như thế nào.Nếu bạn đặt trách nhiệm lên người gọi, và phương pháp thay đổi nó thực hiện (quyết định lưu trữ khối thay vì thực thi nó ngay lập tức), bạn sẽ phải tìm mọi người gọi phương thức đó và yêu cầu họ sao chép nó. Không vui. Phương thức này sẽ biết rõ hơn người gọi liệu khối có cần được sao chép hay không. Thật không may, đó là trách nhiệm của người gọi để đảm bảo rằng nó không tạo ra một chu kỳ giữ lại, mà tôi nghĩ là tại sao Apple hầu như không bao giờ lưu trữ các khối từ các API công cộng của họ. –

+0

@Radu Tôi đã cập nhật câu trả lời của mình để thêm thông tin. Tôi cũng đã bao gồm một số liên kết đến các bài viết thảo luận về các khối sâu hơn có thể ở đây. –

7

Điều này có vẻ tốt. Mặc dù vậy, bạn có thể muốn kiểm tra lại các thông số khối:

@property id myObject; 
@property (copy) void (^myBlock)(NSString *); 

....

- (void)testWithBlock: (void (^)(NSString *))block 
{ 
    NSString *testString = @"Test"; 
    if (block) 
    { 
     block(test); 
     myObject = Block_copy(block); 
     myBlock = block; 
    } 
} 

...

[object testWithBlock: ^(NSString *test) 
{ 
    NSLog(@"[%@]", test); 
}]; 

nên được tốt. Và tôi tin rằng họ thậm chí đang cố gắng loại bỏ Block_copy(), nhưng họ vẫn chưa.

+0

Vì vậy, nếu tôi không sử dụng ARC, tôi có cần tạo bản sao của khối khi nhập phương thức, nhưng ARC thực hiện điều này tự động? Hoặc điều này sẽ tương tự ngay cả khi tôi không sử dụng ARC? – rid

+2

Bạn chỉ cần sao chép khối nếu bạn giữ nó vượt quá khả năng thực hiện của phương thức hiện tại. ARC sẽ sao chép nó cho bạn nếu bạn lưu trữ nó trong một biến khối không cục bộ mạnh. Nếu bạn lưu trữ nó như là một 'id', thì ARC sẽ chỉ giữ lại nó, điều đó không đủ tốt. –

+0

@KenThomases, tôi không chắc mình hoàn toàn hiểu được. Bạn có thể xin vui lòng gửi một ví dụ về nơi tôi sẽ cần phải sao chép nó? – rid

4

Như các khối lập trình chủ đề hướng dẫn cho biết dưới 'Copying Blocks':

Thông thường, bạn không cần phải sao chép (hoặc giữ) một khối. Bạn chỉ cần tạo một bản sao khi bạn mong đợi khối được sử dụng sau khi hủy bỏ phạm vi mà nó được khai báo.

Trong trường hợp bạn mô tả, về cơ bản bạn có thể nghĩ về khối đơn giản là tham số cho phương pháp của mình, giống như thể là int hoặc loại nguyên thủy khác. Khi phương thức được gọi, không gian trên ngăn xếp sẽ được phân bổ cho các tham số của phương thức và do đó khối sẽ sống trên ngăn xếp trong suốt quá trình thực thi phương thức của bạn (giống như tất cả các tham số khác). Khi khung ngăn xếp được bật ra khỏi đầu ngăn xếp khi trả về phương thức, bộ nhớ ngăn xếp được cấp phát cho khối sẽ được deallocated. Vì vậy, trong quá trình thực hiện phương thức của bạn, khối được đảm bảo là còn sống, do đó không có quản lý bộ nhớ để xử lý ở đây (trong cả hai trường hợp ARC và không phải ARC). Nói cách khác, mã của bạn là tốt. Bạn có thể chỉ cần gọi khối bên trong phương thức.

Khi văn bản được trích dẫn gợi ý, thời gian duy nhất bạn cần sao chép một khối rõ ràng là khi bạn muốn nó có thể truy cập từ bên ngoài phạm vi nơi nó được tạo (trong trường hợp của bạn, vượt quá tuổi thọ của khung ngăn xếp phương pháp của bạn). Ví dụ: giả sử bạn muốn một phương thức tìm nạp một số dữ liệu từ web và chạy một khối mã khi quá trình tìm nạp hoàn tất. phương pháp chữ ký của bạn có thể trông giống như:

- (void)getDataFromURL:(NSURL *)url completionHandler:(void(^)(void))completionHandler;

Kể từ khi dữ liệu lấy xảy ra không đồng bộ, bạn sẽ muốn giữ khối xung quanh (nhiều khả năng trong một thuộc tính của lớp học của bạn) và sau đó chạy các khối một khi dữ liệu đã được được tìm nạp đầy đủ.Trong trường hợp này, việc thực hiện của bạn có thể trông giống như:

@interface MyClass 

@property (nonatomic, copy) void(^dataCompletion)(NSData *); 

@end 



@implementation MyClass 
@synthesize dataCompletion = _dataCompletion; 

- (void)getDataFromURL:(NSURL *)url completionHandler:(void(^)(NSData *fetchedData))completionHandler { 
    self.dataCompletion = completionHandler; 
    [self fetchDataFromURL:url]; 
} 

- (void)fetchDataFromURL:(NSURL *)url { 
    // Data fetch starts here 
} 

- (void)finishedFetchingData:(NSData *)fetchedData { 
    // Called when the data is done being fetched 
    self.dataCompletion(fetchedData) 
    self.dataCompletion = nil; 
} 

Trong ví dụ này, sử dụng một tài sản với một copy ngữ nghĩa sẽ thực hiện một Block_copy() trên khối và sao chép nó vào đống. Điều này xảy ra trong dòng self.dataCompletion = completionHandler. Do đó, khối được di chuyển từ khung ngăn xếp của phương thức -getDataFromURL:completionHandler: đến vùng heap cho phép nó được gọi sau trong phương thức finishedFetchingData:. Trong phương thức thứ hai, dòng self.dataCompletion = nil vô hiệu hóa thuộc tính và gửi Block_release() tới khối được lưu trữ, do đó giải phóng nó.

Sử dụng một tài sản theo cách này là tốt đẹp vì nó về cơ bản sẽ xử lý tất cả các quản lý bộ nhớ khối cho bạn (chỉ cần đảm bảo đó là một tài sản copy (hoặc strong) chứ không phải chỉ đơn giản là một retain) và sẽ làm việc trong cả hai phi Các trường hợp ARC và ARC. Thay vào đó, nếu bạn muốn sử dụng biến số liệu thô để lưu trữ khối và đang hoạt động trong môi trường không phải ARC, bạn sẽ phải gọi số Block_copy(), Block_retain()Block_release() ở tất cả các vị trí thích hợp nếu bạn muốn giữ khối xung quanh dài hơn tuổi thọ của phương thức mà nó được truyền dưới dạng tham số. Cùng mã trên được viết bằng một Ivar thay vì một tài sản sẽ trông như thế này:

@interface MyClass { 
    void(^dataCompletion)(NSData *); 
} 

@end 



@implementation MyClass 

- (void)getDataFromURL:(NSURL *)url completionHandler:(void(^)(NSData *fetchedData))completionHandler { 
    dataCompletion = Block_copy(completionHandler); 
    [self fetchDataFromURL:url]; 
} 

- (void)fetchDataFromURL:(NSURL *)url { 
    // Data fetch starts here 
} 

- (void)finishedFetchingData:(NSData *)fetchedData { 
    // Called when the data is done being fetched 
    dataCompletion(fetchedData) 
    Block_release(dataCompletion); 
    dataCompletion = nil; 
} 
+0

bạn chỉ có thể sử dụng '[completionHandler copy]' thay vì 'Block_copy (completionHandler)' và '[dataCompletion release]' thay vì 'Block_release (dataCompletion)' – user102008

0

Bạn biết có hai loại khối:

  1. khối lưu trữ trong ngăn xếp, những cái mà bạn viết rõ ràng là^{...} và biến mất ngay khi hàm được tạo trong trả về, giống như các biến ngăn xếp thông thường. Khi bạn gọi một khối ngăn xếp sau khi chức năng nó thuộc về đã trở lại, những điều xấu xảy ra.

  2. khối trong heap, những thứ bạn nhận được khi bạn sao chép một khối khác, những người sống miễn là một số đối tượng khác giữ tham chiếu đến chúng, giống như đối tượng thông thường.

Lý do duy nhất để bạn sao chép khối là khi bạn có một khối, hoặc có thể là khối ngăn xếp (khối địa phương rõ ràng^{...} hoặc đối số phương pháp có nguồn gốc bạn không Không biết), và rằng bạn muốn mở rộng tuổi thọ của nó ra khỏi giới hạn một trong các khối ngăn xếp, và rằng trình biên dịch đã không làm công việc đó cho bạn.

Hãy suy nghĩ: giữ một khối trong một biến mẫu.

Thêm khối trong bộ sưu tập như NSArray.

Đó là những ví dụ phổ biến về trường hợp bạn nên sao chép một khối khi bạn không chắc chắn nó đã là một khối đống.

Lưu ý rằng trình biên dịch thực hiện nó cho bạn khi một khối được gọi trong khối khác.

Các vấn đề liên quan