Apa Bahaya Metode Berputar di Objective-C?

235

Saya telah mendengar orang mengatakan bahwa metode swizzling adalah praktik yang berbahaya. Bahkan nama swizzling menunjukkan bahwa itu sedikit curang.

Metode Swizzling adalah memodifikasi pemetaan sehingga memanggil pemilih A benar-benar akan memanggil implementasi B. Salah satu penggunaannya adalah untuk memperluas perilaku kelas sumber tertutup.

Bisakah kita memformalisasikan risiko sehingga siapa pun yang memutuskan apakah akan menggunakan swizzling dapat membuat keputusan yang tepat apakah layak untuk apa yang mereka coba lakukan.

Misalnya

  • Penamaan Tabrakan : Jika kelas nanti memperluas fungsinya untuk memasukkan nama metode yang telah Anda tambahkan, itu akan menyebabkan masalah besar. Mengurangi risiko dengan penamaan metode swizzled yang masuk akal.
Robert
sumber
Sudah tidak terlalu baik untuk mendapatkan sesuatu yang Anda harapkan dari kode yang Anda baca
Martin Babacaev
ini terdengar seperti cara yang tidak bersih untuk melakukan apa yang dilakukan oleh dekorator python dengan sangat bersih
Claudiu

Jawaban:

439

Saya pikir ini adalah pertanyaan yang sangat hebat, dan ini memalukan bahwa alih-alih menangani pertanyaan yang sebenarnya, sebagian besar jawaban telah mengacaukan masalah dan hanya mengatakan untuk tidak menggunakan swizzling.

Menggunakan metode mendesis seperti menggunakan pisau tajam di dapur. Beberapa orang takut pisau tajam karena mereka pikir mereka akan memotong diri mereka sendiri dengan buruk, tetapi kenyataannya pisau tajam lebih aman .

Metode swizzling dapat digunakan untuk menulis kode yang lebih baik, lebih efisien, lebih dapat dipelihara. Itu juga dapat disalahgunakan dan menyebabkan bug yang mengerikan.

Latar Belakang

Seperti halnya semua pola desain, jika kita sepenuhnya menyadari konsekuensi dari pola tersebut, kita dapat membuat keputusan yang lebih tepat tentang apakah akan menggunakannya atau tidak. Lajang adalah contoh yang baik dari sesuatu yang cukup kontroversial, dan untuk alasan yang baik - mereka benar-benar sulit diimplementasikan dengan benar. Banyak orang masih memilih untuk menggunakan lajang. Hal yang sama dapat dikatakan tentang swizzling. Anda harus membentuk opini Anda sendiri setelah Anda sepenuhnya memahami yang baik dan yang buruk.

Diskusi

Berikut adalah beberapa perangkap metode swizzling:

  • Metode swizzling bukan atom
  • Mengubah perilaku kode yang tidak dimiliki
  • Kemungkinan konflik penamaan
  • Swizzling mengubah argumen metode ini
  • Urutan swizzles penting
  • Sulit dipahami (terlihat rekursif)
  • Sulit di-debug

Poin-poin ini semuanya valid, dan dalam mengatasinya kita dapat meningkatkan pemahaman kita tentang metode swizzling serta metodologi yang digunakan untuk mencapai hasil. Saya akan mengambil masing-masing sekaligus.

Metode swizzling bukan atom

Saya belum melihat implementasi metode swizzling yang aman digunakan bersamaan 1 . Ini sebenarnya bukan masalah di 95% kasus yang Anda ingin menggunakan metode swizzling. Biasanya, Anda hanya ingin mengganti implementasi suatu metode, dan Anda ingin implementasi itu digunakan untuk seumur hidup program Anda. Ini berarti Anda harus melakukan metode Anda +(void)load. The loadmetode kelas dijalankan secara serial pada awal aplikasi Anda. Anda tidak akan memiliki masalah dengan konkurensi jika Anda melakukan swizzling di sini. Namun, jika Anda gagal +(void)initialize, Anda bisa berakhir dengan kondisi balapan dalam implementasi swizzling Anda dan runtime bisa berakhir dalam keadaan aneh.

Mengubah perilaku kode yang tidak dimiliki

