重写 Objective-C 框架方法是一个好主意吗?

4

ObjC有一种非常独特的覆盖方法,即您可以通过“类别”或“Swizzling”来覆盖OSX自己框架中的函数。甚至可以覆盖仅在内部使用的“深层”函数。

有人能否给我提供一个很好的理由来做这件事的例子?一些你会在发布的商业软件中使用而不仅仅是一些内部使用的工具?

例如,也许你想改进一些内置方法,或者可能有一个框架方法中的错误需要修复。

此外,您能解释为什么这可以通过ObjC的功能最好实现,而不是C++ / Java等语言吗?我的意思是,我听说过加载C库的能力,但允许替换某些以前加载的同名函数。ObjC如何更好地修改库行为?


2
Swizzling已经成为一个禁忌,如果使用它将导致iOS应用程序被iTunes拒绝。 - Till
你的帖子中似乎有太多问题了:),但你所关注的内容很有趣。但我确定为什么交换方法可能不是一个好选择,因为这个问题的答案在这里,https://dev59.com/9nI-5IYBdhLWcg3w3cg8 - Futur
3个回答

2
如果你不仅仅是想进行简单的 swizzling,而是要实际修改库,我可以想到一些有用的例子。
自 iOS 5 起,NSURLConnection 提供了 sendAsynchronousRequest:queue:completionHandler: 方法,它是一种基于块(闭包)的方式,可以从任何可通过 URL(本地或远程)标识的资源中执行异步加载。这是一种非常有用的方法,因为它使您的代码更加清晰、简洁,比传统的委托模式更容易将相关部分的代码放在一起。
该方法在iOS 4中没有提供。因此,在我的项目中,当应用程序启动时(通过适当的+(void)load),我检查该方法是否已定义。如果没有,我会将其实现补丁到类上。从此以后,程序的其他部分可以按照iOS 5的规范编写,而无需执行任何版本或可用性检查,就像我只针对iOS 5一样,除了它也可以在iOS 4上运行。
在Java或C ++中,我想通过创建自己的类来发出URL连接,每次调用时执行运行时检查来实现相同类型的事情。这是一个更糟糕的解决方案,因为它更难退回。反过来,如果有一天我决定仅支持iOS 5,我只需删除添加了sendAsynchronousRequest:...的源文件即可。没有其他变化。
关于方法混淆,我只看到它被建议在某些情况下使用,即当有人想要更改现有类的功能,但无法访问创建该类的代码时。因此,通常你试图通过对其实现进行假设来从外部修改逻辑不透明的代码。我不会支持任何语言中的这个想法。我猜在Objective-C中更多地推荐它是因为苹果更倾向于使事情变得不透明(例如,在iOS 3.1之前要显示自定义相机视图的每个应用程序,以及在iOS 4.0之前要对相机输入执行自定义处理的每个应用程序等),而不是因为这是Objective-C中的一个好主意。它不是。

编辑:进一步阐述 - 我不能发布完整的代码,因为我是作为工作的一部分编写的,但我有一个名为NSURLConnectionAsyncForiOS4的类,它实现了sendAsynchronousRequest:queue:completionHandler:。该实现实际上非常简单,只是将操作调度到指定队列,通过旧的sendSynchronousRequest:...接口执行同步加载,然后将结果发布到处理程序上。

