iOS 編寫高質量Objective-C程式碼(四)

QiShare發表於2018-08-24

級別: ★★☆☆☆
標籤:「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程式碼(四)
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

請看圖解:

iOS 編寫高質量Objective-C程式碼(四)

  • 好處:通過協議來降低程式碼的耦合性。(解耦
    必要的時候協議還可以替代繼承。因為遵守同一個協議的類可以有很多,不一定要繼承。

百說不如一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宣告(即不會宣告例項變數和存取方法)。

  2. 通過關聯物件,為分類新增屬性。(詳情見第二篇 - 第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

相關文章