Giữ lại chu kỳ trên `self` với các khối


167

Tôi e rằng câu hỏi này khá cơ bản, nhưng tôi nghĩ nó có liên quan đến rất nhiều lập trình viên Objective-C đang tham gia vào các khối.

Điều tôi đã nghe là vì các khối bắt các biến cục bộ được tham chiếu trong chúng dưới dạng constbản sao, sử dụng selftrong một khối có thể dẫn đến chu kỳ giữ lại, nếu khối đó được sao chép. Vì vậy, chúng tôi phải sử dụng __blockđể buộc khối phải xử lý trực tiếp selfthay vì sao chép nó.

__block typeof(self) bself = self;
[someObject messageWithBlock:^{ [bself doSomething]; }];

Thay vì chỉ

[someObject messageWithBlock:^{ [self doSomething]; }];

Những gì tôi muốn biết là như sau: nếu điều này là đúng, có cách nào để tôi có thể tránh được sự xấu xí (ngoài việc sử dụng GC) không?


3
Tôi thích gọi selfproxy của tôi thischỉ để lật mọi thứ xung quanh. Trong JavaScript tôi gọi các bao thisđóng của tôi self, vì vậy nó cảm thấy tốt và cân bằng. :)
devios1

Tôi tự hỏi có cần phải thực hiện bất kỳ hành động tương đương nào không nếu tôi đang sử dụng các khối Swift
Ben Lu

@BenLu hoàn toàn! trong các bao đóng Swift (và các hàm được truyền xung quanh có đề cập đến bản thân hoặc rõ ràng) sẽ tự giữ lại. Đôi khi điều này là mong muốn và đôi khi nó tạo ra một chu kỳ (vì chính việc đóng cửa được sở hữu bởi chính bản thân (hoặc thuộc sở hữu của một cái gì đó tự sở hữu). Lý do chính điều này xảy ra là do ARC.
Ethan

1
Để tránh sự cố, cách thích hợp để xác định 'bản thân' được sử dụng trong một khối là '__typeof (bản thân) __weak yếuSelf = self;' để có một tài liệu tham khảo yếu.
XLE_22

Câu trả lời:


169

Nói đúng ra, thực tế đó là bản sao const không liên quan gì đến vấn đề này. Các khối sẽ giữ lại bất kỳ giá trị obj-c nào được ghi lại khi chúng được tạo. Nó chỉ xảy ra rằng cách giải quyết cho vấn đề const-copy giống hệt với cách giải quyết cho vấn đề giữ lại; cụ thể là sử dụng __blocklớp lưu trữ cho biến.

Trong mọi trường hợp, để trả lời câu hỏi của bạn, không có sự thay thế thực sự ở đây. Nếu bạn đang thiết kế API dựa trên khối của riêng mình và thật hợp lý để làm như vậy, bạn có thể có khối được truyền giá trị selftrong như một đối số. Thật không may, điều này không có ý nghĩa đối với hầu hết các API.

Xin lưu ý rằng việc tham khảo một chiếc ngà có cùng một vấn đề. Nếu bạn cần tham chiếu một chiếc ngà trong khối của mình, hãy sử dụng một thuộc tính thay thế hoặc sử dụng bself->ivar.


Phụ lục: Khi biên dịch dưới dạng ARC, __blockkhông còn phá vỡ các chu kỳ. Nếu bạn đang biên dịch cho ARC, bạn cần sử dụng __weakhoặc __unsafe_unretainedthay vào đó.


Không vấn đề gì! Nếu điều này trả lời câu hỏi cho sự hài lòng của bạn, tôi sẽ đánh giá cao nếu bạn có thể chọn đây là câu trả lời chính xác cho câu hỏi của mình. Nếu không, xin vui lòng cho tôi biết làm thế nào tôi có thể trả lời câu hỏi của bạn tốt hơn.
Lily Ballard

4
Không sao, Kevin. Vì vậy, bạn sẽ trì hoãn việc chọn câu trả lời cho câu hỏi ngay lập tức, vì vậy tôi phải quay lại một lát sau. Chúc mừng.
Jonathan Sterling

__unsafe_unretained id belf = self;

4
@JKLaiho: Chắc chắn, __weakcũng tốt. Nếu bạn biết rằng thực tế là đối tượng không thể nằm ngoài phạm vi khi khối được gọi thì __unsafe_unretainednhanh hơn một chút, nhưng nói chung sẽ không tạo ra sự khác biệt. Nếu bạn sử dụng __weak, hãy đảm bảo ném nó vào một __strongbiến cục bộ và kiểm tra xem có phải niltrước khi làm bất cứ điều gì với nó không.
Lily Ballard

2
@Rpranata: Vâng. __blockTác dụng phụ của việc không giữ lại và phát hành hoàn toàn là do không thể có lý do chính đáng về điều đó. Với ARC, trình biên dịch đã đạt được khả năng đó, và __blockhiện tại vẫn giữ lại và phát hành. Nếu bạn cần tránh điều đó, bạn cần sử dụng __unsafe_unretained, hướng dẫn trình biên dịch không thực hiện bất kỳ việc giữ lại hoặc phát hành nào trên giá trị trong biến.
Lily Ballard

66

Chỉ dùng:

__weak id weakSelf = self;

[someObject someMethodWithBlock:^{
    [weakSelf someOtherMethod];
}];

Để biết thêm thông tin: WWDC 2011 - Khối và công văn lớn trong thực tế .

https://developer.apple.com/ideo/wwdc/2011/?id=308

Lưu ý: nếu điều đó không hiệu quả, bạn có thể thử

__weak typeof(self)weakSelf = self;

2
Và bạn có tìm thấy nó không :)?
Tieme

