PerformanceSelector có thể gây rò rỉ vì không biết bộ chọn của nó


1258

Tôi đang 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 xem bộ chọn có tồn tại hay không, nhưng tại sao điều đó lại gây rò rỉ? Và làm cách nào để thay đổi mã của mình để tôi không nhận được cảnh báo này nữa?


3
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 thứ gì đó không tồn tại, nhưng đó không phải là vấn đề.
Eduardo Scoz

6
@matt tại sao gọi một phương thức động trên một đối tượng là thực tiễn xấu? Không phải toàn bộ mục đích của NSSelectorFromString () để hỗ trợ thực hành này sao?
Eduardo Scoz

7
Bạn nên / cũng có thể kiểm tra [_controll responssToSelector: mySelector] trước khi cài đặt nó qua
biểu diễnSelector

50
@mattacular Mong muốn tôi có thể bỏ phiếu: "Đó ... là một thực tiễn tồi."
ctpenrose

6
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 của bộ chọn là gì. Nếu mã thực tế của bạn đang gọi NSSelectorFromString () bằng một chuỗi được xây dựng hoặc cung cấp trong thời gian chạy, thì bạn phải sử dụng NSSelectorFromString ().
Chris Trang

Câu trả lời:


1211

Giải pháp

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 đơn giản nên được bỏ qua và thật dễ dàng để xử lý. Đâ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 chặt chẽ hơn (mặc dù khó đọc và không có người bảo vệ):

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

Giải trình

Điều đang diễn ra ở đây là bạn đang hỏi 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ả đều NSObjectphản hồi methodForSelector:, nhưng bạn cũng có thể sử dụng class_getMethodImplementationtrong 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>). Các con trỏ hàm này được gọi là IMPs và là typedefcác con trỏ hàm ed đơn giản ( 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 luôn khớp chính xác.

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_cmdcủa mỗi lệnh 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 cho trình biên dịch biết bạn đang làm gì và không tạo cảnh báo do các loại con trỏ không khớp).

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

Ví dụ phức tạp

Khi bộ chọn nhận đối số hoặc trả về 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, bộ thực thi cần biết phải làm gì với kết quả của phương thức bạn đang gọi. Kết quả có thể là bất cứ điều gì: void, int, char, NSString *, id,, vv ARC thường lấy thông tin này từ tiêu đề của các loại đối tượng bạn đang sử dụng. 3

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

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

Cuộc gọi 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, cuối cùng bạn có thể 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 mục số 3 ở trên (nghĩa là phương thức bạn đang gọi trả về một đối tượng mới).

Đối với các bộ chọn bạn đang cố gắng gọi trả về voidhoặc các đối tượng khác, bạn có thể kích hoạ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 đã thấy Clang trải qua một vài lần lặp lại về cách nó xử lý các giá trị trả về không được gán cho các biến cục bộ. Không có lý do gì khi 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ề methodForSelector:ngay cả khi bạn không muốn sử dụng nó. Từ quan điểm của nhà soạn nhạc, nó là một đối tượng. Điều đó có nghĩa là nếu phương thức bạn đang gọi,someMethod đang trả về một đối tượng không (bao gồm void), bạn có thể kết thúc bằng một giá trị con trỏ rác được giữ lại / giải phóng và sụp đổ.

Đối số bổ sung

Một xem xét là đây là cảnh báo tương tự sẽ xảy ra performSelector:withObject:và bạn có thể gặp phải các vấn đề tương tự mà không khai báo phương thức đó tiêu thụ tham số như thế nào. ARC cho phép khai báo các tham số đã tiêu thụ và nếu phương thức tiêu thụ tham số đó, cuối cùng bạn có thể sẽ gửi tin nhắn đến zombie và gặp sự cố. Có nhiều cách để giải quyết vấn đề này với việc đúc cầu, nhưng thực sự sẽ tốt hơn nếu chỉ sử dụng IMPphương pháp con trỏ hàm và hàm ở trên. Vì các tham 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 về bất cứ điều gì. (Tôi đã kiểm tra điều này một năm trước bằng cách xem nguồn, nhưng không có tài liệu tham khảo ngay bây giờ.)

