我听说人们说方法混乱是一种危险的做法。甚至连起草的名字都暗示这有点作弊。
方法Swizzling正在修改映射,以便调用选择器A实际上将调用实现B。此方法的一种用法是扩展封闭源类的行为。
我们是否可以将风险正式化,以便任何决定是否使用毛毛雨的人都可以做出明智的决定,以决定这样做是否值得。
例如
- 命名冲突:如果该类以后扩展其功能以包括您添加的方法名称,则将引起大量问题。通过合理命名混淆方法来降低风险。
我听说人们说方法混乱是一种危险的做法。甚至连起草的名字都暗示这有点作弊。
方法Swizzling正在修改映射,以便调用选择器A实际上将调用实现B。此方法的一种用法是扩展封闭源类的行为。
我们是否可以将风险正式化,以便任何决定是否使用毛毛雨的人都可以做出明智的决定,以决定这样做是否值得。
例如
Answers:
我认为这是一个非常好的问题,很遗憾,大多数答案都没有解决实际问题,而是大多数回答都绕过了这个问题,并简单地说不要使用麻烦。
使用铁板烧的方法就像在厨房里用锋利的刀。有些人害怕锋利的刀,因为他们认为自己会严重割伤自己,但事实是锋利的刀更安全。
方法混乱可用于编写更好,更高效,更可维护的代码。它也可能被滥用并导致可怕的错误。
与所有设计模式一样,如果我们完全了解该模式的后果,则可以就是否使用它做出更明智的决定。单例是一个很有争议的很好的例子,并且有充分的理由-它们确实很难正确实现。但是,许多人仍然选择使用单例。关于下垂也可以这样说。一旦完全了解好与坏,就应该形成自己的见解。
这是方法混乱的一些陷阱:
这些要点都是有效的,在解决这些问题时,我们可以提高我们对方法混乱的理解以及用于获得结果的方法的理解。我一次接一个。
我还没有看到可以安全地同时使用方法swizzling的实现1。实际上,在95%的情况下,您都不希望使用方法混乱。通常,您只想替换一个方法的实现,并且希望在程序的整个生命周期中都使用该实现。这意味着您应该使方法陷入混乱+(void)load
。将load
在您的应用程序的启动串行执行类方法。如果您在此处进行复杂处理,那么并发不会有任何问题。如果你要陷入困境+(void)initialize
但是,,则可能会在混乱的实现中遇到竞争条件,并且运行时可能会陷入怪异的状态。
这是一个令人毛骨悚然的问题,但这很重要。目标是能够更改该代码。人们指出这很重要的原因是NSButton
,您不仅要更改要为其更改内容的一个实例,还需要更改NSButton
应用程序中的所有实例。出于这个原因,您在打喷嚏时应格外小心,但不必完全避免。
这样想吧...如果您在类中重写了一个方法而没有调用超类方法,则可能会引起问题。在大多数情况下,超类期望该方法被调用(除非另有说明)。如果您将同样的想法应用到问题上,则涵盖了大多数问题。始终调用原始实现。如果您不这样做,则可能是为了安全起见,所做的更改太多。
命名冲突是整个可可中的一个问题。我们经常在类别中为类名和方法名加上前缀。不幸的是,命名冲突在我们的语言中是一个困扰。但是,如果出现混乱,则不必如此。我们只需要改变对方法略微混乱的思考方式即可。大部分的处理都是这样的:
@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
这很好用,但是如果my_setFrame:
在其他地方定义会发生什么呢?这个问题并不是唯一的问题,但是我们仍然可以解决它。解决方法还具有解决其他陷阱的额外好处。这是我们要做的:
@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
尽管这看起来不太像Objective-C(因为它使用的是函数指针),但它避免了任何命名冲突。原则上,它所做的事情与标准混淆完全相同。对于已经定义了一段时间的使用毛发的人来说,这可能是一个变化,但是最后,我认为它会更好。因此定义了swizzling方法:
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
这是我脑海中最大的一个。这就是不应进行标准方法筛选的原因。您正在更改传递给原始方法的实现的参数。这是发生的地方:
[self my_setFrame:frame];
该行的作用是:
objc_msgSend(self, @selector(my_setFrame:), frame);
它将使用运行时来查找的实现my_setFrame:
。找到实现后,它将使用给定的参数调用实现。它找到的实现是的原始实现setFrame:
,因此继续进行调用,但是_cmd
参数与实际情况不符setFrame:
。现在my_setFrame:
。最初的实现使用一个从未期望过的参数来调用。不好
有一个简单的解决方案-使用上面定义的替代滚动方法。参数将保持不变!
方法混乱的顺序很重要。假设setFrame:
仅在上定义NSView
,请想象这样的事情顺序:
[NSButton swizzle:@selector(setFrame:) with:@selector(my_buttonSetFrame:)];
[NSControl swizzle:@selector(setFrame:) with:@selector(my_controlSetFrame:)];
[NSView swizzle:@selector(setFrame:) with:@selector(my_viewSetFrame:)];
当方法NSButton
失效时会发生什么?很好的解决方法是确保不会替换setFrame:
所有视图的实现,因此它将拉起实例方法。这将使用现有的实现setFrame:
在NSButton
类中重新定义,以便交换实现不会影响所有视图。现有的实现是在上定义的NSView
。当NSControl
继续使用时(再次使用NSView
实现),将会发生相同的事情。
因此,当您调用setFrame:
按钮时,它将调用您的混乱方法,然后直接跳至setFrame:
最初在上定义的方法NSView
。在NSControl
和NSView
绞合的实现将不会被调用。
但是,如果订单是:
[NSView swizzle:@selector(setFrame:) with:@selector(my_viewSetFrame:)];
[NSControl swizzle:@selector(setFrame:) with:@selector(my_controlSetFrame:)];
[NSButton swizzle:@selector(setFrame:) with:@selector(my_buttonSetFrame:)];
由于视图混写首先发生,控制混写就能拉起正确的方法。同样,由于控件的刷新在按钮的刷新之前发生,因此按钮将拉出控件的控件的实现setFrame:
。这有点令人困惑,但这是正确的顺序。我们如何确保事情的秩序?
同样,只是用来load
使事情变得混乱。如果您陷入困境,load
而仅对正在加载的类进行更改,那将是安全的。该load
方法保证将在任何子类之前调用超类加载方法。我们将获得正确的订单!
观察传统定义的混淆方法,我认为很难说出正在发生什么。但是,看看我们在上面做过的另一种选择,这很容易理解。这个已经解决了!
调试过程中的困惑之一是看到一个奇怪的回溯,在回溯中,混乱的名字混在一起,一切都变得混乱。同样,替代实现解决了这个问题。您将在回溯中看到名称明确的函数。仍然很难调试,因为很难记住调试产生的影响。很好地记录代码(即使您认为自己是唯一会看到它的人)。遵循良好做法,您会没事的。调试不比多线程代码难。
如果使用正确,方法冲洗是安全的。您可以采取的一种简单的安全措施是只进入load
。像编程中的许多事情一样,它可能很危险,但是了解其后果将使您能够正确使用它。
1如果使用蹦床,可以使用上述定义的旋转方法使线程安全。您将需要两个蹦床。在方法开始时,您必须将函数指针分配给store
旋转的函数,直到store
指向的地址更改为止。这样可以避免在设置store
功能指针之前调用swizzled方法的任何竞争情况。然后,如果尚未在类中定义实现,则需要使用蹦床,并需要进行蹦床查找并正确调用超类方法。定义该方法,以便它动态查找超级实现,将确保无效调用的顺序无关紧要。
首先,我将准确定义方法的含义:
方法混淆比这更笼统,但这是我感兴趣的情况。
危险:
原班的变化。我们没有自己正在上课的班级。如果班级改变,我们的麻烦可能会停止。
难以维护。您不仅需要编写和维护复杂的方法。您必须编写和维护执行麻烦的代码
难以调试。很难跟踪麻烦的发生,有些人甚至可能没有意识到已经进行了麻烦。如果有麻烦带来的错误(可能是由于原始类的更改所致),将很难解决。
总而言之,您应该尽量减少麻烦,并考虑原始类的更改如何影响您的麻烦。另外,您应该清楚地评论和记录您的工作(或完全避免这样做)。
真正危险的不是漩涡本身。正如您所说,问题是它通常用于修改框架类的行为。假设您对那些“危险”的私有类如何工作有所了解。即使您的修改在今天起作用,Apple仍然有可能会在将来更改类,并导致您的修改失败。同样,如果有许多不同的应用程序执行此操作,则在不破坏大量现有软件的情况下,Apple很难更改框架。
谨慎而明智地使用它可以生成优美的代码,但是通常,它只会导致混乱的代码。
我说应该禁止使用它,除非您碰巧知道它为特定的设计任务提供了非常优雅的机会,但是您需要清楚地知道为什么它很好地适用于这种情况,以及为什么替代方案不能很好地适用于这种情况。
例如,方法筛选的一个很好的应用是isa筛选,这就是ObjC如何实现键值观察。
一个不好的例子可能是依靠方法混乱作为扩展类的一种方法,这导致了极高的耦合。
方法混淆可能对单元测试很有帮助。
它允许您编写一个模拟对象,并使用该模拟对象代替实际对象。您的代码保持整洁,并且单元测试具有可预测的行为。假设您要测试一些使用CLLocationManager的代码。您的单元测试可能会使startUpdatingLocation混乱,因此它将给您的委托提供一组预定的位置,并且您的代码不必更改。