天天看點

iOS應用架構談 網絡層設計方案--RTNetworking前言網絡層跟業務對接部分的設計網絡層的安全機制網絡層的優化方案總結

iOS應用架構談 開篇 

iOS應用架構談 view層的組織和調用方案 

iOS應用架構談 網絡層設計方案 

iOS應用架構談 本地持久化方案及動态部署 

iOS應用架構談 元件化方案

https://github.com/casatwy/RTNetworking

前言

網絡層在一個App中也是一個不可缺少的部分,工程師們在網絡層能夠發揮的空間也比較大。另外,蘋果對網絡請求部分已經做了很好的封裝,業界的AFNetworking也被廣泛使用。其它的ASIHttpRequest,MKNetworkKit啥的其實也都還不錯,但前者已經棄坑,後者也在棄坑的邊緣。在實際的App開發中,Afnetworking已經成為了事實上各大App的标準配置。

網絡層在一個App中承載了API調用,使用者記錄檔記錄,甚至是即時通訊等任務。我接觸過一些App(開源的和不開源的)的代碼,在看到網絡層這一塊時,尤其是在看到各位架構師各顯神通展示了各種技巧,我非常為之感到興奮。但有的時候,往往也對于其中的一些缺陷感到失望。

關于網絡層的設計方案會有很多,需要權衡的地方也會有很多,甚至于争議的地方都會有很多。但無論如何,我都不會對這些問題做出任何逃避,我會在這篇文章中給出我對它們的看法和解決方案,觀點絕不中立,不會跟大家打太極。

這篇文章就主要會講這些方面:

  1. 網絡層跟業務對接部分的設計
  2. 網絡層的安全機制實作
  3. 網絡層的優化方案

網絡層跟業務對接部分的設計

在安居客App的架構更新換代的時候,我深深地感覺到網絡層跟業務對接部分的設計有多麼重要,是以我對它做的最大改變就是針對網絡層跟業務對接部分的改變。網絡層跟業務層對接部分設計的好壞,會直接影響到業務工程師實作功能時的心情。

在正式開始講設計之前,我們要先讨論幾個問題:

  1. 使用哪種互動模式來跟業務層做對接?
  2. 是否有必要将API傳回的資料封裝成對象然後再傳遞給業務層?
  3. 使用集約化調用方式還是離散型調用方式去調用API?
這些問題讨論完畢之後,我會給出一個完整的設計方案來給大家做參考,設計方案是魚,讨論的這些問題是漁,我什麼都授了,大家各取所需。
使用哪種互動模式來跟業務層做對接?
這裡其實有兩個問題:
  1. 以什麼方式将資料傳遞給業務層?
  2. 傳遞什麼樣的資料給業務層?

以什麼方式将資料傳遞給業務層?

iOS開發領域有很多對象間資料的傳遞方式,我看到的大多數App在網絡層所采用的方案主要集中于這三種:Delegate,Notification,Block。KVO和Target-Action我目前還沒有看到有使用的。

目前我知道邊鋒主要是采用的block,大智慧主要采用的是Notification,安居客早期以Block為主,後面改成了以Delegate為主,阿裡沒發現有通過Notification來做資料傳遞的地方(可能有),Delegate、Block以及target-action都有,阿裡iOS App網絡層的作者說這是為了友善業務層選擇自己合适的方法去使用。這裡大家都是各顯神通,每次我看到這部分的時候,我都喜歡問作者為什麼采用這種互動方案,但很少有作者能夠說出個條條框框來。

然而在我這邊,我的意見是以Delegate為主,Notification為輔。原因如下:

  • 盡可能減少跨層資料交流的可能,限制耦合
  • 統一回調方法,便于調試和維護
  • 在跟業務層對接的部分隻采用一種對接手段(在我這兒就是隻采用delegate這一個手段)限制靈活性,以此來交換應用的可維護性