Ức chế

Khi cố gắng nghĩ về một tình huống mà việc ngăn chặn cảnh báo này là cần thiết và thiết kế mã tốt, tôi sẽ trở nên trống rỗng. Ai đó hãy chia sẻ nếu họ đã có kinh nghiệm trong đó việc tắt tiếng cảnh báo này là cần thiết (và những điều trên không xử lý mọi việc đúng cách).

Hơn

Có thể xây dựng một NSMethodInvocation để xử lý việc 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: họ phương thức lần đầ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 nên tạo cảnh bá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õ cách xử lý bộ nhớ khi gửi tin nhắn 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 phôi kiểu C trên các con trỏ hàm thô.

Với sự ra đời của Swift, Apple đã ghi nhận performSelector:họ các phương pháp là "vốn không an toàn" và chúng 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 của Objective-C cho phép performSelector: (quản lý bộ nhớ thủ công)
  2. Objective-C với ARC cảnh báo việc sử dụng performSelector:
  3. Swift không có quyền truy cập performSelector:và ghi lại các phương thức này là "vốn không an toàn"

Tuy nhiên, ý tưởng gửi tin nhắn dựa trên bộ chọn có tên không phải là một 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_cmdđược thêm vào khi bạn gọi một phương thức.

2 Gọi một NULLchức năng là không an toàn trong C. Bộ phận 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 ta có một đối tượng. Do đó, chúng tôi biết rằng chúng tôi sẽ nhận được IMPtừ methodForSelector:(mặc dù có thể _objc_msgForward, nhập vào hệ thống chuyển tiếp tin nhắn). Về cơ bản, với ngườ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ể nhận được thông tin sai nếu khai báo cho bạn các đối tượng idvà bạn không nhập tất cả các tiêu đề. Bạn có thể kết thúc với sự cố trong mã mà trình biên dịch cho là ổn. Đ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 nên chọn chữ ký nào trong hai chữ ký phương thức.

4 Xem tài liệu tham khảo ARC về giá trị lợi nhuận giữ lạigiá trị trả về unretained để biết thêm chi tiết.


@wbyoung Nếu mã của bạn giải quyết vấn đề giữ lại, tôi tự hỏi tại sao performSelector:các phương thức không được thực hiện theo cách này. Chúng có chữ ký phương thức nghiêm ngặt (trả về id, lấy một hoặc hai idgiây), do đó không cần xử lý các kiểu nguyên thủy.
Tricertops

1
@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 / phát hành). Mối quan tâm chủ yếu dựa trên loại trả lại.
wbyoung

2
"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

2
void (*func)(id, SEL) = (void *)imp;không biên dịch, tôi đã thay thế nó bằngvoid (*func)(id, SEL) = (void (*)(id, SEL))imp;
Davyd Geyl

1
đổi void (*func)(id, SEL) = (void *)imp;thành <…> = (void (*))imp;hoặc<…> = (void (*) (id, SEL))imp;
Isaak Osipovich Dunayevsky

1182

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 đang gặp lỗi ở một số nơi và muốn sử dụng hệ thống macro C để ẩn các pragma, bạn có thể xác định một macro để dễ dàng loại bỏ cảnh báo hơn:

#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]
);

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 có.
Eric

4
@Eric Không, không thể, trừ khi bạn gọi các phương thức vui nhộn như "initS Something" hoặc "newS Something" hoặc "SomethingCopy".
Andrey Tarantsov

3
@Julian Điều đó không hoạt động, nhưng điều đó sẽ tắt cảnh báo cho toàn bộ tệp - bạn có thể không cần hoặc không muốn điều đó. Bao bọc nó với poppush-pragmas sạch hơn và an toàn hơn nhiều.
Emil

2
Tất cả điều này là nó làm im lặng 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 sẽ gặp khá nhiều khó khăn.
Andra Todorescu

