performSelector dapat menyebabkan kebocoran karena pemilihnya tidak diketahui

1258

Saya mendapatkan peringatan berikut oleh kompiler ARC:

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

Inilah yang saya lakukan:

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

Mengapa saya mendapat peringatan ini? Saya mengerti kompiler tidak dapat memeriksa apakah pemilih ada atau tidak, tetapi mengapa hal itu menyebabkan kebocoran? Dan bagaimana saya bisa mengubah kode saya sehingga saya tidak mendapatkan peringatan ini lagi?

Eduardo Scoz
sumber
3
Nama variabel itu dinamis, itu tergantung pada banyak hal lainnya. Ada risiko yang saya sebut sesuatu yang tidak ada, tapi bukan itu masalahnya.
Eduardo Scoz
6
@ tikar mengapa memanggil metode secara dinamis pada objek menjadi praktik yang buruk? Bukankah seluruh tujuan NSSelectorFromString () untuk mendukung praktik ini?
Eduardo Scoz
7
Anda harus / juga dapat menguji [_controller respondsToSelector: mySelector] sebelum mengaturnya melalui performSelector:
mattacular
50
@mattacular Wish aku bisa memilih: "Itu ... adalah praktik yang buruk."
ctpenrose
6
Jika Anda tahu stringnya adalah literal, cukup gunakan @selector () agar kompiler dapat mengetahui apa nama pemilihnya. Jika kode aktual Anda memanggil NSSelectorFromString () dengan string yang dibuat atau disediakan saat runtime, maka Anda harus menggunakan NSSelectorFromString ().
Chris Page

Jawaban:

1211

Larutan

Compiler memperingatkan tentang hal ini karena suatu alasan. Sangat jarang peringatan ini diabaikan begitu saja, dan mudah ditangani. Begini caranya:

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

Atau lebih singkatnya (meskipun sulit dibaca & tanpa penjaga):

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

Penjelasan

Apa yang terjadi di sini adalah Anda meminta controller untuk pointer fungsi C untuk metode yang sesuai dengan controller. Semua NSObjectmerespons methodForSelector:, tetapi Anda juga dapat menggunakan class_getMethodImplementationdalam runtime Objective-C (berguna jika Anda hanya memiliki referensi protokol, seperti id<SomeProto>). Pointer fungsi ini disebut IMPs, dan typedefpointer fungsi ed sederhana ( id (*IMP)(id, SEL, ...)) 1 . Ini mungkin dekat dengan tanda tangan metode aktual dari metode ini, tetapi tidak akan selalu sama persis.

Setelah Anda memiliki IMP, Anda perlu melemparkannya ke pointer fungsi yang mencakup semua detail yang dibutuhkan ARC (termasuk dua argumen tersembunyi implisit selfdan _cmdsetiap panggilan metode Objective-C). Ini ditangani di baris ketiga ( (void *)di sebelah kanan hanya memberitahu kompiler bahwa Anda tahu apa yang Anda lakukan dan tidak menghasilkan peringatan karena jenis pointer tidak cocok).

Akhirnya, Anda memanggil penunjuk fungsi 2 .

Contoh Kompleks

Saat pemilih mengambil argumen atau mengembalikan nilai, Anda harus mengubah sedikit hal:

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;

Alasan untuk Peringatan

Alasan untuk peringatan ini adalah bahwa dengan ARC, runtime perlu tahu apa yang harus dilakukan dengan hasil dari metode yang Anda panggil. Hasilnya bisa apa saja: void, int, char, NSString *, id, dll ARC biasanya mendapat informasi ini dari header dari jenis objek Anda bekerja dengan. 3

Sebenarnya hanya ada 4 hal yang akan dipertimbangkan ARC untuk nilai pengembalian: 4

  1. Jenis mengabaikan non-object ( void, int, dll)
  2. Pertahankan nilai objek, lalu lepaskan ketika tidak lagi digunakan (asumsi standar)
  3. Lepaskan nilai objek baru ketika tidak lagi digunakan (metode dalam init/ copykeluarga atau dikaitkan dengan ns_returns_retained)
  4. Jangan lakukan apa pun & asumsikan nilai objek yang dikembalikan akan valid dalam lingkup lokal (hingga kumpulan rilis terbanyak dikeringkan, dikaitkan dengan ns_returns_autoreleased)

