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?
ios
objective-c
memory-leaks
automatic-ref-counting
Eduardo Scoz
sumber
sumber
Jawaban:
Larutan
Compiler memperingatkan tentang hal ini karena suatu alasan. Sangat jarang peringatan ini diabaikan begitu saja, dan mudah ditangani. Begini caranya:
Atau lebih singkatnya (meskipun sulit dibaca & tanpa penjaga):
Penjelasan
Apa yang terjadi di sini adalah Anda meminta controller untuk pointer fungsi C untuk metode yang sesuai dengan controller. Semua
NSObject
meresponsmethodForSelector:
, tetapi Anda juga dapat menggunakanclass_getMethodImplementation
dalam runtime Objective-C (berguna jika Anda hanya memiliki referensi protokol, sepertiid<SomeProto>
). Pointer fungsi ini disebutIMP
s, dantypedef
pointer 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 implisitself
dan_cmd
setiap 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:
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. 3Sebenarnya hanya ada 4 hal yang akan dipertimbangkan ARC untuk nilai pengembalian: 4
void
,int
, dll)init
/copy
keluarga atau dikaitkan denganns_returns_retained
)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
void
atau 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 dikembalikanmethodForSelector:
meskipun Anda tidak ingin menggunakannya. Dari perspektif kompiler, itu adalah objek. Itu berarti bahwa jika metode yang Anda panggil,,someMethod
mengembalikan non objek (termasukvoid
), 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 menggunakanIMP
metodologi 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:
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
NSMethodInvocation
menangani 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:
performSelector:
(manajemen memori manual)performSelector:
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,
self
dan_cmd
yang secara implisit ditambahkan ketika Anda memanggil metode.2 Memanggil
NULL
fungsi tidak aman di C. Penjaga yang digunakan untuk memeriksa keberadaan pengontrol memastikan bahwa kami memiliki objek. Karena itu kami tahu kami akan mendapatkanIMP
darimethodForSelector:
(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
id
Anda 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.
sumber
performSelector:
metode tidak diterapkan dengan cara ini. Mereka memiliki tanda tangan metode yang ketat (kembaliid
, mengambil satu atau duaid
s), sehingga tidak ada jenis primitif yang perlu ditangani.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!void (*func)(id, SEL) = (void *)imp;
tidak mengkompilasi, saya telah menggantinya denganvoid (*func)(id, SEL) = (void (*)(id, SEL))imp;
void (*func)(id, SEL) = (void *)imp;
ke<…> = (void (*))imp;
atau<…> = (void (*) (id, SEL))imp;
Dalam kompiler LLVM 3.0 di Xcode 4.2 Anda dapat menekan peringatan sebagai berikut:
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:
Anda dapat menggunakan makro seperti ini:
Jika Anda membutuhkan hasil dari pesan yang dilakukan, Anda dapat melakukan ini:
sumber
pop
danpush
-pragma jauh lebih bersih dan lebih aman.if ([_target respondsToSelector:_selector]) {
logika yang serupa.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).
sumber
__attribute
ke 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).SEL
dan menetapkan pemilih yang berbeda tergantung pada situasinya? Way to go, bahasa dinamis ...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.
sumber
Sebagai solusi hingga kompiler memungkinkan mengganti peringatan, Anda dapat menggunakan runtime
dari pada
Anda harus melakukannya
sumber
[_controller performSelector:NSSelectorFromString(@"someMethod")];
danobjc_msgSend(_controller, NSSelectorFromString(@"someMethod"));
tidak setara! Lihat di Metode Signature Ketidakcocokan dan Kelemahan besar dalam pengetikan lemah Objective-C mereka menjelaskan masalah secara mendalam.Untuk mengabaikan kesalahan hanya di file dengan selector perform, tambahkan #pragma sebagai berikut:
Ini akan mengabaikan peringatan pada baris ini, tetapi masih membiarkannya sepanjang sisa proyek Anda.
sumber
#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.#pragma clang diagnostic warning push
sebelum Anda melakukan perubahan apa pun dan#pragma clang diagnostic warning pop
mengembalikan keadaan sebelumnya. Berguna jika Anda mematikan banyak dan tidak ingin memiliki banyak mengaktifkan kembali jalur pragma dalam kode Anda.Aneh tapi benar: jika dapat diterima (yaitu hasilnya batal dan Anda tidak keberatan membiarkan siklus runloop satu kali), tambahkan penundaan, bahkan jika ini nol:
Ini menghilangkan peringatan, mungkin karena meyakinkan kompiler bahwa tidak ada objek yang dapat dikembalikan dan entah bagaimana salah kelola.
sumber
Berikut ini adalah makro yang diperbarui berdasarkan jawaban yang diberikan di atas. Ini harus memungkinkan Anda untuk membungkus kode Anda bahkan dengan pernyataan pengembalian.
sumber
return
tidak harus berada di dalam makro;return SUPPRESS_PERFORM_SELECTOR_LEAK_WARNING([_target performSelector:_action withObject:self]);
juga berfungsi dan terlihat lebih waras.Kode ini tidak melibatkan flag compiler atau panggilan runtime langsung:
NSInvocation
memungkinkan beberapa argumen diset sehingga tidak sepertiperformSelector
ini akan bekerja pada metode apa punsumber
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.
sumber
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
/selector
paradigma, mendukung hal-hal seperti protokol, blok, dll Namun, ada satu drop-in penggantiperformSelector
bahwa saya telah digunakan beberapa kali sekarang:Ini tampaknya menjadi pengganti yang bersih, aman-ARC, dan hampir identik untuk
performSelector
tanpa harus banyak peduli denganobjc_msgSend()
.Padahal, saya tidak tahu apakah ada analog yang tersedia di iOS.
sumber
[[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!id
dari-performSelector:...
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. :)Jawaban Matt Galloway pada utas ini menjelaskan alasannya:
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".
sumber
@ c-road menyediakan tautan yang tepat dengan deskripsi masalah di sini . Di bawah ini Anda dapat melihat contoh saya, ketika performSelector menyebabkan kebocoran memori.
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: ... dan tidak ada kebocoran memori dalam hal lain:
sumber
Untuk menjadikan makro Scott Thompson lebih umum:
Kemudian gunakan seperti ini:
sumber
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
param
bisanil
jika Anda menginginkannya:Rute aman, perilaku konseptual yang sama:
Rute aman, perilaku yang sedikit berbeda:
(Lihat respons ini )
Gunakan utas apa pun sebagai pengganti
[NSThread mainThread]
.Rute Berbahaya
Membutuhkan beberapa jenis kompiler yang dibungkam, yang pasti akan rusak. Perhatikan bahwa pada saat ini, ia melakukan istirahat di Swift .
sumber
performSelectorOnMainThread
ini 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.- (void)
metode adalah masalah sebenarnya.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.
sumber
self
melalui ivar (misalnya,ivar
bukanself->ivar
).Alih-alih menggunakan pendekatan blok, yang memberi saya beberapa masalah:
Saya akan menggunakan NSInvocation, seperti ini:
sumber
Jika Anda tidak perlu memberikan argumen apa pun, penyelesaian yang mudah adalah dengan menggunakan
valueForKeyPath
. Ini bahkan mungkin terjadi padaClass
objek.sumber
Anda juga bisa menggunakan protokol di sini. Jadi, buat protokol seperti ini:
Di kelas Anda yang perlu memanggil pemilih Anda, Anda kemudian memiliki @ properti.
Ketika Anda perlu memanggil
@selector(doSomethingWithObject:)
instance MyObject, lakukan ini:sumber