2
Điều này chỉ nên được sử dụng khi được bao bọc bởi một if ([_target respondsToSelector:_selector]) {logic hoặc tương tự.

208

Tôi đoán về điều này là: vì trình chọn không xác định được trình biên dịch, ARC không thể thực thi quản lý bộ nhớ phù 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 suy nghĩ của nhà xây dựng thuận tiện so với làm cho phương pháp; sự trở lại trước đây theo quy ước một đối tượng tự động; cái sau một đối tượng giữ lại. Quy ướ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ớ phù hợp.

Nếu điều này là chính xác, 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 rằng mọi thứ đều ổn khi 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ổ).


5
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ề làm thế nào tôi có thể bỏ qua cảnh báo mặc dù và làm cho nó biến mất? Tôi ghét phải cảnh báo ngồi trong mã của mình mãi mãi cho cuộc gọi an toàn là gì.
Eduardo Scoz

84
Vì vậy, tôi đã nhận được xác nhận từ ai đó 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

5
Câu trả lời này đặt ra một số câu hỏi, như nếu ARC cố gắng đưa ra quyết định khi nào sẽ phát hành một cái gì đó dựa trên tên phương thức và phương thức, thì làm thế nào để "đếm tham chiếu"? Hành vi bạn mô tả âm thanh chỉ tốt hơn một chút so với hoàn toàn tùy ý, nếu ARC giả sử mã tuân theo một quy ước nhất định trái ngược với việc thực sự theo dõi các tham chiếu cho dù có tuân theo quy ước nào.
aroth

8
ARC tự động hóa quá trình thêm giữ lại và phát hành khi biên dịch. Nó không phải là bộ sưu tập rác (đó là lý do tại sao nó rất nhanh và chi phí thấp). Nó không độc đoán chút nào. Các quy tắc mặc định dựa trên các công ướ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 __attributevào 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 cho trình biên dịch không thể xử lý đúng mẫu này (một mẫu đã từng rất phổ biến, nhưng đã được thay thế bằng các mẫu mạnh hơn trong những năm gần đây).
Rob Napier

8
Vì vậy, chúng ta không còn có thể có một loại ngà SELvà gán các bộ chọn khác nhau tùy theo tình huống? Con đường để đi, ngôn ngữ năng động ...
Nicolas Miari

121

Trong Cài đặt bản dựng dự án của bạn , bên dưới Cờ cảnh báo khác ( WARNING_CFLAGS), hãy thêm
-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 khiến đối tượng của bạn bị giữ lại hoặc sao chép.


12
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 xem bên dưới Giai đoạn xây dựng-> Biên dịch nguồn, bạn có thể đặt cho mỗi tệp Trình biên dịch cờ (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 nên sử dụng các bộ chọn theo cách này, vì vậy tôi chỉ loại trừ nó và để lại các tệp khác.
Michael

111

Như một cách giải quyết 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>


8
ARC công nhận các quy ước về ca cao và sau đó thêm các bản 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 phái_queue_create (), bạn phải gửi_release (). Tóm lại, nếu bạn muốn tránh các 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ở mỗi tệp bằng cách sử dụng cờ trình biên dịch -fno-objc-arc trên tệp đó.
jluckyiv

8
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 gõ rõ ràng. Nó thường hoạt động theo sự trùng hợp ngẫu nhiên, nhưng tôi không coi "bởi sự trùng hợp" là chính xác.
bbum

21
Đừng làm vậy, [_controller performSelector:NSSelectorFromString(@"someMethod")];objc_msgSend(_controller, NSSelectorFromString(@"someMethod"));không tương đương! Có một cái nhìn về sự không phù hợp của Chữ ký Phương thứcMột điểm yếu lớn trong cách gõ yếu của Objective-C, họ đang giải thích vấn đề một cách sâu sắc.
0xced

5
@ 0xced Trong trường hợp này, nó ổn. objc_msgSend sẽ không tạo ra sự không khớp chữ ký phương thức cho bất kỳ bộ chọn nào hoạt động chính xác trong PerformanceSelector: hoặc các biến thể của nó vì chúng chỉ 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), nhân đôi và NSInteger / long, và kiểu trả về của bạn là void, con trỏ hoặc dài, thì objc_msgSend sẽ hoạt động chính xác.
Matt Gallagher