Ini adalah masalah dengan swizzling, tapi ini semacam intinya. Tujuannya adalah untuk dapat mengubah kode itu. Alasan orang menunjukkan ini sebagai masalah besar adalah karena Anda tidak hanya mengubah hal-hal untuk satu hal NSButtonyang ingin Anda ubah, tetapi sebagai gantinya untuk semua NSButtonkejadian dalam aplikasi Anda. Untuk alasan ini, Anda harus berhati-hati saat berdecak, tetapi Anda tidak perlu menghindarinya sama sekali.

Pikirkan seperti ini ... jika Anda mengganti metode di kelas dan Anda tidak memanggil metode kelas super, Anda dapat menyebabkan masalah muncul. Dalam kebanyakan kasus, kelas super mengharapkan metode itu dipanggil (kecuali didokumentasikan sebaliknya). Jika Anda menerapkan pemikiran yang sama pada swizzling, Anda telah membahas sebagian besar masalah. Selalu panggil implementasi asli. Jika tidak, Anda mungkin berubah terlalu banyak untuk aman.

Kemungkinan konflik penamaan

Konflik penamaan adalah masalah di seluruh Kakao. Kami sering mengawali nama kelas dan nama metode dalam kategori. Sayangnya, konflik penamaan adalah wabah dalam bahasa kita. Dalam kasus swizzling, mereka tidak harus seperti itu. Kita hanya perlu mengubah cara kita berpikir tentang metode yang sedikit meliuk-liuk. Kebanyakan swizzling dilakukan seperti ini:

@interface NSView : NSObject
- (void)setFrame:(NSRect)frame;
@end

@implementation NSView (MyViewAdditions)

- (void)my_setFrame:(NSRect)frame {
    // do custom work
    [self my_setFrame:frame];
}

+ (void)load {
    [self swizzle:@selector(setFrame:) with:@selector(my_setFrame:)];
}

@end

Ini berfungsi dengan baik, tetapi apa yang akan terjadi jika my_setFrame:didefinisikan di tempat lain? Masalah ini tidak unik untuk swizzling, tetapi kita bisa mengatasinya. Solusinya memiliki manfaat tambahan untuk mengatasi perangkap lain juga. Inilah yang kami lakukan sebagai gantinya:

@implementation NSView (MyViewAdditions)

static void MySetFrame(id self, SEL _cmd, NSRect frame);
static void (*SetFrameIMP)(id self, SEL _cmd, NSRect frame);

static void MySetFrame(id self, SEL _cmd, NSRect frame) {
    // do custom work
    SetFrameIMP(self, _cmd, frame);
}

+ (void)load {
    [self swizzle:@selector(setFrame:) with:(IMP)MySetFrame store:(IMP *)&SetFrameIMP];
}

@end

Meskipun ini terlihat sedikit kurang seperti Objective-C (karena menggunakan pointer fungsi), ia menghindari konflik penamaan. Pada prinsipnya, ia melakukan hal yang sama persis seperti swizzling standar. Ini mungkin sedikit perubahan bagi orang yang telah menggunakan swizzling seperti yang telah didefinisikan untuk sementara waktu, tetapi pada akhirnya, saya pikir itu lebih baik. Metode swizzling didefinisikan sebagai berikut:

typedef IMP *IMPPointer;

BOOL class_swizzleMethodAndStore(Class class, SEL original, IMP replacement, IMPPointer store) {
    IMP imp = NULL;
    Method method = class_getInstanceMethod(class, original);
    if (method) {
        const char *type = method_getTypeEncoding(method);
        imp = class_replaceMethod(class, original, replacement, type);
        if (!imp) {
            imp = method_getImplementation(method);
        }
    }
    if (imp && store) { *store = imp; }
    return (imp != NULL);
}

@implementation NSObject (FRRuntimeAdditions)
+ (BOOL)swizzle:(SEL)original with:(IMP)replacement store:(IMPPointer)store {
    return class_swizzleMethodAndStore(self, original, replacement, store);
}
@end

Berputar dengan mengubah nama metode mengubah argumen metode ini

Ini yang terbesar di pikiran saya. Inilah alasan mengapa metode standar swizzling tidak boleh dilakukan. Anda mengubah argumen yang diteruskan ke implementasi metode asli. Di sinilah terjadi:

[self my_setFrame:frame];

Apa yang dilakukan garis ini adalah:

