【程式碼優化】呼叫optional delegates的最佳方法

NSTopGun發表於2014-08-24

【轉載請註明出處】http://www.cnblogs.com/lexingyu/p/3932475.html

本文是以下兩篇blog的綜合脫水,感謝兩位作者為解放碼農生產力所做的深入思考=。=
Smart Proxy Delegation
Elegant Delegation

使用delegate的情境通常是這樣

定義class和delegate

@protocol TestObjectDelegate <NSObject>
@optional
- (void)testObjectMethod;
- (NSString *)testObjectMethodWithReturnValue;

@end


@interface TestObject : NSObject

@property (nonatomic, weak) id<TestObjectDelegate> delegate;

- (void)print;
- (void)printWithLog;

@end

在類的內部呼叫delegate的方法

- (void)print
{
    //call the delegate to do the real work
}

呼叫的方法通常有以下兩種

普通青年:

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

這個辦法的缺點是
1)引入了大量glue code,每個optional function都需要3行程式碼。尤其在開啟clang的-Warc-repeated-use-of-weak時,多次使用self.delegate(通常情況下,是weak)會被警告;
【程式碼優化】呼叫optional delegates的最佳方法

所以很可能還得這麼寫

- (void)print
{
    id <TestObjectDelegate> delegate = self.delegate;
    if ([delegate respondsToSelector:@selector(testObjectMethod)])
    {
        [delegate testObjectMethod];
    }
}

2)呼叫的方法名需要寫兩次,很可能寫錯導致方法未被呼叫;
3)對於高頻率呼叫的方法而言,意味著需要反覆呼叫respondToSeletor,效能上有所影響(RunTime可能會對respondToSeletor進行快取,因此在大部分應用上這一點不需要計入考量)。

文藝青年
先新增flag

@interface TestObject : NSObject
{
    struct
    {
        unsigned int respond2TestObjectMethod:1;
    }_flags;
}

@property (nonatomic, weak) id<TestObjectDelegate> delegate;

- (void)print;

@end

再過載setDelegate以設定flag,將respondToSeletor的結果快取起來

- (void)setDelegate:(id<TestObjectDelegate>)delegate
{
    _delegate = delegate;
    
    BOOL respond2TestObjectMethod = [delegate respondsToSelector:@selector(testObjectMethod)];
    _flags.respond2TestObjectMethod = respond2TestObjectMethod ? 1 : 0;
}

最後在print中直接使用快取的結果

- (void)print
{
    if (_flags.respond2TestObjectMethod)
    {
        [self.delegate testObjectMethod];
    }
}

這個方法被Apple廣泛採用,在SDK中隨處可見。
它的優點是將respondToSeletor的結果手動快取了起來,不需要做效能上的猜測,同時避開了
-Warc-repeated-use-of-weak的警告。
但遺憾的是,程式碼的冗餘並沒有被移除,反而更為嚴重(呼叫時仍然需要3行glue code,且在標頭檔案和setDelegate中新增了大量程式碼)。當delegate中的方法名需要變動時,需要同時修改多處程式碼,真如噩夢一般。

嗯。。。。。。抱歉這裡沒有二逼青年
【程式碼優化】呼叫optional delegates的最佳方法

外國友人的想法

實際上我們真正想要的是類似於這樣的東西

- (void)print
{
    [self.delegateProxy testObjectMethod];
}

把glue code也好,其他額外處理也好,都放到一個統一的地方。在呼叫的時候,一句話簡單明瞭,解決問題。
那麼具體怎麼做呢?
其實,OC的方法呼叫,或者準確地說,訊息傳遞,就是這樣一種機制。這裡上一張自繪的圖以便說明
【程式碼優化】呼叫optional delegates的最佳方法

OC中任何一次方法呼叫,都會從1開始走這個流程,一個步驟不行就進行下一步。若所有4個步驟走完仍然無法找到對應的impletation,則觸發異常,程式crash。簡單說一下各個步驟的作用
1)在類的方法表(methodList)中,根據seletor查詢對應的impletation;
2) resolveInstanceMethod用於集中處理類中一些類似的方法,比如在使用core data時需要指定多個property為@dynamic,它們的setter和getter就可以集中在這個方法裡做;
3)forwardingTargetForSelector,作用是將本物件無法處理的呼叫資訊轉給另一個物件處理,但不改變呼叫資訊;
4)forwardInvocation,作用是根據methodSignatureForSelector和呼叫引數等資訊生成的NSInvocation來指定一個物件處理本次呼叫,在指定時可以對呼叫資訊做任意的修改,比如增加引數個數。

3被稱為Fast message forwarding,相應地4則是Regular message forwarding,二者合在一起才是完整的Message forwarding

C語言在呼叫函式時,需要知道函式的原型,以便將引數放入暫存器或壓入棧中,並視情況預留返回值的空間。OC作為C語言的超集,也需要顧及這一點。函式的呼叫資訊在OC中以NSMethodSignature的形式存在,在Regular message forwarding中由methodSignatureForSelector返回。