Panggilan untuk methodForSelector:mengasumsikan bahwa nilai balik dari metode yang dipanggil adalah objek, tetapi tidak mempertahankan / melepaskannya. Jadi Anda bisa membuat kebocoran jika objek Anda seharusnya dirilis seperti pada # 3 di atas (yaitu, metode yang Anda panggil mengembalikan objek baru).

Untuk penyeleksi yang Anda coba panggil balik itu voidatau yang bukan objek, Anda dapat mengaktifkan fitur kompiler untuk mengabaikan peringatan, tetapi itu mungkin berbahaya. Saya telah melihat dentang pergi melalui beberapa iterasi bagaimana ia menangani nilai-nilai kembali yang tidak ditugaskan ke variabel lokal. Tidak ada alasan bahwa dengan ARC diaktifkan, ARC tidak dapat mempertahankan dan melepaskan nilai objek yang dikembalikan methodForSelector:meskipun Anda tidak ingin menggunakannya. Dari perspektif kompiler, itu adalah objek. Itu berarti bahwa jika metode yang Anda panggil,, someMethodmengembalikan non objek (termasuk void), Anda bisa berakhir dengan nilai penunjuk sampah dipertahankan / dirilis dan macet.

Argumen tambahan

Satu pertimbangan adalah bahwa ini adalah peringatan yang sama akan terjadi dengan performSelector:withObject:dan Anda bisa mengalami masalah yang sama dengan tidak menyatakan bagaimana metode itu mengkonsumsi parameter. ARC memungkinkan untuk mendeklarasikan parameter yang dikonsumsi , dan jika metode tersebut menggunakan parameter tersebut, Anda mungkin pada akhirnya akan mengirim pesan ke zombie dan crash. Ada beberapa cara untuk mengatasi hal ini dengan casting bridged, tetapi sebenarnya akan lebih baik untuk hanya menggunakan IMPmetodologi fungsi pointer di atas. Karena parameter yang dikonsumsi jarang menjadi masalah, ini tidak mungkin muncul.

Selektor Statis

Menariknya, kompiler tidak akan mengeluh tentang pemilih yang dinyatakan secara statis:

[_controller performSelector:@selector(someMethod)];

Alasannya adalah karena kompiler sebenarnya dapat merekam semua informasi tentang pemilih dan objek selama kompilasi. Tidak perlu membuat asumsi tentang apa pun. (Saya memeriksa ini setahun yang lalu dengan melihat sumbernya, tetapi tidak memiliki referensi sekarang.)

Penekanan

Dalam mencoba memikirkan situasi di mana penindasan terhadap peringatan ini akan diperlukan dan desain kode yang baik, saya hampir kosong. Seseorang tolong bagikan jika mereka memiliki pengalaman di mana membungkam peringatan ini diperlukan (dan di atas tidak menangani hal-hal dengan benar).

Lebih

Dimungkinkan untuk membangun dan NSMethodInvocationmenangani ini juga, tetapi melakukannya membutuhkan lebih banyak pengetikan dan juga lebih lambat, jadi ada sedikit alasan untuk melakukannya.

Sejarah

Ketika performSelector:keluarga metode pertama kali ditambahkan ke Objective-C, ARC tidak ada. Saat membuat ARC, Apple memutuskan bahwa peringatan harus dibuat untuk metode ini sebagai cara membimbing pengembang untuk menggunakan cara lain untuk secara eksplisit menentukan bagaimana memori harus ditangani ketika mengirim pesan sewenang-wenang melalui pemilih bernama. Di Objective-C, pengembang dapat melakukan ini dengan menggunakan gips gaya C pada pointer fungsi mentah.