88

Để 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.


6
Tôi tập hợp rằng bạn cũng có thể bật lại cảnh báo ngay sau khi phương thức được đề cập đến #pragma clang diagnostic warning "-Warc-performSelector-leaks". Tôi biết nếu tôi tắt cảnh báo, tôi muốn bật lại vào thời điểm sớm nhất có thể, vì vậy tôi không vô tình để một cảnh báo khác không lường trước được. Không chắc đây là một vấn đề, nhưng đó chỉ là thực tế của tôi mỗi khi tôi tắt cảnh báo.
Cướp

2
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 pushtrước khi 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 mình.
deanWombourne

Nó sẽ chỉ bỏ qua các dòng sau?
hfossli

70

Lạ nhưng đúng: nếu chấp nhận được (nghĩa là kết quả là không có giá trị và bạn không ngại để chu kỳ runloop một lần), hãy thêm độ trễ, ngay cả khi điều này bằng 0:

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

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


2
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 hoặc nó có cùng các vấn đề không nhưng Xcode không đủ thông minh để cảnh báo bạn với mã này?
Aaron Brager

Đây là về mặt ngữ nghĩa không giống nhau! Sử dụng PerformanceSelector: withObject: AfterDelay: sẽ thực hiện bộ chọn trong lần chạy tiếp theo của runloop. Do đó, phương pháp này trả về ngay lập tức.
Florian

10
@Florian Tất nhiên là 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à void và chu kỳ runloop. Đó là câu đầu tiên trong câu trả lời của tôi.
matt

34

Đây là một macro cập nhật dựa trên câu trả lời ở trên. Điều này sẽ cho phép bạn bọc mã của bạn ngay cả với một tuyên bố trở lại.

#define SUPPRESS_PERFORM_SELECTOR_LEAK_WARNING(code)                        \
    _Pragma("clang diagnostic push")                                        \
    _Pragma("clang diagnostic ignored \"-Warc-performSelector-leaks\"")     \
    code;                                                                   \
    _Pragma("clang diagnostic pop")                                         \


SUPPRESS_PERFORM_SELECTOR_LEAK_WARNING(
    return [_target performSelector:_action withObject:self]
);

6
returnkhông cần phải ở trong vĩ mô; return SUPPRESS_PERFORM_SELECTOR_LEAK_WARNING([_target performSelector:_action withObject:self]);cũng hoạt động và trông saner hơn.
UASI

31

Mã này không liên quan đến cờ trình biên dịch hoặc các cuộc gọi thời gian chạy trực tiếp:

SEL selector = @selector(zeroArgumentMethod);
NSMethodSignature *methodSig = [[self class] instanceMethodSignatureForSelector:selector];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
[invocation setSelector:selector];
[invocation setTarget:self];
[invocation invoke];

NSInvocationcho phép nhiều đối số được đặt để không giống như performSelectorđiều này sẽ hoạt động trên bất kỳ phương thức nào.


3
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 hoặc nó có cùng các vấn đề không nhưng Xcode không đủ thông minh để cảnh báo bạn với mã này?
Aaron Brager

1
Bạn có thể nói nó giải quyết các vấn đề quản lý bộ nhớ; nhưng điều này là vì về cơ bản nó cho phép bạn chỉ định hành vi. Ví dụ, bạn có thể chọn để cho phép gọi giữ lại các đối số hay không. Theo hiểu biết hiện tại của tôi, nó cố gắng khắc phục các sự cố không khớp chữ ký có thể xuất hiện bằng cách tin tưởng rằng bạn biết bạn đang làm gì và không cung cấp cho nó dữ liệu không chính xác. Tôi không chắc chắn nếu tất cả các kiểm tra có thể được thực hiện trong thời gian chạy. Như đã đề cập trong một bình luận khác, mikeash.com/pyblog/ Hãy giải thích một cách độc đáo những gì không phù hợp có thể làm.
Mihai Timar

