Câu hỏi performSelector có thể gây rò rỉ vì bộ chọn của nó không xác định


Tôi nhận được cảnh báo sau bởi trình biên dịch ARC:

"performSelector may cause a leak because its selector is unknown".

Đây là những gì tôi đang làm:

[_controller performSelector:NSSelectorFromString(@"someMethod")];

Tại sao tôi nhận được cảnh báo này? Tôi hiểu trình biên dịch không thể kiểm tra nếu bộ chọn tồn tại hay không, nhưng tại sao điều đó lại gây ra rò rỉ? Và làm cách nào tôi có thể thay đổi mã của mình để tôi không nhận được cảnh báo này nữa?


1190
2017-08-10 20:23


gốc


Tên của biến là động, nó phụ thuộc vào rất nhiều thứ khác. Có nguy cơ tôi gọi cái gì đó không tồn tại, nhưng đó không phải là vấn đề. - Eduardo Scoz
@matt tại sao sẽ gọi một phương pháp động trên một đối tượng là thực hành xấu? Không phải là toàn bộ mục đích của NSSelectorFromString () để hỗ trợ thực hành này? - Eduardo Scoz
Bạn nên / cũng có thể kiểm tra [_controller respondsToSelector: mySelector] trước khi thiết lập nó thông qua performSelector: - mattacular
@mattacular Ước gì tôi có thể bỏ phiếu: "Đó ... là thực hành tồi." - ctpenrose
Nếu bạn biết chuỗi là một chữ, chỉ cần sử dụng @selector () để trình biên dịch có thể cho biết tên bộ chọn là gì. Nếu mã thực của bạn đang gọi NSSelectorFromString () với một chuỗi được tạo hoặc được cung cấp khi chạy, thì bạn phải sử dụng NSSelectorFromString (). - Chris Page


Các câu trả lời:


Dung dịch

Trình biên dịch cảnh báo về điều này vì một lý do. Rất hiếm khi cảnh báo này chỉ đơn giản là bị bỏ qua và dễ dàng làm việc xung quanh. Dưới đây là cách thực hiện:

if (!_controller) { return; }
SEL selector = NSSelectorFromString(@"someMethod");
IMP imp = [_controller methodForSelector:selector];
void (*func)(id, SEL) = (void *)imp;
func(_controller, selector);

Hoặc nhiều hơn nữa (mặc dù khó đọc và không có bảo vệ):

SEL selector = NSSelectorFromString(@"someMethod");
((void (*)(id, SEL))[_controller methodForSelector:selector])(_controller, selector);

Giải trình

Điều gì đang xảy ra ở đây là bạn đang yêu cầu bộ điều khiển cho con trỏ hàm C cho phương thức tương ứng với bộ điều khiển. Tất cả các NSObjects trả lời methodForSelector:, nhưng bạn cũng có thể sử dụng class_getMethodImplementation trong thời gian chạy Objective-C (hữu ích nếu bạn chỉ có một tham chiếu giao thức, như id<SomeProto>). Những con trỏ hàm này được gọi IMPvà đơn giản typedefed con trỏ chức năng (id (*IMP)(id, SEL, ...))1. Điều này có thể gần với chữ ký phương thức thực tế của phương thức, nhưng sẽ không luôn khớp chính xác.

Một khi bạn có IMP, bạn cần truyền nó tới một con trỏ hàm bao gồm tất cả các chi tiết mà ARC cần (bao gồm cả hai đối số ẩn ẩn) self và _cmd của mọi cuộc gọi phương thức-Objective-C). Điều này được xử lý trong dòng thứ ba ( (void *) ở phía bên tay phải chỉ cần nói cho trình biên dịch biết bạn đang làm gì và không tạo cảnh báo vì các loại con trỏ không khớp).

Cuối cùng, bạn gọi con trỏ hàm2.

Ví dụ phức tạp

Khi bộ chọn lấy đối số hoặc trả về một giá trị, bạn sẽ phải thay đổi mọi thứ một chút:

SEL selector = NSSelectorFromString(@"processRegion:ofView:");
IMP imp = [_controller methodForSelector:selector];
CGRect (*func)(id, SEL, CGRect, UIView *) = (void *)imp;
CGRect result = _controller ?
  func(_controller, selector, someRect, someView) : CGRectZero;

Lý do cảnh báo

Lý do cho cảnh báo này là với ARC, thời gian chạy cần phải biết phải làm gì với kết quả của phương thức mà bạn đang gọi. Kết quả có thể là bất cứ điều gì: void, int, char, NSString *, idARC thường nhận được thông tin này từ tiêu đề của loại đối tượng mà bạn đang làm việc.3

Thực sự chỉ có 4 điều mà ARC sẽ xem xét cho giá trị trả về:4

  1. Bỏ qua các loại không phải đối tượng (void, int, v.v.)
  2. Giữ lại giá trị đối tượng, sau đó giải phóng khi nó không còn được sử dụng nữa (giả định tiêu chuẩn)
  3. Phát hành các giá trị đối tượng mới khi không còn sử dụng nữa (các phương thức trong init/ copy gia đình hoặc do ns_returns_retained)
  4. Không làm gì và giả định giá trị đối tượng được trả về sẽ hợp lệ trong phạm vi cục bộ (cho đến khi hầu hết các bản phát hành bên trong được thoát ra, được gán cho ns_returns_autoreleased)

Cuộc gọi đến methodForSelector: giả định rằng giá trị trả về của phương thức mà nó gọi là một đối tượng, nhưng không giữ lại / giải phóng nó. Vì vậy, bạn có thể kết thúc tạo ra một rò rỉ nếu đối tượng của bạn được cho là sẽ được phát hành như trong # 3 ở trên (có nghĩa là, phương pháp bạn đang gọi trả về một đối tượng mới).

Đối với các công cụ chọn bạn đang cố gắng gọi trở lại đó void hoặc các đối tượng không phải là đối tượng khác, bạn có thể bật các tính năng của trình biên dịch để bỏ qua cảnh báo, nhưng nó có thể nguy hiểm. Tôi đã nhìn thấy Clang đi qua một vài lần lặp về cách nó xử lý các giá trị trả về mà không được gán cho các biến cục bộ. Không có lý do gì với ARC được kích hoạt mà nó không thể giữ lại và giải phóng giá trị đối tượng được trả về từ methodForSelector:mặc dù bạn không muốn sử dụng nó. Từ quan điểm của trình biên dịch, nó là một đối tượng sau khi tất cả. Điều đó có nghĩa là nếu phương pháp bạn gọi, someMethod, trả về một đối tượng không (bao gồm void), bạn có thể kết thúc với một giá trị con trỏ rác được giữ lại / phát hành và sụp đổ.

Đối số bổ sung

Một xem xét là đây là cùng một cảnh báo sẽ xảy ra với performSelector:withObject: và bạn có thể gặp phải các vấn đề tương tự khi không khai báo phương thức đó tiêu thụ thông số như thế nào. ARC cho phép khai báo thông số tiêu thụvà nếu phương pháp này tiêu thụ thông số, có thể bạn sẽ gửi tin nhắn tới một zombie và sự cố. Có nhiều cách để giải quyết vấn đề này với tính năng đúc cầu nối, nhưng thực sự tốt hơn là chỉ cần sử dụng IMP và phương thức con trỏ hàm ở trên. Vì các thông số tiêu thụ hiếm khi là một vấn đề, điều này không có khả năng xuất hiện.

Bộ chọn tĩnh

Thật thú vị, trình biên dịch sẽ không phàn nàn về các bộ chọn được khai báo tĩnh:

[_controller performSelector:@selector(someMethod)];

Lý do cho điều này là bởi vì trình biên dịch thực sự có thể ghi lại tất cả các thông tin về bộ chọn và đối tượng trong quá trình biên dịch. Nó không cần phải đưa ra bất kỳ giả định nào về bất cứ điều gì. (Tôi đã kiểm tra điều này một năm trước đây bằng cách nhìn vào nguồn, nhưng không có một tham chiếu ngay bây giờ.)

Loại bỏ