Dengan diperkenalkannya Swift, Apple telah mendokumentasikan dalam performSelector:keluarga metode sebagai "inheren tidak aman" dan mereka tidak tersedia untuk Swift.

Seiring waktu, kami telah melihat perkembangan ini:

  1. Versi awal Objective-C memungkinkan performSelector:(manajemen memori manual)
  2. Objective-C dengan ARC memperingatkan untuk penggunaan performSelector:
  3. Swift tidak memiliki akses ke performSelector:dan mendokumentasikan metode ini sebagai "secara inheren tidak aman"

Akan tetapi, gagasan untuk mengirim pesan berdasarkan pemilih yang dinamai bukan fitur yang "pada dasarnya tidak aman". Ide ini telah berhasil digunakan sejak lama di Objective-C dan juga banyak bahasa pemrograman lainnya.


1 Semua metode Objective-C memiliki dua argumen tersembunyi, selfdan _cmdyang secara implisit ditambahkan ketika Anda memanggil metode.

2 Memanggil NULLfungsi tidak aman di C. Penjaga yang digunakan untuk memeriksa keberadaan pengontrol memastikan bahwa kami memiliki objek. Karena itu kami tahu kami akan mendapatkan IMPdari methodForSelector:(meskipun mungkin _objc_msgForward, masuk ke sistem penerusan pesan). Pada dasarnya, dengan adanya penjaga, kita tahu bahwa kita memiliki fungsi untuk memanggil.

3 Sebenarnya, itu mungkin untuk mendapatkan info yang salah jika menyatakan objek sebagai idAnda dan Anda tidak mengimpor semua header. Anda bisa berakhir dengan crash dalam kode yang menurut kompiler baik-baik saja. Ini sangat jarang, tetapi bisa terjadi. Biasanya Anda hanya akan mendapat peringatan bahwa ia tidak tahu mana dari dua tanda tangan metode yang dapat dipilih.

4 Lihat referensi ARC pada nilai retur dan nilai retur tanpa retret untuk detail lebih lanjut.

wbyoung
sumber
@wbyoung Jika kode Anda memecahkan masalah penahanan, saya bertanya-tanya mengapa performSelector:metode tidak diterapkan dengan cara ini. Mereka memiliki tanda tangan metode yang ketat (kembali id, mengambil satu atau dua ids), sehingga tidak ada jenis primitif yang perlu ditangani.
Tricertops
1
@Andy argumen ditangani berdasarkan definisi prototipe metode (tidak akan dipertahankan / dirilis). Kekhawatiran sebagian besar didasarkan pada jenis pengembalian.
wbyoung
2
"Contoh Kompleks" memberikan kesalahan Cannot initialize a variable of type 'CGRect (*)(__strong id, SEL, CGRect, UIView *__strong)' with an rvalue of type 'void *'saat menggunakan Xcode terbaru. (5.1.1) Namun, saya belajar banyak!
Stan James
2
void (*func)(id, SEL) = (void *)imp;tidak mengkompilasi, saya telah menggantinya denganvoid (*func)(id, SEL) = (void (*)(id, SEL))imp;
Davyd Geyl
1
ubah void (*func)(id, SEL) = (void *)imp;ke <…> = (void (*))imp;atau<…> = (void (*) (id, SEL))imp;
Isaak Osipovich Dunayevsky
1182

Dalam kompiler LLVM 3.0 di Xcode 4.2 Anda dapat menekan peringatan sebagai berikut:

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

Jika Anda mendapatkan kesalahan di beberapa tempat, dan ingin menggunakan sistem makro C untuk menyembunyikan pragma, Anda bisa mendefinisikan makro untuk membuatnya lebih mudah untuk menekan peringatan:

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

Anda dapat menggunakan makro seperti ini:

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

Jika Anda membutuhkan hasil dari pesan yang dilakukan, Anda dapat melakukan ini:

