Objective-C'de Yöntem Swizzling'in Tehlikeleri Nelerdir?


235

İnsanların dolaşımın tehlikeli bir uygulama olduğunu söylediklerini duydum. Swizzling adı bile bunun bir hile olduğunu gösteriyor.

Yöntem Swizzling , eşlemeyi değiştirerek A seçicisinin aslında B uygulamasını çağırmasını sağlayacaktır. Bunun bir kullanımı kapalı kaynak sınıflarının davranışını genişletmektir.

Riskleri resmileştirebilir, böylece swizzling kullanılıp kullanılmayacağına karar veren herkes, yapmaya çalıştıkları şey için buna değip değmeyeceğine dair bilinçli bir karar verebilir.

Örneğin

  • Çarpışmaları Adlandırma : Sınıf daha sonra eklediğiniz yöntem adını içerecek şekilde işlevlerini genişletirse, çok büyük sorunlara neden olur. Swizzled yöntemleri mantıklı bir şekilde adlandırarak riski azaltın.

Okuduğunuz koddan beklediğiniz başka bir şey almak çok iyi değil
Martin Babacaev

Bu python dekoratörler çok temiz ne yapmak için kirli bir yol gibi geliyor
Claudiu

Yanıtlar:


439

Bunun gerçekten harika bir soru olduğunu düşünüyorum ve gerçek soruyu ele almak yerine çoğu cevabın sorunu çözdüğü ve sadece swizzling kullanmamanın söylendiği utanç verici.

Cızırtılı yöntem kullanmak mutfakta keskin bıçak kullanmak gibidir. Bazı insanlar keskin bıçaklardan korkarlar çünkü kendilerini kötü bir şekilde keseceklerini düşünürler, ancak gerçek şu ki keskin bıçaklar daha güvenlidir .

Yöntem swizzling daha iyi, daha verimli, daha sürdürülebilir kod yazmak için kullanılabilir. Ayrıca istismar edilebilir ve korkunç hatalara yol açabilir.

Arka fon

Tüm tasarım modellerinde olduğu gibi, modelin sonuçlarının tamamen farkında olursak, onu kullanıp kullanmama konusunda daha bilinçli kararlar verebiliriz. Singletons oldukça tartışmalı bir şeye iyi bir örnektir ve iyi bir nedenden dolayı - düzgün bir şekilde uygulanması gerçekten zordur. Yine de birçok kişi hala singleton kullanmayı tercih ediyor. Aynı şey swizzling için de söylenebilir. Hem iyiyi hem de kötüyü tam olarak anladıktan sonra kendi fikrinizi oluşturmalısınız.

Tartışma

İşte yöntem swizzling tuzaklarından bazıları:

  • Yöntem swizzling atomik değil
  • Sahip olunmayan kodun davranışını değiştirir
  • Olası adlandırma çakışmaları
  • Swizzling, yöntemin argümanlarını değiştirir
  • Swizzles sırası önemlidir
  • Anlaması zor (özyinelemeli görünüyor)
  • Hata ayıklamak zor

Bu noktaların hepsi geçerlidir ve bunları ele alırken hem yöntem swizzling anlayışımızı hem de sonuca ulaşmak için kullanılan metodolojiyi geliştirebiliriz. Her birini birer birer alacağım.

Yöntem swizzling atomik değil

Henüz aynı anda kullanmak güvenli bir yöntem swizzling bir uygulama görmedim 1 . Yöntem swizzling kullanmak istediğiniz vakaların% 95'inde bu bir sorun değildir. Genellikle, bir yöntemin uygulanmasını değiştirmek istersiniz ve bu uygulamanın programınızın tüm ömrü boyunca kullanılmasını istersiniz. Bu, yönteminizi girerek yapmanız gerektiği anlamına gelir +(void)load. loadSınıf yöntemi uygulamanın başlangıcında seri yürütülür. Burada dolandırıcılık yaparsanız eşzamanlılık ile ilgili herhangi bir sorun olmaz. +(void)initializeBununla birlikte, eğer swizzle içindeyseniz, swizzling uygulamanızda bir yarış koşulu ile sonuçlanabilir ve çalışma zamanı garip bir duruma gelebilir.

Sahip olunmayan kodun davranışını değiştirir

Bu kıpır kıpır bir sorun, ama bütün mesele bu. Amaç bu kodu değiştirebilmektir. İnsanların bunu büyük bir şey olarak göstermesinin nedeni, sadece şeyleri değiştirmek istediğiniz bir örnek için bir NSButtonşeyleri değiştirmek değil, bunun yerine NSButtonuygulamanızdaki tüm örnekler için bir şeyleri değiştirmenizdir . Bu nedenle, swizzle yaparken dikkatli olmalısınız, ancak tamamen kaçınmanıza gerek yoktur.