20

Chà, rất nhiều câu trả lời ở đây, nhưng vì điều này hơi khác một chút, kết hợp một vài câu trả lời tôi nghĩ tôi đã đưa nó vào. Tôi đang sử dụng danh mục NSObject để kiểm tra để đảm bảo bộ chọn trả về khoảng trống và cũng loại bỏ trình biên dịch cảnh báo.

#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#import "Debug.h" // not given; just an assert

@interface NSObject (Extras)

// Enforce the rule that the selector used must return void.
- (void) performVoidReturnSelector:(SEL)aSelector withObject:(id)object;
- (void) performVoidReturnSelector:(SEL)aSelector;

@end

@implementation NSObject (Extras)

// Apparently the reason the regular performSelect gives a compile time warning is that the system doesn't know the return type. I'm going to (a) make sure that the return type is void, and (b) disable this warning
// See http://stackoverflow.com/questions/7017281/performselector-may-cause-a-leak-because-its-selector-is-unknown

- (void) checkSelector:(SEL)aSelector {
    // See http://stackoverflow.com/questions/14602854/objective-c-is-there-a-way-to-check-a-selector-return-value
    Method m = class_getInstanceMethod([self class], aSelector);
    char type[128];
    method_getReturnType(m, type, sizeof(type));

    NSString *message = [[NSString alloc] initWithFormat:@"NSObject+Extras.performVoidReturnSelector: %@.%@ selector (type: %s)", [self class], NSStringFromSelector(aSelector), type];
    NSLog(@"%@", message);

    if (type[0] != 'v') {
        message = [[NSString alloc] initWithFormat:@"%@ was not void", message];
        [Debug assertTrue:FALSE withMessage:message];
    }
}

- (void) performVoidReturnSelector:(SEL)aSelector withObject:(id)object {
    [self checkSelector:aSelector];

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
    // Since the selector (aSelector) is returning void, it doesn't make sense to try to obtain the return result of performSelector. In fact, if we do, it crashes the app.
    [self performSelector: aSelector withObject: object];
#pragma clang diagnostic pop    
}

- (void) performVoidReturnSelector:(SEL)aSelector {
    [self checkSelector:aSelector];

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
    [self performSelector: aSelector];
#pragma clang diagnostic pop
}

@end

Có nên thay thế 'v' bằng _C_VOID không? _C_VOID được khai báo trong <objc / runtime.h>.
Rik Renich

16

Vì lợi ích của hậu thế, tôi đã quyết định ném mũ vào vòng :)

Gần đây, tôi đã thấy ngày càng tái cấu trúc khỏi target/ selectormô hình, ủng hộ những thứ như giao thức, khối, v.v. Tuy nhiên, có một thay thế thả xuống performSelectormà tôi đã sử dụng một vài lần bây giờ:

[NSApp sendAction: NSSelectorFromString(@"someMethod") to: _controller from: nil];

Đây dường như là một sự thay thế sạch sẽ, an toàn ARC và gần như giống hệt nhau performSelectormà không cần phải quan tâm nhiều objc_msgSend().

Mặc dù vậy, tôi không biết nếu có một tương tự có sẵn trên iOS.


6
Cảm ơn vì đã bao gồm điều này .. Nó có sẵn trong iOS : [[UIApplication sharedApplication] sendAction: to: from: forEvent:]. Tôi đã xem xét nó một lần, nhưng cảm thấy thật khó xử khi sử dụng một lớp liên quan đến giao diện người dùng ở giữa tên miền hoặc dịch vụ của bạn chỉ để thực hiện một cuộc gọi động .. Cảm ơn vì đã bao gồm cả điều này!
Eduardo Scoz

2
Ew! Nó sẽ có nhiều chi phí hơn (vì nó cần kiểm tra xem phương thức có khả dụng hay không và tiến hành chuỗi phản hồi nếu không) và có hành vi lỗi khác (đi lên chuỗi phản hồi và trả về KHÔNG nếu không tìm thấy gì mà đáp ứng với phương thức, thay vì đơn giản là sụp đổ). Nó cũng không hoạt động khi bạn muốn idtừ-performSelector:...
tc.

