iOS網絡模塊優化(失敗重發、緩存請求有網發送)

  iOS開發中,通常都是經過AFN搭建一個簡易的網絡模塊來進行與服務器的通訊,這一模塊要優化好沒那麼簡單,須要花費不少時間與精力,僅僅根據這幾年來的填坑經驗,總結下這一塊的須要注意的地方,也是給本身梳理下知識。html

  以前寫的博客提到了DNS優化、請求數據大小的優化(http://www.cnblogs.com/ziyi--caolu/p/8058577.html)。這裏主要想理一理合理的併發數以及網絡請求可靠性的保障。git

  優化的理論以前,先創建代碼樣例,假設咱們有這樣兩個類:  github

@interface ZYRequest : NSObject<NSCopying>

@end

@interface ZYRequestManager : NSObject

@end

   ZYRequest類用來處理公共的邏輯,Manager負責管理Request。在iOS開發中,不少時候會遇到多個Request集中發送的狀況,好比說第一次進入App首頁,須要請求骨架文件、首頁Banner圖片、展現Cell數據等等,若是這時候併發數太少,那些須要優先展現的數據請求可能會被次要的數據請求「阻塞」住。若是併發數太大,帶寬有限的場景下,會增長請求的總體延遲。通常而言,在實際開發中,讓請求的併發數限制在3~~5便可(也能夠給每一個請求設計優先級,而後在調度隊列裏面讓優先級高的請求先出隊列)。面試

  請求的可靠性保障是個很容易被忽視的問題,見過的不少App的網絡請求都是隻進行一次請求,失敗後直接給用戶提示網絡錯誤。比較好的作法,是將Request按業務分類:算法

    第一類,關鍵核心業務,指望在任何條件下能百分百送達服務器。數據庫

    第二類,重要的內容請求、數據展現,須要較高的成功率。設計模式

    第三類,通常性內容請求,對成功率無要求。緩存

  理論上來講,須要咱們應該儘可能讓每一個請求的成功率都達到最高,可是客戶端流量、帶寬、電量、服務器壓力等都是有限的資源,因此只能採起將關鍵性請求作高強度的可靠性保證。服務器

    

    代碼Github地址:https://github.com/wzpziyi1/iOSNetwork微信

    

  1、代碼結構分析

    其中Storage文件夾裏面,主要是處理將NSData數據緩存到沙盒的,實際我將它們的調用封裝在ZYRequestCache文件裏面,關於數據存儲到沙盒、數據庫,從沙盒取出數據、從數據庫取出數據,刪除、查詢等等全部操做,都是封裝在ZYRequestCache裏面,直接調用它的接口便可。

    數據庫採用的是realm數據庫,而且實現了在子線程進行數據的存取,不佔用主線程的資源,以避免形成卡頓。因爲是第一次使用realm,踩了不少坑。全部的關於數據庫的操做,都封裝在ZYRequestRealm文件裏面,裏面也有許多操做realm時踩過的坑的提示,最須要注意的一點是,在realm數據庫的使用中,對同一份數據的讀、寫、查詢後使用,都必須是在同一線程,在編碼時因爲將除查詢操做外的其餘數據庫操做放在子線程中,形成了各類線程錯誤的崩潰。

    YQDHttpClinetCore文件是基於AFN的封裝,在裏面設置了超時時間爲5s,主要是由於我設置的重發請求次數是3次,那麼真正交互的超時時間會是15s,若是有須要能夠自行進行調整。

    ZYRequest文件裏面是全部發送一次請求所須要的數據,例如url\params\method\type等。

    ZYRequestManager文件裏面是進行request調度的主題邏輯,也沒有進行復雜的算法,不按照優先級別,只是一個先入先出隊列來進行調用的。裏面有兩個dispatch_queue:

//這個串行隊列用來控制任務有序的執行
@property (nonatomic, strong) dispatch_queue_t taskQueue;

//添加、刪除隊列,維護添加與刪除request在同一個線程
@property (nonatomic, strong) dispatch_queue_t addDelQueue;

    taskQueue主要是用來處理調度隊列的,也就是requestQueue,讓它在子線程進行循環查詢、處理request,而後再併發進行網絡請求,這樣能夠防止請求不少的狀況下,卡住主線程。

    addDelQeueu主要是用來處理requestQueue裏面的requset增長與刪除的。在添加和刪除的時候,採用的方案都是串行+同步,主要是避免數據競爭。(由於在AFN發送request要刪除requestQueue裏面的request的時候,是併發狀態)

    在處理最大併發數的時候,我使用的是dispatch_semaphore_t(信號量),設置最大併發數是4。

    邏輯並不複雜,須要注意的是,如何避免數據競爭,如何儘量的不消耗主線程資源。 

 

  2、針對百分百送達服務器的請求  

    根據業務來講,這類請求應用的地方不少。相似於咱們發微信發消息時,消息數據一旦從數據框中發出,從用戶的角度感知這條消息是必定會到達對方的;在小說閱讀App的書架收藏功能,理論上來講戶收藏一本書時,在用戶感知角度,這本書必定會被收藏進入書架的等業務。若是網絡環境差,網絡模塊會在後臺悄悄重試,一段時間仍然沒法成功的話,就直接通知用戶發送失敗了,可是即便失敗,請求數據也會保存在本地,以便用戶從新觸發此條請求數據的發送。

    對於這類請求的處理,第一步並非直接發送,而是存入本地數據庫中,一旦存入了數據庫,即便是殺掉進程、斷電、重啓等極端操做,請求數據也依舊存在,咱們只須要在App重啓或者進入該業務界面時,還原請求數據到內存中,從新進行發送便可。代碼闡釋:

    

#import <Foundation/Foundation.h>
#import "ZYRequestMacro.h"
#import <Realm/Realm.h>



typedef NS_ENUM(NSInteger, ZYRequestReliability){

    //若是沒有發送成功,就放入調度隊列再次發送
    ZYRequestReliabilityRetry,
    
    //必需要成功的請求,若是不成功就存入DB,而後在網絡好的狀況下繼續發送,相似微信發消息
    //須要注意的是,這類請求不須要回調的
    //相似於發微信成功與否
    //就是一定成功的請求,只須要在有網的狀態下,一定成功
    ZYRequestReliabilityStoreToDB,
    
    //普通請求,成不成功不影響業務,不須要從新發送
    //相似統計、後臺拉取本地已有的配置之類的請求
    ZYRequestReliabilityNormal
};


@interface ZYRequest : RLMObject<NSCopying>

//存入數據庫的惟一標示
@property (nonatomic, assign) int requestId;

/**請求參數對*/
@property (nonatomic, strong) NSDictionary *params;


/**
 請求的url
 */
@property (nonatomic, copy) NSString *urlStr;

/**
 請求重複策略,默認重發
 */
@property (nonatomic, assign) ZYRequestReliability reliability;

/**
 請求方法,默認get請求
 */
@property (nonatomic, assign) YQDRequestType method;


/**
 是否須要緩存響應的數據,若是cacheKey爲nil,就不會緩存響應的數據
 */
@property (nonatomic, copy) NSString *cacheKey;

/**
 請求沒發送成功,從新發送的次數
 */
@property (nonatomic, assign, readonly) int retryCount;


/**
 realm不支持NSDictionary,因此params直接轉化爲字符串存儲
 只在請求須要存入數據庫中,此參數纔有相應的做用
 ZYRequestReliabilityStoreToDB這種類型下
 */
@property (nonatomic, copy, readonly) NSString *paramStr;


- (void)reduceRetryCount;
@end

     第一類請求就是ZYRequestReliabilityStoreToDB,requestId是它存入數據庫的惟一標示,下面是請求的發送流程:

    

- (void)sendRequest:(ZYRequest *)request successBlock:(SuccessBlock)successBlock failureBlock:(FailedBlock)failedBlock
{
    //若是是ZYRequestReliabilityStoreToDB類型
    //第一時間先存儲到數據庫,而後再發送該請求,若是成功再從數據庫中移除
    //不成功再出發某機制從數據庫中取出從新發送
    if (request.reliability == ZYRequestReliabilityStoreToDB)
    {
        [[ZYRequestCache sharedInstance] saveRequestToRealm:request];
    }
    
    [self queueAddRequest:request successBlock:successBlock failureBlock:failedBlock];
    [self dealRequestQueue];
}


//在成功的時候移除realm數據庫中的緩存
 if (request.reliability == ZYRequestReliabilityStoreToDB)
{
    [[ZYRequestCache sharedInstance] deleteRequestFromRealmWithRequestId:request.requestId];
}

//請求失敗以後,根據約定的錯誤碼判斷是否須要再次請求
                //這裏,-1001是AFN的超時error
 if (error.code == -1001 &&request.retryCount > 0)
{
    [request reduceRetryCount];
    [self queueAddRequest:request successBlock:successBlock failureBlock:failedBlock];
    [self dealRequestQueue];
}
else  //處理錯誤信息
{
    failedBlock(error);
}

     若是是ZYRequestReliabilityStoreToDB請求,第一步是存入數據庫。

    第二步,將請求添加到調度隊列裏面,讓調度隊列調用AFN去處理該請求。在AFN的成功block裏面,判斷狀態碼,若是是真的成功狀態,那麼將數據庫裏面的請求移除掉,若是是失敗狀態,將從新請求的次數遞減,再添加到調度隊列末尾從新排隊請求。

    我設計的是,最多重發三次請求。另外還有一個定時器,這個定時器會每隔60s,從數據庫查詢須要全部存儲的請求,而後將它們嘗試加入調度隊列再次發送。這樣的設計,即便App被kill,再次重啓60s以後,也會把數據庫中的請求拿出來進行發送。(只是一種思路,實際開發中會進入到具體業務纔講請求拿出來發送)

    經過上面的幾個步驟,基本上能夠極大的提升請求的可靠性,可是真的100%是沒法實現的,若是用戶卸載App,再下載,相關數據就沒法恢復了。

 

  3、失敗重發

    第二類請求的可靠性爲ZYRequestReliabilityRetry,這類請求的例子能夠是咱們App啓動時用戶看到的首頁,首頁的內容從服務器獲取,若是第一次請求就失敗體驗較差,這種場景下咱們應該容許請求有機會多試幾回,增長一個retryCount便可。

/**
 請求沒發送成功,從新發送的次數
 */
@property (nonatomic, assign, readonly) int retryCount;

     在Manager裏面,有一個調度隊列:

@property (nonatomic, strong) NSMutableArray *requestQueue;

     每次將請求加入這個隊列,而後在AFN發送完成回調以後,若是失敗就進行重發,實際開發時,須要自行處理失敗重發的錯誤碼判斷:

//請求失敗以後,根據約定的錯誤碼判斷是否須要再次請求
//這裏,-1001是AFN的超時error
if (error.code == -1001 &&request.retryCount > 0)
{
    [request reduceRetryCount];
    [self queueAddRequest:request successBlock:successBlock failureBlock:failedBlock];
    [self dealRequestQueue];
}

     在這裏,是設置若是超時才進行重發請求,也能夠將這個判斷去掉,只要retryCount大於0即進行重發。通常開發的時候會和後臺肯定一些錯誤碼,根據錯誤碼的類型判斷是否須要重發會更合理些。

    通常3次的重試基本能夠排除網絡抖動的狀況。三次失敗以後便可認爲請求失敗,經過產品交互告知用戶。

    第三類請求的重要性最低,好比進入Controller的UV採集打點、收集數據等。這類請求只須要作一次,即便失敗也不會對App體驗產生什麼負面影響。

 

  4、設計思路

    這是某天在開發羣裏羣友發出來的一道面試題,當一個複雜界面上的數據要根據n個請求返回的數據進行更新的時候,要求設計一個架構來發送這些請求。

    當時簡單的和羣友聊了聊,趁着最近有時間就本身擼了一套這樣的機制,首先是疑問:

    一、爲何須要設計框架?全部請求直接利用AFN併發發送不行麼?(不行,由於網絡帶寬是有限的,這樣作會致使數據返回總體慢上不少,並且,一個網絡請求的超時時間是必定的,一次性併發極可能形成原本能夠發送成功的請求超時)

    二、基於問題1,架構如何設計?(我的認爲,面試官主要是想考察對平時寫代碼對於封裝、設計模式、網絡回調的理解,若是僅僅只是一個最大併發的限制,明顯不會是理想答案,那麼須要注意的點?除開最大併發,開發中網絡錯誤時,都有錯誤碼返回,對這一塊應該作好處理。當請求失敗時,需不須要進行重發?請求之間需不須要設置依賴?須要不要有優先級等等)

 

    這一次的設計,並無依賴、優先級等,只進行了重發、最大併發設計。思路是同樣的,無非就是一個調度隊列進行request的處理,這個隊列的出隊規則能夠是按優先級高低來進行,固然得本身封裝優先隊列的算法。這裏是最簡單的先進先出隊列,每次請求失敗,要進行重發的話,就把請求丟到隊列末尾。額,理論上來講,請求無限多的狀況下,調度隊列會是個死循環,這樣會形成主線程卡頓,因此把它放到子線程來處理。在併發發送請求之下,不作處理的話,會併發的刪除調度隊列裏面的request,那麼如何避免數據競爭?在代碼裏面都有解答,以上。

相關文章
相關標籤/搜索