IT-Swarm.Net

Làm cách nào để tránh tự chụp trong các khối khi triển khai API?

Tôi có một ứng dụng đang hoạt động và tôi đang làm việc để chuyển đổi nó thành ARC trong Xcode 4.2. Một trong những cảnh báo kiểm tra trước liên quan đến việc bắt giữ self một cách mạnh mẽ trong một khối dẫn đến chu kỳ giữ lại. Tôi đã tạo một mẫu mã đơn giản để minh họa vấn đề. Tôi tin rằng tôi hiểu điều này có nghĩa là gì nhưng tôi không chắc chắn cách "chính xác" hoặc được đề xuất để thực hiện loại kịch bản này.

  • tự là một thể hiện của lớp MyAPI
  • mã dưới đây được đơn giản hóa để chỉ hiển thị các tương tác với các đối tượng và khối có liên quan đến câu hỏi của tôi
  • giả sử rằng MyAPI lấy dữ liệu từ một nguồn từ xa và MyDataProcessor hoạt động trên dữ liệu đó và tạo ra một đầu ra
  • bộ xử lý được cấu hình với các khối để truyền đạt tiến trình & trạng thái

mẫu mã:

// code sample
self.delegate = aDelegate;

self.dataProcessor = [[MyDataProcessor alloc] init];

self.dataProcessor.progress = ^(CGFloat percentComplete) {
    [self.delegate myAPI:self isProcessingWithProgress:percentComplete];
};

self.dataProcessor.completion = ^{
    [self.delegate myAPIDidFinish:self];
    self.dataProcessor = nil;
};

// start the processor - processing happens asynchronously and the processor is released in the completion block
[self.dataProcessor startProcessing];

Câu hỏi: tôi đang làm gì "sai" và/hoặc nên sửa đổi điều này như thế nào để phù hợp với quy ước ARC?

222
XJones

Câu trả lời ngắn

Thay vì truy cập trực tiếp self, bạn nên truy cập gián tiếp, từ một tham chiếu sẽ không được giữ lại. Nếu bạn không sử dụng Đếm tham chiếu tự động (ARC) , bạn có thể làm điều này:

__block MyDataProcessor *dp = self;
self.progressBlock = ^(CGFloat percentComplete) {
    [dp.delegate myAPI:dp isProcessingWithProgress:percentComplete];
}

Từ khóa __block đánh dấu các biến có thể được sửa đổi bên trong khối (chúng tôi không làm điều đó) nhưng chúng cũng không tự động được giữ lại khi khối được giữ lại (trừ khi bạn đang sử dụng ARC). Nếu bạn làm điều này, bạn phải chắc chắn rằng không có gì khác sẽ cố gắng thực thi khối sau khi phiên bản MyDataProcessor được phát hành. (Với cấu trúc mã của bạn, đó không phải là vấn đề.) Đọc thêm về __block .

Nếu bạn đang sử dụng ARC , ngữ nghĩa của __block thay đổi và tham chiếu sẽ được giữ lại, trong trường hợp đó bạn nên khai báo __weak thay vào đó.

Câu trả lời dài

Giả sử bạn có mã như thế này:

self.progressBlock = ^(CGFloat percentComplete) {
    [self.delegate processingWithProgress:percentComplete];
}

Vấn đề ở đây là bản thân đang giữ một tham chiếu đến khối; trong khi đó, khối phải giữ lại một tham chiếu đến bản thân để tìm nạp thuộc tính ủy nhiệm của nó và gửi cho đại biểu một phương thức. Nếu mọi thứ khác trong ứng dụng của bạn giải phóng tham chiếu đến đối tượng này, thì số giữ lại của nó sẽ không bằng 0 (vì khối đang trỏ đến nó) và khối không làm gì sai (vì đối tượng đang trỏ vào nó) và vì vậy cặp đối tượng sẽ rò rỉ vào đống, chiếm bộ nhớ nhưng mãi mãi không thể truy cập mà không có trình gỡ lỗi. Đáng thương, thật đấy.