Şöyle düşünün ... sınıftaki bir yöntemi geçersiz kılarsanız ve süper sınıf yöntemini çağırmazsanız, sorunların ortaya çıkmasına neden olabilirsiniz. Çoğu durumda, süper sınıf bu yöntemin çağrılmasını bekler (aksi belirtilmedikçe). Aynı düşünceyi swizzling'e uygularsanız, çoğu konuyu ele aldınız. Daima orijinal uygulamayı arayın. Eğer yapmazsan, muhtemelen güvende olmak için çok fazla değişiyorsun.

Olası adlandırma çakışmaları

Adlandırma anlaşmazlıkları tüm Kakao genelinde bir sorundur. Kategorilere sınıf adlarını ve yöntem adlarını sık sık önek olarak ekleriz. Ne yazık ki, adlandırma çatışmaları dilimizde bir veba. Yine de dolandırıcılık durumunda olmak zorunda değiller. Sadece yöntem dolandırıcılığı hakkında düşünme şeklimizi değiştirmemiz gerekiyor. Çoğu swizzling böyle yapılır:

@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

Bu iyi çalışıyor, ancak my_setFrame:başka bir yerde tanımlanırsa ne olurdu ? Bu sorun dolandırıcılığa özgü değil, ama yine de bu sorunu çözebiliriz. Geçici çözümün diğer tuzaklara karşı ek bir yararı da vardır. Bunun yerine yaptıklarımız:

@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

Bu, Objective-C'ye (işlev işaretçileri kullandığından) biraz daha az benzese de, adlandırma çakışmalarını önler. Prensip olarak, standart swizzling ile aynı şeyi yapıyor. Bu, bir süredir tanımlandığı gibi swizzling'i kullanan insanlar için biraz değişiklik olabilir, ancak sonunda, daha iyi olduğunu düşünüyorum. Swizzling yöntemi bu şekilde tanımlanır:

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

Yöntemleri yeniden adlandırarak dolaşma yöntemin argümanlarını değiştirir

Aklımdaki büyük olan bu. Bu, standart yöntem swizzlinginin yapılmaması gerektiğidir. Özgün yöntemin uygulanmasına iletilen bağımsız değişkenleri değiştiriyorsunuz. İşte burada:

[self my_setFrame:frame];

Bu çizginin yaptığı:

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

Hangi uygulama çalışma aramak için kullanır my_setFrame:. Uygulama bulunduğunda, aynı argümanlarla uygulamayı başlatır. Bulduğu uygulama orijinal uygulamasıdır setFrame:, bu yüzden devam eder ve bunu çağırır, ancak _cmdargüman setFrame:olması gerektiği gibi değildir . Şimdi my_setFrame:. Orijinal uygulama, asla almasını beklemediği bir argümanla çağrılıyor. Bu iyi değil.

Basit bir çözüm var - yukarıda tanımlanan alternatif swizzling tekniğini kullanın. Argümanlar değişmeden kalacaktır!

Swizzles sırası önemlidir

Yöntemlerin değişme sırası önemlidir. Varsayım setFrame:sadece üzerinde tanımlanır, NSViewbu şeylerin sırasını hayal edin:

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

Üzerinde yöntem NSButtondonduğunda ne olur ? Çoğu swizzling, setFrame:tüm görünümlerin uygulanmasının yerini almamasını sağlayacaktır , bu nedenle örnek yöntemini çekecektir . Bu, uygulamada alış verişin tüm görünümleri etkilememesi setFrame:için NSButtonsınıfta yeniden tanımlamak için mevcut uygulamayı kullanır . Mevcut uygulama, tanımlanan uygulamadır NSView. Aynı şey swizzling yaparken de olur NSControl(yine NSViewuygulamayı kullanarak ).

Bir setFrame:düğmeyi çağırdığınızda, swizzled yönteminizi çağırır ve doğrudan setFrame:başlangıçta tanımlanan yönteme atlar NSView. NSControlVe NSViewswizzled uygulamalar denilen edilmeyecektir.

Peki ya sipariş olsaydı:

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

Görünümde şişme ilk olarak gerçekleştiğinden, kontrolde şişme doğru yöntemi çekebilecektir . Benzer şekilde, kontrol dalgalanması düğmenin şişmesinden önce olduğundan, düğme kontrolün swizzled uygulamasını yukarı çekecektir setFrame:. Bu biraz kafa karıştırıcı, ama bu doğru sıra. Bu şeylerin düzenini nasıl sağlayabiliriz?