Trong cố gắng để suy nghĩ về một tình huống mà sự đàn áp của cảnh báo này sẽ là cần thiết và thiết kế mã tốt, tôi đang đi lên trống. Ai đó xin vui lòng chia sẻ nếu họ đã có một kinh nghiệm mà im lặng cảnh báo này là cần thiết (và ở trên không xử lý những điều đúng).

Hơn

Có thể xây dựng một NSMethodInvocation để xử lý điều này, nhưng làm như vậy đòi hỏi phải gõ nhiều hơn và cũng chậm hơn, vì vậy có rất ít lý do để làm điều đó.

Lịch sử

Khi mà performSelector: gia đình của phương pháp đầu tiên được thêm vào Objective-C, ARC không tồn tại. Trong khi tạo ARC, Apple đã quyết định rằng một cảnh báo sẽ được tạo cho các phương thức này như một cách để hướng dẫn các nhà phát triển sử dụng các phương tiện khác để xác định rõ ràng cách xử lý bộ nhớ khi gửi các thông báo tùy ý qua bộ chọn có tên. Trong Objective-C, các nhà phát triển có thể thực hiện điều này bằng cách sử dụng các khuôn mẫu kiểu C trên các con trỏ hàm nguyên.

Với sự ra đời của Swift, Apple đã ghi lại các performSelector: gia đình của các phương pháp như "vốn không an toàn" và họ không có sẵn cho Swift.

Theo thời gian, chúng ta đã thấy sự tiến triển này:

  1. Phiên bản đầu tiên của Objective-C cho phép performSelector: (quản lý bộ nhớ thủ công)
  2. Mục tiêu-C với ARC cảnh báo để sử dụng performSelector:
  3. Swift không có quyền truy cập vào performSelector: và ghi lại các phương pháp này là "vốn không an toàn"

Tuy nhiên, ý tưởng gửi thư dựa trên bộ chọn có tên không phải là tính năng "vốn không an toàn". Ý tưởng này đã được sử dụng thành công trong một thời gian dài trong Objective-C cũng như nhiều ngôn ngữ lập trình khác.


1 Tất cả các phương thức Objective-C có hai đối số ẩn, self và _cmd được thêm vào ngầm khi bạn gọi một phương thức.

2Gọi một NULL chức năng là không an toàn trong C. Các bảo vệ được sử dụng để kiểm tra sự hiện diện của bộ điều khiển đảm bảo rằng chúng tôi có một đối tượng. Do đó chúng tôi biết chúng tôi sẽ nhận được IMP từ methodForSelector: (mặc dù nó có thể là _objc_msgForward, nhập vào hệ thống chuyển tiếp thư). Về cơ bản, với bảo vệ tại chỗ, chúng tôi biết chúng tôi có một chức năng để gọi.

3 Trên thực tế, nó có thể cho nó để có được những thông tin sai nếu tuyên bố bạn đối tượng như id và bạn không nhập tất cả các tiêu đề. Bạn có thể kết thúc với các lỗi trong mã mà trình biên dịch nghĩ là tốt. Điều này rất hiếm, nhưng có thể xảy ra. Thông thường bạn sẽ chỉ nhận được một cảnh báo rằng nó không biết chữ ký của hai phương thức để chọn.

4 Xem tham chiếu ARC trên giá trị trả lại được giữ lại và giá trị trả về không trả về để biết thêm chi tiết.


1142
2017-11-18 21:44



@ wbyoung Nếu mã của bạn giải quyết được vấn đề giữ lại, tôi tự hỏi tại sao performSelector: các phương pháp không được triển khai theo cách này. Họ có chữ ký nghiêm ngặt về phương thức (trả lại id, lấy một hoặc hai ids), vì vậy không cần xử lý các loại nguyên thủy. - Tricertops
@Andy đối số được xử lý dựa trên định nghĩa của nguyên mẫu của phương thức (nó sẽ không được giữ lại / giải phóng). Mối quan tâm chủ yếu dựa trên loại trả về. - wbyoung
"Ví dụ phức tạp" đưa ra lỗi Cannot initialize a variable of type 'CGRect (*)(__strong id, SEL, CGRect, UIView *__strong)' with an rvalue of type 'void *' khi sử dụng Xcode mới nhất. (5.1.1) Tuy nhiên, tôi đã học được rất nhiều! - Stan James
void (*func)(id, SEL) = (void *)imp; không biên dịch, tôi đã thay thế bằng void (*func)(id, SEL) = (void (*)(id, SEL))imp; - Davyd
thay đổi void (*func)(id, SEL) = (void *)imp; đến <…> = (void (*))imp; hoặc là <…> = (void (*) (id, SEL))imp; - Isaak Osipovich Dunayevsky