2
Bạn có thể kiểm tra video tại đây - developer.apple.com/ideo/wwdc/2011/ trên
nanjunda

Bạn có thể tham khảo bản thân bên trong "someOtherMethod" không? Sẽ tự tham chiếu những người yếu đuối vào thời điểm đó hay điều đó cũng sẽ tạo ra một chu kỳ giữ lại?
Oren

Xin chào @Oren, nếu bạn cố gắng tự tham khảo bên trong "someOtherMethod", bạn sẽ nhận được cảnh báo Xcode. Cách tiếp cận của tôi chỉ làm cho một tài liệu tham khảo yếu của bản thân.
3lvis

1
Tôi chỉ nhận được một cảnh báo khi tham chiếu bản thân trực tiếp trong khối. Đặt bản thân vào một sốOtherMethod không gây ra bất kỳ cảnh báo nào. Có phải vì xcode không đủ thông minh hay nó không phải là vấn đề? Có phải tham chiếu bản thân trong một sốOtherMethod đã đề cập đến chính mình vì đó là những gì bạn đang gọi phương thức này?
Oren

22

Điều này có thể rõ ràng, nhưng bạn chỉ phải thực hiện selfbí danh xấu xí khi bạn biết bạn sẽ có một chu kỳ giữ lại. Nếu khối chỉ là một thứ duy nhất thì tôi nghĩ bạn có thể bỏ qua việc giữ lại một cách an toàn self. Trường hợp xấu là khi bạn có khối làm giao diện gọi lại chẳng hạn. Giống như ở đây:

typedef void (^BufferCallback)(FullBuffer* buffer);

@interface AudioProcessor : NSObject {…}
@property(copy) BufferCallback bufferHandler;
@end

@implementation AudioProcessor

- (id) init {
    
    [self setBufferCallback:^(FullBuffer* buffer) {
        [self whatever];
    }];
    
}

Ở đây, API không có nhiều ý nghĩa, nhưng nó sẽ có ý nghĩa khi giao tiếp với một siêu lớp chẳng hạn. Chúng tôi giữ lại bộ xử lý bộ đệm, bộ xử lý bộ đệm giữ chúng tôi. So sánh với một cái gì đó như thế này:

typedef void (^Callback)(void);

@interface VideoEncoder : NSObject {…}
- (void) encodeVideoAndCall: (Callback) block;
@end

@interface Foo : NSObject {…}
@property(retain) VideoEncoder *encoder;
@end

@implementation Foo
- (void) somewhere {
    [encoder encodeVideoAndCall:^{
        [self doSomething];
    }];
}