盡可能減少跨層資料交流的可能,限制耦合
什麼叫跨層資料交流?就是某一層(或子產品)跟另外的與之沒有直接對接關系的層(或子產品)産生了資料交換。為什麼這種情況不好?嚴格來說應該是大部分情況都不好,有的時候跨層資料交流确實也是一種需求。之是以說不好的地方在于,

它會導緻代碼混亂,破壞子產品的封裝性

。我們在做分層架構的目的其中之一就在于下層對上層有一次抽象,讓上層可以不必關心下層細節而執行自己的業務。

是以,如果下層細節被跨層暴露,一方面你很容易是以失去鄰層對這個暴露細節的保護;另一方面,你又不可能不去處理這個細節,是以處理細節的相關代碼就會散落各地,最終難以維護。

說得具象一點就是,我們考慮這樣一種情況: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,如果想知道這個block裡面都做了些什麼事情,這時候就比較蛋疼了。
- (void)someFunctionWithBlock:(SomeBlock *)block
{
    ... ...

 -> block();  //當你單步走到這兒的時候,要想知道block裡面都做了哪些事情的話,就很麻煩。

    ... ...
}
      
  • block會延長相關對象的生命周期

block會給内部所有的對象引用計數加一,這一方面會帶來潛在的retain cycle,不過我們可以通過Weak Self的手段解決。另一方面比較重要就是,它會延長對象的生命周期。

在網絡回調中使用block,是block導緻對象生命周期被延長的其中一個場合,當ViewController從window中卸下時,如果尚有請求帶着block在外面飛,然後block裡面引用了ViewController(這種場合非常常見),那麼ViewController是不能被及時回收的,即便你已經取消了請求,那也還是必須得等到請求着陸之後才能被回收。

然而使用delegate就不會有這樣的問題,delegate是弱引用,哪怕請求仍然在外面飛,,ViewController還是能夠及時被回收的,回收之後指針自動被置為了nil,無傷大雅。

  • block在離散型場景下不符合使用的規範

block和delegate乍看上去在作用上是很相似,但是關于它們的選型有一條嚴格的規範:當回調之後要做的任務在每次回調時都是一緻的情況下,選擇delegate,在回調之後要做的任務在每次回調時無法保證一緻,選擇block。在離散型調用的場景下,每一次回調都是能夠保證任務一緻的,是以适用delegate。這也是蘋果原生的網絡調用也采用delegate的原因,因為蘋果也是基于離散模型去設計網絡調用的,而且本文即将要介紹的網絡層架構也是基于離散型調用的思路去設計的。

在集約型調用的場景下,使用block是合理的,因為每次請求的類型都不一樣,那麼自然回調要做的任務也都會不一樣,是以隻能采用block。AFNetworking就是屬于集約型調用,是以它采用了block來做回調。

就我所知,目前大部分公司的App網絡層都是集約型調用,是以廣泛采取了block回調。但是在App的網絡層架構設計中直接采用集約型調用來為業務服務的思路是有問題的,是以在遷移到離散型調用時,一定要注意這一點,記得遷回delegate回調。關于離散型和集約型調用的介紹和如何選型,我在後面的

集約型API調用方式和離散型API調用方式的選擇?

小節中有詳細的介紹。

是以平時盡量不要濫用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這樣的對象。這種做法是能夠提高後續操作代碼的可讀性的。在比較直覺的思路裡面,是需要這部分轉化過程的,但這部分轉化過程的成本是很大的,主要成本在于:

  1. 數組内容的轉化成本較高:數組裡面每項都要轉化成Item對象,如果Item對象中還有類似數組,就很頭疼。
  2. 轉化之後的資料在大部分情況是不能直接被展示的,為了能夠被展示,還需要第二次轉化。
  3. 隻有在API傳回的資料高度标準化時,這些對象原型(Item)的可複用程度才高,否則容易出現類型爆炸,提高維護成本。
  4. 調試時通過對象原型檢視資料内容不如直接通過NSDictionary/NSArray直覺。
  5. 同一API的資料被不同View展示時,難以控制資料轉化的代碼,它們有可能會散落在任何需要的地方。