Yine, sadece loadişleri karıştırmak için kullanın . İçeri girerseniz loadve yalnızca yüklenen sınıfta değişiklikler yaparsanız, güvende olursunuz. loadSüper sınıf yük yöntemi, herhangi bir alt sınıfları önce adı verilecek bu yöntem garanti eder. Doğru siparişi alacağız!

Anlaması zor (özyinelemeli görünüyor)

Geleneksel olarak tanımlanmış swizzled yöntemine baktığımızda, neler olduğunu anlatmanın gerçekten zor olduğunu düşünüyorum. Ancak yukarıda dolandırıcılık yapmanın alternatif yoluna bakıldığında, anlaşılması oldukça kolaydır. Bu zaten çözüldü!

Hata ayıklamak zor

Hata ayıklama sırasındaki karışıklıklardan biri, swizzled isimlerin karıştığı ve her şeyin kafanızda karıştığı garip bir geri iz görmektir. Yine, alternatif uygulama bunu ele almaktadır. Geriye çekmelerde açıkça adlandırılmış işlevleri göreceksiniz. Yine de, swizzling'in hatalarını gidermek zor olabilir, çünkü swizzling'in ne gibi bir etkisi olduğunu hatırlamak zor. Kodunuzu iyi bir şekilde belgelendirin (kodu görecek tek kişi olduğunuzu düşünseniz bile). İyi uygulamaları takip edin, iyi olacaksınız. Hata ayıklamak çok iş parçacıklı koddan daha zor değildir.

Sonuç

Doğru şekilde kullanılırsa yöntem swizzling güvenlidir. Alabileceğiniz basit bir güvenlik önlemi sadece içeri girmektir load. Programlamadaki birçok şey gibi, tehlikeli olabilir, ancak sonuçların anlaşılması onu düzgün bir şekilde kullanmanıza izin verecektir.


1 Yukarıda tanımlanan swizzling yöntemini kullanarak, trambolin kullanacak olursanız iş parçacığını güvenli hale getirebilirsiniz. İki trambolin gerekir. Yöntemin başlangıcında, işaret storeeden adres değişene kadar dönen bir işleve işlev işaretçisini atamanız gerekir store. Bu, storeişlev işaretçisini ayarlamadan önce swizzled yönteminin çağrıldığı herhangi bir yarış koşulunu önler . Daha sonra uygulamanın sınıfta önceden tanımlanmadığı durumlarda bir trambolin kullanmanız ve trambolin aramasına sahip olmanız ve süper sınıf yöntemini doğru bir şekilde çağırmanız gerekir. Yöntemi dinamik olarak süper uygulamaya bakacak şekilde tanımlamak, dönen çağrıların sırasının önemli olmamasını sağlayacaktır.


24
Şaşırtıcı derecede bilgilendirici ve teknik olarak sağlam bir cevap. Tartışma gerçekten ilginç. Bunu yazmaya zaman ayırdığınız için teşekkürler.
Ricardo Sanchez-Saez

Deneyimlerinizde dolaşımın uygulama mağazalarında kabul edilebilir olup olmadığını belirtebilir misiniz? Sizi stackoverflow.com/questions/8834294 adresine de yönlendiriyorum .
Dickey Singh

3
Swizzling App Store'da kullanılabilir. Birçok uygulama ve çerçeve yapar (bizimki dahil). Yukarıdaki her şey hala geçerlidir ve özel yöntemleri değiştiremezsiniz (aslında, teknik olarak yapabilirsiniz, ancak reddedilme riski ve bunun tehlikeleri farklı bir iş parçacığı içindir).
wbyoung

Şaşırtıcı cevap. Ama neden doğrudan + swizzle yerine class_swizzleMethodAndStore () içindeki swizzling'i:: store :? Neden bu ek fonksiyon?
Frizlab

2
@Frizlab güzel soru! Gerçekten sadece stil bir şey. Doğrudan Objective-C çalışma zamanı API'sı (C) ile çalışan bir grup kod yazıyorsanız, tutarlılık için C stili olarak adlandırmak güzeldir. Bunun dışında düşünebilmemin tek nedeni, saf C'de bir şey yazdıysanız, daha da çağrılabilir. Yine de, Objective-C yönteminde hepsini yapamamanız için hiçbir neden yok.
wbyoung

12