Trong những tình huống này tôi không làm selfrăng cưa. Bạn có một chu kỳ giữ lại, nhưng hoạt động này chỉ tồn tại trong thời gian ngắn và cuối cùng khối sẽ thoát khỏi bộ nhớ, phá vỡ chu trình. Nhưng kinh nghiệm của tôi với các khối là rất nhỏ và có thể là selfrăng cưa xuất hiện như một cách thực hành tốt nhất trong thời gian dài.


6
Điểm tốt. Đó chỉ là một chu kỳ duy trì nếu bản thân giữ cho khối tồn tại. Trong trường hợp các khối không bao giờ được sao chép hoặc các khối có thời lượng giới hạn được bảo đảm (ví dụ: khối hoàn thành cho hoạt hình UIView), bạn không phải lo lắng về điều đó.
Lily Ballard

6
Về nguyên tắc, bạn đã đúng. Tuy nhiên, nếu bạn thực thi mã trong ví dụ, bạn sẽ gặp sự cố. Thuộc tính khối phải luôn luôn được khai báo là copy, không retain. Nếu họ chỉ là vậy retain, thì không có gì đảm bảo họ sẽ bị chuyển khỏi ngăn xếp, điều đó có nghĩa là khi bạn đi thực thi nó, nó sẽ không còn ở đó nữa. (và sao chép và khối đã sao chép được tối ưu hóa để giữ lại)
Dave DeLong

Ah, chắc chắn, một lỗi đánh máy. Tôi đã trải qua retaingiai đoạn này một lúc trước và nhanh chóng nhận ra điều bạn đang nói :) Cảm ơn!
zoul

Tôi khá chắc chắn retainbị bỏ qua hoàn toàn cho các khối (trừ khi chúng đã di chuyển khỏi ngăn xếp với copy).
Steven Fisher

@Dave DeLong, Không, nó sẽ không bị sập vì @property (giữ lại) chỉ được sử dụng cho một tham chiếu đối tượng, không phải khối .. Không cần phải sử dụng một bản sao ở đây ..
DeniziOS

20

Đăng một câu trả lời khác bởi vì đây cũng là một vấn đề đối với tôi. Ban đầu tôi nghĩ rằng tôi phải sử dụng blockSelf ở bất cứ đâu có một tài liệu tham khảo tự bên trong một khối. Đây không phải là trường hợp, nó chỉ khi bản thân đối tượng có một khối trong đó. Và trên thực tế, nếu bạn sử dụng blockSelf trong những trường hợp này, đối tượng có thể nhận được dealloc'd trước khi bạn lấy lại kết quả từ khối và sau đó nó sẽ bị sập khi cố gắng gọi nó, vì vậy rõ ràng bạn muốn giữ lại cho đến khi phản hồi quay lại.

Trường hợp đầu tiên chứng minh khi một chu kỳ giữ lại sẽ xảy ra bởi vì nó chứa một khối được tham chiếu trong khối:

#import <Foundation/Foundation.h>

typedef void (^MyBlock)(void);

@interface ContainsBlock : NSObject 

@property (nonatomic, copy) MyBlock block;

- (void)callblock;

@end 

@implementation ContainsBlock
@synthesize block = _block;

- (id)init {
    if ((self = [super init])) {

        //__block ContainsBlock *blockSelf = self; // to fix use this.
        self.block = ^{
                NSLog(@"object is %@", self); // self retain cycle
            };
    }
    return self;
}

- (void)dealloc {
    self.block = nil;
    NSLog (@"ContainsBlock"); // never called.
    [super dealloc];
} 

- (void)callblock {
    self.block();
} 

@end 

 int main() {
    ContainsBlock *leaks = [[ContainsBlock alloc] init];
    [leaks callblock];
    [leaks release];
}

Bạn không cần blockSelf trong trường hợp thứ hai vì đối tượng gọi không có khối trong đó sẽ gây ra chu kỳ giữ lại khi bạn tự tham chiếu:

#import <Foundation/Foundation.h>

typedef void (^MyBlock)(void);

@interface BlockCallingObject : NSObject 
@property (copy, nonatomic) MyBlock block;
@end