其實我們的理想情況是希望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];
    }
}
      
  • 要點1:reformer是一個符合ReformerProtocol的對象,它提供了通用的方法供Manager使用。
  • 要點2:API的原始資料(JSON對象)由Manager執行個體保管,reformer方法裡面取Manager的原始資料(manager.rawData)做轉換,然後傳遞出去。蓮蓬頭的水管部分是Manager,負責提供原始水流(資料流),reformer就是不同的模式,換什麼reformer就能出來什麼水流。
  • 要點3:例子中舉的場景是一個API資料被多個View使用的情況,展現了reformer的一個特點:可以根據需要改變同一資料來源的展示方式。比如API資料展示的是“附近的小區”,那麼這個資料可以被清單(XXXView)和地圖(YYYView)共用,不同的view使用的資料的轉化方式不一樣,這就通過不同的reformer解決了。
  • 要點4:在一個view用來同一展示不同API資料的情況,reformer是絕佳利器。比如安居客的清單view的資料來源可能有三個:二手房清單API,租房清單API,新房清單API。這些API傳回來的資料的value可能一緻,但是key都是不一緻的。這時候就可以通過同一個reformer來做資料的标準化輸出,這樣就使得view代碼複用成為可能。這展現了reformer另外一個特點:同一個reformer出來的資料是高度标準化的。形象點說就是:隻要蓮蓬頭不換,哪怕水管的水變成海水或者污水了,也依舊能夠輸出符合洗澡要求的淡水水流。舉個例子:
- (void)apiManagerDidSuccess:(APIManager *)manager
{
    // 這個回調方法有可能是來自二手房清單APIManager的回調,也有可能是租房,也有可能是新房。但是在Controller層面我們不需要對它做額外區分,隻要是同一個reformer出來的資料,我們就能保證是一定能被self.XXXView使用的。這樣的保證由reformer的實作者來提供。
    NSDictionary *reformedXXXData = [manager fetchDataWithReformer:self.XXXReformer];
    [self.XXXView configWithData:reformedXXXData];
}
      
  • 要點5:有沒有發現,使用reformer之後,Controller的代碼簡潔了很多?而且,資料原型在這種情況下就沒有必要存在了,随之而來的成本也就被我們繞過了。

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調用方式。原因如下:
  • 原因1:目前請求正在外面飛着的時候,根據不同的業務需求存在兩種不同的請求起飛政策:一個是取消新發起的請求,等待外面飛着的請求着陸。另一個是取消外面飛着的請求,讓新發起的請求起飛。集約化的API調用方式如果要滿足這樣的需求,那麼每次要調用的時候都要多寫一部分判斷和取消的代碼,手段就做不到很幹淨。

前者的業務場景舉個例子就是重新整理頁面的請求,重新整理詳情,重新整理清單等。後者的業務場景舉個例子是清單多元度篩選,比如你先篩選了商品類型,然後篩選了價格區間。當然,後者的情況不一定每次篩選都要調用API,我們先假設這種篩選每次都必須要通過調用API才能獲得資料。

如果是離散型的API調用,在編寫不同的APIManager時候就可以針對不同的API設定不同的起飛政策,在實際使用的時候,就可以不必關心起飛政策了,因為APIMananger裡面已經寫好了。

  • 原因2:便于針對某個API請求來進行AOP。在集約型的API調用方式下,如果要針對某個API請求的起飛和着陸過程進行AOP,這代碼得寫成什麼樣。。。噢,尼瑪這畫面太美别說看了,我都不敢想。
  • 原因3:當API請求的着陸點消失時,離散型的API調用方式能夠更加透明地處理這種情況。