id result;
SuppressPerformSelectorLeakWarning(
    result = [_target performSelector:_action withObject:self]
);
Scott Thompson
sumber
Metode ini dapat menyebabkan kebocoran memori ketika optimasi diatur ke selain dari Tidak Ada.
Eric
4
@Eric Tidak, tidak bisa, kecuali jika Anda menggunakan metode lucu seperti "initSomething" atau "newSomething" atau "somethingCopy".
Andrey Tarantsov
3
@Julian Itu berfungsi, tapi itu mematikan peringatan untuk seluruh file - Anda mungkin tidak perlu atau menginginkan itu. Membungkusnya dengan popdan push-pragma jauh lebih bersih dan lebih aman.
Emil
2
Semua ini dilakukan adalah membungkam kompiler. Ini tidak menyelesaikan masalah. Jika pemilih tidak ada Anda cukup kacau.
Andra Todorescu
2
Ini harus digunakan hanya ketika dibungkus oleh if ([_target respondsToSelector:_selector]) {logika yang serupa.
208

Dugaan saya tentang ini adalah ini: karena pemilih tidak dikenal oleh kompiler, ARC tidak dapat menerapkan manajemen memori yang tepat.

Bahkan, ada kalanya manajemen memori dikaitkan dengan nama metode dengan konvensi tertentu. Secara khusus, saya sedang memikirkan konstruktor kenyamanan versus membuat metode; mantan mengembalikan dengan konvensi objek autoreleased; yang terakhir merupakan objek yang dipertahankan. Konvensi didasarkan pada nama-nama pemilih, jadi jika kompiler tidak mengetahui pemilih, maka tidak dapat menegakkan aturan manajemen memori yang tepat.

Jika ini benar, saya pikir Anda dapat menggunakan kode Anda dengan aman, asalkan Anda memastikan semuanya baik-baik saja untuk manajemen memori (misalnya, bahwa metode Anda tidak mengembalikan objek yang mereka alokasikan).

sergio
sumber
5
Terima kasih atas jawabannya, saya akan melihat lebih dalam untuk melihat apa yang sedang terjadi. Adakah ide tentang bagaimana saya bisa mem-bypass peringatan dan membuatnya menghilang? Saya tidak suka memiliki peringatan yang tersimpan dalam kode saya selamanya untuk panggilan yang aman.
Eduardo Scoz
84
Jadi saya mendapat konfirmasi dari seseorang di Apple di forum mereka bahwa memang ini masalahnya. Mereka akan menambahkan penggantian yang terlupakan untuk memungkinkan orang menonaktifkan peringatan ini di rilis mendatang. Terima kasih.
Eduardo Scoz
5
Jawaban ini menimbulkan beberapa pertanyaan, seperti jika ARC mencoba membuat keputusan kapan akan merilis sesuatu berdasarkan nama konvensi dan metode, lalu bagaimana "penghitungan referensi"? Perilaku yang Anda gambarkan terdengar sedikit lebih baik daripada arbitrer, jika ARC mengasumsikan kode mengikuti konvensi tertentu sebagai lawan benar-benar melacak referensi tidak peduli apa konvensi diikuti.
Agustus
8
ARC mengotomatiskan proses penambahan retain dan rilis pada saat kompilasi. Ini bukan pengumpulan sampah (itulah sebabnya ia sangat cepat dan overhead rendah). Sama sekali tidak sewenang-wenang. Aturan default didasarkan pada konvensi ObjC yang sudah mapan yang telah diterapkan secara konsisten selama beberapa dekade. Ini menghindari perlunya menambahkan secara eksplisit __attributeke setiap metode yang menjelaskan manajemen memorinya. Tetapi itu juga membuat mustahil bagi penyusun untuk menangani pola ini dengan benar (sebuah pola yang dulunya sangat umum, tetapi telah diganti dengan pola yang lebih kuat dalam beberapa tahun terakhir).
Rob Napier
8
Jadi kita tidak bisa lagi memiliki ivar tipe SELdan menetapkan pemilih yang berbeda tergantung pada situasinya? Way to go, bahasa dinamis ...
Nicolas Miari
121

Di Pengaturan Bangun proyek Anda , di bawah Bendera Peringatan Lainnya ( WARNING_CFLAGS), tambahkan
-Wno-arc-performSelector-leaks

Sekarang pastikan pemilih yang Anda panggil tidak menyebabkan objek Anda dipertahankan atau disalin.

0xced
sumber
12
Catatan Anda dapat menambahkan bendera yang sama untuk file tertentu daripada keseluruhan proyek. Jika Anda melihat di bawah Build Phases-> Compile Sources, Anda dapat mengatur per file Flag Compiler (seperti yang ingin Anda lakukan untuk mengecualikan file dari ARC). Dalam proyek saya hanya satu file yang harus menggunakan penyeleksi dengan cara ini, jadi saya hanya mengecualikannya dan meninggalkan yang lain.
Michael
111

Sebagai solusi hingga kompiler memungkinkan mengganti peringatan, Anda dapat menggunakan runtime

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

dari pada

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

Anda harus melakukannya

#import <objc/message.h>

sialiv
sumber
8
ARC mengakui konvensi Kakao dan kemudian menambahkan retensi dan rilis berdasarkan konvensi tersebut. Karena C tidak mengikuti konvensi tersebut, ARC memaksa Anda untuk menggunakan teknik manajemen memori manual. Jika Anda membuat objek CF, Anda harus CFRelease () itu. Jika Anda dispatch_queue_create (), Anda harus dispatch_release (). Intinya, jika Anda ingin menghindari peringatan ARC, Anda dapat menghindarinya dengan menggunakan objek C dan manajemen memori manual. Juga, Anda dapat menonaktifkan ARC pada basis per file dengan menggunakan flag -fno-objc-arc compiler pada file itu.
jluckyiv
8
Bukan tanpa casting, kamu tidak bisa. Varargs tidak sama dengan daftar argumen yang diketik secara eksplisit. Biasanya ini akan bekerja secara kebetulan, tetapi saya tidak menganggap "kebetulan" sebagai hal yang benar.
bbum
21
Jangan lakukan itu, [_controller performSelector:NSSelectorFromString(@"someMethod")];dan objc_msgSend(_controller, NSSelectorFromString(@"someMethod"));tidak setara! Lihat di Metode Signature Ketidakcocokan dan Kelemahan besar dalam pengetikan lemah Objective-C mereka menjelaskan masalah secara mendalam.
0xced
5
@ 0xced Dalam hal ini, tidak apa-apa. objc_msgSend tidak akan membuat metode signature mismatch untuk setiap pemilih yang akan bekerja dengan benar di performSelector: atau variasinya karena mereka hanya pernah mengambil objek sebagai parameter. Selama semua parameter Anda adalah pointer (termasuk objek), ganda dan NSInteger / panjang, dan tipe pengembalian Anda batal, pointer atau panjang, maka objc_msgSend akan bekerja dengan benar.
Matt Gallagher
88

Untuk mengabaikan kesalahan hanya di file dengan selector perform, tambahkan #pragma sebagai berikut:

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

Ini akan mengabaikan peringatan pada baris ini, tetapi masih membiarkannya sepanjang sisa proyek Anda.

Barlow Tucker
sumber
6
Saya yakin Anda juga dapat mengaktifkan kembali peringatan segera setelah metode yang dimaksud #pragma clang diagnostic warning "-Warc-performSelector-leaks". Saya tahu jika saya mematikan peringatan, saya ingin menyalakannya kembali pada saat yang paling cepat, jadi saya tidak sengaja membiarkan peringatan lain yang tidak terduga lewat. Sepertinya ini bukan masalah, tapi ini hanya praktikku setiap kali aku mematikan peringatan.
Rob
2
Anda juga dapat memulihkan keadaan konfigurasi kompiler sebelumnya dengan menggunakan #pragma clang diagnostic warning pushsebelum Anda melakukan perubahan apa pun dan #pragma clang diagnostic warning popmengembalikan keadaan sebelumnya. Berguna jika Anda mematikan banyak dan tidak ingin memiliki banyak mengaktifkan kembali jalur pragma dalam kode Anda.
deanWombourne
Itu hanya akan mengabaikan baris berikut?
hfossli
70

Aneh tapi benar: jika dapat diterima (yaitu hasilnya batal dan Anda tidak keberatan membiarkan siklus runloop satu kali), tambahkan penundaan, bahkan jika ini nol:

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

Ini menghilangkan peringatan, mungkin karena meyakinkan kompiler bahwa tidak ada objek yang dapat dikembalikan dan entah bagaimana salah kelola.

matt
sumber
2
Apakah Anda tahu apakah ini benar-benar menyelesaikan masalah manajemen memori terkait, atau apakah ia memiliki masalah yang sama tetapi Xcode tidak cukup pintar untuk memperingatkan Anda dengan kode ini?
Aaron Brager
Ini secara semantik bukan hal yang sama! Menggunakan performSelector: withObject: AfterDelay: akan melakukan pemilih dalam menjalankan runloop berikutnya. Oleh karena itu, metode ini segera kembali.
Florian
10
@Florian Tentu saja tidak sama! Baca jawaban saya: Saya katakan jika dapat diterima, karena hasilnya batal dan siklus runloop. Itulah kalimat pertama dari jawaban saya.
matt
34

Berikut ini adalah makro yang diperbarui berdasarkan jawaban yang diberikan di atas. Ini harus memungkinkan Anda untuk membungkus kode Anda bahkan dengan pernyataan pengembalian.

#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]
);
syvex
sumber
6
returntidak harus berada di dalam makro; return SUPPRESS_PERFORM_SELECTOR_LEAK_WARNING([_target performSelector:_action withObject:self]);juga berfungsi dan terlihat lebih waras.
uasi
31

