美文网首页iOSQiShare文章汇总iOS Developer
iOS 编写高质量Objective-C代码(四)

iOS 编写高质量Objective-C代码(四)

作者: QiShare | 来源:发表于2018-08-24 22:09 被阅读252次

    级别: ★☆☆☆☆
    标签:「OC分类」「Category」
    作者: MrLiuQ
    审校: Xs·H

    前言:
    这几篇文章是小编在钻研《Effective Objective-C 2.0》的知识产出,其中包含作者和小编的观点,以及小编整理的一些demo。希望能帮助大家以简洁的文字快速领悟原作者的精华。
    在这里,QiShare团队向原作者Matt Galloway表达诚挚的敬意。

    文章目录如下:
    iOS 编写高质量Objective-C代码(一)
    iOS 编写高质量Objective-C代码(二)
    iOS 编写高质量Objective-C代码(三)
    iOS 编写高质量Objective-C代码(四)


    本篇的主题是:协议与分类(protocol & category

    先简单介绍一下今天的主角:协议分类

    • 协议(protocol):OC中的协议与Java里的接口(interface)类似,OC不支持多继承,但是可以通过协议来实现委托模式
    • 分类(category):分类可以为既有类添加新的功能。分类是把“双刃剑”,用得好可以发挥OC的高动态性,用的不好则会留下很多坑。而本文就是对category的一些研究。

    一、通过委托与数据源协议进行对象间通信

    委托模式(又称代理):某对象将一类方法(任务)交给另一个对象帮忙完成。
    类似于:老板把一类任务交给某个leader去完成。

    举例来说,当某对象要从另一个对象获取数据时,就可以使用委托模式。通过实现协议来获取数据,这样的协议一般被称为“数据源协议”(Data Source Protocol)。类似于UITableViewUITableViewDataSource

    再举例来说,当一个对象要有一些事件响应时,就可以使用委托模式。通过实现一个协议(一般称为delegate),让代理对象帮助该对象处理事件响应。类似于UITableViewUITableViewDelegate

    请看图解:
    • 好处:通过协议来降低代码的耦合性。(解耦
      必要的时候协议还可以替代继承。因为遵守同一个协议的类可以有很多,不一定要继承。

    百说不如一Demo:这是小编整理的关于Button动画的例子

    • QiCircleAnimationView.h:
    @class QiAnimationButton;
    @protocol QiAnimationButtonDelegate <NSObject>
    
    @optional
    - (void)animationButton:(QiAnimationButton *)button willStartAnimationWithCircleView:(QiCircleAnimationView *)circleView;
    - (void)animationButton:(QiAnimationButton *)button didStartAnimationWithCircleView:(QiCircleAnimationView *)circleView;
    - (void)animationButton:(QiAnimationButton *)button willStopAnimationWithCircleView:(QiCircleAnimationView *)circleView;
    - (void)animationButton:(QiAnimationButton *)button didStopAnimationWithCircleView:(QiCircleAnimationView *)circleView;
    - (void)animationButton:(QiAnimationButton *)button didRevisedAnimationWithCircleView:(QiCircleAnimationView *)circleView;
    
    @end
    
    
    @interface QiAnimationButton : UIButton
    
    @property (nonatomic, weak) id<QiAnimationButtonDelegate> delegate;
    
    - (void)startAnimation;//!< 开始动画
    - (void)stopAnimation;//!< 结束动画
    
    @end
    
    • QiAnimationButton.m中:
      就可以通过这样的方式回调
    if ([self.delegate respondsToSelector:@selector(animationButton:willStartAnimationWithCircleView:)]) {
        [self.delegate animationButton:self willStartAnimationWithCircleView:_circleView];
    }
    
    /* .... */
    
    if ([self.delegate respondsToSelector:@selector(animationButton:didStartAnimationWithCircleView:)]) {
        [self.delegate animationButton:self didStartAnimationWithCircleView:_circleView];
    }
    

    这种形式的例子很多,所以,就会写出很多类似于这样格式的代码:

    if ([self.delegate respondsToSelector:@selector(xxxFunction)]) {
        [self.delegate xxxFunction];
    }
    

    解释:因为该协议内的方法是@optional修饰的,所以遵守协议的Class可以选择性地实现协议里的方法。因此,代理对象在调用回调方法时,需要先检查一下Class有没有实现该协议里的方法?如果实现了,就回调;如果没有实现,就接着往下走。

    考虑性能优化:

    大家设想一下,这样一个场景:回调方法被频繁回调。也就是说,某回调方法被调用的频率很高。那么每调用一次回调方法都要去查一下Class有没有实现该回调方法。所以性能上会变差。

    解决方案:实现一个含有位段的结构体,把委托对象能否响应某个协议方法的信息缓存起来,以优化程序执行效率。

    百说不如一Demo,下面请看小编整理的Demo~

    1. 声明一个结构体DelegateFlags
    @interface QiAnimationButton () {
        
        struct DelegateFlags {
            int doWillStartAnimation : 1;
            int doDidStartAnimation : 1;
            int doWillStopAnimation : 1;
            int doDidStopAnimation : 1;
            int doDidRevisedAnimation : 1;
        };
    }
    
    1. 声明一个属性:
    @property (nonatomic, assign) struct DelegateFlags delegateFlags;
    
    1. 重写delegateset方法:将是否实现该协议方法的信息缓存起来
    - (void)setDelegate:(id<QiAnimationButtonDelegate>)delegate {
        
        _delegate = delegate;
        _delegateFlags.doWillStartAnimation = [delegate respondsToSelector:@selector(animationButton:willStartAnimationWithCircleView:)];
        _delegateFlags.doDidStartAnimation = [delegate respondsToSelector:@selector(animationButton:didStartAnimationWithCircleView:)];
        _delegateFlags.doWillStopAnimation = [delegate respondsToSelector:@selector(animationButton:willStopAnimationWithCircleView:)];
        _delegateFlags.doDidStopAnimation = [delegate respondsToSelector:@selector(animationButton:didStopAnimationWithCircleView:)];
        _delegateFlags.doDidRevisedAnimation = [delegate respondsToSelector:@selector(animationButton:didRevisedAnimationWithCircleView:)];
    }
    
    1. 直接通过_delegateFlags缓存的值判断能否回调
    if (_delegateFlags.doWillStartAnimation) {
       [self.delegate animationButton:self willStartAnimationWithCircleView:_circleView];
    }
    
    /* .... */
    
    if (_delegateFlags.doDidStartAnimation) {
       [self.delegate animationButton:self didStartAnimationWithCircleView:_circleView];
    }
    

    二、把复杂类的实现代码分散到便于管理的数个分类之中

    • 使用分类机制,把一些很复杂的类“瘦身”,划分成各个易于管理的分类。
    • 把私有方法作为一个单独的分类,已隐藏实现细节。

    好处:
    1. 把复杂的类拆成小块,解耦。易于维护,易于管理。
    2. 便于调试:遇到问题能快速定位是哪个分类。

    小编看法:视具体情况而定,拆分的同时,也会多出很多文件。如果一个类过于臃肿(比如有几千行代码),可以考虑给他瘦身,拆分成多个分类。

    三、总是为第三方分类的名称加前缀

    • 分类机制最大的功能:就是为不能修改源码的既有类中添加新的功能。

    这时候我们要:

    • 在分类类名前,加上专有前缀。
    • 在分类方法名前,加上专有前缀。

    最大限度上避免重名可能带来的bug,而且这种bug很难排查。

    原因在于:分类的方法会直接添加在类中,而分类是在运行期把方法加入主类。这时候,如果出现方法重名,后一个写入的分类方法会把前一个覆盖掉。多次覆盖的结果总以最后一个分类为准。所以我们要加前缀,尽量避免重名带来的bug。

    四、勿在分类中声明属性

    不要在分类中声明属性,但可以在类扩展(extension)中声明属性,这样属性就不会暴露在外面。

    举个例子:(类扩展)

    // QiShare.m
    @interface QiShare ()
    /* 属性可以声明在这里 */
    @end
    
    
    @implementation QiShare
    /* ... */
    @end
    
    1. 不能在分类中直接声明属性。如果声明了,编译时会报如下警告:
      Property 'name' requires method 'setName:' to be defined - use @dynamic or provide a method implementation in this category
      解释:分类无法合成相关的实例变量,需要开发者为该属性实现存取方法(get和set)。因为没有生成实例变量,set方法行不通。get方法可以返回固定值。或者使用@dynamic声明(即不会声明实例变量和存取方法)。
    1. 通过关联对象,为分类添加属性。(详情见第二篇 - 第5条)

    所以,
    1. 建议把属性都放在主类中。
    2. 不到迫不得已,尽量不要在分类中通过关联对象添加属性。因为关联对象的内存管理问题上很容易出错,使用时需要重点提防。

    五、使用“class-continuation分类”隐藏实现细节

    这里的“class-continuation分类” 指的就是 类扩展(extension)。

    我们可以把一些私有的属性声明在类扩展里,这样在导入.h文件时,看不到类扩展声明的属性。
    目的:把公共接口中向外暴露的内容最小化,隐藏一些属性和实现细节。

    这里补充一个小知识点:大家都知道Objective-C,但听说过Objective-C++吗?

    Objective-C++是Objective-C和C++的混编,编译时会生成.mm文件。
    这时候会遇到一个问题:因为只有类的.mm文件才能同时编译OC和C++。所以,当一个类所导入所有文件树中包含C++文件,此类的.m文件就会被编译成.mm文件。
    那么,OC怎么解决呢?用类扩展

    举个例子:

    #import "OCClass.h"
    #import "CppClass.cpp"
    
    @interface OCClass () {
        SomeCppClass *_cppClass;
    }
    
    @end
    
    @implementation OCClass
    
    /* ... */
    
    @end
    

    这样,.h文件中就没有C++代码了,如果只看头文件甚至都不知道底层有C++的代码。其实,我们的系统也是这样做的。比如WebKit、CoreAnimation等,很多底层代码都是通过C++写的。

    小结:类扩展的应用场景
    1. 向类中新增实例变量或属性
    2. 在.h文件中把属性声明为“只读”,而类的内部又想修改此属性,可以在类扩展中重声明为“可读写”。
    3. 私有方法的原型可以声明在类扩展里。
    4. 如果不想让外部知道类中遵守了哪些协议,可以在类扩展中遵守协议。

    六、通过协议提供匿名对象

    1. 可以通过协议提供匿名对象,例如:id<someProtocol> delegate。delegate对象的类型不限,只要能遵从这个协议的对象都可以。协议里规定了对象所需要实现的方法。
    2. 使用匿名对象来隐藏类型名称和类名。
    3. 对象只要实现协议里的方法即可(@optional修饰的可以选择性实现),其余的实现细节都被隐藏起来了。

    最后,特别致谢:《Effective Objective-C 2.0》第四章

    关注我们的途径有:
    QiShare(简书)
    QiShare(掘金)
    QiShare(知乎)
    QiShare(GitHub)
    QiShare(CocoaChina)
    QiShare(StackOverflow)
    QiShare(微信公众号)

    推荐文章:
    糖是甜的,你也是: 致 async

    相关文章

      网友评论

      • Corbin___:分类关键属性不会自动管理内存的吗

      本文标题:iOS 编写高质量Objective-C代码(四)

      本文链接:https://www.haomeiwen.com/subject/mteiiftx.html