本文是如下兩篇blog的綜合脫水,感謝兩位做者爲解放碼農生產力所作的深刻思考=。=
Smart Proxy Delegation
Elegant Delegationhtml
@protocol TestObjectDelegate <NSObject> @optional - (void)testObjectMethod; - (NSString *)testObjectMethodWithReturnValue; @end @interface TestObject : NSObject @property (nonatomic, weak) id<TestObjectDelegate> delegate; - (void)print; - (void)printWithLog; @end
- (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
@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所須要的。
鋪墊了這麼多,終於到了正題。
在這裏構建了一個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
接着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的完整實現,有很多值得學習的點哦。
終於寫完啦!!!!