原文地址:http://casatwy.com/iosying-yong-jia-gou-tan-wang-luo-ceng-she-ji-fang-an.htmlhtml
前言ios
網絡層在一個App中也是一個不可缺乏的部分,工程師們在網絡層可以發揮的空間也比較大。另外,蘋果對網絡請求部分已經作了很好的封裝,業界的AFNetworking也被普遍使用。其它的ASIHttpRequest,MKNetworkKit啥的其實也都還不錯,但前者已經棄坑,後者也在棄坑的邊緣。在實際的App開發中,Afnetworking已經成爲了事實上各大App的標準配置。git
網絡層在一個App中承載了API調用,用戶操做日誌記錄,甚至是即時通信等任務。我接觸過一些App(開源的和不開源的)的代碼,在看到網絡層這一塊時,尤爲是在看到各位架構師各顯神通展現了各類技巧,我很是爲之感到興奮。但有的時候,每每也對於其中的一些缺陷感到失望。github
關於網絡層的設計方案會有不少,須要權衡的地方也會有不少,甚至於爭議的地方都會有不少。但不管如何,我都不會對這些問題作出任何逃避,我會在這篇文章中給出我對它們的見解和解決方案,觀點毫不中立,不會跟你們打太極。算法
這篇文章就主要會講這些方面:設計模式
在安居客App的架構更新換代的時候,我深深地感受到網絡層跟業務對接部分的設計有多麼重要,所以我對它作的最大改變就是針對網絡層跟業務對接部分的改變。網絡層跟業務層對接部分設計的好壞,會直接影響到業務工程師實現功能時的心情。api
在正式開始講設計以前,咱們要先討論幾個問題:數組
這些問題討論完畢以後,我會給出一個完整的設計方案來給你們作參考,設計方案是魚,討論的這些問題是漁,我什麼都授了,你們各取所需。緩存
使用哪一種交互模式來跟業務層作對接?安全
這裏其實有兩個問題:
iOS開發領域有不少對象間數據的傳遞方式,我看到的大多數App在網絡層所採用的方案主要集中於這三種:Delegate,Notification,Block。KVO和Target-Action我目前尚未看到有使用的。
目前我知道邊鋒主要是採用的block,大智慧主要採用的是Notification,安居客早期以Block爲主,後面改爲了以Delegate爲主,阿里沒發現有經過Notification來作數據傳遞的地方(可能有),Delegate、Block以及target-action都有,阿里iOS App網絡層的做者說這是爲了方便業務層選擇本身合適的方法去使用。這裏你們都是各顯神通,每次我看到這部分的時候,我都喜歡問做者爲何採用這種交互方案,但不多有做者可以說出個條條框框來。
然而在我這邊,個人意見是以Delegate爲主,Notification爲輔。緣由以下:
儘量減小跨層數據交流的可能,限制耦合
什麼叫跨層數據交流?就是某一層(或模塊)跟另外的與之沒有直接對接關係的層(或模塊)產生了數據交換。爲何這種狀況很差?嚴格來講應該是大部分狀況都很差,有的時候跨層數據交流確實也是一種需求。之因此說很差的地方在於,它會致使代碼混亂,破壞模塊的封裝性
。咱們在作分層架構的目的其中之一就在於下層對上層有一次抽象,讓上層能夠沒必要關心下層細節而執行本身的業務。
因此,若是下層細節被跨層暴露,一方面你很容易所以失去鄰層對這個暴露細節的保護;另外一方面,你又不可能不去處理這個細節,因此處理細節的相關代碼就會散落各地,最終難以維護。
說得具象一點就是,咱們考慮這樣一種狀況:A<-B<-C。當C有什麼事件,經過某種方式告知B,而後B執行相應的邏輯。一旦告知方式不合理,讓A有了跨層知道C的事件的可能,你 就很難保證A層業務工程師在未來不會對這個細節做處理。一旦業務工程師在A層產生處理操做,有多是補充邏輯,也有多是執行業務,那麼這個細節的相關處理代碼就會有一部分散落在A層。然而前者是不該該散落在A層的,後者有多是需求。另外,由於B層是對A層抽象的,執行補充邏輯的時候,有可能和B層針對這個事件的處理邏輯產生衝突,這是咱們很不但願看到的。
那麼什麼狀況跨層數據交流會成爲需求?在網絡層這邊,信號從2G變成3G變成4G變成Wi-Fi,這個是跨層數據交流的其中一個需求。不過其餘的跨層數據交流需求我暫時也想不到了,哈哈,應該也就這一個吧。
嚴格來講,使用Notification來進行網絡層和業務層之間數據的交換,並不表明這必定就是跨層數據交流,可是使用Notification給跨層數據交流開了一道口子,由於Notification的影響面不可控制,只要存在實例就存在被影響的可能。另外,這也會致使誰都不能保證相關處理代碼就在惟一的那個地方,進而帶來維護災難。做爲架構師,在這裏給業務工程師限制其操做的靈活性是必要的。另外,Notification也支持一對多的狀況,這也給代碼散落提供了條件。同時,Notification所對應的響應方法很難在編譯層面做限制,不一樣的業務工程師會給他取不一樣的名字,這也會給代碼的可維護性帶來災難。
手機淘寶架構組的俠武同窗曾經給我分享過一個問題,在這裏我也分享給你們:曾經有一個工程師在監聽Notification以後,沒有寫釋放監聽的代碼,固然,找到這個緣由又是很漫長的一段故事,如今找到緣由了,然而監聽這個Notification的對象有那麼多,不知道具體是哪一個Notificaiton,也不知道那個沒釋放監聽的對象是誰。後來折騰了好久你們都沒辦法的時候,有一個經驗豐富的工程師提出用hook(Method Swizzling)的方式,最終找到了那個沒釋放監聽的對象,bug修復了。
我分享這個問題的目的並非想強調Notification多麼多麼很差,Notification自己就是一種設計模式,在屬於他的問題領域內,Notification是很是好的一種解決方案。但我想強調的是,對於網絡層這個問題領域內來看,架構師首先必定要限制代碼的影響範圍,在能用影響範圍小的方案的時候就儘可能採用這種小的方案,不然未來要是有什麼奇怪需求或者出了什麼小問題,維護起來就很是麻煩。所以Notification這個方案不能做爲首選方案,只能做爲備選。
那麼Notification也不是徹底不能使用,當需求要求跨層時,咱們就可使用Notification,好比前面提到的網絡條件切換,並且這個需求也是須要知足一對多的。
因此,爲了符合前面所說的這些要求,使用Delegate可以很好地避免跨層訪問,同時限制了響應代碼的形式,相比Notification而言有更好的可維護性。
而後咱們順便來講說爲何儘可能不要用block。
咱們在調試的時候常常會單步追蹤到某一個地方以後,發現尼瑪這裏有個block,若是想知道這個block裏面都作了些什麼事情,這時候就比較蛋疼了。
- (void)someFunctionWithBlock:(SomeBlock *)block { ... ... -> block(); //當你單步走到這兒的時候,要想知道block裏面都作了哪些事情的話,就很麻煩。 ... ... }
block會給內部全部的對象引用計數加一,這一方面會帶來潛在的retain cycle,不過咱們能夠經過Weak Self的手段解決。另外一方面比較重要就是,它會延長對象的生命週期。
在網絡回調中使用block,是block致使對象生命週期被延長的其中一個場合,當ViewController從window中卸下時,若是尚有請求帶着block在外面飛,而後block裏面引用了ViewController(這種場合很是常見),那麼ViewController是不能被及時回收的,即使你已經取消了請求,那也仍是必須得等到請求着陸以後才能被回收。
然而使用delegate就不會有這樣的問題,delegate是弱引用,哪怕請求仍然在外面飛,,ViewController仍是可以及時被回收的,回收以後指針自動被置爲了nil,無傷大雅。
因此平時儘可能不要濫用block,尤爲是在網絡層這裏。
統一回調方法,便於調試和維護
前面講的是跨層問題,區分了Delegate和Notification,順帶談了一下Block。而後如今談到的這個狀況,就是另外一個採用Block方案不是很合適的狀況。首先,Block自己無好壞對錯之分,只有合適不合適。在這一節要講的狀況裏,Block沒法作到回調方法的統一,調試和維護的時候也很難在調用棧上顯示出來,找的時候會很蛋疼。
在網絡請求和網絡層接受請求的地方時,使用Block沒問題。可是在得到數據交給業務方時,最好仍是經過Delegate去通知到業務方。由於Block所包含的回調代碼跟調用邏輯放在同一個地方,會致使那部分代碼變得很長,由於這裏麪包括了調用前和調用後的邏輯。從另外一個角度說,這在必定程度上違背了single function,single task
的原則,在須要調用API的地方,就只要寫API調用相關的代碼,在回調的地方,寫回調的代碼。
而後我看到大部分App裏,當業務工程師寫代碼寫到這邊的時候,也意識到了這個問題。所以他們會在block裏面寫個一句話的方法接收參數,而後作轉發,而後就能夠把這個方法放在其餘地方了,繞過了Block的回調着陸點不統一
的狀況。好比這樣:
[API callApiWithParam:param successed:^(Response *response){ [self successedWithResponse:response]; } failed:^(Request *request, NSError *error){ [self failedWithRequest:request error:error]; }];
這實質上跟使用Delegate的手段沒有什麼區別,只是繞了一下,不過仍是沒有解決統一回調方法的問題,由於block裏面寫的方法名字可能在不一樣的ViewController對象中都會不同,畢竟業務工程師也是不少人,各人有各人的想法。因此架構師在這邊不要貪圖方便,仍是使用delegate的手段吧,業務工程師那邊就能不用那麼繞了。Block是目前大部分第三方網絡庫都採用的方式,由於在發送請求的那一部分,使用Block可以比較簡潔,所以在請求那一層是沒有問題的,只是在交換數據以後,仍是轉變成delegate比較好,好比AFNetworking裏面:
[AFNetworkingAPI callApiWithParam:self.param successed:^(Response *response){ if ([self.delegate respondsToSelector:@selector(successWithResponse:)]) { [self.delegate successedWithResponse:response]; } } failed:^(Request *request, NSError *error){ if ([self.delegate respondsToSelector:@selector(failedWithResponse:)]) { [self failedWithRequest:request error:error]; } }];
這樣在業務方這邊回調函數就可以比較統一,便於維護。
綜上,對於以什麼方式將數據交付給業務層?
這個問題的回答是這樣:
儘量經過Delegate的回調方式交付數據,這樣能夠避免沒必要要的跨層訪問。當出現跨層訪問的需求時(好比信號類型切換),經過Notification的方式交付數據。正常狀況下應該是避免使用Block的。
我見過很是多的App的網絡層在拿到JSON數據以後,會將數據轉變成對應的對象原型。注意,我這裏指的不是NSDictionary,而是相似Item這樣的對象。這種作法是可以提升後續操做代碼的可讀性的。在比較直覺的思路里面,是須要這部分轉化過程的,但這部分轉化過程的成本是很大的,主要成本在於:
其實咱們的理想狀況是但願API的數據下發以後就可以直接被View所展現。首先要說的是,這種狀況很是少。另外,這種作法使得View和API聯繫緊密,也是咱們不但願發生的。
在設計安居客的網絡層數據交付這部分時,我添加了reformer(名字而已,叫什麼都好)這個對象用於封裝數據轉化的邏輯,這個對象是一個獨立對象,事實上,它是做爲Adaptor模式存在的。咱們能夠這麼理解:想象一下咱們洗澡時候使用的蓮蓬頭,水管裏出來的水是API下發的原始數據。reformer就是蓮蓬頭上的不一樣水流擋板,須要什麼模式,就撥到什麼模式。
在實際使用時,代碼觀感是這樣的:
先定義一個protocol:
@protocol ReformerProtocol <NSObject> - (NSDictionary)reformDataWithManager:(APIManager *)manager; @end
在Controller裏是這樣:
@property (nonatomic, strong) id<ReformerProtocol> XXXReformer; @property (nonatomic, strong) id<ReformerProtocol> YYYReformer; #pragma mark - APIManagerDelegate - (void)apiManagerDidSuccess:(APIManager *)manager { NSDictionary *reformedXXXData = [manager fetchDataWithReformer:self.XXXReformer]; [self.XXXView configWithData:reformedXXXData]; NSDictionary *reformedYYYData = [manager fetchDataWithReformer:self.YYYReformer]; [self.YYYView configWithData:reformedYYYData]; } 在APIManager裏面,fetchDataWithReformer是這樣: - (NSDictionary)fetchDataWithReformer:(id<ReformerProtocol>)reformer { if (reformer == nil) { return self.rawData; } else { return [reformer reformDataWithManager:self]; } }
- (void)apiManagerDidSuccess:(APIManager *)manager { // 這個回調方法有多是來自二手房列表APIManager的回調,也有多是租房,也有多是新房。可是在Controller層面咱們不須要對它作額外區分,只要是同一個reformer出來的數據,咱們就能保證是必定能被self.XXXView使用的。這樣的保證由reformer的實現者來提供。 NSDictionary *reformedXXXData = [manager fetchDataWithReformer:self.XXXReformer]; [self.XXXView configWithData:reformedXXXData]; }
reformer本質上就是一個符合某個protocol的對象,在controller須要從api manager中得到數據的時候,順便把reformer傳進去,因而就能得到通過reformer從新洗過的數據,而後就能夠直接使用了。
更抽象地說,reformer實際上是對數據轉化邏輯的一個封裝。在controller從manager中取數據以後,而且把數據交給view以前,這期間或多或少都是要作一次數據轉化的,有的時候不一樣的view,對應的轉化邏輯還不同,可是展現的數據是同樣的。並且每每這一部分代碼都很是複雜,且跟業務強相關,直接上代碼,未來就會很難維護。因此咱們能夠考慮採用不一樣的reformer封裝不一樣的轉化邏輯,而後讓controller根據須要選擇一個合適的reformer裝上,就像洗澡的蓮蓬頭,須要什麼樣的水流(數據的表現形式)就換什麼樣的頭,然而水(數據)都是同樣的。這種作法可以大大提升代碼的可維護性,以及減小ViewController的體積。
總結一下,reformer事實上是把轉化的代碼封裝以後再從主體業務中拆分了出來,拆分出來以後不光下降了原有業務的複雜度,更重要的是,它提升了數據交付的靈活性。另外,因爲Controller負責調度Manager和View,所以它是知道Manager和View之間的關係的,Controller知道了這個關係以後,就有了充要條件來爲不一樣的View選擇不一樣的Reformer,並用這個Reformer去改造Mananger的數據,而後ViewController得到了通過reformer處理過的數據以後,就能夠直接交付給view去使用。Controller所以獲得瘦身,負責業務數據轉化的這部分代碼也不用寫在Controller裏面,提升了可維護性。
因此reformer機制可以帶來如下好處:
好處1:繞開了API數據原型的轉換,避免了相關成本。
好處2:在處理單View對多API,以及在單API對多View的狀況時,reformer提供了很是優雅的手段來響應這種需求,隔離了轉化邏輯和主體業務邏輯,避免了維護災難。
好處3:轉化邏輯集中,且將轉化次數轉爲只有一次。使用數據原型的轉化邏輯至少有兩次,第一次是把JSON映射成對應的原型,第二次是把原型轉變成能被View處理的數據。reformer一步到位。另外,轉化邏輯在reformer裏面,未來若是API數據有變,就只要去找到對應reformer而後改掉就行了。
好處4:Controller所以能夠省去很是多的代碼,下降了代碼複雜度,同時提升了靈活性,任什麼時候候切換reformer而沒必要切換業務邏輯就能夠應對不一樣View對數據的須要。
好處5:業務數據和業務有了適當的隔離。這麼作的話,未來若是業務邏輯有修改,換一個reformer就行了。若是其餘業務也有相同的數據轉化邏輯,其餘業務直接拿這個reformer就能夠用了,不用重寫。另外,若是controller有修改(好比UI交互方式改變),能夠放心換controller,徹底不用擔憂業務數據的處理。
在不使用特定對象表徵數據的狀況下,如何保持數據可讀性?
不使用對象來表徵數據的時候,事實上就是使用NSDictionary的時候。事實上,這個問題就是,如何在NSDictionary表徵數據的狀況下保持良好的可讀性?
蘋果已經給出了很是好的作法,用固定字符串作key,好比你在接收到KeyBoardWillShow的Notification時,帶了一個userInfo,他的key就都是相似UIKeyboardAnimationCurveUserInfoKey這樣的,因此咱們採用這樣的方案來維持可讀性。下面我舉一個例子:
PropertyListReformerKeys.h extern NSString * const kPropertyListDataKeyID; extern NSString * const kPropertyListDataKeyName; extern NSString * const kPropertyListDataKeyTitle; extern NSString * const kPropertyListDataKeyImage; PropertyListReformer.h #import "PropertyListReformerKeys.h" ... ... PropertyListReformer.m NSString * const kPropertyListDataKeyID = @"kPropertyListDataKeyID"; NSString * const kPropertyListDataKeyName = @"kPropertyListDataKeyName"; NSString * const kPropertyListDataKeyTitle = @"kPropertyListDataKeyTitle"; NSString * const kPropertyListDataKeyImage = @"kPropertyListDataKeyImage"; - (NSDictionary *)reformData:(NSDictionary *)originData fromManager:(APIManager *)manager { ... ... ... ... NSDictionary *resultData = nil; if ([manager isKindOfClass:[ZuFangListAPIManager class]]) { resultData = @{ kPropertyListDataKeyID:originData[@"id"], kPropertyListDataKeyName:originData[@"name"], kPropertyListDataKeyTitle:originData[@"title"], kPropertyListDataKeyImage:[UIImage imageWithUrlString:originData[@"imageUrl"]] }; } if ([manager isKindOfClass:[XinFangListAPIManager class]]) { resultData = @{ kPropertyListDataKeyID:originData[@"xinfang_id"], kPropertyListDataKeyName:originData[@"xinfang_name"], kPropertyListDataKeyTitle:originData[@"xinfang_title"], kPropertyListDataKeyImage:[UIImage imageWithUrlString:originData[@"xinfang_imageUrl"]] }; } if ([manager isKindOfClass:[ErShouFangListAPIManager class]]) { resultData = @{ kPropertyListDataKeyID:originData[@"esf_id"], kPropertyListDataKeyName:originData[@"esf_name"], kPropertyListDataKeyTitle:originData[@"esf_title"], kPropertyListDataKeyImage:[UIImage imageWithUrlString:originData[@"esf_imageUrl"]] }; } return resultData; } PropertListCell.m #import "PropertyListReformerKeys.h" - (void)configWithData:(NSDictionary *)data { self.imageView.image = data[kPropertyListDataKeyImage]; self.idLabel.text = data[kPropertyListDataKeyID]; self.nameLabel.text = data[kPropertyListDataKeyName]; self.titleLabel.text = data[kPropertyListDataKeyTitle]; }
這一大段代碼看下來,我若是不說一下要點,那基本上就白寫了哈:
咱們先看一下結構:
---------------------------------- ----------------------------------------- | | | | | PropertyListReformer.m | | PropertyListReformer.h | | | | | | #import PropertyListReformer.h | <------- | #import "PropertyListReformerKeys.h" | | NSString * const key = @"key" | | | | | | | ---------------------------------- ----------------------------------------- . /|\ | | | | --------------------------------- | | | PropertyListReformerKeys.h | | | | extern NSString * const key; | | | ---------------------------------
使用Const字符串來表徵Key,字符串的定義跟着reformer的實現文件走,字符串的extern聲明放在獨立的頭文件內。
這樣reformer生成的數據的key都使用Const字符串來表示,而後每次別的地方須要使用相關數據的時候,把PropertyListReformerKeys.h這個頭文件import進去就行了。
另外要注意的一點是,若是一個OriginData可能會被多個Reformer去處理的話,Key的命名規範須要可以表徵出其對應的reformer名字。若是reformer是PropertyListReformer
,那麼Key的名字就是PropertyListKeyXXXX
。
這麼作的好處就是,未來遷移的時候至關方便,只要扔頭文件就能夠了,只扔頭文件是不會致使拔出蘿蔔帶出泥的狀況的。並且也避免了自定義對象帶來的額外代碼體積。
另外,關於交付的NSDictionary,其實具體仍是看view的需求,reformer的設計初衷是:經過reformer轉化出來的能夠直接是View,或者是view直接可使用的對象(包括NSDictionary)
。好比地圖標點列表API的數據,經過reformer轉化以後就能夠直接變成MKAnnotation,而後MKMapView就能夠直接使用了。這裏說的只是當你的需求是交付NSDictionary時,如何保證可讀性的狀況,再強調一下哈,reformer交付的是view直接可使用的對象,交付出去的能夠是NSDictionary,也能夠是UIView,跟DataSource結合以後交付的甚至能夠是UITableViewCell/UICollectionViewCell。不要被NSDictionary或所謂的轉化成model再交付
的思想侷限。
綜上,我對交付什麼樣的數據給業務層?
這個問題的回答就是這樣:
對於業務層而言,由Controller根據View和APIManager之間的關係,選擇合適的reformer將View能夠直接使用的數據(甚至reformer能夠用來直接生成view)轉化好以後交付給View。對於網絡層而言,只須要保持住原始數據便可,不須要主動轉化成數據原型。而後數據採用NSDictionary加Const字符串key來表徵,避免了使用對象來表徵帶來的遷移困難,同時不失去可讀性。
集約型API調用方式和離散型API調用方式的選擇?
集約型API調用其實就是全部API的調用只有一個類,而後這個類接收API名字,API參數,以及回調着陸點(能夠是target-action,或者block,或者delegate等各類模式的着陸點)做爲參數。而後執行相似startRequest
這樣的方法,它就會去根據這些參數起飛去調用API了,而後得到API數據以後再根據指定的着陸點去着陸。好比這樣:
集約型API調用方式:
[APIRequest startRequestWithApiName:@"itemList.v1" params:params success:@selector(success:) fail:@selector(fail:) target:self];
離散型API調用是這樣的,一個API對應於一個APIManager,而後這個APIManager只須要提供參數就能起飛,API名字、着陸方式都已經集成入APIManager中。好比這樣:
離散型API調用方式:
@property (nonatomic, strong) ItemListAPIManager *itemListAPIManager; // getter - (ItemListAPIManager *)itemListAPIManager { if (_itemListAPIManager == nil) { _itemListAPIManager = [[ItemListAPIManager alloc] init]; _itemListAPIManager.delegate = self; } return _itemListAPIManager; } // 使用的時候就這麼寫: [self.itemListAPIManager loadDataWithParams:params];
集約型API調用和離散型API調用這二者實現方案不是互斥的,單看下層,你們都是集約型。由於發起一個API請求以後,除去業務相關的部分(好比參數和API名字等),剩下的都是要統一處理的:加密,URL拼接,API請求的起飛和着陸,這些處理若是不用集約化的方式來實現,做者非癲即癡。然而對於整個網絡層來講,尤爲是業務方使用的那部分,我傾向於提供離散型的API調用方式,並不建議在業務層的代碼直接使用集約型的API調用方式。緣由以下:
前者的業務場景舉個例子就是刷新頁面的請求,刷新詳情,刷新列表等。後者的業務場景舉個例子是列表多維度篩選,好比你先篩選了商品類型,而後篩選了價格區間。固然,後者的狀況不必定每次篩選都要調用API,咱們先假設這種篩選每次都必需要經過調用API才能得到數據。
若是是離散型的API調用,在編寫不一樣的APIManager時候就能夠針對不一樣的API設置不一樣的起飛策略,在實際使用的時候,就能夠沒必要關心起飛策略了,由於APIMananger裏面已經寫好了。
當一個頁面的請求正在天上飛的時候,用戶等了很久不耐煩了,小手點了個back,而後ViewController被pop被回收。此時請求的着陸點就沒了。這是很危險的狀況,着陸點要是沒了,就很容易crash的。通常來講處理這個狀況都是在dealloc的時候取消當前頁面全部的請求。若是是集約型的API調用,這個代碼就要寫到ViewController的dealloc裏面,但若是是離散型的API調用,這個代碼寫到APIManager裏面就能夠了,而後隨着ViewController的回收進程,APIManager也會被跟着回收,這部分代碼就獲得了調用的機會。這樣業務方在使用的時候就能夠沒必要關心着陸點消失的狀況了,從而更加關注業務。
loadNextPage
方法去加載下一頁,頁碼的管理就不用業務方去管理了。還有就是,若是要針對業務請求參數進行驗證,好比用戶填寫註冊信息,在離散型的APIManager裏面實現就會很是輕鬆。綜上,關於集約型的API調用和離散型的API調用,我傾向於這樣:對外提供一個BaseAPIManager來給業務方作派生,在BaseManager裏面採用集約化的手段組裝請求,放飛請求,然而業務方調用API的時候,則是以離散的API調用方式來調用。若是你的App只提供了集約化的方式,而沒有離散方式的通道,那麼我建議你再封裝一層,便於業務方使用離散的API調用方式來放飛請求。
怎麼作APIManager的繼承?
若是要作成離散型的API調用,那麼使用繼承是逃不掉的。BaseAPIManager裏面負責集約化的部分,外部派生的XXXAPIManager負責離散的部分,對於BaseAPIManager來講,離散的部分有一些是必要的,好比API名字等,而咱們派生的目的,也是爲了提供這些數據。
我在這篇文章裏面列舉了種種繼承的壞處,呼籲你們儘可能不要使用繼承。可是如今到了不起不用繼承的時候,因此我得提醒一下你們別把繼承用壞了。
在APIManager的狀況下,咱們最直覺的思路是BaseAPIManager提供一些空方法來給子類作重載,好比apiMethodName
這樣的函數,然而個人建議是,不要這麼作。咱們能夠用IOP的方式來限制派生類的重載。
大概就是長這樣:
BaseAPIManager的init方法裏這麼寫:
// 注意是weak。 @property (nonatomic, weak) id<APIManager> child; - (instancetype)init { self = [super init]; if ([self confirmsToProtocol:@protocol(APIManager)]) { self.child = (id<APIManager>)self; } else { // 不遵照這個protocol的就讓他crash,防止派生類亂來。 NSAssert(NO, "子類必需要實現APIManager這個protocol。"); } return self; } protocol這麼寫,把本來要重載的函數都定義在這個protocol裏面,就不用在父類裏面寫空方法了: @protocol APIManager <NSObject> @required - (NSString *)apiMethodName; ... @end 而後在父類裏面若是要使用的話,就這麼寫: [self requestWithAPIName:[self.child apiMethodName] ......];
簡單說就是在init的時候檢查本身是否符合預先設計的子類的protocol,這就要求全部子類必須遵照這個protocol,全部針對父類的重載、覆蓋也都以這個protocol爲準,protocol之外的方法不容許重載、覆蓋。而在父類的代碼裏,能夠沒必要遵照這個protocol,保持了將來維護的靈活性。
這麼作的好處就是避免了父類寫空方法,同時也給子類帶上了緊箍咒:要想當個人孩子,就要遵照這些規矩,不能亂來。業務方在實現子類的時候,就能夠根據protocol中的方法去一一實現,而後約定就比較好作了:不容許重載父類方法,只容許選擇實現或不實現protocol中的方法。
關於這個的具體的論述在這篇文章裏面有,感興趣的話能夠看看。
網絡層與業務層對接部分的小總結
這一節主要是講了如下這些點:
使用這個機制的目的主要有兩點:
解決方案:設計簽名
要達到第一個目的其實很簡單,服務端須要給你一個密鑰,每次調用API時,你使用這個密鑰再加上API名字和API請求參數算一個hash出來,而後請求的時候帶上這個hash。服務端收到請求以後,按照一樣的密鑰一樣的算法也算一個hash出來,而後跟請求帶來的hash作一個比較,若是一致,那麼就表示這個API的調用者確實是你的APP。爲了避免讓別人也獲取到這個密鑰,你最好不要把這個密鑰存儲在本地,直接寫死在代碼裏面就行了。另外適當增長一下求Hash的算法的複雜度,那就是各類Hash算法(好比MD5)加點鹽,再回爐跑一次Hash啥的。這樣就能解決第一個目的了:確保你的API是來自於你本身的App。
通常狀況下大部分公司不會出現須要知足第二種狀況的需求,除非公司開發了本身的API平臺給第三方使用。這個需求跟上面的需求有一點不一樣:符合受權的API請求者不僅是一個。因此在這種狀況下,須要的安全機制會更加複雜一點。
這裏有一個較容易實現的方案:客戶端調用API的時候,把本身的密鑰經過一個可逆的加密算法加密後連着請求和加密以後的Hash一塊兒送上去。固然,這個可逆的加密算法確定是放在在調用API的SDK裏面,編譯好的。而後服務端拿到加密後的密鑰和加密的Hash以後,解碼獲得原始密鑰,而後再用它去算Hash,最後再進行比對。
使用這個機制的主要目的有兩點:
解決方案:HTTPS
目前使用HTTPS的主要目的在於防止運營商往你的Response Data裏面加廣告啥的(中間人攻擊),面對的威脅範圍更廣。從2011年開始,國外業界就已經提倡全部的請求(不光是API,還有網站)都走HTTPS,國內差很少晚了兩年(2013年左右)纔開始提倡這事,天貓是這兩個月纔開始作HTTPS的全APP遷移。
關於速度,HTTPS確定是比HTTP慢的,畢竟多了一次握手,但掛上SPDY以後,有了連接複用,這方面的性能就有了較大提高。這裏的性能提高並非說一個請求原來要500ms能完成,而後如今只要300ms,這是不對的。所謂總體性能是基於大量請求去討論的:一樣的請求量(假設100個)在短時間發生時,掛上SPDY以後完成這些任務所要花的時間比不用SPDY要少。SPDY還有Header壓縮的功能,不過由於一個API請求自己已經比較小了,壓縮數據量所帶來的性能提高不會特別明顯,因此就單個請求來看,性能的提高是比較小的。不過這是下一節要討論的事兒了,這兒只是順帶說一下。
這一節說了兩種安全機制,通常來講第一種是標配,第二種屬於可選配置。不過隨着我國互聯網基礎設施的完善,移動設備性能的提升,以及優化技術的提升,第二種配置的缺點(速度慢)正在愈來愈微不足道,所以HTTPS也會成爲不久以後的將來App的網絡層安全機制標配。各位架構師們,若是你的App尚未掛HTTPS,如今就已經能夠開始着手這件事情了。
網絡層的優化手段主要從如下三方面考慮:
這三方面是全部優化手段的內容,各類五花八門的優化手段基本上都不會逃脫這三方面,下面我就會分別針對這三方面講一下各自對應的優化手段。
在API發起請求創建連接的環節,大體會分這些步驟:
其實第三步的優化手段跟第二步的優化手段是一致的,我會在講第二步的時候一塊兒講掉。
1.1 針對發起請求的優化手段
其實要解決的問題就是網絡層該不應爲此API調用發起請求。
對於大部分API調用請求來講,有些API請求所帶來的數據的時效性是比較長的,好比商品詳情,好比App皮膚等。那麼咱們就能夠針對這些數據作本地緩存,這樣下次請求這些數據的時候就能夠沒必要再發起新的請求。
通常是把API名字和參數拼成一個字符串而後取MD5做爲key,存儲對應返回的數據。這樣下次有一樣請求的時候就能夠直接讀取這裏面的數據。關於這裏有一個緩存策略的問題須要討論:何時清理緩存?
要麼就是根據超時時間限制進行清理,要麼就是根據緩存數據大小進行清理。這個策略的選擇要根據具體App的操做日誌來決定。
好比安居客App,日誌數據記錄顯示用戶平均使用時長不到3分鐘,可是用戶查看房源詳情的次數比較多,而房源詳情數據量較大。那麼這個時候,就適合根據使用時長來作緩存,我當時給安居客設置的緩存超時時間就是3分鐘,這樣可以保證這個緩存可以在大部分用戶使用時間產生做用。嗯,極端狀況下作什麼緩存手段不考慮,只要可以服務好80%的用戶就能夠了,並且針對極端狀況採用的優化手段對大部分普通用戶而言是沒必要要的,作了反而會對他們有影響。
再好比網絡圖片緩存,數據量基本上都特別大,這種就比較適合針對緩存大小來清理緩存的策略。
另外,以前的緩存的前提都是基於內存的。咱們也能夠把須要清理的緩存存儲在硬盤上(APP的本地存儲,我就先用硬盤來表示了,雖然不多有手機硬盤的說法,哈哈),好比前面提到的圖片緩存,由於圖片頗有可能在很長時間以後,再被顯示的,那麼本來須要被清理的圖片緩存,咱們就能夠考慮存到硬盤上去。當下次再有顯示網絡圖片的需求的時候,咱們能夠先從內存中找,內存找不到那就從硬盤上找,這都找不到,那就發起請求吧。
固然,有些時效性很是短的API數據,就不能使用這個方法了,好比用戶的資金數據,那就須要每次都調用了。
這個我在前面提到過,就是針對重複請求的發起和取消,是有對應的請求策略的。咱們先說取消策略。
若是是界面刷新請求這種,並且存在重複請求的狀況(下拉刷新時,在請求着陸以前用戶不斷執行下拉操做),那麼這個時候,後面重複操做致使的API請求就能夠沒必要發送了。
若是是條件篩選這種,那就取消前面已經發送的請求。雖然頗有可能這個請求已經被執行了,那麼取消所帶來的性能提高就基本沒有了。但若是這個請求還在隊列中待執行的話,那麼對應的此次連接就能夠省掉了。
以上是一種,另一種狀況就是請求策略:相似用戶操做日誌的請求策略。
用戶操做會觸發操做日誌上報Server,這種請求特別頻繁,可是是暗地裏進行的,不須要用戶對此有所感知。因此也不必操做一次就發起一次的請求。在這裏就能夠採用這樣的策略:在本地記錄用戶的操做記錄,當記錄滿30條的時候發起一次請求將操做記錄上傳到服務器。而後每次App啓動的時候,上傳一次上次遺留下來沒上傳的操做記錄。這樣可以有效下降用戶設備的耗電量,同時提高網絡層的性能。
小總結
針對創建鏈接這部分的優化就是這樣的原則:能不發請求的就儘可能不發請求,必需要發請求時,能合併請求的就儘可能合併請求。然而,任何優化手段都是有前提的,並且也不能保證對全部需求都能起做用,有些API請求就是不符合這些優化手段前提的,那就老老實實發請求吧。不過這類API請求所佔比例通常不大,大部分的請求都或多或少符合優化條件,因此針對發送請求的優化手段仍是值得作的。
1.2 & 1.3 針對DNS域名解析作的優化,以及創建連接的優化
其實在整個DNS鏈路上也是有DNS緩存的,理論上也是可以提升速度的。這個鏈路上的DNS緩存在PC用戶上效果明顯,由於PC用戶的DNS鏈路相對穩定,信號源不會變來變去。可是在移動設備的用戶這邊,鏈路上的DNS緩存所帶來的性能提高就不太明顯了。由於移動設備的實際使用場景比較複雜,網絡信號源會常常變換,信號源每變換一次,對應的DNS解析鏈路就會變換一次,那麼原鏈路上的DNS緩存就不起做用了。並且信號源變換的狀況特別特別頻繁,因此對於移動設備用戶來講,鏈路的DNS緩存咱們基本上能夠默認爲沒有。因此大部分時間是手機系統自帶的本地DNS緩存在起做用,可是通常來講,移動設備上網的需求也特別頻繁,專門爲咱們這個App所作的DNS緩存頗有可能會被別的DNS緩存給擠出去被清理掉,這種狀況是特別多的,用戶看一下子知乎刷一下微博查一下地圖逛一逛點評再聊個Q,回來以後頗有可能屬於你本身的App的本地DNS緩存就沒了。這還沒完,這裏還有一個只有在中國特點社會主義的互聯網環境中才會有的問題:國內的互聯網環境因爲GFW的存在,就使得DNS服務速度會比正常狀況慢很多。
基於以上三個緣由所致使的最終結果就是,API請求在DNS解析階段的耗時會不少。
那麼針對這個的優化方案就是,索性直接走IP請求,那不就繞過DNS服務的耗時了嘛。
另一個,就是上面提到的創建連接時候的第三步,國內的網絡環境分北網通南電信(固然實際狀況更復雜,這裏隨便說說),不一樣服務商之間的鏈接,延時是很大的,咱們須要想辦法讓用戶在最適合他的IP上給他提供服務,那麼就針對咱們繞過DNS服務的手段有一個額外要求:儘量不要讓用戶使用對他來講很慢的IP。
因此綜上所述,方案就應該是這樣:本地有一份IP列表,這些IP是全部提供API的服務器的IP,每次應用啓動的時候,針對這個列表裏的全部IP取ping延時時間,而後取延時時間最小的那個IP做爲從此發起請求的IP地址。
針對創建鏈接的優化手段實際上是跟DNS域名解析的優化手段是同樣的。不過這須要你的服務器提供服務的網絡狀況要多,通常如今的服務器都是雙網卡,電信和網通。因爲中國特點的互聯網ISP分佈,南北網絡之間存在瓶頸,而咱們App針對連接的優化手段主要就是着手於如何減輕這個瓶頸對App產生的影響,因此須要維護一個IP列表,這樣就能就近鏈接了,就起到了優化的效果。
咱們通常都是在應用啓動的時候得到本地列表中全部IP的ping值,而後經過NSURLProtocol的手段將URL中的HOST修改成咱們找到的最快的IP。另外,這個本地IP列表也會須要經過一個API來維護,通常是天天第一次啓動的時候讀一次API,而後更新到本地。
若是你還不熟悉NSURLProtocol應該怎麼玩,看完官方文檔和這篇文章以及這個Demo以後,你確定就會了,其實很簡單的。另外,剛纔提到那篇文章的做者(mattt)還寫了這個基於NSURLProtocol的工具,至關好用,是能夠直接拿來集成到項目中的。
不用NSURLProtocol的話,用其餘手段也能夠作到這一點,但那些手段未免又比較愚蠢。
這個很好理解,傳輸的數據少了,那麼天然速度就上去了。這裏沒什麼花樣能夠講的,就是壓縮唄。各類壓縮。
創建連接自己是屬於比較消耗資源的操做,耗電耗時。SPDY自帶連接複用以及數據壓縮的功能,因此服務端支持SPDY的時候,App直接掛SPDY就能夠了。若是服務端不支持SPDY,也可使用PipeLine,蘋果原生自帶這個功能。
通常來講業界內廣泛的認識是SPDY優於PipeLine,而後即使如此,SPDY可以帶來的網絡層效率提高其實也沒有文獻上的圖表那麼明顯,但仍是有性能提高的。還有另一種比較笨的連接複用的方法,就是維護一個隊列,而後將隊列裏的請求壓縮成一個請求發出去,之因此會存在滯留在隊列中的請求,是由於在上一個請求還在外面飄的時候。這種作法最終的效果表面上看跟連接複用差異不大,但並非真正的連接複用,只能說是請求合併。
仍是說回來,我建議最好是用SPDY,SPDY和pipeline雖然都屬於連接複用的範疇,可是pipeline並非真正意義上的連接複用,SPDY的連接複用相對pipeline而言更爲完全。SPDY目前也有現成的客戶端SDK可使用,一個是twitter的CocoaSPDY,另外一個是Voxer/iSPDY,這兩個庫都很活躍,你們能夠挑合適的採用。
不過目前業界趨勢是傾向於使用HTTP/2.0來代替SPDY,不過目前HTTP/2.0尚未正式出臺,相關實現大部分都處在demo階段,因此咱們仍是先SPDY搞起就行了。將來頗有可能會放棄SPDY,轉而採用HTTP/2.0來實現網絡的優化。這是要提醒各位架構師注意的事情。嗯,我也不知道HTTP/2.0何時能出來。
漁說完了,魚來了
這裏是我當年設計並實現的安居客的網絡層架構代碼。固然,該脫敏的地方我都已經脫敏了,因此編不過是正常的,哈哈哈。可是代碼比較齊全,重要地方註釋我也寫了不少。另外,爲了讓你們可以把這些代碼看明白,我還附帶了當年介紹這個框架演講時的PPT。(補充說明一下,評論區好多人問PPT找不着在哪兒,PPT也在上面提到的repo裏面,是個key後綴名的文件,用keynote打開
)
而後就是,當年也有不少問題其實考慮得並無如今清楚,因此有些地方仍是作得不夠好,好比攔截器和繼承。並且當時的優化手段只有本地cache,安居客沒有那麼多IP能夠給我ping,當年也沒流行SPDY,並且API也還不支持HTTPS,因此當時的代碼裏面沒有在這些地方作優化,比較原始。然而整個架構的基本思路一直沒有變化:優先服務於業務方。另外,安居客的網絡層多了一個service的概念,這是我這篇文章中沒有講的。主要是由於安居客的API提供方不少,二手房,租房,新房,X項目等等API都是不一樣的API team提供的,以service做區分,若是你的app也是相似的狀況,我也建議你設計一套service機制。如今這些service被我刪得只剩下一個google的service,由於其餘service都屬於敏感內容。
另外,這裏面提供的PPT我很但願你們可以花時間去看看,在PPT裏面有些更加細的東西我在博客裏沒有寫,主要是我比較懶,而後這篇文章拖的時間比較長了,花時間搬運這個沒什麼意思,不過內容仍是值得各位讀者去看的。關於PPT裏面你們有什麼問題的,也能夠在評論區問,我都會回答。
第一部分主要講了網絡層應當如何跟業務層進行數據交互,進行數據交互時採用怎樣的數據格式,以及設計時代碼結構上的一些問題,諸如繼承的處理,回調的處理,交互方式的選擇,reformer的設計,保持數據可讀性等等等等,主要偏重於設計(這但是藝術活,哈哈哈)。
第二部分講了網絡安全上,客戶端要作的兩點。固然,從網絡安全的角度上講,服務端也要作不少不少事情,客戶端要作的一些邊角細節的事情也還會有不少,好比作一些代碼混淆,儘量避免代碼中明文展現key。不過大頭主要就是這兩個,並且也都是須要服務端同窗去配合的。主要偏重於介紹。(主要是也沒啥好實踐的,google一下教程照着來就行了)。
第三部分講了優化,優化的全部方面都已經列出來了,若是業界再有七七八八的別的手段,也基本逃離不出本文的範圍。這裏有些優化手段是須要服務端同窗配合的,有些不須要,你們看各自狀況來決定。主要偏重於實踐。
最後給出了我以前在安居客作的網絡層架構的主要代碼,以及當時演講時的PPT。關於代碼或PPT中有任何問題,均可以在評論區問我。
這一篇文章出得比較晚,由於公司的事情,中間間隔了一個禮拜,但願你們諒解。另外,隔了一個禮拜以後我再寫,發現有些地方我已經想不起來當初是應該怎麼行文下去的了,而後發以前我把文章又看了幾遍,儘量把斷片的地方抹平了,若是你們讀起來有什麼地方感受奇怪的,或者講到一半就沒了的,那應該就是斷片了。在評論區跟我說一下,我補上去。
而後若是有須要勘誤的地方,也請在評論區指出,幫助我把錯的地方訂正回來,若是有沒講到的地方,但你又特別想要了解的,也能夠在評論區提出來,我會補上去。說不定看完以後你腦殼裏還會有不少個問號,也請在評論區問出來哈,說不定別人也有跟你同樣的問題,他就能在評論區找到答案了。