2
@tc. Nó không "đi lên chuỗi phản hồi" trừ khi to:không, không. Nó chỉ đi thẳng vào đối tượng được nhắm mục tiêu mà không cần kiểm tra trước. Vì vậy, không có "chi phí cao hơn". Đó không phải là một giải pháp tuyệt vời, nhưng lý do bạn đưa ra không phải là lý do. :)
matt

15

Câu trả lời của Matt Galloway về chủ đề này giải thích lý do:

Hãy xem xét những điều sau đây:

id anotherObject1 = [someObject performSelector:@selector(copy)];
id anotherObject2 = [someObject performSelector:@selector(giveMeAnotherNonRetainedObject)];

Bây giờ, làm thế nào ARC có thể biết rằng cái đầu tiên trả về một đối tượng có số giữ lại là 1 nhưng cái thứ hai trả về một đối tượng được tự động phát hành?

Có vẻ như nói chung là an toàn để chặn cảnh báo nếu bạn bỏ qua giá trị trả về. Tôi không chắc cách thực hành tốt nhất là gì nếu bạn thực sự cần lấy một đối tượng được giữ lại từ PerformanceSelector - ngoài "không làm điều đó".


14

@ c-road cung cấp liên kết đúng với mô tả vấn đề ở đây . Dưới đây bạn có thể xem ví dụ của tôi, khi PerformanceSelector gây rò rỉ bộ nhớ.

@interface Dummy : NSObject <NSCopying>
@end

@implementation Dummy

- (id)copyWithZone:(NSZone *)zone {
  return [[Dummy alloc] init];
}

- (id)clone {
  return [[Dummy alloc] init];
}

@end

void CopyDummy(Dummy *dummy) {
  __unused Dummy *dummyClone = [dummy copy];
}

void CloneDummy(Dummy *dummy) {
  __unused Dummy *dummyClone = [dummy clone];
}

void CopyDummyWithLeak(Dummy *dummy, SEL copySelector) {
  __unused Dummy *dummyClone = [dummy performSelector:copySelector];
}

void CloneDummyWithoutLeak(Dummy *dummy, SEL cloneSelector) {
  __unused Dummy *dummyClone = [dummy performSelector:cloneSelector];
}

int main(int argc, const char * argv[]) {
  @autoreleasepool {
    Dummy *dummy = [[Dummy alloc] init];
    for (;;) { @autoreleasepool {
      //CopyDummy(dummy);
      //CloneDummy(dummy);
      //CloneDummyWithoutLeak(dummy, @selector(clone));
      CopyDummyWithLeak(dummy, @selector(copy));
      [NSThread sleepForTimeInterval:1];
    }} 
  }
  return 0;
}

Phương pháp duy nhất, gây rò rỉ bộ nhớ trong ví dụ của tôi là CopyDummyWithLeak. Lý do là ARC không biết, copySelector trả về đối tượng được giữ lại.

Nếu bạn chạy Công cụ rò rỉ bộ nhớ, bạn có thể thấy hình ảnh sau: nhập mô tả hình ảnh ở đây ... và không có rò rỉ bộ nhớ trong bất kỳ trường hợp nào khác: nhập mô tả hình ảnh ở đây


6

Để làm cho macro của Scott Thompson chung chung hơn:

// String expander
#define MY_STRX(X) #X
#define MY_STR(X) MY_STRX(X)

#define MYSilenceWarning(FLAG, MACRO) \
_Pragma("clang diagnostic push") \
_Pragma(MY_STR(clang diagnostic ignored MY_STR(FLAG))) \
MACRO \
_Pragma("clang diagnostic pop")

Sau đó sử dụng nó như thế này:

MYSilenceWarning(-Warc-performSelector-leaks,
[_target performSelector:_action withObject:self];
                )