objc_msgSend(self, @selector(my_setFrame:), frame);

Yang akan menggunakan runtime untuk mencari implementasi my_setFrame:. Setelah implementasi ditemukan, ia memanggil implementasi dengan argumen yang sama yang diberikan. Implementasi yang ditemukannya adalah implementasi asli setFrame:, jadi berjalan di depan dan memanggil itu, tetapi _cmdargumennya tidak setFrame:seperti yang seharusnya. Sekarang my_setFrame:. Implementasi asli dipanggil dengan argumen yang tidak pernah diharapkan akan diterima. Ini tidak baik.

Ada solusi sederhana - gunakan teknik swizzling alternatif yang didefinisikan di atas. Argumen akan tetap tidak berubah!

Urutan swizzles penting

Urutan metode mendapatkan hal-hal yang membingungkan. Dengan asumsi setFrame:hanya didefinisikan NSView, bayangkan urutan hal-hal ini:

[NSButton swizzle:@selector(setFrame:) with:@selector(my_buttonSetFrame:)];
[NSControl swizzle:@selector(setFrame:) with:@selector(my_controlSetFrame:)];
[NSView swizzle:@selector(setFrame:) with:@selector(my_viewSetFrame:)];

Apa yang terjadi ketika metode pada NSButtonswizzled? Yah sebagian besar swizzling akan memastikan bahwa itu tidak menggantikan implementasi setFrame:untuk semua tampilan, sehingga akan memunculkan metode instance. Ini akan menggunakan implementasi yang ada untuk mendefinisikan ulang setFrame:di NSButtonkelas sehingga pertukaran implementasi tidak mempengaruhi semua tampilan. Implementasi yang ada adalah yang didefinisikan NSView. Hal yang sama akan terjadi ketika swizzling on NSControl(lagi menggunakan NSViewimplementasi).

Ketika Anda memanggil setFrame:sebuah tombol, karena itu ia akan memanggil metode swizzled Anda, dan kemudian melompat langsung ke setFrame:metode yang awalnya didefinisikan NSView. The NSControldan NSViewswizzled implementasi tidak akan dipanggil.

Tapi bagaimana jika pesanannya adalah:

[NSView swizzle:@selector(setFrame:) with:@selector(my_viewSetFrame:)];
[NSControl swizzle:@selector(setFrame:) with:@selector(my_controlSetFrame:)];
[NSButton swizzle:@selector(setFrame:) with:@selector(my_buttonSetFrame:)];

Karena pandangan swizzling terjadi terlebih dahulu, kontrol swizzling akan dapat menarik metode yang tepat. Demikian juga, karena kontrol swizzling adalah sebelum tombol swizzling, tombol akan menarik implementasi swizzled kontrol setFrame:. Ini agak membingungkan, tetapi ini adalah urutan yang benar. Bagaimana kita memastikan urutan ini?

Sekali lagi, cukup gunakan loaduntuk memutar hal-hal. Jika Anda masuk loaddan hanya membuat perubahan pada kelas yang sedang dimuat, Anda akan aman. The loadMetode jaminan bahwa metode yang super beban kelas akan dipanggil sebelum subclass. Kami akan mendapatkan pesanan yang tepat dan tepat!

Sulit dipahami (terlihat rekursif)

Melihat metode swizzled yang didefinisikan secara tradisional, saya pikir sangat sulit untuk mengatakan apa yang terjadi. Tetapi melihat cara alternatif yang telah kami lakukan di atas, cukup mudah untuk dipahami. Yang ini sudah dipecahkan!

Sulit di-debug

Salah satu kebingungan selama debugging adalah melihat backtrace aneh di mana nama-nama swizzled dicampuradukkan dan semuanya tercampur aduk di kepala Anda. Sekali lagi, implementasi alternatif membahas hal ini. Anda akan melihat fungsi yang diberi nama dengan jelas di backtraces. Namun, swizzling dapat menjadi sulit untuk di-debug karena sulit untuk mengingat apa dampak dari swizzling. Dokumentasikan kode Anda dengan baik (walaupun Anda pikir hanya Anda yang akan melihatnya). Ikuti praktik yang baik, dan Anda akan baik-baik saja. Tidak sulit untuk melakukan debug daripada kode multi-threaded.

Kesimpulan

