在日常開(kāi)發(fā)中我們經(jīng)常會(huì)遇到這樣的場(chǎng)景,有很多模塊的delegate需要通過(guò)一個(gè)公共類來(lái)轉(zhuǎn)發(fā)回調(diào)事件戳护。比如采用MVP模式開(kāi)發(fā)一個(gè)復(fù)雜的UI交互,其中(許多)View要通過(guò)Presenter來(lái)轉(zhuǎn)發(fā)網(wǎng)絡(luò)回調(diào)、文件訪問(wèn)、數(shù)據(jù)庫(kù)等多種不同的Delegate的回調(diào)事件睬辐。標(biāo)準(zhǔn)做法是在Presenter中實(shí)現(xiàn)各個(gè)不同的Delegate,然后再轉(zhuǎn)發(fā)給(多個(gè))View宾肺。這樣做繁瑣且代碼重復(fù)溯饵,更重要的是不方便對(duì)多個(gè)View做回調(diào)控制。例如:用戶在首次觀看視頻時(shí)锨用,點(diǎn)擊「退出」按鈕會(huì)響應(yīng)「視頻退出事件」此時(shí)會(huì)彈出一個(gè)confirm丰刊,如果選擇「取消」則中斷后續(xù)的事件回調(diào)。對(duì)于這樣的場(chǎng)景使用proxy是最方便的增拥。如果有這么一個(gè)Proxy啄巧,則Presenter不需要轉(zhuǎn)發(fā)一次事件回調(diào)洪橘,而只要將(多個(gè))view添加到proxy接收Delegate的回調(diào)就可以了。如果用ObjeC來(lái)實(shí)現(xiàn)proxy是非常簡(jiǎn)單的棵帽,只需要實(shí)現(xiàn)三個(gè)runtime方法即可:
/** 判斷是否可以響應(yīng)Selector */
- (BOOL)respondsToSelector:(SEL)aSelector {
for (id target in self.targets) {
if ([target respondsToSelector:aSelector]) {
return YES;
}
}
return NO;
}
/** 解析方法簽名 */
- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector {
NSMethodSignature *signature = nil;
for (id target in self.targets) {
if ((signature = [target methodSignatureForSelector:selector])) {
break;
}
}
return signature;
}
/** 消息轉(zhuǎn)發(fā) */
- (void)forwardInvocation:(NSInvocation *)invocation {
for (id target in targets) {
// 這里可添加更多的「控制」
[context invokeWithTarget:target];
}
}
但是對(duì)于pure swift來(lái)說(shuō),因?yàn)閯?dòng)態(tài)能力幾乎為零——使用Mirror只能做些簡(jiǎn)單的屬性操作而不能對(duì)方法做反射操作渣玲,所以要實(shí)現(xiàn)類似上面ObjC版的proxy功能需要解決三個(gè)問(wèn)題:
- pure swift中的protocol沒(méi)有@optional逗概,也即沒(méi)有respondsToSelector的能力
- proxy是一個(gè)通用的實(shí)現(xiàn),而delegate中的方法簽名是各不相同的忘衍,也即沒(méi)有@selector的能力
- swift是泛型+類型推導(dǎo)的編程范式逾苫,而各回調(diào)方法的參數(shù)類型是完全不同的便脊。另外proxy內(nèi)部維護(hù)target列表的類型必然是Any——也就是無(wú)類型呵扛。這就導(dǎo)致無(wú)法通過(guò)「as」強(qiáng)轉(zhuǎn)出原方法簽名并執(zhí)行,也即沒(méi)有invocation等能力
因?yàn)樯鲜龅娜齻€(gè)理由陷寝,在pure swift的實(shí)現(xiàn)版本中代碼冗余一些是必然的但至少能實(shí)現(xiàn)搀捷。下面我們來(lái)逐個(gè)解決上面列出的問(wèn)題:
- 對(duì)于第一個(gè)問(wèn)題星掰,所有的靜態(tài)語(yǔ)言都只能用繼承(多態(tài))來(lái)解決。還好swift的extension(或者其它支持類mixin的靜態(tài)語(yǔ)言)可以借助編譯器做到多態(tài)而又不會(huì)侵入到target中(所有單繼承的OO語(yǔ)言嫩舟,對(duì)于繼承是即愛(ài)又恨)
- 第二個(gè)問(wèn)題的標(biāo)準(zhǔn)做法還是繼承氢烘,通過(guò)一個(gè)proxy的子類實(shí)現(xiàn)所有會(huì)用到的delegate的協(xié)議。在swift中則是通過(guò)extension來(lái)包裝proxy的「通用」invoke方法家厌。這個(gè)方式可以很方便的解決方法簽名的強(qiáng)類型校驗(yàn)播玖,但缺點(diǎn)是delegate中的每一個(gè)方法都需要包裝一遍,其實(shí)這只能算是強(qiáng)類型語(yǔ)言的特點(diǎn)不能算缺陷饭于。另外還可以用swift 5.0中的@dynamicCallable + @dynamicmemberlookup實(shí)現(xiàn)對(duì)delegate中方法的訪問(wèn)「動(dòng)態(tài)」訪問(wèn)蜀踏,只不過(guò)實(shí)現(xiàn)起來(lái)還會(huì)更繁瑣一些
- 第三個(gè)問(wèn)題與第二個(gè)問(wèn)題是緊密相連的,如果采用@dynamicCallable + @dynamicmemberlookup方式則可以使用([String : Any?]) -> (Instance) -> ()對(duì)delegate的各方法做currying掰吕。如果采用包裝proxy的「通用」invoke方法的實(shí)現(xiàn)方式則可以通過(guò)(T) -> ()對(duì)個(gè)方法做currying果覆,其中T為delegate協(xié)議類型。
結(jié)合下面的代碼會(huì)更容易理解上面的解釋:
/**
* 通過(guò)事件代理
TODO:
1. 并發(fā)控制
2. 設(shè)定target響應(yīng)優(yōu)先級(jí)
3. 對(duì)target設(shè)定所在的回調(diào)線程
*/
class Proxy {
/** 緩存轉(zhuǎn)發(fā)的target列表 */
private var targets: [Any] = []
/** 記錄事件流掛起/終止情況 */
private var suspendOfEvents: [String: (Bool, Bool)] = [:]
/** 記錄恢復(fù)事件流時(shí)對(duì)應(yīng)target的Index和需要執(zhí)行的回調(diào)closure */
private var resumeOfEvents: [String: (Int, (Any) -> () -> ())] = [:]
/** 添加目標(biāo)對(duì)象 */
func addTarget(target: Any) {
targets.append(target)
}
/** 掛起事件流 */
@discardableResult
func suspend(signature: String = #function,
isInterrupt: Bool = false) -> ((Bool) -> ())? {
if suspendOfEvents.keys.contains(signature) {
// 標(biāo)記掛起
suspendOfEvents[signature] = (true, isInterrupt)
// 將resume包裝成(isInterrupt) -> ()殖熟,便于外部使用
return {
[weak self] isInterrupt in
if let self = self {
self.resume(signature: signature, isInterrupt: isInterrupt)
}
}
} else {
return nil
}
}
/** 恢復(fù)事件流 */
func resume(signature: String, isInterrupt: Bool = false) {
if let (idx, handler) = resumeOfEvents[signature] {
// 標(biāo)記恢復(fù)
suspendOfEvents[signature] = (false, isInterrupt)
// 繼續(xù)執(zhí)行后續(xù)的事件流
invoking(signature: signature, index: idx, handler: handler)
}
}
/** 通用的事件回調(diào)執(zhí)行器 */
func invoke<T>(signature: String = #function,
apply: @escaping (T) -> ()) {
// 刪除未執(zhí)行完的事件流
removeSuspendEvent(signature: signature)
// 記錄執(zhí)行情況
suspendOfEvents[signature] = (false, false)
// 執(zhí)行事件流随静,將外部傳入的apply(用于實(shí)際的方法調(diào)用)包裝成(target) -> () -> ()
invoking(signature: signature) { (target: Any) in
return {
// 利用as保證類型安全,T為具體的協(xié)議類型
(target as? T).map(apply)
}
}
}
/** 處理事件流的執(zhí)行吗讶、掛起燎猛、恢復(fù) */
private func invoking(signature: String,
index: Int = 0,
handler: @escaping (Any) -> () -> ()) {
// 迭代執(zhí)行事件流
for idx in index..<targets.count {
let target = targets[idx]
// 查詢掛起、中斷狀態(tài)
if let (isSuspend, isInterrupt) = suspendOfEvents[signature] {
if isInterrupt {
// 事件流中斷
print("cancel \(signature) at \(type(of: target))")
break
} else if isSuspend {
// 掛起事件流程
resumeOfEvents[signature] = (idx, handler)
print("suspend \(signature) at \(type(of: target))")
continue
}
}
// 執(zhí)行事件流
handler(target)()
}
}
/** 刪除執(zhí)行中的事件流 */
private func removeSuspendEvent(signature: String) {
if suspendOfEvents.keys.contains(signature) {
resumeOfEvents.removeValue(forKey: signature)
suspendOfEvents.removeValue(forKey: signature)
}
}
}
簡(jiǎn)單說(shuō)明一下:
- invoke為proxy的通用執(zhí)行方法照皆,通過(guò)#function獲取方法簽名
- invoking用于處理事件流重绷,target在具體回調(diào)時(shí)可以通過(guò)suspend來(lái)控制是否允許后續(xù)的事件流掛起或者終止
- resume方法因?yàn)樾枰獋魅雜ignature比較麻煩,所以在suspend中通過(guò)currying包裝resume簡(jiǎn)化外部使用
下面為測(cè)試代碼 —— 協(xié)議與Targets部分:
// 代理實(shí)例
var proxy = Proxy()
// 用于測(cè)試的協(xié)議
protocol Testable {
func say()
func hello(name: String)
}
// 為Testable協(xié)議提供默認(rèn)的實(shí)現(xiàn)
extension Testable {
func say() {
print("Testable say()")
}
func hello(name: String) {
print("Testable hello(name:)")
}
}
struct Boo : Testable {
func say() {
print("boo say()")
// 掛起事件流
proxy.suspend()
}
}
struct Foo : Testable {
func say() {
print("Foo say()")
}
func hello(name: String) {
print("foo hello \(name)")
}
}
// 向代理添加target
proxy.addTarget(target: Boo())
proxy.addTarget(target: Foo())
下面為測(cè)試代碼 —— 事件源部分:
/** 用于模擬的事件源 */
struct EventsSource {
let proxy: Proxy
}
/** 分組協(xié)議 */
protocol Groupable { }
/** 實(shí)現(xiàn)「分組」協(xié)議 */
extension EventsSource : Groupable { }
/** 「事件回調(diào)」分組 */
struct Eventer<Subject: Groupable> {
var subject: Subject
init(subject: Subject) {
self.subject = subject
}
}
extension Groupable {
var events: Eventer<Self> {
get {
return Eventer(subject: self)
}
}
}
/** 包裝proxy的通用invoke */
extension Eventer where Subject == EventsSource {
func say() {
// target: Testable用于讓編譯器推斷泛型類型
self.subject.proxy.invoke { (target: Testable) in
target.say()
}
}
func hello(name: String) {
// 因?yàn)闊o(wú)法直接獲得原方法的簽名膜毁,所以要用(T) -> ()進(jìn)行currying
self.subject.proxy.invoke { (target: Testable) in
target.hello(name: name)
}
}
}
上面的測(cè)試代碼在定義EventsSource后昭卓,使用曾經(jīng)分享過(guò)的「分組」技巧來(lái)包裝Proxy中的invoke方法愤钾。
let source = EventsSource(proxy: proxy)
print(">>> call say ...")
source.events.say()
print(">>> call hello ...")
source.events.hello(name: "fh")
print(">>> resume say ...")
proxy.resume(signature: "say()")
// 執(zhí)行結(jié)果如下:
>>> call say ...
boo say()
suspend say() at Foo
>>> call hello ...
Testable hello(name:)
foo hello fh
>>> resume say ...
Foo say()