Kode ini tidak melibatkan flag compiler atau panggilan runtime langsung:

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

NSInvocationmemungkinkan beberapa argumen diset sehingga tidak seperti performSelectorini akan bekerja pada metode apa pun

Benedict Cohen
sumber
3
Apakah Anda tahu apakah ini benar-benar menyelesaikan masalah manajemen memori terkait, atau apakah ia memiliki masalah yang sama tetapi Xcode tidak cukup pintar untuk memperingatkan Anda dengan kode ini?
Aaron Brager
1
Anda bisa mengatakan itu memecahkan masalah manajemen memori; tetapi ini karena pada dasarnya memungkinkan Anda menentukan perilaku. Misalnya, Anda dapat memilih untuk membiarkan doa mempertahankan argumen atau tidak. Sejauh pengetahuan saya saat ini, ia mencoba untuk memperbaiki masalah ketidakcocokan tanda tangan yang mungkin muncul dengan percaya bahwa Anda tahu apa yang Anda lakukan dan tidak memberikan data yang salah. Saya tidak yakin apakah semua pemeriksaan dapat dilakukan saat runtime. Seperti disebutkan dalam komentar lain, mikeash.com/pyblog/... dengan baik menjelaskan apa yang bisa dilakukan ketidakcocokan.
Mihai Timar
20

