【代碼優化】調用optional delegates的最佳方法

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

使用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)會被警告;
請輸入圖片描述
因此極可能還得這麼寫ios

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

2)調用的方法名須要寫兩次,極可能寫錯致使方法未被調用;
3)對於高頻率調用的方法而言,意味着須要反覆調用respondToSeletor,性能上有所影響(RunTime可能會對respondToSeletor進行緩存,所以在大部分應用上這一點不須要計入考量)。git

  • 文藝青年
    先添加flag
@interface TestObject : NSObject
{
    struct
    {
        unsigned int respond2TestObjectMethod:1;
    }_flags;
}

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

- (void)print;

@end

再重載setDelegate以設置flag,將respondToSeletor的結果緩存起來github

- (void)setDelegate:(id<TestObjectDelegate>)delegate
{
    _delegate = delegate;

    BOOL respond2TestObjectMethod = [delegate respondsToSelector:@selector(testObjectMethod)];
    _flags.respond2TestObjectMethod = respond2TestObjectMethod ? 1 : 0;
}

最後在print中直接使用緩存的結果objective-c

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

這個方法被Apple普遍採用,在SDK中隨處可見。
它的優勢是將respondToSeletor的結果手動緩存了起來,不須要作性能上的猜想,同時避開了
-Warc-repeated-use-of-weak的警告。
但遺憾的是,代碼的冗餘並無被移除,反而更爲嚴重(調用時仍然須要3行glue code,且在頭文件和setDelegate中添加了大量代碼)。當delegate中的方法名須要變更時,須要同時修改多處代碼,真如噩夢通常。segmentfault

嗯。。。。。。抱歉這裏沒有二逼青年
請輸入圖片描述緩存

外國友人的想法

實際上咱們真正想要的是相似於這樣的東西app

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

把glue code也好,其餘額外處理也好,都放到一個統一的地方。在調用的時候,一句話簡單明瞭,解決問題。
那麼具體怎麼作呢?
其實,OC的方法調用,或者準確地說,消息傳遞,就是這樣一種機制。這裏上一張自繪的圖以便說明
請輸入圖片描述
OC中任何一次方法調用,都會從1開始走這個流程,一個步驟不行就進行下一步。若全部4個步驟走完仍然沒法找到對應的impletation,則觸發異常,程序crash。簡單說一下各個步驟的做用
1)在類的方法表(methodList)中,根據seletor查找對應的impletation;
2) resolveInstanceMethod用於集中處理類中一些相似的方法,好比在使用core data時須要指定多個property爲@dynamic,它們的setter和getter就能夠集中在這個方法裏作;
3)forwardingTargetForSelector,做用是將本對象沒法處理的調用信息轉給另外一個對象處理,但不改變調用信息;
4)forwardInvocation,做用是根據methodSignatureForSelector和調用參數等信息生成的NSInvocation來指定一個對象處理本次調用,在指定時能夠對調用信息作任意的修改,好比增長參數個數。ide

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的完整實現,有很多值得學習的點哦。

終於寫完啦!!!!
請輸入圖片描述

相關文章
相關標籤/搜索