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


222

Tôi có một ứng dụng đang hoạt động và tôi đang chuyển đổi nó thành ARC trong Xcode 4.2. Một trong những cảnh báo trước kiểm tra liên quan đến việc bắt giữ selfmạ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?

Câu trả lời:


509

Câu trả lời ngắn

Thay vì truy cập selftrực tiếp, bạn nên truy cập gián tiếp, từ một tài liệu tham khảo 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ể thực hiện việc này:

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

Các __blockbiến dấu từ khóa đó có thể thay đổi bên trong các khối (chúng tôi không làm điều đó) nhưng họ cũng không được tự động 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 các __blockthay đổi và tham chiếu sẽ được giữ lại, trong trường hợp đó bạn nên khai báo __weakthay thế.

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ẽ bằng 0 (vì khối này đ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 selftrực tiếp đến đạ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 được chu trình giữ lại luôn gọi cho đạ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à lỗi, là vì chúng không nhất thiết phải đánh vần số phận cho ứng dụng của bạn. Nếu MyDataProcessorcó 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 đúng đắn cần làm là sử dụng một #pragmađể triệt tiêu 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 trong số 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: __weaksẽ cố gắng không tham chiếu khi đối tượng được phát hành; __unsafe_unretainedsẽ để lại cho bạn một con trỏ không hợp lệ; __blocksẽ thực 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ì dpkhông được sử dụng ở bất kỳ nơi nào khác).

Điều 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.


1
Câu trả lời tuyệt vời! Cảm ơn, tôi hiểu rõ hơn về những gì đang diễn ra và làm thế nào tất cả những thứ này hoạt động. Trong trường hợp này, tôi có quyền kiểm soát mọi thứ vì vậy tôi sẽ tái kiến ​​trúc một số đối tượng khi cần thiết.
XJones

18
O_O Tôi vừa đi qua với một vấn đề hơi khác, bị mắc kẹt khi đọc, và bây giờ rời khỏi trang này cảm thấy tất cả hiểu biết và mát mẻ. Cảm ơn!
Orc JMR

là chính xác, nếu vì một lý do nào đó vào thời điểm thực thi khối dpsẽ được phát hành (ví dụ nếu đó là bộ điều khiển xem và nó được bật lên), thì dòng [dp.delegate ...sẽ gây ra EXC_BADACCESS?
peetonn

Tài sản giữ khối (ví dụ: dataProcess.proceed) nên stronghay weak?
djskinner

1
Bạn có thể xem libextobjc , cung cấp hai macro tiện dụng được gọi @weakify(..)@strongify(...)cho phép bạn sử dụng selftheo khối theo kiểu không giữ lại.

25

Ngoài ra còn có tùy chọn để chặn 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 khỉ xung quanh với __weak, selfrăng cưa và Ivar prefixing rõ ràng.


8
Nghe có vẻ như một thực tiễn rất tệ khi mất hơn 3 dòng mã có thể được thay thế bằng __weak id yếuSelf = self;
Ben Sinclair

3
Thường có một khối mã lớn hơn có thể hưởng lợi từ các cảnh báo bị triệt tiêu.
zoul

2
Ngoại trừ __weak id weakSelf = self;có hành vi cơ bản khác với việc đàn áp cảnh báo. Câu hỏi bắt đầu với "... nếu bạn chắc chắn rằng chu kỳ giữ lại sẽ bị phá vỡ"
Tim

Quá thường xuyên mọi người mù quáng làm cho các biến yếu, mà không thực sự hiểu sự phân nhánh. Ví dụ, tôi đã thấy mọi người làm suy yếu một đối tượng và sau đó, trong khối họ thực hiện: [array addObject:weakObject];Nếu yếu tố này đã được phát hành, điều này gây ra sự cố. Rõ ràng đó không được ưa thích trong một chu kỳ giữ lại. Bạn phải hiểu liệu khối của bạn có thực sự sống đủ lâu để đảm bảo sự suy yếu hay không, và liệu bạn có muốn hành động trong khối đó phụ thuộc vào việc liệu đối tượng yếu có còn hiệu lực hay không.
mahboudz

14

Đố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ụngid

#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;
};

Đồng ý, điều này có thể gây ra một vấn đề bên trong khối. ReactiveCocoa có một giải pháp thú vị khác cho vấn đề này cho phép bạn tiếp tục sử dụng selfbên trong khối @weakify (tự); khối id = ^ {@strongify (tự); [self.delegate myAPIDidFinish: tự]; };
Damien Pontifex

@dmpontifex đó là một macro từ libextobjc github.com/jspahrsummers/libextobjc
Elechtron 3/03/2015

11

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

EDIT: Mỗi lần chuyển đổi sang ghi chú phát hành ARC , một đối tượng được khai báo với __blockbộ lưu trữ 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];

Không nhận ra rằng __blocktừ khóa tránh giữ lại nó là tham chiếu. Cảm ơn! Tôi cập nhật câu trả lời nguyên khối của tôi. :-)
benzado

3
Theo tài liệu của Apple "Trong chế độ đếm tham chiếu thủ công, __block id x; có tác dụng không giữ lại x. Trong chế độ ARC, __block id x; mặc định giữ lại x (giống như tất cả các giá trị khác)."
XJones

11

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 đánh máy để sử dụng trong các khối:

__typeof(self) __weak welf = self;

Tôi đặt đó 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".


Bạn có chắc không? Liên kết này và các tài liệu clang dường như nghĩ cả hai có thể và nên được sử dụng để giữ một tham chiếu đến đối tượng nhưng không phải là một liên kết sẽ gây ra chu kỳ giữ lại: stackoverflow.com/questions/19227982/USE-block-and-weak
Kendall Helmstetter Gelner 6/2/2015

Từ các tài liệu clang : clang.llvm.org/docs/BlockL LanguageSpec.html "Trong các ngôn ngữ Objective-C và Objective-C ++, chúng tôi cho phép trình xác định __weak cho các biến __block của loại đối tượng. những biến này sẽ được giữ mà không giữ lại tin nhắn được gửi đi. "
Kendall Helmstetter Gelner 6/2/2015


6

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.



-1

Nếu bạn chắc chắn rằng mã của bạn sẽ không tạo chu kỳ giữ lại hoặc chu trình đó sẽ bị hỏng sau đó, thì cách đơn giản nhất để tắt tiếng cảnh báo là:

// 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];

Lý do điều này hoạt động là vì trong khi truy cập điểm của các thuộc tính được tính đến bởi phân tích của Xcode, và do đó

x.y.z = ^{ block that retains x}

được xem là giữ lại bởi x của y (ở bên trái của bài tập) và bởi y của x (ở bên phải), các cuộc gọi phương thức không phải chịu cùng một phân tích, ngay cả khi chúng là các cuộc gọi phương thức truy cập thuộc tính tương đương với truy cập điểm, ngay cả khi các phương thức truy cập thuộc tính đó được tạo bởi trình biên dịch, do đó, trong

[x y].z = ^{ block that retains x}

chỉ bên phải được xem là tạo ra một giữ (theo y của x) và không có cảnh báo chu kỳ giữ lại được tạo ra.

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.