Yah, banyak jawaban di sini, tetapi karena ini sedikit berbeda, menggabungkan beberapa jawaban saya pikir saya akan memasukkannya. Saya menggunakan kategori NSObject yang memeriksa untuk memastikan pemilih kembali batal, dan juga menekan kompiler peringatan.

#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
Chris Prince
sumber
Haruskah 'v' diganti oleh _C_VOID? _C_VOID dideklarasikan dalam <objc / runtime.h>.
Rik Renich
16

Demi anak cucu, saya telah memutuskan untuk melemparkan topi saya ke atas ring :)

Baru-baru ini saya telah melihat semakin banyak restrukturisasi jauh dari target/ selectorparadigma, mendukung hal-hal seperti protokol, blok, dll Namun, ada satu drop-in pengganti performSelectorbahwa saya telah digunakan beberapa kali sekarang:

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

Ini tampaknya menjadi pengganti yang bersih, aman-ARC, dan hampir identik untuk performSelectortanpa harus banyak peduli dengan objc_msgSend().

Padahal, saya tidak tahu apakah ada analog yang tersedia di iOS.

Patrick Perini
sumber
6
Terima kasih untuk termasuk ini .. Ini tersedia dalam iOS: [[UIApplication sharedApplication] sendAction: to: from: forEvent:]. Saya pernah melihatnya sekali, tapi rasanya agak canggung menggunakan kelas terkait UI di tengah domain atau layanan Anda hanya untuk melakukan panggilan dinamis .. Terima kasih sudah memasukkan ini!
Eduardo Scoz
2
Ew! Ini akan memiliki lebih banyak overhead (karena perlu memeriksa apakah metode ini tersedia dan berjalan di rantai responden jika tidak) dan memiliki perilaku kesalahan yang berbeda (berjalan di rantai responden dan mengembalikan TIDAK jika tidak dapat menemukan apa pun yang merespons metode, bukan hanya menabrak). Ini juga tidak berfungsi ketika Anda menginginkan iddari-performSelector:...
tc.
2
@tc. Itu tidak "berjalan menaiki rantai responden" kecuali to:nihil, yang tidak. Itu hanya langsung ke objek yang ditargetkan tanpa memeriksa sebelumnya. Jadi tidak ada "overhead lagi". Itu bukan solusi yang bagus, tetapi alasan Anda memberi bukanlah alasan. :)
matt
15