Trong trình biên dịch LLVM 3.0 trong Xcode 4.2, bạn có thể chặn cảnh báo như sau:

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
    [self.ticketTarget performSelector: self.ticketAction withObject: self];
#pragma clang diagnostic pop

Nếu bạn gặp lỗi ở một vài nơi và muốn sử dụng hệ thống macro C để ẩn các pragmas, bạn có thể xác định macro để làm cho nó dễ dàng hơn để chặn cảnh báo:

#define SuppressPerformSelectorLeakWarning(Stuff) \
    do { \
        _Pragma("clang diagnostic push") \
        _Pragma("clang diagnostic ignored \"-Warc-performSelector-leaks\"") \
        Stuff; \
        _Pragma("clang diagnostic pop") \
    } while (0)

Bạn có thể sử dụng macro như thế này:

SuppressPerformSelectorLeakWarning(
    [_target performSelector:_action withObject:self]
);

Nếu bạn cần kết quả của thông báo được thực hiện, bạn có thể làm điều này:

id result;
SuppressPerformSelectorLeakWarning(
    result = [_target performSelector:_action withObject:self]
);

1171
2017-10-28 19:30



Phương pháp này có thể gây rò rỉ bộ nhớ khi tối ưu hóa được đặt thành bất kỳ thứ gì khác ngoài Không. - Eric
@Eric Không thể, trừ khi bạn đang gọi các phương thức hài hước như "initSomething" hoặc "newSomething" hoặc "somethingCopy". - Andrey Tarantsov
@Julian Điều đó làm việc, nhưng điều đó sẽ tắt cảnh báo cho toàn bộ tập tin - bạn có thể không cần hoặc muốn điều đó. Wrappping nó với pop và push-pragmas sạch hơn và an toàn hơn nhiều. - Emil
Tất cả điều này làm là nó im lặng lên trình biên dịch. Điều này không giải quyết được vấn đề. Nếu bộ chọn không tồn tại bạn đang khá nhiều hơi say. - Andra Todorescu
Điều này chỉ nên được sử dụng khi được bao bọc bởi if ([_target respondsToSelector:_selector]) { hoặc logic tương tự. - Barry


Tôi đoán về điều này là: vì bộ chọn không được biết đến với trình biên dịch, ARC không thể thực thi quản lý bộ nhớ thích hợp.

Trong thực tế, có những lúc quản lý bộ nhớ được gắn với tên của phương thức theo một quy ước cụ thể. Cụ thể, tôi đang nghĩ về nhà thầu tiện lợi đấu với chế tạo phương pháp; sự trở lại cũ bằng cách quy ước một đối tượng tự động; sau đó một đối tượng được giữ lại. Quy ước được dựa trên tên của bộ chọn, vì vậy nếu trình biên dịch không biết bộ chọn, thì nó không thể thực thi quy tắc quản lý bộ nhớ thích hợp.

Nếu điều này đúng, tôi nghĩ rằng bạn có thể sử dụng mã của mình một cách an toàn, miễn là bạn đảm bảo mọi thứ đều ổn như quản lý bộ nhớ (ví dụ: các phương thức của bạn không trả về các đối tượng mà chúng phân bổ).


206
2017-08-10 20:43



Cảm ơn câu trả lời, tôi sẽ xem xét thêm về điều này để xem những gì đang xảy ra. Bất kỳ ý tưởng về cách tôi có thể bỏ qua các cảnh báo mặc dù và làm cho nó biến mất? Tôi ghét phải có cảnh báo ngồi trong mã của tôi mãi mãi cho một cuộc gọi an toàn là gì. - Eduardo Scoz
Vì vậy, tôi đã nhận được xác nhận từ một người nào đó tại Apple trong diễn đàn của họ rằng đây thực sự là trường hợp. Họ sẽ thêm ghi đè bị lãng quên để cho phép mọi người tắt cảnh báo này trong các bản phát hành trong tương lai. Cảm ơn. - Eduardo Scoz
Câu trả lời này đặt ra một số câu hỏi, giống như nếu ARC cố gắng đưa ra quyết định khi nào phát hành một cái gì đó dựa trên quy ước và tên phương thức, thì làm thế nào để "đếm tham chiếu"? Hành vi bạn mô tả âm thanh chỉ nhỉnh hơn một cách hoàn toàn tùy ý, nếu ARC giả định mã tuân theo một quy ước nhất định trái ngược với thực sự theo dõi các tham chiếu bất kể quy ước nào được tuân theo. - aroth
ARC tự động hóa quá trình thêm giữ lại và phát hành tại biên dịch. Nó không phải là bộ sưu tập rác (đó là lý do tại sao nó cực kỳ nhanh và cực thấp). Nó không phải là tùy ý chút nào. Các quy tắc mặc định được dựa trên các quy ước ObjC được thiết lập tốt đã được áp dụng nhất quán trong nhiều thập kỷ. Điều này tránh sự cần thiết phải thêm một cách rõ ràng __attributecho mọi phương thức giải thích việc quản lý bộ nhớ của nó. Nhưng nó cũng khiến người khiếu nại không thể xử lý đúng mẫu này (một mẫu được sử dụng rất phổ biến, nhưng đã được thay thế bằng các mô hình mạnh mẽ hơn trong những năm gần đây). - Rob Napier
Vì vậy, chúng tôi không còn có thể có một loại ngà SEL và chỉ định các bộ chọn khác nhau tùy thuộc vào tình huống? Con đường để đi, ngôn ngữ năng động ... - Nicolas Miari


Trong dự án của bạn Cài đặt bản dựng, Dưới Cờ cảnh báo khác (WARNING_CFLAGS), thêm vào
-Wno-arc-performSelector-leaks

Bây giờ chỉ cần đảm bảo rằng bộ chọn bạn đang gọi không làm cho đối tượng của bạn được giữ lại hoặc sao chép.


119
2017-10-31 13:57



Lưu ý rằng bạn có thể thêm cùng một cờ cho các tệp cụ thể thay vì toàn bộ dự án. Nếu bạn nhìn vào Build Compases-> Compile Sources, bạn có thể đặt cho mỗi tệp Compiler Flags (giống như bạn muốn làm để loại trừ các tệp khỏi ARC). Trong dự án của tôi chỉ cần một tập tin nên sử dụng selectors theo cách này, vì vậy tôi chỉ loại trừ nó và để lại những người khác. - Michael


Như một giải pháp cho đến khi trình biên dịch cho phép ghi đè cảnh báo, bạn có thể sử dụng thời gian chạy

objc_msgSend(_controller, NSSelectorFromString(@"someMethod"));

thay vì

[_controller performSelector:NSSelectorFromString(@"someMethod")];

Bạn sẽ phải

#import <objc/message.h>


110
2017-08-16 04:56



ARC công nhận các quy ước của Cocoa và sau đó bổ sung giữ lại và phát hành dựa trên các quy ước đó. Vì C không tuân theo các quy ước đó, ARC buộc bạn phải sử dụng các kỹ thuật quản lý bộ nhớ thủ công. Nếu bạn tạo một đối tượng CF, bạn phải CFRelease () nó. Nếu bạn dispatch_queue_create (), bạn phải dispatch_release (). Tóm lại, nếu bạn muốn tránh cảnh báo ARC, bạn có thể tránh chúng bằng cách sử dụng các đối tượng C và quản lý bộ nhớ thủ công. Ngoài ra, bạn có thể vô hiệu hóa ARC trên cơ sở từng tệp bằng cách sử dụng cờ trình biên dịch -fno-objc-arc trên tệp đó. - jluckyiv
Không phải không đúc, bạn không thể. Varargs không giống như một danh sách đối số được nhập rõ ràng. Nó thường sẽ hoạt động bằng sự trùng hợp ngẫu nhiên, nhưng tôi không xem xét "trùng hợp ngẫu nhiên" là chính xác. - bbum
Đừng làm thế, [_controller performSelector:NSSelectorFromString(@"someMethod")]; và objc_msgSend(_controller, NSSelectorFromString(@"someMethod")); không tương đương! Hãy xem Phương thức Chữ ký không khớp và Một điểm yếu lớn trong việc đánh máy yếu của Objective-C họ đang giải thích vấn đề theo chiều sâu. - 0xced
@ 0xced Trong trường hợp này, nó ổn. objc_msgGửi sẽ không tạo ra một chữ ký phương thức không khớp cho bất kỳ bộ chọn nào có thể đã hoạt động chính xác trong performSelector: hoặc các biến thể của nó vì chúng chỉ bao giờ lấy các đối tượng làm tham số. Miễn là tất cả các tham số của bạn là con trỏ (bao gồm các đối tượng), đôi và NSInteger / dài, và kiểu trả về của bạn là void, pointer hoặc long, sau đó objc_msgSend sẽ hoạt động chính xác. - Matt Gallagher


Để bỏ qua lỗi chỉ trong tệp có bộ chọn thực hiện, hãy thêm #pragma như sau:

#pragma clang diagnostic ignored "-Warc-performSelector-leaks"

Điều này sẽ bỏ qua cảnh báo trên dòng này, nhưng vẫn cho phép nó trong suốt phần còn lại của dự án của bạn.


87
2018-01-18 21:31



Tôi thu thập rằng bạn cũng có thể bật lại cảnh báo ngay lập tức sau khi phương pháp được đề cập #pragma clang diagnostic warning "-Warc-performSelector-leaks". Tôi biết nếu tôi tắt một cảnh báo, tôi muốn bật nó trở lại vào thời điểm sớm nhất có thể, vì vậy tôi không vô tình để cho một cảnh báo bất ngờ khác. Nó không chắc rằng đây là một vấn đề, nhưng nó chỉ là thực hành của tôi bất cứ khi nào tôi tắt một cảnh báo. - Rob
Bạn cũng có thể khôi phục trạng thái cấu hình trình biên dịch trước đó bằng cách sử dụng #pragma clang diagnostic warning push trước khi bạn thực hiện bất kỳ thay đổi nào và #pragma clang diagnostic warning pop để khôi phục trạng thái trước đó. Hữu ích nếu bạn đang tắt tải và không muốn có nhiều dòng pragma kích hoạt lại trong mã của bạn. - deanWombourne
Nó sẽ chỉ bỏ qua dòng sau? - hfossli


Lạ lùng nhưng đúng: nếu có thể chấp nhận được (tức là kết quả là vô hiệu và bạn không ngại để chu kỳ runloop một lần), thêm một sự chậm trễ, ngay cả khi đây là số không:

[_controller performSelector:NSSelectorFromString(@"someMethod")
    withObject:nil
    afterDelay:0];

Điều này loại bỏ các cảnh báo, có lẽ vì nó yên tâm trình biên dịch mà không có đối tượng có thể được trả lại và bằng cách nào đó mismanaged.


67
2017-11-11 19:19



Bạn có biết nếu điều này thực sự giải quyết các vấn đề quản lý bộ nhớ liên quan, hay nó có cùng vấn đề nhưng Xcode không đủ thông minh để cảnh báo bạn bằng mã này? - Aaron Brager
Đây là ngữ nghĩa không giống nhau! Sử dụng performSelector: withObject: AfterDelay: sẽ thực hiện bộ chọn trong lần chạy tiếp theo của runloop. Do đó, phương thức này trả về ngay lập tức. - Florian
@ Florian Tất nhiên nó không giống nhau! Đọc câu trả lời của tôi: tôi nói nếu chấp nhận được, bởi vì kết quả là vô hiệu và các chu kỳ runloop. Đó là câu đầu tiên câu trả lời của tôi. - matt