從以上說明不難看出,1和2的作用是在類內部尋找impletation,而3和4則是在類外部尋找合適的其他類的例項來處理呼叫資訊。顯而易見,3和4正是delegateProxy所需要的。

鋪墊了這麼多,終於到了正題。

用Message forwarding機制,來構建一個delegateProxy

在這裡構建了一個NSProxy的派生類作為delegateProxy,像這樣

@interface CDDelegateProxy : NSProxy

@property (nonatomic, weak, readonly) id delegate;
@property (nonatomic, strong, readonly) Protocol *protocol;
@property (nonatomic, strong, readonly) NSValue *defaultReturnValue;

@end

delegateProxy中分別儲存了被代理的delegate物件、delegate對應的protocol和方法未找到時提供的預設值。
在.m檔案中,首先將glue code放入,像這樣

//供外部需要時使用
- (BOOL)respondsToSelector:(SEL)selector
{
    return [_delegate respondsToSelector:selector];
}

//Fast message forwarding, 存放glue code
- (id)forwardingTargetForSelector:(SEL)selector
{
    id delegate = _delegate;
    return [delegate respondsToSelector:selector] ? delegate : self;
}

嗯。。。至此似乎就完事了=。=
大部分情況下確實如此。但當方法不存在又需要一個預設返回值時,比如

- (void)printWithLog
{
    //這裡已經用上delegateProxy了,哈哈
    NSString *logInfo = [self.delegateProxy testObjectMethodWithReturnValue];
    NSLog(@"%@", logInfo);
}

就需要用到Regular message forwarding了。具體做法如下

//Regular message forwarding
- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector
{
    id delegate = _delegate;
    NSMethodSignature *signature = [delegate methodSignatureForSelector:selector];
    
    //若delegate未實現對應方法,則從protocol的宣告中獲取MethodSignature
    if (!signature)
    {
        if (!_signatures) _signatures = [self methodSignaturesForProtocol:_protocol];
        signature = CFDictionaryGetValue(_signatures, selector);
    }
    
    //此處如果return nil, 則不會觸發forwardInvocation
    return signature;
}

- (void)forwardInvocation:(NSInvocation *)invocation
{
    //若預設返回值和invocation中指定的返回值一致,則取預設返回值
    if (_defaultReturnValue
        && strcmp(_defaultReturnValue.objCType, invocation.methodSignature.methodReturnType) == 0)
    {
        char buffer[invocation.methodSignature.methodReturnLength];
        [_defaultReturnValue getValue:buffer];
        [invocation setReturnValue:&buffer];
    }
}

首先由methodSignatureForSelector根據protocol中的方法宣告,返回一個signature,再由forwardInvocation判斷與預設的返回值是否型別一致,一致則返回預設的預設值(即剛才提到的defaultReturnValue)。

這樣,delegateProxy就構建完畢了。在使用的時候,應注意delegateProxy的作用只是在類內部保持呼叫的簡潔,對於外部程式碼而言,它應該是透明的。具體來說,首先應該將deleagteProxy定義在class extension中

//.m檔案中
@interface SomeObject ()<TestObjectDelegate>

@property (nonatomic, strong) id<TestObjectDelegate> delegateProxy;

@end

這裡將delegateProxy直接宣告為id的形式,目的是使之後編碼時仍然能夠享有Xcode對protocol中方法的自動提示補全。
接著override delegate(真正id被定義在標頭檔案中)

- (void)setDelegate:(id <TestObjectDelegate>)delegate 
{
  self.delegateProxy = delegate ? (id <TestObjectDelegate>)[[CDDelegateProxy alloc] initWithDelegate:delegate] : nil;
}
- (id <TestObjectDelegate>)delegate
 {
  return ((CDDelegateProxy *)self.delegateProxy).delegate;
}

這個步驟看著有些繁瑣,可以通過巨集來簡化,比如

#define CD_DELEGATE_PROXY_CUSTOM(protocolname, GETTER, SETTER) \
- (id<protocolname>)GETTER { return ((PSTDelegateProxy *)self.GETTER##Proxy).delegate; } \
- (void)SETTER:(id<protocolname>)delegate { self.GETTER##Proxy = delegate ? (id<protocolname>)[[PSTDelegateProxy alloc] initWithDelegate:delegate conformingToProtocol:@protocol(protocolname) defaultReturnValue:nil] : nil; }

#define CD_DELEGATE_PROXY(protocolname) PST_DELEGATE_PROXY_CUSTOM(protocolname, delegate, setDelegate)

在使用的使用可以簡單地

CD_DELEGATE_PROXY(id <PSPDFResizableViewDelegate>)

當然,對於比較個性化的delegate的名稱,可以通過擴充套件這個巨集來實現。

如此一來,外部訪問delegate時,獲取到的仍然是正確的物件。
以上,就是呼叫optional delegates的最佳方法,從起因到原理到解決方案的完整闡述。

文中為便於說明,使用了我自己寫的一個簡化版的delegateProxy,這裡提供一個原作者Peter steinberger的完整實現,有不少值得學習的點哦。

終於寫完啦!!!!

相關文章