İlk önce yöntem swizzling ile ne demek istediğimi tanımlayacağım:

  • Başlangıçta bir yönteme (A olarak adlandırılır) gönderilen tüm çağrıları yeni bir yönteme (B olarak adlandırılır) yönlendirir.
  • Yöntem B'ye sahibiz
  • A yöntemine sahip değiliz
  • Yöntem B biraz işe yarar, sonra yöntem A'yı çağırır.

Yöntem swizzling bundan daha genel, ama ilgilendiğim durum bu.

Tehlikeleri:

  • Orijinal sınıftaki değişiklikler . Dolandırıcı olduğumuz sınıfa sahip değiliz. Sınıf değişirse swizzle çalışmayı durdurabilir.

  • Bakımı zor . Sadece swizzled yöntemini yazmak ve korumak zorunda değilsiniz. Swizzle hazırlayan kodu yazmalı ve korumalısınız

  • Hata ayıklamak zor . Bir swizzle akışını takip etmek zordur, bazı insanlar swizzle önceden oluşturulduğunu bile fark etmeyebilir. Swizzle'dan getirilen hatalar varsa (belki de orijinal sınıftaki değişikliklere aidat) çözülmesi zor olacaktır.

Özet olarak, swizzling'i minimumda tutmalı ve orijinal sınıftaki değişikliklerin swizzle'ınızı nasıl etkileyebileceğini düşünmelisiniz. Ayrıca ne yaptığınızı açıkça yorumlamalı ve belgelemelisiniz (veya sadece bundan tamamen kaçınmalısınız).


@Herhangi bir cevaplarınızı güzel bir biçimde birleştirdim. Düzenlemek / eklemek için çekinmeyin. Girdiniz için teşekkürler.
Robert

Ben senin psikolojinin hayranıyım. "Büyük dolandırıcılık gücüyle büyük dolandırıcılık sorumluluğu gelir."
Jacksonkr

7

Gerçekten tehlikeli olan, dönen değil. Sorun, dediğiniz gibi, genellikle çerçeve sınıflarının davranışını değiştirmek için kullanılır. Bu özel sınıfların nasıl çalıştığı hakkında "tehlikeli" bir şey bildiğinizi varsayar. Değişiklikleriniz bugün çalışıyor olsa bile, Apple'ın gelecekte sınıfı değiştirme ve değişikliklerinizin bozulmasına neden olma ihtimali her zaman vardır. Ayrıca, birçok farklı uygulama yaparsa, Apple'ın mevcut yazılımı çok fazla bozmadan çerçeveyi değiştirmesini zorlaştırır.


Böyle Devs eklediklerini biçmek gerektiğini söyleyebilirim - kodlarını sıkı bir şekilde belirli bir uygulamaya bağladılar. Bu nedenle, bu uygulama, kodlarını olduğu gibi sıkıca birleştirmek için açık kararları olmasaydı, kodlarını kırmaması gereken ince bir şekilde değişirse onların hatasıdır.
Arafangion

Tabii, ancak iOS'un bir sonraki sürümü altında çok popüler bir uygulamanın sonlandığını söyleyin. Geliştiricinin hatası olduğunu söylemek daha kolaydır ve daha iyi bilmeleri gerekir, ancak Apple'ı kullanıcının gözünde kötü gösterir. Kodunuzu biraz kafa karıştırıcı yapabileceğinizden başka, kendi yöntemlerinizi veya hatta sınıfları dolaştırırken herhangi bir zarar görmüyorum. Başkası tarafından kontrol edilen kodu değiştirdiğinizde biraz daha riskli olmaya başlar - ve tam olarak swizzling'in en cazip olduğu durum budur.
Caleb

Apple, siwzzling ile değiştirilebilen temel sınıfların tek sağlayıcısı değildir. Cevabınız herkesi kapsayacak şekilde düzenlenmelidir.
AR

@AR Belki de, soru iOS ve iPhone olarak etiketlenmiştir, bu yüzden bu bağlamda düşünmeliyiz. İOS uygulamalarının iOS tarafından sağlananlar dışındaki tüm kitaplıkları ve çerçeveleri statik olarak bağlaması gerektiği göz önüne alındığında, Apple aslında uygulama geliştiricilerinin katılımı olmadan çerçeve sınıflarının uygulanmasını değiştirebilen tek varlıktır. Ancak, benzer sorunların diğer platformları etkilediği noktasında hemfikirim ve tavsiyenin de fırtınanın ötesinde genelleştirilebileceğini söyleyebilirim. Genel olarak, tavsiye: Ellerinizi kendinize saklayın. Diğer kişilerin koruduğu bileşenleri değiştirmektan kaçının.
Caleb

