objc的最大一个特性就是动态性,利用动态特性,我们可以做很多事情,包括对已有的类和对象添加替换方面属性。但是这也给我们带来了一些隐藏的风险,以及让人很不愉快的一些体验。
绝对不能做的事情
假如我们定义一个category,如下面:
1 | @interface UIView (My) |
那么会发生什么事情?
系统方法会被替换。这是什么原因呢?是因为主类被先加载,然后才会去加载category,然后会导致原来的方法被替换。
那么我们是不是可以利用这种特性来屏蔽一些我们不能修改的类的方法呢?答案是绝对不能这么做。首先,我们替换了方法,可能会导致一些未知的问题。其次,这种替换是不能保证顺序的,在另一个地方也做了这样的处理,我们无法预知最后是哪一个方法。
所以我们绝对不能使用category来替换原有的方法,真的要替换可以使用method_exchange。
隐藏的风险
有些时候在使用category给第三方类增加方法时,也有可能不小心替换了原有的私有方法,从而导致了一些未知的问题。所以也就是为什么大家提倡在方法名前面增加扩展,以最大可能的避免重名。
理解性下降
越多的扩展就会引入越多的概念,很多api并非系统api,可能会产生一些意义相近,但是有细微区别的情况,这些都会对我们的理解产生不好的影响。
当我们大量引入这类文件的时候,会产生一大串扩展的方法。特别是当我们把这类util方法作为pch文件全局引入的时候。
性能降低
越多的category,会让一个类的方法变多,使方法调用所产生开销也会越大,缓存命中失效的概率也会变大。虽然这些多余的开销微不足道,但是在比较常用的对象上,比如UIView,就会累积。当然这些性能都不是问题。
丑陋的api
方法名前加前缀的确是一种解决方案,至少尽可能的减少了冲突的可能,但并不代表不会冲突。
然而这种api却是极其丑陋。
1 | - (id)my_property; |
同时这种api使用KeyPath也会非常的麻烦。
甚至可能会出现这样的情况,大家都为同一个功能添加了一个方法:
1 | - (CGFloat)my_height; |
改善
那么我们就要讨论如何改善这种情况了。
第三方帮助类
这种方式可以说是最优雅的,虽然有点破坏面向对象。
其中系统就提供了几种关于frame的帮助方法:
1 | CGRectGetHeight() |
其中React Native中应用了比较多的转换类:
1 | [Convert stringValue:obj]; |
这样就避免了在原有类中添加方法了。实际情况是,很多场景我们也不应该把这些方法加入到扩展中,因为从逻辑上来说,某些方法也不应该出现在这个类中,我们不要单纯的因为方便给一个内容引入太多概念。
属性扩展
另一种方式是通过第三方对象转换到自身的方法。比如上面height属性可以改为:
1 | view.my.height |
这样也会带来一些优点,比如可以按照功能设计不同的第三方对象:
1 | obj.convertor.stringValue() |
同时这样做还有一个好处就是可以动态的替换第三方转换对象,从而实现更灵活的控制:
1 | protocol Convertable { |
只要我们把上面的convertor替换掉,就可以更换时间的format方式了。
这样做的缺点是增加了一个对象的生成,但这并不会产生太多的负面影响。
保持私有化
如果你真的需要使用扩展方法,那么尽量的保持这类方法的私有化,特别是对一些第三方库来说。
然后通过import来部分载入我们所需要的方法。如果一开始就让这些方法暴露在外,那么会形成比较恶性的情况。
比如我们在NSObject上添加了一个关于model的方法,然而我们在UIView上也将会看到,并可以使用了,我们需要尽量去避免这样的设计。
最后
说了这么多,是想建议第三方库的作者不要仅仅为了方便从而滥用category,好好的思考如何才能更好的表达意义。