當一個頁面的請求正在天上飛的時候,使用者等了好久不耐煩了,小手點了個back,然後ViewController被pop被回收。此時請求的着陸點就沒了。這是很危險的情況,着陸點要是沒了,就很容易crash的。一般來說處理這個情況都是在dealloc的時候取消目前頁面所有的請求。如果是集約型的API調用,這個代碼就要寫到ViewController的dealloc裡面,但如果是離散型的API調用,這個代碼寫到APIManager裡面就可以了,然後随着ViewController的回收程序,APIManager也會被跟着回收,這部分代碼就得到了調用的機會。這樣業務方在使用的時候就可以不必關心着陸點消失的情況了,進而更加關注業務。
  • 原因4:離散型的API調用方式能夠最大程度地給業務方提供靈活性,比如reformer機制就是基于離散型的API調用方式的。另外,如果是針對提供翻頁機制的API,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 conformsToProtocol:@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中的方法。

關于這個的具體的論述在這篇文章裡面有,感興趣的話可以看看。

網絡層與業務層對接部分的小總結
這一節主要是講了以下這些點:
  1. 使用delegate來做資料對接,僅在必要時采用Notification來做跨層通路
  2. 傳遞NSDictionary給業務層,使用Const字元串作為Key來保持可讀性
  3. 提供reformer機制來處理網絡層回報的資料,這個機制很重要,好處極多
  4. 網絡層上部分使用離散型設計,下部分使用集約型設計
  5. 設計合理的繼承機制,讓派生出來的APIManager受到限制,避免混亂
  6. 應該不止這5點...

網絡層的安全機制

判斷API的調用請求是來自于經過授權的APP

使用這個機制的目的主要有兩點:
  1. 確定API的調用者是來自你自己的APP,防止競争對手爬你的API
  2. 如果你對外提供了需要注冊才能使用的API平台,那麼你需要有這個機制來識别是否是注冊使用者調用了你的API
解決方案:設計簽名

要達到第一個目的其實很簡單,服務端需要給你一個密鑰,每次調用API時,你使用這個密鑰再加上API名字和API請求參數算一個hash出來,然後請求的時候帶上這個hash。服務端收到請求之後,按照同樣的密鑰同樣的算法也算一個hash出來,然後跟請求帶來的hash做一個比較,如果一緻,那麼就表示這個API的調用者确實是你的APP。為了不讓别人也擷取到這個密鑰,你最好不要把這個密鑰存儲在本地,直接寫死在代碼裡面就好了。另外适當增加一下求Hash的算法的複雜度,那就是各種Hash算法(比如MD5)加點鹽,再回爐跑一次Hash啥的。這樣就能解決第一個目的了:確定你的API是來自于你自己的App。

一般情況下大部分公司不會出現需要滿足第二種情況的需求,除非公司開發了自己的API平台給第三方使用。這個需求跟上面的需求有一點不同:符合授權的API請求者不隻是一個。是以在這種情況下,需要的安全機制會更加複雜一點。

這裡有一個較容易實作的方案:用戶端調用API的時候,把自己的密鑰通過一個可逆的加密算法加密後連着請求和加密之後的Hash一起送上去。當然,這個可逆的加密算法肯定是放在在調用API的SDK裡面,編譯好的。然後服務端拿到加密後的密鑰和加密的Hash之後,解碼得到原始密鑰,然後再用它去算Hash,最後再進行比對。

保證傳輸資料的安全

使用這個機制的主要目的有兩點:
  1. 防止中間人攻擊,比如說營運商很喜歡往使用者的Http請求裡面塞廣告...
  2. SPDY依賴于HTTPS,而且是未來HTTP/2的基礎,他們能夠提高你APP在網絡層整體的性能。
解決方案: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,現在就已經可以開始着手這件事情了。

網絡層的優化方案

網絡層的優化手段主要從以下三方面考慮:
  1. 針對連結建立環節的優化
  2. 針對連結傳輸資料量的優化
  3. 針對連結複用的優化
這三方面是所有優化手段的内容,各種五花八門的優化手段基本上都不會逃脫這三方面,下面我就會分别針對這三方面講一下各自對應的優化手段。

1. 針對連結建立環節的優化

在API發起請求建立連結的環節,大緻會分這些步驟:
  1. 發起請求
  2. DNS域名解析得到IP
  3. 根據IP進行三次握手(HTTPS四次握手),連結建立成功