Metode swizzling aman jika digunakan dengan benar. Tindakan keamanan sederhana yang dapat Anda lakukan adalah hanya masuk load. Seperti banyak hal dalam pemrograman, ini bisa berbahaya, tetapi memahami konsekuensinya akan memungkinkan Anda menggunakannya dengan benar.


1 Dengan menggunakan metode swizzling yang didefinisikan di atas, Anda dapat membuat thread aman jika Anda menggunakan trampolin. Anda akan membutuhkan dua trampolin. Pada awal metode, Anda harus menetapkan penunjuk fungsi store,, ke fungsi yang berputar sampai alamat yang storemenunjuk berubah. Ini akan menghindari kondisi balapan di mana metode swizzled dipanggil sebelum Anda dapat mengatur storepointer fungsi. Anda kemudian perlu menggunakan trampolin dalam kasus di mana implementasinya belum didefinisikan di kelas dan memiliki pencarian trampolin dan memanggil metode kelas super dengan benar. Menentukan metode sehingga secara dinamis mencari implementasi super akan memastikan bahwa urutan panggilan swizzling tidak masalah.

kita masih muda
sumber
24
Jawaban yang luar biasa informatif dan jelas secara teknis. Diskusi itu sangat menarik. Terima kasih telah meluangkan waktu untuk menulis ini.
Ricardo Sanchez-Saez
Bisakah Anda tunjukkan jika swizzling dalam pengalaman Anda dapat diterima di toko aplikasi. Saya mengarahkan Anda ke stackoverflow.com/questions/8834294 juga.
Dickey Singh
3
Swizzling dapat digunakan di App store. Banyak aplikasi dan kerangka kerja yang dilakukan (termasuk kami). Segala sesuatu di atas masih berlaku, dan Anda tidak dapat merusak metode pribadi (sebenarnya, secara teknis Anda bisa, tetapi Anda akan menghadapi risiko penolakan & bahayanya adalah utas berbeda).
wbyoung
Jawaban yang luar biasa. Tapi mengapa membuat swizzling di class_swizzleMethodAndStore () bukannya langsung di + swizzle: with: store :? Mengapa ini fungsi tambahan?
Frizlab
2
@Frizlab pertanyaan bagus! Ini benar-benar hanya gaya. Jika Anda menulis banyak kode yang bekerja langsung dengan API runtime Objective-C (dalam C), maka menyenangkan untuk menyebutnya gaya C untuk konsistensi. Satu-satunya alasan yang bisa saya pikirkan selain ini adalah jika Anda menulis sesuatu dalam C murni, maka itu masih lebih bisa dipanggil. Tidak ada alasan Anda tidak bisa melakukan semuanya dalam metode Objective-C.
wbyoung
12

Pertama saya akan mendefinisikan dengan tepat apa yang saya maksud dengan metode swizzling:

  • Merutekan ulang semua panggilan yang awalnya dikirim ke metode (disebut A) ke metode baru (disebut B).
  • Kami memiliki Metode B
  • Kami tidak memiliki metode A
  • Metode B melakukan beberapa pekerjaan kemudian memanggil metode A.

Metode swizzling lebih umum daripada ini, tetapi ini adalah kasus yang saya minati.

Bahaya:

  • Perubahan di kelas asli . Kami tidak memiliki kelas yang sedang kami geliat. Jika kelas berubah swizzle kami mungkin berhenti bekerja.

  • Sulit dipertahankan . Anda tidak hanya harus menulis dan memelihara metode swizzled. Anda harus menulis dan memelihara kode yang membentuk swizzle sebelumnya

  • Sulit di-debug . Sulit untuk mengikuti aliran swizzle, beberapa orang bahkan mungkin tidak menyadari swizzle telah terbentuk sebelumnya. Jika ada bug yang diperkenalkan dari swizzle (mungkin karena perubahan di kelas asli) mereka akan sulit untuk diselesaikan.

Singkatnya, Anda harus tetap melakukan swizzling seminimal mungkin dan mempertimbangkan bagaimana perubahan di kelas asli dapat mempengaruhi swizzle Anda. Anda juga harus berkomentar dan mendokumentasikan dengan jelas apa yang Anda lakukan (atau hanya menghindarinya sepenuhnya).