Yöntem swizzling o kadar tehlikeli olabilir. ancak çerçeveler ortaya çıktıkça öncekileri tamamen atlamıyorlar. yine de uygulamanızın beklediği temel çerçeveyi güncellemeniz gerekir. ve bu güncelleme uygulamanızı bozar. ios güncellendiğinde büyük bir sorun olmayacaktır. Kullanım kalıbım varsayılan reuseIdentifier'ı geçersiz kılmaktı. _ReuseIdentifier değişkenine erişimim olmadığı ve tek çözümüm sağlanmadıkça değiştirmek istemediğim için, tek çözümüm her birini alt sınıflara ayırmak ve yeniden tanımlayıcı sağlamak veya swille ve nil ise geçersiz kılmaktı. Uygulama türü anahtar ...
Tembel Kodlayıcı

5

Dikkatli ve akıllıca kullanıldığında, zarif koda yol açabilir, ancak genellikle sadece kafa karıştırıcı koda yol açar.

Belirli bir tasarım görevi için çok zarif bir fırsat sunduğunu bilmediğiniz sürece yasaklanması gerektiğini söylüyorum, ancak bunun neden durum için iyi uygulandığını ve alternatiflerin durum için neden zarif bir şekilde çalışmadığını açıkça bilmeniz gerekir. .

Örneğin, yöntem swizzling'in iyi bir uygulaması, ObjC'nin Anahtar Değer Gözlemini nasıl uyguladığıdır.

Kötü bir örnek, sınıflarınızı genişletmenin bir yolu olarak yöntem swizzling'e güveniyor olabilir ve bu da son derece yüksek birleşmeye yol açar.


1
KVO'dan bahsetmek için -1 — yöntem swizzling bağlamında . isa swizzling başka bir şeydir.
Nikolai Ruhe

@NikolaiRuhe: Aydınlanabilmem için lütfen referans vermekten çekinmeyin.
Arafangion


5

Bu tekniği kullansam da şunu belirtmek isterim:

  • Belgeniz istenmediği halde yan etkilere neden olabileceğinden kodunuzu gizler. Kodu okuduğunda, kodlanmış olup olmadığını görmek için kod tabanını aramayı hatırlamadığı sürece gereken yan etki davranışından habersiz olabilir. Kod yan etkisi swizzled davranışına bağlı her yerde belgelemek her zaman mümkün değildir çünkü bu sorunu hafifletmek için emin değilim.
  • Kodunuzu daha az yeniden kullanılabilir hale getirebilir, çünkü başka bir yerde kullanmak istedikleri swizzled davranışına bağlı bir kod parçası bulan, swizzled yöntemini bulup kopyalamadan basitçe kesip başka bir kod tabanına yapıştıramaz.

4

En büyük tehlikenin, tamamen kazara, istenmeyen birçok yan etki yaratmakta olduğunu hissediyorum. Bu yan etkiler kendilerini 'böcek' olarak gösterebilir ve bu da çözümü bulmak için sizi yanlış yola götürür. Deneyimlerime göre, tehlike okunaksız, kafa karıştırıcı ve sinir bozucu bir koddur. Birisi C ++ 'da işlev işaretçileri aşırı kullandığında gibi.


Tamam, sorun şu ki hata ayıklamak zor. Sorunlar, yorumcular kodun swizzled olduğunu ve hata ayıklama sırasında yanlış yere baktığını fark etmediği için kaynaklanır.
Robert

3
Evet hemen hemen. Ayrıca, aşırı kullanıldığında sonsuz bir zincir veya swizzles ağına girebilirsiniz. Gelecekte bir noktada sadece bir tanesini değiştirdiğinizde ne olacağını hayal edin? Çay bardaklarını istifleme veya 'code jenga' oynama deneyimini beğendim
AR

4

Bunun gibi tuhaf görünümlü kodlarla karşılaşabilirsiniz

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

bazı UI büyü ile ilgili gerçek üretim kodundan.


3

Yöntem testi çok yararlı olabilir birim test etmektir.

Bir sahte nesne yazmanıza ve gerçek nesne yerine o sahte nesneye sahip olmanıza olanak tanır. Kodunuzun temiz kalması ve birim testinizin öngörülebilir davranışı vardır. Diyelim ki CLLocationManager kullanan bazı kodları test etmek istiyorsunuz. Birim testiniz startUpdatingLocation ayarını devredebilir, böylece temsilcinize önceden belirlenmiş bir konum kümesi besler ve kodunuzun değiştirilmesi gerekmez.


isa-swizzling daha iyi bir seçim olacaktır.
vikingosegundo
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.