那个类有一个 + (void)load,这是你添加到一个类中的类方法,它将在该类被加载到内存中后立即执行,实际上作为元类的全局构造函数,并具有所有通常的警告。在我的 +load 中,我直接使用 Objective-C 运行时通过其 C 接口检查是否在 NSURLConnection 上定义了 sendAsynchronousRequest:...。如果没有,则将我的实现添加到 NSURLConnection 中,因此从此以后它就被定义了。这明确不是 swizzling - 我没有调整任何东西的现有实现,我只是在 Apple 的实现不可用时添加了一个用户提供的实现。相关的运行时调用是 objc_getClassclass_getClassMethodclass_addMethod。在代码的其余部分,每当我想执行异步 URL 连接时,我只需编写例如:
[NSURLConnection sendAsynchronousRequest:request
    queue:[self anyBackgroundOperationQueue]
    completionHandler:
    ^(NSURLResponse *response, NSData *data, NSError *blockError)
    {
        if(blockError)
        {
            // oh dear; was it fatal?
        }

        if(data)
        {
            // hooray! You know, unless this was an HTTP request, in
            // which case I should check the response code, etc.
        }

        /* etc */
    }

因此,我的其余代码只是针对iOS 5 API编写的,并且不知道也不关心我在其他地方提供了微小部分iOS 5更改的代码。就像我说的那样,当我不再支持iOS 4时,我将从项目中删除这个代码片段,我的所有其他代码都将继续不知道或不关心。
我有类似的代码来提供NSJSONSerialization的替代部分实现(它在运行时动态创建一个新类并将方法复制到其中);你需要做的唯一调整是,其他地方对NSJSONSerialization的引用将由链接器在加载时解析一次,这并不是你真正想要的。因此,在我的预编译头文件中添加了一个快速的#define,将NSJSONSerialization定义为NSClassFromString(@"NSJSONSerialization")。这样做虽然不太优雅,但在找到一种方法以保持iOS 4支持的同时仅按照iOS 5标准编写项目方面,它是一个类似的行动线路。

请问您能解释一下您解释中的这部分内容吗?“在没有进行任何版本或可用性检查的情况下,按照我只针对iOS 5的方式来运行,除了它也可以在iOS 4上运行。” - 我想学习您是如何做到的。谢谢。 - Futur
我已经编辑了我的帖子,添加了一些更多的细节 - 这样足够了吗?我有点受限制,不能真正展示代码...总共有63行(其中20行是注释),所以这只是一个微小的东西。 - Tommy
对我来说,细节有点多了,但如果它能让你开心,我相信其他人也会喜欢它。 - Theo
@Theo 这是为了让 Futur 开心 — 我明白这与最初提出的问题并不特别相关。 - Tommy
1
实际上,从理论角度来看,如果iOS的框架是用C或C++编写的......并且我们有这个"库加载器功能"可以在存在相同名称的函数时不加载库中的函数.......那么......你可以编写一个sendAsynchronousRequest()函数,但只有在无法从iOS的框架中加载sendAsynchronousRequest()时才加载它。 - Theo
当然,我意识到这个想法对使用iOS的人(你的情况)没有帮助,因为它不是用C语言编写的。如果它是用ObjC编写的,那么最好用一个用ObjC编写的函数来替换它。 :) 我只是试图理解替换操作系统函数的一般理论和实践。 - Theo

1

有好的和坏的情况。由于您没有特别提到任何内容,这些示例将随意展示。

在子类化时覆盖框架方法是完全正常(好主意)的:

  • 当子类化NSView(来自AppKit.framework)时,预期您会覆盖drawRect:(NSRect)。这是用于绘制视图的机制。
  • 创建自定义NSMenu时,可以覆盖insertItemWithTitle:action:keyEquivalent:atIndex:和其他任何方法...

子类化的主要问题是您的行为是否完成重新定义旧行为...还是扩展它(在这种情况下,您的覆盖最终调用[super ...];


话虽如此,但您应该始终避免使用(和覆盖)任何私有 API 方法(这些方法通常在名称前面带有下划线前缀)。这是一个不好的想法
您也不应通过类别覆盖现有方法。这也是不好的。它具有未定义的行为。

是的,我知道子类化方法比如drawRect:是被期望的。initrelease等也是一样的 ;) 我的问题是关于覆盖或者替换那些本来不被设计用于替换的OSX特性。 - Theo

1

如果你谈论的是分类,你不会用它们覆盖方法(因为没有办法调用原始方法,就像在子类中调用super一样),而是完全用自己的方法替换它们,这使得整个想法基本上毫无意义。分类只有在安全地扩展功能方面有用,并且这也是我所看到的唯一用途(这是一个非常好的、优秀的想法),尽管它们可以用于危险的事情。

如果你指的是通过子类化来覆盖,那不是独特的。但在Obj-C中,你可以覆盖所有东西,甚至是私有的未记录方法,而不仅仅是像其他语言中声明为“可覆盖”的内容。就我个人而言,我认为这很好,因为我记得在Delphi和C++中,我曾经“黑掉”私有和受保护成员的访问权限,以解决框架中的内部错误。这不是一个好主意,但在某些时候,它可能是救命稻草。

还有方法交叉调用,但那不是标准的语言特性,那是一个hack。黑掉未记录的内部通常不是一个好主意。

关于“如何解释为什么ObjC的特性最适合这个任务”,答案很简单——Obj-C是动态的,而这种自由几乎所有动态语言都具有(例如Javascript、Python、Ruby、Io等)。除非被人为禁用,否则每种动态语言都有这种特性。
请参考维基百科上关于动态语言的页面以获得更长的解释和更多的例子。例如,在Obj-C和其他动态语言中,更神奇的事情是对象可以在原地更改其类型(类),而无需重新创建。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接