對(duì)于 CTNetworking 設(shè)計(jì)理念和筆者的理解,Casa Taloyum 給出了回復(fù):
- 已發(fā)出的請(qǐng)求是不可能做到真正取消的,所以請(qǐng)求的取消在實(shí)現(xiàn)上就是“即使拿到數(shù)據(jù)也不回調(diào)給業(yè)務(wù)”。這個(gè)在CTNetworking里面是已經(jīng)做好了的。
- Service的概念是用于封裝第三方SDK的,例如我在Github上給到的Marvel API SDK和高德地圖API SDK。一組API中某個(gè)API不規(guī)范的情況,是可以在Service的實(shí)現(xiàn)中或者APIManager的視線中給予適配的。這也就是為什么Service只是一個(gè)protocol而不是一個(gè)具體實(shí)現(xiàn)的原因。所以protocol方式定義的service不是缺點(diǎn),它是功能。
- CTNetworking已經(jīng)足夠成熟了。我有一個(gè)目標(biāo)是將所有的第三方API都以CTNetworking的方式封裝,而完成這一目標(biāo)的所有基礎(chǔ)設(shè)施也都已經(jīng)完善了,所以CTNetworking在朝一個(gè)生態(tài)的方向去發(fā)展。具體可以看我給到的那些示范工程:Marvel API SDK、高德地圖API SDK。
CTNetworking的基礎(chǔ)設(shè)施包括
- bash的代碼自動(dòng)生成腳本,可以提高工程師在離散型API架構(gòu)下的工作效率。2. 基于CTMediator的配置管理,可以做到跨APP時(shí)的代碼復(fù)用,例如Marvel key在不同app上可能會(huì)有不同的key
- 用于實(shí)現(xiàn)API DEMO ViewController的父類,它可以極大地便于將API工程以APP的形式給用QA去做測(cè)試,給開(kāi)發(fā)去做調(diào)試或者當(dāng)API文檔用。
- CTJsbridge已經(jīng)可以跟CTNetworking交互,H5工程師可以很方便地使用基于CTNetworking的網(wǎng)絡(luò)API。
前言
基于 AFNetworking 的二次封裝網(wǎng)上蠻多的,比較好一點(diǎn)的就是 CTNetworking 和 YTKNetwork,但是看了一下源碼過(guò)后發(fā)現(xiàn)都有一些不足的地方,或者說(shuō)不太能滿足我們的業(yè)務(wù)需求??紤]到 AFNetworking 本身就為網(wǎng)絡(luò)層做了很多事情,二次封裝并非是個(gè)復(fù)雜的事情,所以索性自己寫(xiě)了個(gè)便于拓展和維護(hù) (代碼完全脫敏):
代碼地址和用法 : YBNetwork
參考思路:iOS應(yīng)用架構(gòu)談 網(wǎng)絡(luò)層設(shè)計(jì)方案
參考源碼:YTKNetwork CTNetworking
調(diào)研
Casa Taloyum 前輩的文章對(duì)筆者的架構(gòu)思維有著深遠(yuǎn)的影響,記得兩年多前入行不久,看得一知半解,近些時(shí)間要做架構(gòu)方面的工作,又去重溫了一下。
如何設(shè)計(jì)一個(gè)好的網(wǎng)絡(luò)層架構(gòu),在 Casa Taloyum 的文章中已經(jīng)說(shuō)得比較全面了。猿題庫(kù)的 YTKNetwork 相對(duì)比較成熟,兩份代碼核心思想都是將代碼歸為集約處理部分和離散處理部分,在實(shí)現(xiàn)方式上有些差別。
沒(méi)有什么技術(shù)難點(diǎn),直接看了一遍兩份開(kāi)源代碼,優(yōu)點(diǎn)很多,這里羅列一下不足的地方(當(dāng)然只是個(gè)人理解,并且筆者可能更多結(jié)合業(yè)務(wù)來(lái)考慮的):
CTNetworking 不足:
- 使用 IOP 方式建立模塊,化繼承為組合。獨(dú)立
<CTServiceProtocol>和<CTAPIManagerInterceptor>等協(xié)議作為集約管理部分,若個(gè)別接口需要修改這些公共配置,只能在集約管理模塊來(lái)判斷,顯得有一點(diǎn)繁瑣。 - 記錄了一個(gè) request 實(shí)例的所有 task,在 dealloc 中自動(dòng)取消掉還未降落的網(wǎng)絡(luò)請(qǐng)求,但是實(shí)際上網(wǎng)絡(luò)請(qǐng)求任務(wù)會(huì)持有 request,所以自動(dòng)取消策略不成立了。
YTKNetwork 不足:
- 基于多態(tài)的設(shè)計(jì)思路,提供了很多供重載的方法,從設(shè)計(jì)來(lái)看,框架是可以實(shí)例化
YTKBaseRequest子類 直接使用的,那么直接使用時(shí)無(wú)法重載這些方法專門定制(個(gè)人看來(lái)有些地方使用屬性更靈活);并且,當(dāng)一個(gè) reqeust 多次start發(fā)起請(qǐng)求就會(huì)調(diào)用多次這些重載方法,可能造成多余計(jì)算; - 緩存策略使用一個(gè)
YTKBaseRequest的子類YTKRequest來(lái)做,雖然這樣看起來(lái)比較優(yōu)雅,父類和子類各司其職,單一職責(zé),但是緩存策略難免會(huì)更改父類的邏輯,如此就很難不違背開(kāi)閉原則??蚣艿木彺嬷挥幸粋€(gè)失效時(shí)間控制,筆者想要拓展時(shí)發(fā)現(xiàn)要改的東西太多。 - 同一個(gè) request 實(shí)例多次 start 調(diào)用網(wǎng)絡(luò)請(qǐng)求時(shí) (多個(gè)網(wǎng)絡(luò)請(qǐng)求并發(fā)情況),并未作出實(shí)際的處理策略,僅保留最新的
NSURLSessionTask,而對(duì)舊的未結(jié)束的所有NSURLSessionTask喪失了控制權(quán)。 - 網(wǎng)絡(luò)請(qǐng)求任務(wù)強(qiáng)持有所有 request 對(duì)象,在弱網(wǎng)環(huán)境下可能會(huì)有大量 request 對(duì)象無(wú)法釋放,而界面降落點(diǎn)可能不存在了。
共同不足:
- 數(shù)據(jù)回調(diào)都是綁定在 request 上的,既然都未處理一個(gè) request 重復(fù)并發(fā)請(qǐng)求的情況,那么多個(gè)網(wǎng)絡(luò)請(qǐng)求落地時(shí),request 上的數(shù)據(jù)會(huì)突變,業(yè)務(wù)方的處理方式是不可控的,既有可能在回調(diào)業(yè)務(wù)執(zhí)行過(guò)程中發(fā)現(xiàn)數(shù)據(jù)變化了。
實(shí)際上針對(duì)團(tuán)隊(duì)的業(yè)務(wù),架構(gòu)上會(huì)有取舍,所以筆者列這些不足也可以說(shuō)是比較片面的。
實(shí)現(xiàn)
如何進(jìn)行離散請(qǐng)求調(diào)用?
在一個(gè)網(wǎng)絡(luò)請(qǐng)求起飛到降落過(guò)程中,有一系列獨(dú)有的配置始終能代表這一個(gè)網(wǎng)絡(luò)請(qǐng)求。
那么思路就出來(lái)了,只要把一個(gè)針對(duì)某個(gè)接口的配置對(duì)象傳遞過(guò)去,讓網(wǎng)絡(luò)任務(wù)的閉包持有這個(gè)對(duì)象,然后在網(wǎng)絡(luò)回調(diào)處理中,一直傳遞這個(gè)配置對(duì)象,像踢皮球一樣,最終處理好后回調(diào)到業(yè)務(wù)類中。
怎么避免這個(gè)配置對(duì)象瘋狂傳遞?實(shí)際上就可以把網(wǎng)絡(luò)回調(diào)處理邏輯,放在這個(gè)配置對(duì)象中,就像CTNetworking的CTAPIBaseManager配置對(duì)象,只要安全落地就能命中對(duì)應(yīng)的配置對(duì)象;也可以用一個(gè)全局容器把這些配置對(duì)象裝起來(lái),不用一直通過(guò)閉包傳遞,就像YTKNetwork的YTKBaseRequest配置對(duì)象。
所以筆者之前用了一個(gè)奇怪的思路:
Config config = Config.new;
[NetworkManager startWithConfig:config success:^{} failure:^{}];
實(shí)際上這和上面兩個(gè)框架道理是一樣的,筆者內(nèi)部也會(huì)寫(xiě)邏輯去管理所有config,但是這么做不好對(duì)單獨(dú)的網(wǎng)絡(luò)請(qǐng)求進(jìn)行管理,非要管理的話,又需要去持有這個(gè)config了。
實(shí)現(xiàn)代碼類:
- YBNetworkManager : 負(fù)責(zé)組織數(shù)據(jù)發(fā)起網(wǎng)絡(luò)請(qǐng)求,并且管理所有的 NSURLSessionTask
- YBNetworkCache : 負(fù)責(zé)緩存處理
- YBNetworkResponse : 回調(diào)響應(yīng)結(jié)果
- YBBaseRequest : 負(fù)責(zé)離散數(shù)據(jù)配置、網(wǎng)絡(luò)響應(yīng)處理邏輯
集約/離散配置方式
為了更加靈活,并沒(méi)有采用 IOP 方式來(lái)做配置管理,而是采用繼承的方式來(lái)做,為了提高靈活性,定制幾率大的配置使用屬性實(shí)現(xiàn),需要重載的方法使用分類提出來(lái)看起來(lái)保證清晰。
在開(kāi)發(fā)中,需要針對(duì)不同的接口團(tuán)隊(duì)創(chuàng)建不同的YBBaseRequest子類集約配置,比如DefaultServerRequest : YBBaseRequest。在使用時(shí),可以直接實(shí)例化DefaultServerRequest或者子類化DefaultServerRequest進(jìn)行離散配置。
主要思路和 YTKNetwork 基本一樣,當(dāng)然像 CTNetworking 這樣強(qiáng)制子類化來(lái)使用接口更好管理,但是有些時(shí)候顯得有些繁瑣。
筆者這種處理方式雖然需要子類化一些YBBaseRequest進(jìn)行公共配置,但是也保證了每一個(gè)請(qǐng)求接口實(shí)例都可以任意的定制集約管理部分,防止接口抽風(fēng)。
重定向
網(wǎng)絡(luò)落地重定向重寫(xiě)此方法:
- (void)yb_redirection:(void (^)(YBRequestRedirection))redirection response:(YBNetworkResponse *)response {
// 同步或異步的做一些事情
redirection(YBRequestRedirectionSuccess);
}
使用redirection閉包來(lái)達(dá)到可異步重定向的能力,在這之間可以做一些具體網(wǎng)絡(luò)接口無(wú)感知的邏輯。比如攔截到所有接口都可能返回的一個(gè)錯(cuò)誤狀態(tài)碼-1,需要向服務(wù)器驗(yàn)證身份,這里就可以直接讓當(dāng)前請(qǐng)求停止redirection(YBRequestRedirectionStop),然后發(fā)起異步驗(yàn)證的網(wǎng)絡(luò)請(qǐng)求,驗(yàn)證成功后再重定向回調(diào)給業(yè)務(wù)redirection(YBRequestRedirectionSuccess),或重新發(fā)起網(wǎng)絡(luò)請(qǐng)求[self start]。
緩存處理
緩存處理專門提取一個(gè)類來(lái)包裝邏輯,而調(diào)用邏輯仍然放在YBBaseRequest,實(shí)際上代碼量很少,也好修改。
出于業(yè)務(wù)考慮,緩存支持的功能有:
- 內(nèi)存/磁盤(pán)存儲(chǔ)方式
- 緩存命中后是否繼續(xù)發(fā)起網(wǎng)絡(luò)請(qǐng)求
- 緩存的有效時(shí)長(zhǎng)
- 定制緩存的 key
對(duì)于緩存命中的回調(diào),筆者設(shè)置了專門的回調(diào)出口:
//Block
- (void)startWithCache:(nullable YBRequestCacheBlock)cache
success:(nullable YBRequestSuccessBlock)success
failure:(nullable YBRequestFailureBlock)failure;
//Delegate
- (void)request:(__kindof YBBaseRequest *)request cacheWithResponse:(YBNetworkResponse *)response;
對(duì)于 Block 方式 來(lái)說(shuō),獨(dú)立的緩存回調(diào)閉包更好管理。
對(duì)于兩種回調(diào)來(lái)說(shuō),設(shè)計(jì)一個(gè)專門的緩存回調(diào)能降低業(yè)務(wù)工程師的出錯(cuò)率。
對(duì)于網(wǎng)絡(luò)及時(shí)數(shù)據(jù)和緩存數(shù)據(jù)往往在業(yè)務(wù)處理上有細(xì)微的差別,分開(kāi)回調(diào)能避免出于疏忽而去寫(xiě)判斷if (isCache) {...} else {...}(特別是當(dāng)寫(xiě)業(yè)務(wù)的工程師并不知道這個(gè) API 緩存策略是怎樣的)。
緩存有效性驗(yàn)證
內(nèi)部會(huì)在業(yè)務(wù)處理完成網(wǎng)絡(luò)響應(yīng)數(shù)據(jù)后嘗試進(jìn)行緩存,避免將異常數(shù)據(jù)寫(xiě)入緩存(比如數(shù)據(jù)導(dǎo)致 Crash 時(shí))。且提供一個(gè)shouldCacheBlock可根據(jù)請(qǐng)求響應(yīng)成功數(shù)據(jù)判斷是否需要緩存(比如僅當(dāng) code == 0 時(shí)數(shù)據(jù)有效允許緩存)。
重復(fù)網(wǎng)絡(luò)請(qǐng)求處理
提供三種方式:
- 允許重復(fù)網(wǎng)絡(luò)請(qǐng)求
- 取消最舊的網(wǎng)絡(luò)請(qǐng)求
- 取消最新的網(wǎng)絡(luò)請(qǐng)求
舉幾個(gè)例子,當(dāng)接口數(shù)據(jù)并不會(huì)在短時(shí)間變化時(shí),重復(fù)發(fā)起網(wǎng)絡(luò)請(qǐng)求就會(huì)浪費(fèi)網(wǎng)絡(luò)資源,可以選擇方案 2 或 3;比如在搜索業(yè)務(wù)中,用戶往往頻繁的調(diào)用搜索接口,而發(fā)起一次搜索時(shí),之前的搜索請(qǐng)求一般是沒(méi)有意義了,就可以選用方案 2。
網(wǎng)絡(luò)請(qǐng)求釋放處理
提供三種方式:
- 網(wǎng)絡(luò)任務(wù)會(huì)持有
YBBaseRequest實(shí)例,網(wǎng)絡(luò)任務(wù)完成YBBaseRequest實(shí)例才會(huì)釋放 - 網(wǎng)絡(luò)請(qǐng)求將隨著
YBBaseRequest實(shí)例的釋放而取消 - 網(wǎng)絡(luò)請(qǐng)求和
YBBaseRequest實(shí)例無(wú)關(guān)聯(lián)
實(shí)現(xiàn)網(wǎng)絡(luò)任務(wù)對(duì) YBBaseRequest 弱持有 ,當(dāng)YBNetworkManager發(fā)起請(qǐng)求時(shí),讓回調(diào)閉包捕獲弱引用的weakSelf的就行了。
而要讓YBBaseRequest釋放時(shí)自動(dòng)取消網(wǎng)絡(luò)請(qǐng)求只需要簡(jiǎn)單調(diào)用(不過(guò)在“網(wǎng)絡(luò)請(qǐng)求和 YBBaseRequest 實(shí)例無(wú)關(guān)聯(lián)”模式時(shí)是不能取消的)。
舉幾個(gè)例子,若你的控制器出棧以后希望取消未落地的網(wǎng)絡(luò)請(qǐng)求,那么就使用方案 2,注意管理好 YBBaseRequest 的生命周期就行了;若你的網(wǎng)絡(luò)請(qǐng)求是不論如何都不希望它取消的,那么使用方案 3;若你希望網(wǎng)絡(luò)請(qǐng)求任務(wù)始終持有 YBBaseRequest 實(shí)例避免它提前釋放,那么使用方案 1。
回調(diào)處理
為了讓重復(fù)網(wǎng)絡(luò)請(qǐng)求時(shí),每次回調(diào)的數(shù)據(jù)不相互影響,筆者思來(lái)想去還是額外定義了一個(gè)類,而不是直接讓YBBaseRequest持有。
至于為什么要單獨(dú)定義一個(gè)類,其一是單獨(dú)定義一個(gè)類便于拓展回調(diào)內(nèi)容,并且也降低了框架內(nèi)部數(shù)據(jù)流通過(guò)程中的成本(傳遞一個(gè)對(duì)象總比傳遞一堆對(duì)象好處理吧);其二在于一個(gè) API 請(qǐng)求實(shí)例可能發(fā)起多次網(wǎng)絡(luò)請(qǐng)求,從而可能就有多次網(wǎng)絡(luò)落地,響應(yīng)數(shù)據(jù)由YBBaseRequest持有會(huì)出現(xiàn)響應(yīng)數(shù)據(jù)被覆蓋情況。
后語(yǔ)
大體思路就是如此,至于線程安全啥的細(xì)節(jié)就不多說(shuō)了,主要是在加鎖的時(shí)候注意避免同一線程重復(fù)獲取鎖導(dǎo)致死鎖就行了。
一個(gè)看似簡(jiǎn)單的二次封裝也能有這么多值得思考的地方,精益求精并不是一件容易的事。