Robert
sumber
@ semua orang Saya telah menggabungkan jawaban Anda ke dalam format yang bagus. Jangan ragu untuk mengedit / menambahnya. Terima kasih atas masukan Anda.
Robert
Saya penggemar psikologi Anda. "Dengan kekuatan swizzling yang besar datang tanggung jawab swizzling yang hebat."
Jacksonkr
7

Bukan swizzling itu sendiri yang benar-benar berbahaya. Masalahnya adalah, seperti yang Anda katakan, itu sering digunakan untuk memodifikasi perilaku kelas kerangka kerja. Dengan asumsi Anda tahu sesuatu tentang cara kerja kelas privat itu "berbahaya." Bahkan jika modifikasi Anda bekerja hari ini, selalu ada peluang bahwa Apple akan mengubah kelas di masa depan dan menyebabkan modifikasi Anda rusak. Juga, jika banyak aplikasi berbeda melakukannya, itu membuat Apple jauh lebih sulit untuk mengubah kerangka kerja tanpa merusak banyak perangkat lunak yang ada.

Caleb
sumber
Saya akan mengatakan Dev seperti itu harus menuai apa yang mereka tabur - mereka dengan erat menyatukan kode mereka ke implementasi tertentu. Karena itu, kesalahan mereka jika implementasi berubah dengan cara halus yang seharusnya tidak merusak kode mereka jika bukan karena keputusan eksplisit mereka untuk memasangkan kode mereka sekencang yang mereka lakukan.
Arafangion
Tentu, tetapi katakan aplikasi yang sangat populer istirahat di bawah versi iOS berikutnya. Sangat mudah untuk mengatakan itu adalah kesalahan pengembang dan mereka harus tahu lebih baik, tetapi itu membuat Apple terlihat buruk di mata pengguna. Saya tidak melihat ada salahnya merayapi metode Anda sendiri atau bahkan kelas jika Anda ingin melakukan itu, selain dari titik Anda yang dapat membuat kode agak membingungkan. Itu mulai menjadi sedikit lebih berisiko ketika Anda swizzle kode yang dikendalikan oleh orang lain - dan itulah situasi di mana swizzling tampaknya paling menggoda.
Caleb
Apple bukan satu-satunya penyedia kelas dasar yang dapat dimodifikasi melalui siwzzling. Jawaban Anda harus diedit untuk menyertakan semua orang.
AR
@AR Mungkin, tetapi pertanyaannya ditandai iOS dan iPhone, jadi kita harus mempertimbangkannya dalam konteks itu. Mengingat bahwa aplikasi iOS harus secara statis menautkan pustaka dan kerangka kerja apa pun selain yang disediakan oleh iOS, Apple sebenarnya satu-satunya entitas yang dapat mengubah implementasi kelas kerangka kerja tanpa partisipasi dari pengembang aplikasi. Tapi saya setuju dengan poin bahwa masalah serupa mempengaruhi platform lain, dan saya akan mengatakan saran dapat digeneralisasi di luar swizzling juga. Secara umum, sarannya adalah: Jaga tangan Anda untuk diri sendiri. Hindari memodifikasi komponen yang dipelihara orang lain.
Caleb
Metode swizzling bisa jadi berbahaya. tetapi ketika kerangka kerja keluar mereka tidak sepenuhnya menghilangkan yang sebelumnya. Anda masih harus memperbarui kerangka dasar aplikasi Anda harapkan. dan pembaruan itu akan merusak aplikasi Anda. kemungkinan tidak menjadi masalah besar ketika ios diperbarui. Pola penggunaan saya adalah untuk mengganti reuseIdentifier default. Karena saya tidak memiliki akses ke variabel _reuseIdentifier dan tidak ingin menggantinya kecuali itu tidak disediakan, satu-satunya solusi saya adalah dengan subkelas setiap orang dan memberikan pengidentifikasi menggunakan kembali, atau swizzle dan menimpa jika nol. Jenis implementasi adalah kuncinya ...
The Lazy Coder
5

Digunakan dengan hati-hati dan bijak, itu dapat menyebabkan kode yang elegan, tetapi biasanya, itu hanya mengarah pada kode yang membingungkan.

