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 load
metode 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 NSButton
yang ingin Anda ubah, tetapi sebagai gantinya untuk semua NSButton
kejadian 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 _cmd
argumennya 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 NSButton
swizzled? 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 NSButton
kelas 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 NSView
implementasi).
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 NSControl
dan NSView
swizzled 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 load
untuk memutar hal-hal. Jika Anda masuk load
dan hanya membuat perubahan pada kelas yang sedang dimuat, Anda akan aman. The load
Metode 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 store
menunjuk berubah. Ini akan menghindari kondisi balapan di mana metode swizzled dipanggil sebelum Anda dapat mengatur store
pointer 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.