從代理到 RACSignal
ReactiveCocoa 將 Cocoa 中的 Target-Action、KVO、通知中心以及代理等設計模式都橋接到了 RAC 的世界中,我們在隨后的幾篇文章中會介紹 RAC 如何做到了上面的這些事情,而本篇文章會介紹 ReactiveCocoa 是如何把 代理 轉換為信號的。
RACDelegateProxy
從代理轉換成信號所需要的核心類就是 RACDelegateProxy ,這是一個設計的非常巧妙的類;雖然在類的頭文件中,它被標記為私有類,但是我們仍然可以使用 -initWithProtocol: 方法直接初始化該類的實例。
- (instancetype)initWithProtocol:(Protocol *)protocol {
self = [super init];
class_addProtocol(self.class, protocol);
_protocol = protocol;
return self;
}
從初始化方法中,我們可以看出 RACDelegateProxy 是一個包含實例變量 _protocol 的類:
在整個 RACDelegateProxy 類的實現中,你都不太能看出與這個實例變量 _protocol 的關系;稍微對 iOS 有了解的人可能都知道,在 Cocoa 中有一個非常特別的根類 NSProxy ,而從它的名字我們也可以推斷出來, NSProxy 一般用于實現代理(主要是對消息進行轉發),但是 ReactiveCocoa 中這個 delegate 的代理 RACDelegateProxy 并沒有繼承這個 NSProxy 根類:
@interface RACDelegateProxy : NSObject
@end
那么 RACDelegateProxy 是如何作為 Cocoa 中組件的代理,并為原生組件添加 RACSignal 的支持呢?我們以 UITableView 為例來展示 RACDelegateProxy 是如何與 UIKit 組件互動的,我們需要實現的是以下功能:
在點擊所有的 UITableViewCell 時都會自動取消點擊狀態,通常情況下,我們可以直接在代理方法 -tableView:didSelectRowAtIndexPath: 中執行 -deselectRowAtIndexPath:animated: 方法:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
[tableView deselectRowAtIndexPath:indexPath animated:YES];
}
使用信號的話相比而言就比較麻煩了:
RACDelegateProxy *proxy = [[RACDelegateProxy alloc] initWithProtocol:@protocol(UITableViewDelegate)];
objc_setAssociatedObject(self, _cmd, proxy, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
proxy.rac_proxiedDelegate = self;
[[proxy rac_signalForSelector:@selector(tableView:didSelectRowAtIndexPath:)]
subscribeNext:^(RACTuple *value) {
[value.first deselectRowAtIndexPath:value.second animated:YES];
}];
self.tableView.delegate = (id<UITableViewDelegate>)proxy;
- 初始化 RACDelegateProxy 實例,傳入 UITableViewDelegate 協議,并將實例存入視圖控制器以 確保實例不會被意外釋放 造成崩潰;
- 設置代理的 rac_proxiedDelegate 屬性為視圖控制器;
- 使用 -rac_signalForSelector: 方法生成一個 RACSignal ,在 -tableView:didSelectRowAtIndexPath: 方法調用時將方法的參數打包成 RACTuple 向信號中發送新的 next 消息;
- 重新設置 UITableView 的代理;
在 UITableViewDelgate 中的代理方法執行時,實際上會被 RACDelegateProxy 攔截,并根據情況決定是處理還是轉發:
如果 RACDelegateProxy 實現了該代理方法就會交給它處理,如: -tableView:didSelectRowAtIndexPath: ;否則,當前方法就會被轉發到原 delegate 上,在這里就是 UIViewController 對象。
RACDelegateProxy 中有兩個值得特別注意的問題,一是 RACDelegateProxy 是如何進行消息轉發的,有事如何將自己無法實現的消息交由原代理處理,第二是 RACDelegateProxy 如何通過方法 -rac_signalForSelector: 在原方法調用時以 RACTuple 的方式發送到 RACSignal 上。
消息轉發的實現
首先,我們來看 RACDelegateProxy 是如何在無法響應方法時,將方法轉發給原有的代理的; RACDelegateProxy 通過覆寫幾個方法來實現,最關鍵的就是 -forwardInvocation: 方法:
- (void)forwardInvocation:(NSInvocation *)invocation {
[invocation invokeWithTarget:self.rac_proxiedDelegate];
}
當然,作為消息轉發流程的一部分 -methodSignatureForSelector: 方法也需要在 RACDelegateProxy 對象中實現:
- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector {
struct objc_method_description methodDescription = protocol_getMethodDescription(_protocol, selector, NO, YES);
if (methodDescription.name == NULL) {
methodDescription = protocol_getMethodDescription(_protocol, selector, YES, YES);
if (methodDescription.name == NULL) return [super methodSignatureForSelector:selector];
}
return [NSMethodSignature signatureWithObjCTypes:methodDescription.types];
}
我們會從協議的方法中嘗試獲取其中的可選方法和必須實現的方法,最終獲取方法的簽名 NSMethodSignature 對象。
整個方法決議和消息轉發的過程如下圖所示,在整個方法決議和消息轉發的過程中 Objective-C 運行時會再次提供執行該方法的機會。
例子中的代理方法最后也被 -forwardInvocation: 方法成功的轉發到了 UITableView 的原代理上。
從代理到信號
在 RACDelegateProxy 中的另一個非常神奇的方法就是將某一個代理方法轉換成信號的 -signalForSelector: :
- (RACSignal *)signalForSelector:(SEL)selector {
return [self rac_signalForSelector:selector fromProtocol:_protocol];
}
- (RACSignal *)rac_signalForSelector:(SEL)selector fromProtocol:(Protocol *)protocol {
return NSObjectRACSignalForSelector(self, selector, protocol);
}
該方法會在傳入的協議方法被調用時,將協議方法中的所有參數以 RACTuple 的形式發送到返回的信號上,使用者可以通過訂閱這個信號來獲取所有的參數;而方法 NSObjectRACSignalForSelector 的實現還是比較復雜的。
static RACSignal *NSObjectRACSignalForSelector(NSObject *self, SEL selector, Protocol *protocol) {
SEL aliasSelector = RACAliasForSelector(selector);
RACSubject *subject = objc_getAssociatedObject(self, aliasSelector);
if (subject != nil) return subject;
Class class = RACSwizzleClass(self);
subject = [RACSubject subject];
objc_setAssociatedObject(self, aliasSelector, subject, OBJC_ASSOCIATION_RETAIN);
Method targetMethod = class_getInstanceMethod(class, selector);
if (targetMethod == NULL) {
const char *typeEncoding;
if (protocol == NULL) {
typeEncoding = RACSignatureForUndefinedSelector(selector);
} else {
struct objc_method_description methodDescription = protocol_getMethodDescription(protocol, selector, NO, YES);
if (methodDescription.name == NULL) {
methodDescription = protocol_getMethodDescription(protocol, selector, YES, YES);
}
typeEncoding = methodDescription.types;
}
class_addMethod(class, selector, _objc_msgForward, typeEncoding);
} else if (method_getImplementation(targetMethod) != _objc_msgForward) {
const char *typeEncoding = method_getTypeEncoding(targetMethod);
class_addMethod(class, aliasSelector, method_getImplementation(targetMethod), typeEncoding);
class_replaceMethod(class, selector, _objc_msgForward, method_getTypeEncoding(targetMethod));
}
return subject;
}
這個 C 函數總共做了兩件非常重要的事情,第一個是將傳入的選擇子對應的實現變為 _objc_msgForward ,也就是在調用該方法時,會直接進入消息轉發流程,第二是用 RACSwizzleClass 調劑當前類的一些方法。
從 selector 到 objc msgForward
我們具體看一下這部分代碼是如何實現的,在修改選擇子對應的實現之前,我們會先做一些準備工作:
SEL aliasSelector = RACAliasForSelector(selector);
RACSubject *subject = objc_getAssociatedObject(self, aliasSelector);
if (subject != nil) return subject;
Class class = RACSwizzleClass(self);
subject = [RACSubject subject];
objc_setAssociatedObject(self, aliasSelector, subject, OBJC_ASSOCIATION_RETAIN);
Method targetMethod = class_getInstanceMethod(class, selector);
- 獲取選擇子的別名,在這里我們通過為選擇子加前綴 rac_alias_ 來實現;
- 嘗試以 rac_alias_selector 為鍵獲取一個熱信號 RACSubject ;
- 使用 RACSwizzleClass 調劑當前類的一些方法(我們會在下一節中介紹);
- 從當前類中獲取目標方法的結構體 targetMethod ;
在進行了以上的準備工作之后,我們就開始修改選擇子對應的實現了,整個的修改過程會分為三種情況:
下面會按照這三種情況依次介紹在不同情況下,如何將對應選擇子的實現改為 _objc_msgForward 完成消息轉發的。
targetMethod NULL && protocol NULL
在找不到選擇子對應的方法并且沒有傳入協議時,這時執行的代碼最為簡單:
typeEncoding = RACSignatureForUndefinedSelector(selector);
class_addMethod(class, selector, _objc_msgForward, typeEncoding);
我們會通過 RACSignatureForUndefinedSelector 生成一個當前方法默認的類型編碼。
對類型編碼不了解的可以閱讀蘋果的官方文檔 Type Encodings · Apple Developer ,其中詳細解釋了類型編碼是什么,它在整個 Objective-C 運行時有什么作用。
static const char *RACSignatureForUndefinedSelector(SEL selector) {
const char *name = sel_getName(selector);
NSMutableString *signature = [NSMutableString stringWithString:@"v@:"];
while ((name = strchr(name, ':')) != NULL) {
[signature appendString:@"@"];
name++;
}
return signature.UTF8String;
}
該方法在生成類型編碼時,會按照 : 的個數來為 v@: 這個類型編碼添加 @ 字符;簡單說明一下它的意思,ReactiveCocoa 默認所有的方法的返回值類型都為空 void ,都會傳入 self 以及當前方法的選擇子 SEL ,它們的類型編碼可以在下圖中找到,分別是 v@: ;而 @ 代表 id 類型,也就是我們默認代理方法中的所有參數都是 NSObject 類型的。
生成了類型編碼之后,由于我們并沒有在當前類中找到該選擇子對應的方法,所以會使用 class_addMethod 為當前類提供一個方法的實現,直接將當前選擇子的實現改為 _objc_msgForward 。
targetMethod == NULL && protocol != NULL
當類中不存在當前選擇子對應的方法 targetMethod ,但是向當前函數中傳入了協議時,我們會嘗試從協議中獲取方法描述:
struct objc_method_description methodDescription = protocol_getMethodDescription(protocol, selector, NO, YES);
if (methodDescription.name == NULL) {
methodDescription = protocol_getMethodDescription(protocol, selector, YES, YES);
}
typeEncoding = methodDescription.types;
class_addMethod(class, selector, _objc_msgForward, typeEncoding);
這里會使用 protocol_getMethodDescription 兩次從協議中獲取可選和必須實現的方法的描述,并從結構體中拿出類型編碼,最后為類添加這個之前不存在的方法:
在這種情況下,其最后的結果與上一種的完全相同,因為它們都是對不存在該方法,只需要獲得方法的類型編碼并將實現添加為 _objc_msgForward ,交給消息轉發流程進行處理即可。
targetMethod != NULL
在目標方法的實現不為空并且它的實現并不是 _objc_msgForward 時,我們就會進入以下流程修改原有方法的實現:
const char *typeEncoding = method_getTypeEncoding(targetMethod);
class_addMethod(class, aliasSelector, method_getImplementation(targetMethod), typeEncoding);
class_replaceMethod(class, selector, _objc_msgForward, method_getTypeEncoding(targetMethod));
同樣,我們需要獲得目標方法的方法簽名、添加 aliasSelector 這個新方法,最后在修改原方法的實現到 _objc_msgForward 。
上圖展示了在目標方法不為空并且其實現不為 _objc_msgForward 時, NSObjectRACSignalForSelector 是如何修改原方法實現的。
調劑類的方法
NSObjectRACSignalForSelector 在修改原選擇子方法實現的之前就已經修改了當前類很多方法的實現:
- -methodSignatureForSelector:
- -class
- -respondsToSelector
- -forwardInvocation:
整個調劑方法的過程 RACSwizzleClass 還是比較復雜的,我們可以分三部分看下面的代碼:
static Class RACSwizzleClass(NSObject *self) {
Class statedClass = self.class;
Class baseClass = object_getClass(self);
NSString *className = NSStringFromClass(baseClass);
const char *subclassName = [className stringByAppendingString:RACSubclassSuffix].UTF8String;
Class subclass = objc_getClass(subclassName);
if (subclass == nil) {
subclass = objc_allocateClassPair(baseClass, subclassName, 0);
if (subclass == nil) return nil;
RACSwizzleForwardInvocation(subclass);
RACSwizzleRespondsToSelector(subclass);
RACSwizzleGetClass(subclass, statedClass);
RACSwizzleGetClass(object_getClass(subclass), statedClass);
RACSwizzleMethodSignatureForSelector(subclass);
objc_registerClassPair(subclass);
}
object_setClass(self, subclass);
return subclass;
}
- 從當前類 RACDelegateProxy 衍生出一個子類 RACDelegateProxy_RACSelectorSignal ;
- 調用各種 RACSwizzleXXX 方法修改當前子類的一些表現;
- 將 RACDelegateProxy 對象的類設置成自己,這樣就會在查找方法時,找到 RACDelegateProxy_RACSelectorSignal 中的實現;
在修改的幾個方法中最重要的就是 -forwardInvocation: :
static void RACSwizzleForwardInvocation(Class class) {
SEL forwardInvocationSEL = @selector(forwardInvocation:);
Method forwardInvocationMethod = class_getInstanceMethod(class, forwardInvocationSEL);
void (*originalForwardInvocation)(id, SEL, NSInvocation *) = NULL;
if (forwardInvocationMethod != NULL) {
originalForwardInvocation = (__typeof__(originalForwardInvocation))method_getImplementation(forwardInvocationMethod);
}
id newForwardInvocation = ^(id self, NSInvocation *invocation) {
BOOL matched = RACForwardInvocation(self, invocation);
if (matched) return;
if (originalForwardInvocation == NULL) {
[self doesNotRecognizeSelector:invocation.selector];
} else {
originalForwardInvocation(self, forwardInvocationSEL, invocation);
}
};
class_replaceMethod(class, forwardInvocationSEL, imp_implementationWithBlock(newForwardInvocation), "v@:@");
}
這個方法中大部分的內容都是平淡無奇的,在新的 -forwardInvocation: 方法中,執行的 RACForwardInvocation 是實現整個消息轉發的關鍵內容:
static BOOL RACForwardInvocation(id self, NSInvocation *invocation) {
SEL aliasSelector = RACAliasForSelector(invocation.selector);
RACSubject *subject = objc_getAssociatedObject(self, aliasSelector);
Class class = object_getClass(invocation.target);
BOOL respondsToAlias = [class instancesRespondToSelector:aliasSelector];
if (respondsToAlias) {
invocation.selector = aliasSelector;
[invocation invoke];
}
if (subject == nil) return respondsToAlias;
[subject sendNext:invocation.rac_argumentsTuple];
return YES;
}
在 -rac_signalForSelector: 方法返回的 RACSignal 上接收到的參數信號,就是從這個方法發送過去的,新的實現 RACForwardInvocation 改變了原有的 selector 到 aliasSelector ,然后使用 -invoke 完成該調用,而所有的參數會以 RACTuple 的方式發送到信號上。
像其他的方法 -respondToSelector: 等等,它們的實現就沒有這么復雜并且重要了:
id newRespondsToSelector = ^ BOOL (id self, SEL selector) {
Method method = rac_getImmediateInstanceMethod(class, selector);
if (method != NULL && method_getImplementation(method) == _objc_msgForward) {
SEL aliasSelector = RACAliasForSelector(selector);
if (objc_getAssociatedObject(self, aliasSelector) != nil) return YES;
}
return originalRespondsToSelector(self, respondsToSelectorSEL, selector);
};
rac_getImmediateInstanceMethod 從當前類獲得方法的列表,并從中找到與當前 selector 同名的方法 aliasSelector ,然后根據不同情況判斷方法是否存在。
對 class 的修改,是為了讓對象對自己的身份『說謊』,因為我們子類化了 RACDelegateProxy ,并且重新設置了對象的類,將所有的方法都轉發到了這個子類上,如果不修改 class 方法,那么當開發者使用它自省時就會得到錯誤的類,而這是我們不希望看到的。
static void RACSwizzleGetClass(Class class, Class statedClass) {
SEL selector = @selector(class);
Method method = class_getInstanceMethod(class, selector);
IMP newIMP = imp_implementationWithBlock(^(id self) {
return statedClass;
});
class_replaceMethod(class, selector, newIMP, method_getTypeEncoding(method));
}
在最后我們會對獲得方法簽名的 -methodSignatureForSelector: 方法進行修改:
IMP newIMP = imp_implementationWithBlock(^(id self, SEL selector) {
Class actualClass = object_getClass(self);
Method method = class_getInstanceMethod(actualClass, selector);
if (method == NULL) {
struct objc_super target = {
.super_class = class_getSuperclass(class),
.receiver = self,
};
NSMethodSignature * (*messageSend)(struct objc_super *, SEL, SEL) = (__typeof__(messageSend))objc_msgSendSuper;
return messageSend(?, @selector(methodSignatureForSelector:), selector);
}
char const *encoding = method_getTypeEncoding(method);
return [NSMethodSignature signatureWithObjCTypes:encoding];
});
在方法不存在時,通過 objc_msgSendSuper 調用父類的 -methodSignatureForSelector: 方法獲取方法簽名。
方法調用的過程
在一般情況下,Objective-C 中某一消息被發送到一個對象時,它會先獲取當前對象對應的類,然后從類的選擇子表查找該方法對應的實現并執行。
與正常的方法實現查找以及執行過程的簡單不同,如果我們對某一個方法調用了 -rac_signalForSelector: 方法,那么對于同一個對象對應的類的所有方法,它們的執行過程會變得非常復雜:
- 由于當前對象對應的類已經被改成了 Subclass ,即 Class_RACSelectorSignal ,所以會在子類中查找方法的實現;
- 方法對應的實現已經被改成了 -forwardInvocation: ,會直接進入消息轉發流程中處理;
- 根據傳入的選擇子獲取同名選擇子 rac_alias_selector ;
- 拿到當前 NSInvocation 對象中 target 的類,判斷是否可以響應該選擇子;
- 將 NSInvocation 對象中的選擇子改為 rac_alias_selector 并執行其實現;
- 從 NSInvocation 對象中獲取參數并打包成 RACTuple ,以 next 消息的形式發送到持有的 RACSubject 熱信號上;
這時所有的訂閱者才會在該方法被調用時收到消息,完成相應的任務。
總結
ReactiveCocoa 使用了一種非常神奇的辦法把原有的代理模式成功的橋接到 RACSignal 的世界中,并為我們提供了 RACDelegateProxy 這一接口,能夠幫助我們以信號的形式監聽所有的代理方法,可以用 block 的形式去代替原有的方法,為我們減少一些工作量。
References
Github Repo: iOS-Source-Code-Analyze
Follow: Draveness · GitHub
Source: http://draveness.me/racdelegateproxy
來自:http://draveness.me/racdelegateproxy/