Saya mengatakan bahwa itu harus dilarang, kecuali Anda kebetulan tahu bahwa itu memberikan kesempatan yang sangat elegan untuk tugas desain tertentu, tetapi Anda perlu tahu dengan jelas mengapa itu berlaku dengan baik untuk situasi, dan mengapa alternatif tidak bekerja secara elegan untuk situasi tersebut. .

Misalnya, salah satu aplikasi yang baik dari metode swizzling adalah isa swizzling, yaitu bagaimana ObjC mengimplementasikan Key Value Observing.

Contoh yang buruk mungkin bergantung pada metode swizzling sebagai cara untuk memperluas kelas Anda, yang mengarah pada penggandaan yang sangat tinggi.

Arafangion
sumber
1
-1 untuk penyebutan KVO — dalam konteks metode swizzling . isa swizzling hanyalah sesuatu yang lain.
Nikolai Ruhe
@NikolaiRuhe: Silakan memberikan referensi sehingga saya bisa tercerahkan.
Arafangion
Berikut ini adalah referensi untuk detail implementasi KVO . Metode swizzling dijelaskan pada CocoaDev.
Nikolai Ruhe
5

Meskipun saya telah menggunakan teknik ini, saya ingin menunjukkan bahwa:

  • Ini mengaburkan kode Anda karena dapat menyebabkan efek samping yang tidak terdokumentasi, meskipun diinginkan. Ketika seseorang membaca kode dia mungkin tidak menyadari perilaku efek samping yang diperlukan kecuali dia ingat untuk mencari basis kode untuk melihat apakah kode itu telah rusak. Saya tidak yakin bagaimana cara mengatasi masalah ini karena tidak selalu mungkin untuk mendokumentasikan setiap tempat di mana kode tergantung pada efek samping perilaku swizzled.
  • Ini dapat membuat kode Anda kurang dapat digunakan kembali karena seseorang yang menemukan segmen kode yang tergantung pada perilaku swizzled yang ingin mereka gunakan di tempat lain tidak dapat dengan mudah memotong dan menempelkannya ke beberapa basis kode lain tanpa juga menemukan dan menyalin metode swizzled.
pengguna3288724
sumber
4

Saya merasa bahwa bahaya terbesar adalah menciptakan banyak efek samping yang tidak diinginkan, sepenuhnya secara tidak sengaja. Efek samping ini dapat muncul sebagai 'bug' yang pada gilirannya membawa Anda ke jalan yang salah untuk menemukan solusinya. Dalam pengalaman saya, bahayanya adalah kode yang tidak terbaca, membingungkan, dan membuat frustrasi. Seperti ketika seseorang menggunakan pointer fungsi secara berlebihan di C ++.

AR
sumber
Ok, jadi masalahnya adalah sulit untuk di-debug. Masalah ini disebabkan karena pengulas tidak menyadari / lupa bahwa kode telah rusak dan mencari di tempat yang salah ketika debugging.
Robert
3
Ya, cukup banyak. Juga, ketika terlalu sering digunakan, Anda bisa masuk ke rantai atau jaringan swizzles yang tak ada habisnya. Bayangkan apa yang mungkin terjadi ketika Anda mengubah hanya salah satu dari mereka di masa depan? Saya menyamakan pengalaman dengan menumpuk cangkir teh atau memainkan 'kode jenga'
AR
4

Anda mungkin berakhir dengan kode yang terlihat aneh

- (void)addSubview:(UIView *)view atIndex:(NSInteger)index {
    //this looks like an infinite loop but we're swizzling so default will get called
    [self addSubview:view atIndex:index];

dari kode produksi aktual yang terkait dengan beberapa keajaiban UI.

quantumpotato
sumber
3

Metode swizzling bisa sangat membantu dalam pengujian unit.

Hal ini memungkinkan Anda untuk menulis objek tiruan dan memiliki objek tiruan yang digunakan sebagai ganti objek nyata. Kode Anda tetap bersih dan unit test Anda memiliki perilaku yang dapat diprediksi. Katakanlah Anda ingin menguji beberapa kode yang menggunakan CLLocationManager. Tes unit Anda dapat melemahkan startUpdatingLocation sehingga akan memberi makan set lokasi yang telah ditentukan untuk delegasi Anda dan kode Anda tidak perlu berubah.

pengguna3288724
sumber
isa-swizzling akan menjadi pilihan yang lebih baik.
vikingosegundo