Trường hợp đó có thể dễ dàng khắc phục bằng cách làm điều này thay vào đó:

id progressDelegate = self.delegate;
self.progressBlock = ^(CGFloat percentComplete) {
    [progressDelegate processingWithProgress:percentComplete];
}

Trong mã này, tự giữ lại khối, khối đang giữ lại đại biểu và không có chu kỳ (có thể nhìn thấy từ đây; đại biểu có thể giữ lại đối tượng của chúng tôi nhưng điều đó nằm ngoài tầm tay của chúng tôi ngay bây giờ). Mã này sẽ không có nguy cơ bị rò rỉ theo cùng một cách, bởi vì giá trị của thuộc tính đại biểu được nắm bắt khi khối được tạo, thay vì tra cứu khi thực thi. Một tác dụng phụ là, nếu bạn thay đổi ủy nhiệm sau khi khối này được tạo, khối sẽ vẫn gửi thông báo cập nhật cho đại biểu cũ. Điều đó có khả năng xảy ra hay không phụ thuộc vào ứng dụng của bạn.

Ngay cả khi bạn bình tĩnh với hành vi đó, bạn vẫn không thể sử dụng thủ thuật đó trong trường hợp của mình:

self.dataProcessor.progress = ^(CGFloat percentComplete) {
    [self.delegate myAPI:self isProcessingWithProgress:percentComplete];
};

Ở đây bạn đang chuyển trực tiếp self cho đại biểu trong lệnh gọi phương thức, vì vậy bạn phải đưa nó vào đó ở đâu đó. Nếu bạn có quyền kiểm soát định nghĩa của loại khối, điều tốt nhất sẽ là chuyển đại biểu vào khối làm tham số:

self.dataProcessor.progress = ^(MyDataProcessor *dp, CGFloat percentComplete) {
    [dp.delegate myAPI:dp isProcessingWithProgress:percentComplete];
};

Giải pháp này tránh chu trình giữ lại luôn gọi đại biểu hiện tại.

Nếu bạn không thể thay đổi khối, bạn có thể đối phó với nó . Lý do chu kỳ giữ lại là một cảnh báo, không phải lỗi, là vì chúng không nhất thiết phải đánh vần Doom cho ứng dụng của bạn. Nếu MyDataProcessor có thể giải phóng các khối khi hoạt động hoàn tất, trước khi cha mẹ của nó cố gắng giải phóng nó, chu trình sẽ bị phá vỡ và mọi thứ sẽ được dọn sạch đúng cách. Nếu bạn có thể chắc chắn về điều này, thì điều nên làm là sử dụng #pragma để chặn các cảnh báo cho khối mã đó. (Hoặc sử dụng cờ trình biên dịch cho mỗi tệp. Nhưng không tắt cảnh báo cho toàn bộ dự án.)

Bạn cũng có thể xem xét bằng cách sử dụng một thủ thuật tương tự ở trên, khai báo một tham chiếu yếu hoặc không được lưu ý và sử dụng nó trong khối. Ví dụ:

__weak MyDataProcessor *dp = self; // OK for iOS 5 only
__unsafe_unretained MyDataProcessor *dp = self; // OK for iOS 4.x and up
__block MyDataProcessor *dp = self; // OK if you aren't using ARC
self.progressBlock = ^(CGFloat percentComplete) {
    [dp.delegate myAPI:dp isProcessingWithProgress:percentComplete];
}

Tất cả ba điều trên sẽ cung cấp cho bạn một tham chiếu mà không giữ lại kết quả, mặc dù tất cả chúng đều hoạt động hơi khác một chút: __weak sẽ cố gắng không tham chiếu khi đối tượng được phát hành; __unsafe_unretained sẽ để lại cho bạn một con trỏ không hợp lệ; __block thực sự sẽ thêm một mức độ gián tiếp khác và cho phép bạn thay đổi giá trị của tham chiếu từ trong khối (không liên quan trong trường hợp này, vì dp không được sử dụng ở bất kỳ nơi nào khác).