其實第三步的優化手段跟第二步的優化手段是一緻的,我會在講第二步的時候一起講掉。
1.1 針對發起請求的優化手段
其實要解決的問題就是網絡層該不該為此API調用發起請求。
  • 1.1.1 使用緩存手段減少請求的發起次數

對于大部分API調用請求來說,有些API請求所帶來的資料的時效性是比較長的,比如商品詳情,比如App皮膚等。那麼我們就可以針對這些資料做本地緩存,這樣下次請求這些資料的時候就可以不必再發起新的請求。

一般是把API名字和參數拼成一個字元串然後取MD5作為key,存儲對應傳回的資料。這樣下次有同樣請求的時候就可以直接讀取這裡面的資料。關于這裡有一個緩存政策的問題需要讨論:

什麼時候清理緩存?

要麼就是根據逾時時間限制進行清理,要麼就是根據緩存資料大小進行清理。這個政策的選擇要根據具體App的記錄檔來決定。

比如安居客App,日志資料記錄顯示使用者平均使用時長不到3分鐘,但是使用者檢視房源詳情的次數比較多,而房源詳情資料量較大。那麼這個時候,就适合根據使用時長來做緩存,我當時給安居客設定的緩存逾時時間就是3分鐘,這樣能夠保證這個緩存能夠在大部分使用者使用時間産生作用。嗯,極端情況下做什麼緩存手段不考慮,隻要能夠服務好80%的使用者就可以了,而且針對極端情況采用的優化手段對大部分普通使用者而言是不必要的,做了反而會對他們有影響。

再比如網絡圖檔緩存,資料量基本上都特别大,這種就比較适合針對緩存大小來清理緩存的政策。

另外,之前的緩存的前提都是基于記憶體的。我們也可以把需要清理的緩存存儲在硬碟上(APP的本地存儲,我就先用硬碟來表示了,雖然很少有手機硬碟的說法,哈哈),比如前面提到的圖檔緩存,因為圖檔很有可能在很長時間之後,再被顯示的,那麼原本需要被清理的圖檔緩存,我們就可以考慮存到硬碟上去。當下次再有顯示網絡圖檔的需求的時候,我們可以先從記憶體中找,記憶體找不到那就從硬碟上找,這都找不到,那就發起請求吧。

當然,有些時效性非常短的API資料,就不能使用這個方法了,比如使用者的資金資料,那就需要每次都調用了。

  • 1.1.2 使用政策來減少請求的發起次數

這個我在前面提到過,就是針對重複請求的發起和取消,是有對應的請求政策的。我們先說取消政策。

如果是界面重新整理請求這種,而且存在重複請求的情況(下拉重新整理時,在請求着陸之前使用者不斷執行下拉操作),那麼這個時候,後面重複操作導緻的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的話,用其他手段也可以做到這一點,但那些手段未免又比較愚蠢。

2. 針對連結傳輸資料量的優化

這個很好了解,傳輸的資料少了,那麼自然速度就上去了。這裡沒什麼花樣可以講的,就是壓縮呗。各種壓縮。

3. 針對連結複用的優化

建立連結本身是屬于比較消耗資源的操作,耗電耗時。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中有任何問題,都可以在評論區問我。

這一篇文章出得比較晚,因為公司的事情,中間間隔了一個禮拜,希望大家諒解。另外,隔了一個禮拜之後我再寫,發現有些地方我已經想不起來當初是應該怎麼行文下去的了,然後發之前我把文章又看了幾遍,盡可能把斷片的地方抹平了,如果大家讀起來有什麼地方感覺奇怪的,或者講到一半就沒了的,那應該就是斷片了。在評論區跟我說一下,我補上去。

然後如果有需要勘誤的地方,也請在評論區指出,幫助我把錯的地方訂正回來,如果有沒講到的地方,但你又特别想要了解的,也可以在評論區提出來,我會補上去。說不定看完之後你腦袋裡還會有很多個問号,也請在評論區問出來哈,說不定别人也有跟你一樣的問題,他就能在評論區找到答案了。