Jawaban Matt Galloway pada utas ini menjelaskan alasannya:

Pertimbangkan yang berikut ini:

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

Sekarang, bagaimana ARC bisa tahu bahwa yang pertama mengembalikan objek dengan jumlah tetap 1 tetapi yang kedua mengembalikan objek yang autoreleased?

Tampaknya aman untuk menekan peringatan jika Anda mengabaikan nilai pengembalian. Saya tidak yakin apa praktik terbaik adalah jika Anda benar-benar perlu mendapatkan objek yang ditahan dari performSelector - selain "jangan lakukan itu".

c roald
sumber
14

@ c-road menyediakan tautan yang tepat dengan deskripsi masalah di sini . Di bawah ini Anda dapat melihat contoh saya, ketika performSelector menyebabkan kebocoran memori.

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

Satu-satunya metode, yang menyebabkan kebocoran memori dalam contoh saya adalah CopyDummyWithLeak. Alasannya adalah bahwa ARC tidak tahu, bahwa copySelector mengembalikan objek yang dipertahankan.

Jika Anda menjalankan Memory Leak Tool, Anda dapat melihat gambar berikut: masukkan deskripsi gambar di sini ... dan tidak ada kebocoran memori dalam hal lain: masukkan deskripsi gambar di sini

Pavel Osipov
sumber
6

Untuk menjadikan makro Scott Thompson lebih umum:

// 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")

Kemudian gunakan seperti ini:

MYSilenceWarning(-Warc-performSelector-leaks,
[_target performSelector:_action withObject:self];
                )
Ben Flynn
sumber
FWIW, saya tidak menambahkan makro. Seseorang menambahkan itu ke tanggapan saya. Secara pribadi, saya tidak akan menggunakan makro. Pragma ada di sana untuk menyelesaikan kasus khusus dalam kode dan pragma sangat eksplisit dan langsung tentang apa yang terjadi. Saya lebih suka menyimpannya di tempat daripada menyembunyikan, atau mengabstraksi mereka di balik makro, tapi itu hanya saya. YMMV.
Scott Thompson
@ScottThompson Itu adil. Bagi saya mudah untuk mencari makro ini di seluruh basis kode saya dan saya biasanya juga menambahkan peringatan yang tidak dibungkam untuk menangani masalah yang mendasarinya.
Ben Flynn
6

Jangan menekan peringatan!

Ada tidak kurang dari 12 solusi alternatif untuk bermain-main dengan kompiler.
Ketika Anda menjadi pintar pada saat implementasi pertama, beberapa insinyur di Bumi dapat mengikuti jejak Anda, dan kode ini pada akhirnya akan rusak.

Rute Aman:

Semua solusi ini akan berfungsi, dengan beberapa tingkat variasi dari niat awal Anda. Anggaplah itu parambisanil jika Anda menginginkannya:

Rute aman, perilaku konseptual yang sama:

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

Rute aman, perilaku yang sedikit berbeda:

(Lihat respons ini )
Gunakan utas apa pun sebagai pengganti [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]];

Rute Berbahaya

Membutuhkan beberapa jenis kompiler yang dibungkam, yang pasti akan rusak. Perhatikan bahwa pada saat ini, ia melakukan istirahat di Swift .

// AT YOUR OWN RISK
[_controller performSelector:selector];
[_controller performSelector:selector withObject:anArgument];
[_controller performSelector:selector withObject:anArgument withObject:nil];
Arsitektur Swift
sumber
3
Kata-katanya sangat salah. Rute aman sama sekali tidak lebih aman daripada berbahaya. Ini bisa dibilang lebih berbahaya karena menyembunyikan peringatan secara implisit.
Bryan Chen
Saya akan memperbaiki kata-katanya agar tidak menghina, tetapi saya menepati janji saya. Satu-satunya waktu saya menemukan peringatan pembungkaman dapat diterima adalah jika saya tidak memiliki kode. Tidak ada insinyur yang dapat dengan aman menjaga kode yang dibungkam tanpa memahami semua konsekuensinya, yang berarti membaca argumen ini, dan praktik ini sangat berisiko; terutama jika Anda mempertimbangkan 12, bahasa Inggris sederhana, alternatif yang kuat.
SwiftArchitect
1
Tidak. Anda tidak mengerti maksud saya. Menggunakan performSelectorOnMainThreadini tidak cara yang baik untuk membungkam peringatan dan memiliki efek samping. (itu tidak menyelesaikan kebocoran memori) Ekstra #clang diagnostic ignored secara eksplisit menekan peringatan dengan cara yang sangat jelas.
Bryan Chen
Benar bahwa melakukan pemilih pada non - (void)metode adalah masalah sebenarnya.
SwiftArchitect
dan bagaimana Anda memanggil pemilih dengan banyak argumen melalui ini dan aman dalam waktu yang bersamaan? @SwiftArchitect
Catalin
4

Karena Anda menggunakan ARC, Anda harus menggunakan iOS 4.0 atau yang lebih baru. Ini artinya Anda bisa menggunakan blok. Jika alih-alih mengingat pemilih untuk melakukan Anda alih-alih mengambil blok, ARC akan dapat melacak dengan lebih baik apa yang sebenarnya terjadi dan Anda tidak perlu menjalankan risiko secara tidak sengaja memasukkan kebocoran memori.

honus
sumber
Sebenarnya, blok membuatnya sangat mudah untuk secara tidak sengaja membuat siklus penahan yang tidak dapat dipecahkan ARC. Saya masih berharap bahwa ada peringatan kompiler ketika Anda secara implisit digunakan selfmelalui ivar (misalnya, ivarbukan self->ivar).
tc.
Maksudmu seperti -Wimplic-retain-self?
OrangeDog
2

Alih-alih menggunakan pendekatan blok, yang memberi saya beberapa masalah:

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

Saya akan menggunakan NSInvocation, seperti ini:

    -(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];
    }
supersabbath
sumber
1

Jika Anda tidak perlu memberikan argumen apa pun, penyelesaian yang mudah adalah dengan menggunakan valueForKeyPath. Ini bahkan mungkin terjadi pada Classobjek.

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

Anda juga bisa menggunakan protokol di sini. Jadi, buat protokol seperti ini:

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

Di kelas Anda yang perlu memanggil pemilih Anda, Anda kemudian memiliki @ properti.

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

Ketika Anda perlu memanggil @selector(doSomethingWithObject:)instance MyObject, lakukan ini:

[self.source doSomethingWithObject:object];
Damon
sumber
2
Hai Wu, terima kasih, tapi intinya menggunakan NSSelectorFromString adalah ketika Anda tidak tahu pemilih mana yang ingin Anda panggil saat runtime.
Eduardo Scoz