FWIW, tôi đã không thêm macro. Ai đó đã thêm vào câu trả lời của tôi. Cá nhân, tôi sẽ không sử dụng macro. Các pragma ở đó để làm việc xung quanh một trường hợp đặc biệt trong mã và các pragma rất rõ ràng và trực tiếp về những gì đang xảy ra. Tôi thích giữ chúng đúng chỗ hơn là ẩn hoặc trừu tượng chúng đằng sau một macro, nhưng đó chỉ là tôi. YMMV.
Scott Thompson

@ScottThndry Điều đó công bằng. Đối với tôi thật dễ dàng để tìm kiếm macro này trên cơ sở mã của tôi và tôi thường cũng thêm một cảnh báo không im lặng để giải quyết vấn đề cơ bản.
Ben Flynn

6

Đừng kìm nén cảnh báo!

Có không dưới 12 giải pháp thay thế để mày mò trình biên dịch.
Mặc dù bạn đang thông minh tại thời điểm thực hiện đầu tiên, một vài kỹ sư trên Trái đất có thể theo bước chân của bạn và mã này cuối cùng sẽ bị phá vỡ.

Tuyến đường an toàn:

Tất cả các giải pháp này sẽ hoạt động, với một số mức độ khác nhau từ mục đích ban đầu của bạn. Giả sử rằng paramcó thể nilnếu bạn rất mong muốn:

Tuyến đường an toàn, hành vi khái niệm tương tự:

// GREAT
[_controller performSelectorOnMainThread:selector withObject:anArgument waitUntilDone:YES];
[_controller performSelectorOnMainThread:selector withObject:anArgument waitUntilDone:YES modes:@[(__bridge NSString *)kCFRunLoopDefaultMode]];

[_controller performSelector:selector onThread:[NSThread mainThread] withObject:anArgument waitUntilDone:YES];
[_controller performSelector:selector onThread:[NSThread mainThread] withObject:anArgument waitUntilDone:YES modes:@[(__bridge NSString *)kCFRunLoopDefaultMode]];

Tuyến đường an toàn, hành vi hơi khác nhau:

(Xem phản hồi này )
Sử dụng bất kỳ chủ đề thay cho [NSThread mainThread].

// GOOD
[_controller performSelector:selector withObject:anArgument afterDelay:0];
[_controller performSelector:selector withObject:anArgument afterDelay:0 inModes:@[(__bridge NSString *)kCFRunLoopDefaultMode]];

[_controller performSelectorOnMainThread:selector withObject:anArgument waitUntilDone:NO];
[_controller performSelectorOnMainThread:selector withObject:anArgument waitUntilDone:NO];
[_controller performSelectorOnMainThread:selector withObject:anArgument waitUntilDone:NO modes:@[(__bridge NSString *)kCFRunLoopDefaultMode]];

[_controller performSelectorInBackground:selector withObject:anArgument];

[_controller performSelector:selector onThread:[NSThread mainThread] withObject:anArgument waitUntilDone:NO];
[_controller performSelector:selector onThread:[NSThread mainThread] withObject:anArgument waitUntilDone:NO modes:@[(__bridge NSString *)kCFRunLoopDefaultMode]];

Tuyến đường nguy hiểm

Yêu cầu một số loại trình biên dịch im lặng, bị ràng buộc để phá vỡ. Lưu ý rằng tại thời điểm hiện tại, nó đã phá vỡ trong Swift .

// AT YOUR OWN RISK
[_controller performSelector:selector];
[_controller performSelector:selector withObject:anArgument];
[_controller performSelector:selector withObject:anArgument withObject:nil];

3
Các từ ngữ là rất sai. Các tuyến đường an toàn không an toàn hơn nguy hiểm. Nó được cho là nguy hiểm hơn bởi vì nó ẩn cảnh báo ngầm.
Bryan Chen

Tôi sẽ sửa từ ngữ để không xúc phạm, nhưng tôi đứng trước lời nói của tôi. Lần duy nhất tôi thấy cảnh báo im lặng có thể chấp nhận được là nếu tôi không sở hữu mã. Không có kỹ sư nào có thể duy trì một cách an toàn mã im lặng mà không hiểu tất cả các hậu quả, điều đó có nghĩa là đọc lập luận này và thực tế này là rủi ro rõ ràng; đặc biệt nếu bạn xem xét 12, tiếng Anh đơn giản, thay thế mạnh mẽ.
SwiftArchitect