@implementation BlockCallingObject 
@synthesize block = _block;

- (void)dealloc {
    self.block = nil;
    NSLog(@"BlockCallingObject dealloc");
    [super dealloc];
} 

- (void)callblock {
    self.block();
} 
@end

@interface ObjectCallingBlockCallingObject : NSObject 
@end

@implementation ObjectCallingBlockCallingObject 

- (void)doneblock {
    NSLog(@"block call complete");
}

- (void)dealloc {
    NSLog(@"ObjectCallingBlockCallingObject dealloc");
    [super dealloc];
} 

- (id)init {
    if ((self = [super init])) {

        BlockCallingObject *myobj = [[BlockCallingObject alloc] init];
        myobj.block = ^() {
            [self doneblock]; // block in different object than this object, no retain cycle
        };
        [myobj callblock];
        [myobj release];
    }
    return self;
}
@end

int main() {

    ObjectCallingBlockCallingObject *myObj = [[ObjectCallingBlockCallingObject alloc] init];
    [myObj release];

    return 0;
} 

Đó là một quan niệm sai lầm phổ biến và có thể nguy hiểm, bởi vì các khối nên giữ lại selfcó thể không phải do mọi người áp dụng quá nhiều sửa chữa này. Đây là một ví dụ tốt về việc tránh các chu kỳ giữ lại trong mã không ARC, cảm ơn vì đã đăng.
Carl Veazey

9

Cũng cần nhớ rằng chu kỳ giữ lại có thể xảy ra nếu khối của bạn tham chiếu đến một đối tượng khác mà sau đó giữ lại self.

Tôi không chắc chắn rằng Bộ sưu tập rác có thể giúp ích trong các chu kỳ giữ lại này. Nếu đối tượng giữ lại khối (mà tôi sẽ gọi đối tượng máy chủ) tồn tại self(đối tượng khách), thì tham chiếu selfbên trong khối sẽ không được coi là tuần hoàn cho đến khi chính đối tượng giữ lại được giải phóng. Nếu đối tượng máy chủ vượt xa các máy khách của nó, bạn có thể bị rò rỉ bộ nhớ đáng kể.

Vì không có giải pháp sạch, tôi sẽ đề xuất các cách giải quyết sau. Hãy chọn một hoặc nhiều trong số họ để khắc phục vấn đề của bạn.

  • Chỉ sử dụng các khối để hoàn thành và không cho các sự kiện kết thúc mở. Ví dụ: sử dụng các khối cho các phương thức như doSomethingAndWhenDoneExecuteThisBlock:, chứ không phải các phương thức như setNotificationHandlerBlock:. Các khối được sử dụng để hoàn thành có thời gian kết thúc xác định và sẽ được các đối tượng máy chủ giải phóng sau khi chúng được đánh giá. Điều này ngăn chặn chu kỳ giữ lại sống quá lâu ngay cả khi nó xảy ra.
  • Làm điệu nhảy tham chiếu yếu mà bạn mô tả.
  • Cung cấp một phương thức để dọn sạch đối tượng của bạn trước khi nó được phát hành, nó "ngắt kết nối" đối tượng khỏi các đối tượng máy chủ có thể chứa các tham chiếu đến nó; và gọi phương thức này trước khi gọi phát hành trên đối tượng. Mặc dù phương pháp này hoàn toàn tốt nếu đối tượng của bạn chỉ có một khách hàng (hoặc là một người độc thân trong một số ngữ cảnh), nhưng sẽ bị hỏng nếu có nhiều khách hàng. Về cơ bản bạn đang đánh bại cơ chế đếm giữ ở đây; đây là giống như gọi deallocthay vì release.

Nếu bạn đang viết một đối tượng máy chủ, chỉ lấy các đối số khối để hoàn thành. Không chấp nhận đối số khối cho các cuộc gọi lại, chẳng hạn như setEventHandlerBlock:. Thay vào đó, hãy quay lại mô hình đại biểu cổ điển: tạo một giao thức chính thức và quảng cáo một setEventDelegate:phương thức. Không giữ lại đại biểu. Nếu bạn thậm chí không muốn tạo một giao thức chính thức, hãy chấp nhận bộ chọn làm cuộc gọi lại ủy nhiệm.