Cái gì tốt nhất sẽ phụ thuộc vào mã bạn có thể thay đổi và những gì bạn không thể. Nhưng hy vọng điều này đã cho bạn một số ý tưởng về cách tiến hành.

509
benzado

Ngoài ra, còn có tùy chọn triệt tiêu cảnh báo khi bạn tích cực rằng chu kỳ sẽ bị phá vỡ trong tương lai:

#pragma clang diagnostic Push
#pragma clang diagnostic ignored "-Warc-retain-cycles"

self.progressBlock = ^(CGFloat percentComplete) {
    [self.delegate processingWithProgress:percentComplete];
}

#pragma clang diagnostic pop

Bằng cách đó, bạn không cần phải xoay quanh với __weak, self bí danh và tiền tố ivar rõ ràng.

25
zoul

Đối với một giải pháp chung, tôi có các định nghĩa này trong tiêu đề tiền biên dịch. Tránh chụp và vẫn cho phép trình biên dịch trợ giúp bằng cách tránh sử dụng id

#define BlockWeakObject(o) __typeof(o) __weak
#define BlockWeakSelf BlockWeakObject(self)

Sau đó, trong mã bạn có thể làm:

BlockWeakSelf weakSelf = self;
self.dataProcessor.completion = ^{
    [weakSelf.delegate myAPIDidFinish:weakSelf];
    weakSelf.dataProcessor = nil;
};
14
Damien Pontifex

Tôi tin rằng giải pháp không có ARC cũng hoạt động với ARC, sử dụng từ khóa __block:

EDIT: Per the Chuyển sang Ghi chú phát hành ARC , một đối tượng được khai báo với bộ lưu trữ __block vẫn được giữ lại. Sử dụng __weak (ưu tiên) hoặc __unsafe_unretained (để tương thích ngược).

// code sample
self.delegate = aDelegate;

self.dataProcessor = [[MyDataProcessor alloc] init];

// Use this inside blocks
__block id myself = self;

self.dataProcessor.progress = ^(CGFloat percentComplete) {
    [myself.delegate myAPI:myself isProcessingWithProgress:percentComplete];
};

self.dataProcessor.completion = ^{
    [myself.delegate myAPIDidFinish:myself];
    myself.dataProcessor = nil;
};

// start the processor - processing happens asynchronously and the processor is released in the completion block
[self.dataProcessor startProcessing];
11
Tony

Kết hợp một vài câu trả lời khác, đây là những gì tôi sử dụng bây giờ cho một bản thân yếu được gõ để sử dụng trong các khối:

__typeof(self) __weak welf = self;

Tôi đặt nó là Đoạn mã XCode với tiền tố hoàn thành "welf" trong các phương thức/hàm, hàm này sẽ truy cập sau khi chỉ gõ "chúng tôi".

11

cảnh báo => "tự chụp trong khối có khả năng dẫn đến chu kỳ giữ lại"

khi bạn giới thiệu bản thân hoặc tài sản của nó trong một khối được tự giữ lại mạnh mẽ hơn nó cho thấy cảnh báo ở trên.

vì vậy, để tránh nó, chúng ta phải làm cho nó một tuần

__weak typeof(self) weakSelf = self;

vì vậy thay vì sử dụng

blockname=^{
    self.PROPERTY =something;
}

chúng ta nên sử dụng

blockname=^{
    weakSelf.PROPERTY =something;
}

lưu ý: chu kỳ giữ lại thường xảy ra khi một số cách hai đối tượng tham chiếu với nhau mà cả hai đều có số tham chiếu = 1 và phương thức delloc của chúng không bao giờ được gọi.

6
Anurag Bhakuni

Cách mới để làm điều này là sử dụng @weakify và @strongify marco

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

Thông tin thêm về @Weakify @Strongify Marco

1
Jun Jie Gan