Pertanyaan performSelector dapat menyebabkan kebocoran karena pemilihnya tidak diketahui


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 mendapatkan peringatan ini? Saya mengerti kompilator tidak dapat memeriksa apakah pemilih ada atau tidak, tetapi mengapa itu menyebabkan kebocoran? Dan bagaimana saya bisa mengubah kode saya sehingga saya tidak mendapatkan peringatan ini lagi?


1189
2017-08-10 20:23


asal


Jawaban:


Larutan

Compiler memperingatkan tentang ini karena suatu alasan. Sangat jarang bahwa peringatan ini harus diabaikan begitu saja, dan mudah untuk bekerja. 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 singkat (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 NSObjects menanggapi methodForSelector:, tetapi Anda juga bisa menggunakannya class_getMethodImplementation dalam runtime Objective-C (berguna jika Anda hanya memiliki referensi protokol, seperti id<SomeProto>). Pointer fungsi ini dipanggil IMPs, dan sederhana typedefed fungsi pointer (id (*IMP)(id, SEL, ...))1. Ini mungkin dekat dengan metode aktual tanda tangan dari metode, tetapi tidak akan selalu sama persis.

Setelah Anda memilikinya IMP, Anda perlu mentransmisikannya ke penunjuk fungsi yang mencakup semua detail yang dibutuhkan ARC (termasuk dua argumen tersembunyi tersirat self dan _cmd dari setiap panggilan metode Objective-C). Ini ditangani di baris ketiga (the (void *) di sisi kanan cukup beri tahu kompilator bahwa Anda tahu apa yang Anda lakukan dan tidak menghasilkan peringatan karena jenis penunjuk tidak cocok).

Akhirnya, Anda memanggil penunjuk fungsi2.

Contoh Kompleks

Ketika 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 mengetahui apa yang harus dilakukan dengan hasil dari metode yang Anda panggil. Hasilnya bisa berupa apa saja: void, int, char, NSString *, id, dll. ARC biasanya mendapatkan informasi ini dari header jenis objek yang Anda kerjakan.3

Hanya ada 4 hal yang akan dipertimbangkan oleh ARC untuk mengembalikan nilai:4

  1. Abaikan jenis non-objek (void, int, dll.)
  2. Pertahankan nilai objek, lalu lepaskan ketika tidak lagi digunakan (asumsi standar)
  3. Rilis nilai objek baru ketika tidak lagi digunakan (metode di init/ copy keluarga atau dikaitkan dengan ns_returns_retained)
  4. Tidak melakukan apa-apa & menganggap nilai objek yang dikembalikan akan valid dalam lingkup lokal (hingga kumpulan rilisan paling dalam dikeringkan, dikaitkan dengan ns_returns_autoreleased)

Panggilan ke methodForSelector: berasumsi bahwa nilai kembalian 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 pemilih Anda mencoba untuk memanggil kembali itu void atau non-objek lainnya, Anda dapat mengaktifkan fitur kompilator untuk mengabaikan peringatan, tetapi mungkin berbahaya. Saya telah melihat Clang melalui beberapa iterasi tentang bagaimana ia menangani nilai kembali yang tidak ditetapkan ke variabel lokal. Tidak ada alasan bahwa dengan ARC diaktifkan bahwa itu tidak dapat mempertahankan dan melepaskan nilai objek yang dikembalikan methodForSelector:meskipun Anda tidak ingin menggunakannya. Dari perspektif kompilator, itu adalah objek. Itu berarti jika metode yang Anda panggil, someMethod, mengembalikan objek non (termasuk void), Anda bisa berakhir dengan nilai penunjuk sampah yang ditahan / dilepaskan dan macet.

Argumen Tambahan

Salah satu pertimbangannya adalah bahwa ini adalah peringatan yang sama akan terjadi performSelector:withObject: dan Anda bisa mengalami masalah serupa dengan tidak menyatakan bagaimana metode tersebut menggunakan parameter. ARC memungkinkan untuk menyatakan parameter yang dikonsumsi, dan jika metode ini menggunakan parameter, Anda mungkin akhirnya mengirim pesan ke zombie dan crash. Ada beberapa cara untuk mengatasinya dengan casting yang dijembatani, tetapi sebenarnya lebih baik gunakan saja IMP dan fungsi metodologi penunjuk di atas. Karena parameter yang dikonsumsi jarang menjadi masalah, ini tidak mungkin muncul.

Pemilih Statis

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

[_controller performSelector:@selector(someMethod)];

Alasannya adalah karena kompiler sebenarnya mampu 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 penekanan peringatan ini akan diperlukan dan desain kode yang baik, saya menjadi kosong. Seseorang tolong bagikan jika mereka memiliki pengalaman di mana membungkam peringatan ini diperlukan (dan di atas tidak menangani hal dengan benar).

Lebih

Itu mungkin untuk membangun sebuah NSMethodInvocation untuk menangani ini juga, tetapi hal itu membutuhkan lebih banyak pengetikan dan juga lebih lambat, jadi tidak ada 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 acak melalui pemilih bernama. Dalam Objective-C, pengembang dapat melakukan ini dengan menggunakan cast gaya C pada pointer fungsi mentah.

Dengan diperkenalkannya Swift, Apple telah didokumentasikan itu 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 digunakan performSelector:
  3. Swift tidak memiliki akses ke performSelector: dan mendokumentasikan metode ini sebagai "tidak aman secara inheren"

Ide mengirim pesan berdasarkan pemilih bernama tidak, bagaimanapun, "inheren tidak aman" fitur. Ide ini telah berhasil digunakan untuk waktu yang lama di Objective-C serta banyak bahasa pemrograman lainnya.


1 Semua metode Objective-C memiliki dua argumen tersembunyi, self dan _cmd yang secara implisit ditambahkan saat Anda memanggil suatu metode.

2Memanggil a NULL fungsi tidak aman di C. Penjaga yang digunakan untuk memeriksa keberadaan pengendali memastikan bahwa kita memiliki objek. Karena itu kami tahu kami akan mendapatkan IMP dari methodForSelector: (meskipun mungkin _objc_msgForward, masuk ke sistem penerusan pesan). Pada dasarnya, dengan penjaga di tempat, kita tahu kita memiliki fungsi untuk dipanggil.

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

4 Lihat referensi ARC di mempertahankan nilai kembali dan nilai pengembalian yang tidak dapat dikembalikan untuk lebih jelasnya.


1142
2017-11-18 21:44



Di 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 pragmasanya, Anda dapat menentukan 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 ditampilkan, Anda dapat melakukan ini:

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

1170
2017-10-28 19:30



Tebakan saya tentang ini adalah ini: karena pemilih tidak diketahui oleh compiler, ARC tidak dapat menerapkan manajemen memori yang tepat.

Bahkan, ada kalanya manajemen memori terkait dengan nama metode oleh konvensi tertentu. Secara khusus, saya sedang memikirkan konstruktor kenyamanan melawan membuat metode; mantan kembali dengan konvensi objek autoreleased; yang terakhir adalah objek yang ditahan. Konvensi ini didasarkan pada nama-nama pemilih, jadi jika kompilator tidak tahu pemilih, maka tidak dapat menegakkan aturan manajemen memori yang tepat.

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


206
2017-08-10 20:43



Di proyek Anda Pengaturan Build, dibawah Bendera Peringatan Lainnya (WARNING_CFLAGS), tambahkan
-Wno-arc-performSelector-leaks

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


119
2017-10-31 13:57



Sebagai solusi sampai kompilator memungkinkan untuk mengganti peringatan, Anda dapat menggunakan runtime

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

dari pada

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

Anda harus melakukannya

#import <objc/message.h>


110
2017-08-16 04:56



Untuk mengabaikan kesalahan hanya dalam file dengan pemilih kinerja, 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.


87
2018-01-18 21:31



Aneh tapi nyata: jika dapat diterima (yaitu hasil kosong dan Anda tidak keberatan membiarkan siklus runloop sekali), tambahkan penundaan, bahkan jika ini nol:

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

Ini menghapus peringatan, mungkin karena itu meyakinkan kompilator bahwa tidak ada objek yang dapat dikembalikan dan entah bagaimana salah dalam mengelola.


67
2017-11-11 19:19



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

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

34
2018-05-07 14:58



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

NSInvocation memungkinkan beberapa argumen untuk diatur tidak seperti itu performSelector ini akan bekerja dengan metode apa pun.


31
2018-02-01 15:46