Và cuối cùng, mẫu này sẽ rung chuông báo động:

- (void) dealloc {
    [myServerObject phát hànhCallbackBlocksForObject: self];
    ...
}

Nếu bạn đang cố gắng mở các khối có thể tham chiếu selftừ bên trong dealloc, bạn đã gặp rắc rối. dealloccó thể không bao giờ được gọi do chu kỳ giữ lại gây ra bởi các tham chiếu trong khối, điều đó có nghĩa là đối tượng của bạn chỉ đơn giản là bị rò rỉ cho đến khi đối tượng máy chủ bị hủy.


GC sẽ giúp nếu bạn sử dụng __weakmột cách thích hợp.
tc.

Truy tìm bộ sưu tập rác tất nhiên có thể đối phó với các chu kỳ giữ lại. Giữ lại chu kỳ chỉ là một vấn đề đối với môi trường đếm tham chiếu
newacct

Để mọi người đều biết, bộ sưu tập rác đã không được chấp nhận trong OS X v10.8 để ủng hộ Đếm tham chiếu tự động (ARC) và được lên lịch để xóa trong phiên bản tương lai của OS X ( developer.apple.com/l Library / mac / # releasenotes / ObjectiveC / Bắn ).
Ricardo Sanchez-Saez

1

__block __unsafe_unretainedcông cụ sửa đổi được đề xuất trong bài đăng của Kevin có thể gây ra ngoại lệ truy cập xấu trong trường hợp khối được thực thi trong một luồng khác. Tốt hơn hết là chỉ sử dụng công cụ sửa đổi __block cho biến temp và làm cho nó không sau khi sử dụng.

__block SomeType* this = self;
[someObject messageWithBlock:^{
  [this doSomething]; // here would be BAD_ACCESS in case of __unsafe_unretained with
                      //  multithreading and self was already released
  this = nil;
}];

Sẽ không thực sự an toàn hơn nếu chỉ sử dụng __weak thay vì __block để tránh sự cần thiết của biến sau khi sử dụng nó? Ý tôi là, giải pháp này rất tuyệt nếu bạn muốn phá vỡ các loại chu kỳ khác nhưng chắc chắn tôi không thấy bất kỳ lợi thế cụ thể nào cho "tự" giữ chu kỳ trên đó.
ale0xB

Bạn không thể sử dụng __weak nếu mục tiêu nền tảng của bạn là iOS 4.x. Ngoài ra đôi khi bạn cần mã trong khối đã được thực thi cho đối tượng hợp lệ, không phải cho nil.
b1gbr0

1

Bạn có thể sử dụng thư viện libextobjc. Nó khá phổ biến, nó được sử dụng trong ReactiveCocoa chẳng hạn. https://github.com/jspahrsummers/libextobjc

Nó cung cấp 2 macro @weakify và @strongify, vì vậy bạn có thể có:

@weakify(self)
[someObject messageWithBlock:^{
   @strongify(self)
   [self doSomething]; 
}];

Điều này ngăn cản một tham chiếu mạnh trực tiếp để chúng ta không tự chui vào. Ngoài ra, nó ngăn không cho bản thân trở thành con số không, nhưng vẫn làm giảm số lượng giữ lại một cách chính xác. Thêm trong liên kết này: http://aceontech.com/objc/ios/2014/01/10/weakify-a-more-elegant-solution-to-weakelf.html


1
Trước khi hiển thị mã được đơn giản hóa, tốt hơn là nên biết những gì đằng sau nó, mọi người nên biết hai dòng mã thực sự.
Alex Cio

0

Còn cái này thì sao?

- (void) foo {
     __weak __block me = self;

     myBlock = ^ {
        [[me someProp] someMessage];
     }
     ...
 }

Tôi không nhận được cảnh báo trình biên dịch nữa.


-1

Khối: một chu kỳ giữ lại sẽ xảy ra vì nó chứa một khối được tham chiếu trong khối; Nếu bạn tạo bản sao khối và sử dụng biến thành viên, tự sẽ giữ lại.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.