1
Không. Bạn không hiểu ý tôi. Sử dụng performSelectorOnMainThreadkhông một cách tốt để bịt miệng những lời cảnh báo và nó có tác dụng phụ. (nó không giải quyết được sự rò rỉ bộ nhớ) Việc bổ sung #clang diagnostic ignored rõ ràng ngăn chặn cảnh báo theo một cách rất rõ ràng.
Bryan Chen

Đúng là việc thực hiện một bộ chọn trên một - (void)phương thức không phải là vấn đề thực sự.
SwiftArchitect

và làm thế nào để bạn gọi một bộ chọn với nhiều đối số thông qua điều này và được an toàn trong cùng một lúc? @SwiftArchitect
Catalin

4

Bởi vì bạn đang sử dụng ARC, bạn phải sử dụng iOS 4.0 trở lên. Điều này có nghĩa là bạn có thể sử dụng các khối. Nếu thay vì ghi nhớ bộ chọn để thực hiện bạn thay vì lấy một khối, ARC sẽ có thể theo dõi tốt hơn những gì đang thực sự xảy ra và bạn sẽ không gặp rủi ro khi vô tình bị rò rỉ bộ nhớ.


Trên thực tế, các khối làm cho rất dễ dàng vô tình tạo ra một chu trình giữ mà ARC không giải quyết được. Tôi vẫn muốn có một cảnh báo trình biên dịch khi bạn hoàn toàn sử dụng selfthông qua một ivar (ví dụ ivarthay vì self->ivar).
tc.

Bạn có nghĩa là như -Wimplicit-ret-self?
OrangeDog 8/07/2015

2

Thay vì sử dụng cách tiếp cận khối, điều này cho tôi một số vấn đề:

    IMP imp = [_controller methodForSelector:selector];
    void (*func)(id, SEL) = (void *)imp;

Tôi sẽ sử dụng NSInvocation, như thế này:

    -(void) sendSelectorToDelegate:(SEL) selector withSender:(UIButton *)button 

    if ([delegate respondsToSelector:selector])
    {
    NSMethodSignature * methodSignature = [[delegate class]
                                    instanceMethodSignatureForSelector:selector];
    NSInvocation * delegateInvocation = [NSInvocation
                                   invocationWithMethodSignature:methodSignature];


    [delegateInvocation setSelector:selector];
    [delegateInvocation setTarget:delegate];

    // remember the first two parameter are cmd and self
    [delegateInvocation setArgument:&button atIndex:2];
    [delegateInvocation invoke];
    }

1

Nếu bạn không cần phải vượt qua bất kỳ đối số, một cách giải quyết dễ dàng là sử dụng valueForKeyPath. Điều này thậm chí có thể trên một Classđối tượng.

NSString *colorName = @"brightPinkColor";
id uicolor = [UIColor class];
if ([uicolor respondsToSelector:NSSelectorFromString(colorName)]){
    UIColor *brightPink = [uicolor valueForKeyPath:colorName];
    ...
}

-2

Bạn cũng có thể sử dụng một giao thức ở đây. Vì vậy, tạo một giao thức như vậy:

@protocol MyProtocol
-(void)doSomethingWithObject:(id)object;
@end

Trong lớp của bạn cần gọi bộ chọn của bạn, sau đó bạn có @property.

@interface MyObject
    @property (strong) id<MyProtocol> source;
@end

Khi bạn cần gọi @selector(doSomethingWithObject:)một thể hiện của MyObject, hãy làm điều này:

[self.source doSomethingWithObject:object];

2
Xin chào Wu, cảm ơn, nhưng quan điểm của việc sử dụng NSSelectorFromString là khi bạn không biết bạn muốn gọi bộ chọn nào trong thời gian chạy.
Eduardo Scoz
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.