YTKNetwork原始碼解析

J_Knight_發表於1970-01-01

對於iOS開發者來說,就算是沒有用過YTKNetwork框架,應該也見過,聽過了。它是猿題庫技術團隊開源的一個網路請求框架,內部封裝了AFNetworking。它把每個請求例項化,管理它的生命週期,也可以管理多個請求。

在正式講解原始碼之前,我會先講一下該框架所用的架構和設計模式。我總覺得對架構和設計有一定的瞭解的話,會有助於對原始碼的理解。

1. 架構


先上圖:

YTKRequest架構圖

在這裡簡單說明一下:

  1. YTKNetwork框架將每一個請求例項化,YTKBaseRequest是所有請求類的基類,YTKRequest是它的子類。所以如果我們想要傳送一個請求,則需要建立並例項化一個繼承於YTKRequest的自定義的請求類(CustomRequest)併傳送請求。
  2. YTKNetworkAgent是一個單例,負責管理所有的請求類(例如CustomRequest)。當CustomRequest傳送請求以後,會把自己放在YTKNetworkAgent持有的一個字典裡,讓其管理自己。
  3. 我們說YTKNetwork封裝了AFNetworking,實際上是YTKNetworkAgent封裝了AFNetworking,由它負責AFNetworking請求的傳送和AFNetworking的回撥處理。所以如果我們想更換一個第三方網路請求庫,就可以在這裡更換一下。而YTKRequest更多的是隻是負責快取的處理。
  4. YTKNetworkConfig與YTKPriviate的具體職能現在不做介紹,會在後文給出。

OK,現在我們知道了YTKNetwork中類與類之間的關係以及關鍵類的大致職能,接下來我會告訴你YTKNetwork為什麼會採用這種關係來架構,以及採用這種架構會有什麼好處。

2. 設計模式


YTKNetwork框架採用的設計模式是命令模式(Command Pattern)

首先看一下命令模式的定義:

命令模式將請求封裝成物件,以便使用不同的請求,佇列或者日誌來引數化其他物件。命令模式也支援可撤銷的操作。 摘自:《Head First 設計模式》

看一下命令模式的類圖:

命令模式類圖.png

圖中英文的含義:

英文 中文
Command 抽象命令類
ConcreteCommand 命令類的實現類(子類)
Invoker 呼叫者
Receiver 命令接收者(執行者)
Client 客戶端

詳細介紹一下:

  1. 命令模式的本質是對命令的封裝,將發出命令的責任和執行命令的責任分割開。
  2. 命令模式允許請求的一方和接收的一方獨立開來,使得請求的一方不必知道接收請求的一方的介面,更不必知道請求是怎麼被接收,以及操作是否被執行、何時被執行,以及是怎麼被執行的。

可能還是覺得有點抽象,在這裡舉一個《Head First 設計模式》裡的例子,一個客人在餐廳點餐的過程:

  1. 你將點的菜寫在訂單裡,交給了服務員。
  2. 服務員將訂單交給廚師。
  3. 廚師做好菜之後將做好的菜交給服務員。
  4. 最後服務員把菜遞給你。

在這裡,命令就好比是訂單,而你是命令的發起者。你的命令(訂單)通過服務員(呼叫者)交給了命令的執行者(廚師)。 所以至於這道菜具體是誰做,怎麼做,你是不知道的,你做的只是發出命令和接受結果。而且對於餐廳來說,廚師是可以隨便換的,而你可能對此一無所知。反過來,廚師只需要好好把菜做好,至於是誰點的菜也不需要他考慮。

結合上面命令模式的類圖以及餐廳點餐的例子,我們來理清一下YTKNetwork內部的職能

場景 Command ConcreteCommand Invoker Receiver Client
餐廳 空白訂單 填入菜名的訂單 服務員 廚師 客人
YTKNetwork YTKBaseRequest CustomRequest YTKNetworkAgent AFNetworking ViewController/ViewModel

可以看到,YTKNetwork對命令模式的實現是很符合其設計標準的,它將請求的發起者和接收者分離開來(中間隔著呼叫者),可以讓我們隨時更換接受者。

另外,因為封裝了請求,我們既可以管理單個請求,也可以同時管理多個請求,甚至實現璉式請求的傳送。關於多個請求的傳送,我們也可以想象在餐廳裡,你可以在吃的過程中還想起來要吃別的東西,例如點心,飲料之類的,你就可以填多個訂單(當然也可以寫在一起)交給服務員。

相信到這裡,大家應該對YTKNetwork的設計與架構有了足夠的認識了,下面進入到真正的原始碼解析,我們結合一下它的程式碼來看一下YTKNetwork是如何實現和管理網路請求的。

3. 原始碼解析


在真正講解原始碼之前,我先詳細說一下各個類的職責:

3.1 責任介紹

類名 職責
YTKBaseRequest 所有請求類的基類。持有NSURLSessionTask例項,responseData,responseObject,error等重要資料,提供一些需要子類實現的與網路請求相關的方法,處理回撥的代理和block,命令YTKNetworkAgent發起網路請求。
YTKRequest YTKBaseRequest的子類。負責快取的處理:請求前查詢快取;請求後寫入快取。
YTKNetworkConfig 被YTKRequest和YTKNetworkAgent訪問。負責所有請求的全域性配置,例如baseUrl和CDNUrl等等。
YTKNetworkPrivate 提供JSON驗證,appVersion等輔助性的方法;給YTKBaseRequest增加一些分類。
YTKNetworkAgent 真正發起請求的類。負責發起請求,結束請求,並持有一個字典來儲存正在執行的請求。
YTKBatchRequest 可以發起批量請求,持有一個陣列來儲存所有的請求類。在請求執行後遍歷這個陣列來發起請求,如果其中有一個請求返回失敗,則認定本組請求失敗。
YTKBatchRequestAgent 負責管理多個YTKBatchRequest例項,持有一個陣列來儲存YTKBatchRequest。支援新增和刪除YTKBatchRequest例項。
YTKChainRequest 可以發起鏈式請求,持有一個陣列來儲存所有的請求類。當某個請求結束後才能發起下一個請求,如果其中有一個請求返回失敗,則認定本請求鏈失敗。
YTKChainRequestAgent 負責管理多個YTKChainRequestAgent例項,持有一個陣列來儲存YTKChainRequest。支援新增和刪除YTKChainRequest例項。

OK,現在知道了YTKNetwork內部的責任分配,下面我們先從單個請求的全部流程(配置,發起,結束)來看一下YTKNetwork都做了什麼。

3.2 單個請求

3.21 單個請求的配置

官方的教程建議我們將請求的全域性配置是在AppDelegate.m檔案裡,設定baseUrl以及cdnUrl等引數。

- (BOOL)application:(UIApplication *)application 
   didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
   YTKNetworkConfig *config = [YTKNetworkConfig sharedConfig];
   config.baseUrl = @"http://yuantiku.com";
   config.cdnUrl = @"http://fen.bi";
}
複製程式碼

如果我們需要新建一個註冊的請求,則需要建立一個繼承於YTKRequest的註冊介面的類RegisterApi,並將針對該請求引數配置好:

// RegisterApi.h
#import "YTKRequest.h"

@interface RegisterApi : YTKRequest

- (id)initWithUsername:(NSString *)username password:(NSString *)password;

@end


// RegisterApi.m
#import "RegisterApi.h"

@implementation RegisterApi {
    NSString *_username;
    NSString *_password;
}

//初始化的時候將兩個引數值傳入
- (id)initWithUsername:(NSString *)username password:(NSString *)password {
    self = [super init];
    if (self) {
        _username = username;
        _password = password;
    }
    return self;
}

//需要和baseUrl拼接的地址
- (NSString *)requestUrl {
    // “ http://www.yuantiku.com ” 在 YTKNetworkConfig 中設定,這裡只填除去域名剩餘的網址資訊
    return @"/iphone/register";
}

//請求方法,某人是GET
- (YTKRequestMethod)requestMethod {
    return YTKRequestMethodPOST;
}

//請求體
- (id)requestArgument {
    return @{
        @"username": _username,
        @"password": _password
    };
}

@end
複製程式碼

現在我們知道如何配置全域性的引數和針對某個請求的引數了,接下來看一下單個請求是如何發起的。

3.22 單個請求的發起

還是剛才的註冊API,在例項化以後,直接呼叫startWithCompletionBlockWithSuccess:failure方法(或start方法)就可以發起它:

//LoginViewController.m
- (void)loginButtonPressed:(id)sender {
    NSString *username = self.UserNameTextField.text;
    NSString *password = self.PasswordTextField.text;
    if (username.length > 0 && password.length > 0) {
        RegisterApi *api = [[RegisterApi alloc] initWithUsername:username password:password];
        [api startWithCompletionBlockWithSuccess:^(YTKBaseRequest *request) {
            // 你可以直接在這裡使用 self
            NSLog(@"succeed");
        } failure:^(YTKBaseRequest *request) {
            // 你可以直接在這裡使用 self
            NSLog(@"failed");
        }];
    }
}
複製程式碼

上面是以block的形式回撥,YTKNetwork也支援代理的回撥:

//LoginViewController.m
- (void)loginButtonPressed:(id)sender {
    NSString *username = self.UserNameTextField.text;
    NSString *password = self.PasswordTextField.text;
    if (username.length > 0 && password.length > 0) {
        RegisterApi *api = [[RegisterApi alloc] initWithUsername:username password:password];
        api.delegate = self;
        [api start];
    }
}

- (void)requestFinished:(YTKBaseRequest *)request {
    NSLog(@"succeed");
}

- (void)requestFailed:(YTKBaseRequest *)request {
    NSLog(@"failed");
}
複製程式碼

有兩點需要注意的是:

  1. 必須給自定義請求類(RegisterApi)呼叫startWithCompletionBlockWithSuccess:failure方法(或start方法),才能真正發起請求。
  2. 在同時設定了回撥代理和回撥block的情況下,首先回撥的是回撥代理方法,然後再走回撥block。

知道了YTKRequest請求是如何在外部發起的,我們現在從startWithCompletionBlockWithSuccess:failure方法開始,來看一下YTKNetwork都做了什麼:

首先來到YTKBaseRequest類(因為最早是由它定義的該方法):

//YTKBaseRequest.m
//傳入成功和失敗的block,並儲存起來
- (void)startWithCompletionBlockWithSuccess:(YTKRequestCompletionBlock)success
                                    failure:(YTKRequestCompletionBlock)failure {
    //儲存成功和失敗的回撥block,便於將來呼叫
    [self setCompletionBlockWithSuccess:success failure:failure];
    //發起請求
    [self start];
}

//儲存成功和失敗的block
- (void)setCompletionBlockWithSuccess:(YTKRequestCompletionBlock)success
                              failure:(YTKRequestCompletionBlock)failure {
    self.successCompletionBlock = success;
    self.failureCompletionBlock = failure;
}
複製程式碼

當儲存完成功和失敗的block以後,呼叫start方法,於是來到了YTKRequest類(注意,雖然YTKBaseRequest也實現了start方法,但是由於YTKRequest類是它的子類並也實現了start方法,所以這裡最先走的是YTKRequest類的start方法):

//YTKRequest.m
- (void)start {
    
    //1. 如果忽略快取 -> 請求
    if (self.ignoreCache) {
        [self startWithoutCache];
        return;
    }

    //2. 如果存在下載未完成的檔案 -> 請求
    if (self.resumableDownloadPath) {
        [self startWithoutCache];
        return;
    }

    //3. 獲取快取失敗 -> 請求
    if (![self loadCacheWithError:nil]) {
        [self startWithoutCache];
        return;
    }

    //4. 到這裡,說明一定能拿到可用的快取,可以直接回撥了(因為一定能拿到可用的快取,所以一定是呼叫成功的block和代理)
    _dataFromCache = YES;

    dispatch_async(dispatch_get_main_queue(), ^{
        
        //5. 回撥之前的操作
        //5.1 快取處理
        [self requestCompletePreprocessor];
        
        //5.2 使用者可以在這裡進行真正回撥前的操作
        [self requestCompleteFilter];
        
        YTKRequest *strongSelf = self;
        
        //6. 執行回撥
        //6.1 請求完成的代理
        [strongSelf.delegate requestFinished:strongSelf];
        
        //6.2 請求成功的block
        if (strongSelf.successCompletionBlock) {
            strongSelf.successCompletionBlock(strongSelf);
        }
        
        //7. 把成功和失敗的block都設定為nil,避免迴圈引用
        [strongSelf clearCompletionBlock];
    });
}
複製程式碼

我們之前說過YTKRequest負責快取的相關處理,所以在上面這個start方法裡,它做的是請求之前快取的查詢和檢查工作:

  • 如果忽略快取,或者快取獲取失敗,呼叫startWithoutCache方法(參考1-3的情況),發起請求。
  • 如果能成功獲取到快取,則直接回撥(參考4-7的情況)。

我們來看一下每一步的具體實現:

  1. ignoreCache屬性是使用者手動設定的,如果使用者強制忽略快取,則無論是否快取是否存在,直接傳送請求。
  2. resumableDownloadPath是斷點下載路徑,如果該路徑不為空,說明有未完成的下載任務,則直接傳送請求繼續下載。
  3. loadCacheWithError:方法驗證了載入快取是否成功的方法(返回值為YES,說明可以載入快取;反之亦然),看一下具體實現:
//YTKRequest.m
- (BOOL)loadCacheWithError:(NSError * _Nullable __autoreleasing *)error {
    
    // 快取時間小於0,則返回(快取時間預設為-1,需要使用者手動設定,單位是秒)
    if ([self cacheTimeInSeconds] < 0) {
        if (error) {
            *error = [NSError errorWithDomain:YTKRequestCacheErrorDomain code:YTKRequestCacheErrorInvalidCacheTime userInfo:@{ NSLocalizedDescriptionKey:@"Invalid cache time"}];
        }
        return NO;
    }

    // 是否有快取的後設資料,如果沒有,返回錯誤
    if (![self loadCacheMetadata]) {
        if (error) {
            *error = [NSError errorWithDomain:YTKRequestCacheErrorDomain code:YTKRequestCacheErrorInvalidMetadata userInfo:@{ NSLocalizedDescriptionKey:@"Invalid metadata. Cache may not exist"}];
        }
        return NO;
    }

    // 有快取,再驗證是否有效
    if (![self validateCacheWithError:error]) {
        return NO;
    }

    // 有快取,而且有效,再驗證是否能取出來
    if (![self loadCacheData]) {
        if (error) {
            *error = [NSError errorWithDomain:YTKRequestCacheErrorDomain code:YTKRequestCacheErrorInvalidCacheData userInfo:@{ NSLocalizedDescriptionKey:@"Invalid cache data"}];
        }
        return NO;
    }

    return YES;
}
複製程式碼

先講一下什麼是後設資料:後設資料是指資料的資料,在這裡描述了快取資料本身的一些特徵:包括版本號,快取時間,敏感資訊等等, 稍後會做詳細介紹。

我們來看一下上面關於快取的後設資料的獲取方法:loadCacheMetadata方法

//YTKRequest.m
- (BOOL)loadCacheMetadata {
    
    NSString *path = [self cacheMetadataFilePath];
    NSFileManager * fileManager = [NSFileManager defaultManager];
    if ([fileManager fileExistsAtPath:path isDirectory:nil]) {
        @try {
            //將序列化之後被儲存在磁碟裡的檔案反序列化到當前物件的屬性cacheMetadata
            _cacheMetadata = [NSKeyedUnarchiver unarchiveObjectWithFile:path];
            return YES;
        } @catch (NSException *exception) {
            YTKLog(@"Load cache metadata failed, reason = %@", exception.reason);
            return NO;
        }
    }
    return NO;
}
複製程式碼

cacheMetadata(YTKCacheMetadata) 是當前reqeust類用來儲存快取後設資料的屬性。 YTKCacheMetadata類被定義在YTKRequest.m檔案裡面:

//YTKRequest.m
@interface YTKCacheMetadata : NSObject<NSSecureCoding>

@property (nonatomic, assign) long long version;
@property (nonatomic, strong) NSString *sensitiveDataString;
@property (nonatomic, assign) NSStringEncoding stringEncoding;
@property (nonatomic, strong) NSDate *creationDate;
@property (nonatomic, strong) NSString *appVersionString;

@end
複製程式碼

它描述的是快取的版本號,敏感資訊,建立時間,app版本等資訊,並支援序列化處理,可以儲存在磁碟裡。 因此,loadCacheMetadata方法的目的是將之前被序列化儲存的快取後設資料資訊反序列化,賦給自身的cacheMetadata屬性上。

現在獲取了快取的後設資料並賦給了自身的cacheMetadata屬性上,那麼接下來就要逐一驗證後設資料裡的各項資訊是否符合要求,在下面的validateCacheWithError:裡面驗證:

//YTKRequest.m
- (BOOL)validateCacheWithError:(NSError * _Nullable __autoreleasing *)error {
    
    // 是否大於過期時間
    NSDate *creationDate = self.cacheMetadata.creationDate;
    NSTimeInterval duration = -[creationDate timeIntervalSinceNow];
    if (duration < 0 || duration > [self cacheTimeInSeconds]) {
        if (error) {
            *error = [NSError errorWithDomain:YTKRequestCacheErrorDomain code:YTKRequestCacheErrorExpired userInfo:@{ NSLocalizedDescriptionKey:@"Cache expired"}];
        }
        return NO;
    }

    // 快取的版本號是否符合
    long long cacheVersionFileContent = self.cacheMetadata.version;
    if (cacheVersionFileContent != [self cacheVersion]) {
        if (error) {
            *error = [NSError errorWithDomain:YTKRequestCacheErrorDomain code:YTKRequestCacheErrorVersionMismatch userInfo:@{ NSLocalizedDescriptionKey:@"Cache version mismatch"}];
        }
        return NO;
    }
    
    // 敏感資訊是否符合
    NSString *sensitiveDataString = self.cacheMetadata.sensitiveDataString;
    NSString *currentSensitiveDataString = ((NSObject *)[self cacheSensitiveData]).description;
    if (sensitiveDataString || currentSensitiveDataString) {
        // If one of the strings is nil, short-circuit evaluation will trigger
        if (sensitiveDataString.length != currentSensitiveDataString.length || ![sensitiveDataString isEqualToString:currentSensitiveDataString]) {
            if (error) {
                *error = [NSError errorWithDomain:YTKRequestCacheErrorDomain code:YTKRequestCacheErrorSensitiveDataMismatch userInfo:@{ NSLocalizedDescriptionKey:@"Cache sensitive data mismatch"}];
            }
            return NO;
        }
    }
    
    // app的版本是否符合
    NSString *appVersionString = self.cacheMetadata.appVersionString;
    NSString *currentAppVersionString = [YTKNetworkUtils appVersionString];
    if (appVersionString || currentAppVersionString) {
        if (appVersionString.length != currentAppVersionString.length || ![appVersionString isEqualToString:currentAppVersionString]) {
            if (error) {
                *error = [NSError errorWithDomain:YTKRequestCacheErrorDomain code:YTKRequestCacheErrorAppVersionMismatch userInfo:@{ NSLocalizedDescriptionKey:@"App version mismatch"}];
            }
            return NO;
        }
    }
    return YES;
}
複製程式碼

如果每項後設資料資訊都能通過,再在loadCacheData方法裡面驗證快取是否能被取出來:

//YTKRequest.m
- (BOOL)loadCacheData {
    
    NSString *path = [self cacheFilePath];
    NSFileManager *fileManager = [NSFileManager defaultManager];
    NSError *error = nil;

    if ([fileManager fileExistsAtPath:path isDirectory:nil]) {
        NSData *data = [NSData dataWithContentsOfFile:path];
        _cacheData = data;
        _cacheString = [[NSString alloc] initWithData:_cacheData encoding:self.cacheMetadata.stringEncoding];
        switch (self.responseSerializerType) {
            case YTKResponseSerializerTypeHTTP:
                // Do nothing.
                return YES;
            case YTKResponseSerializerTypeJSON:
                _cacheJSON = [NSJSONSerialization JSONObjectWithData:_cacheData options:(NSJSONReadingOptions)0 error:&error];
                return error == nil;
            case YTKResponseSerializerTypeXMLParser:
                _cacheXML = [[NSXMLParser alloc] initWithData:_cacheData];
                return YES;
        }
    }
    return NO;
}
複製程式碼

如果通過了最終的考驗,則說明當前請求對應的快取是符合各項要求並可以被成功取出,也就是可以直接進行回撥了。

當確認快取可以成功取出後,手動設定dataFromCache屬性為 YES,說明當前的請求結果是來自於快取,而沒有通過網路請求。

然後在真正回撥之前做了如下處理:

//YTKRequest.m:
- (void)start{

    ....

    //5. 回撥之前的操作
    //5.1 快取處理
    [self requestCompletePreprocessor];

    //5.2 使用者可以在這裡進行真正回撥前的操作
    [self requestCompleteFilter];

    ....
}

複製程式碼

5.1:requestCompletePreprocessor方法:

//YTKRequest.m:
- (void)requestCompletePreprocessor {
    
    [super requestCompletePreprocessor];

    //是否非同步將responseData寫入快取(寫入快取的任務放在專門的佇列ytkrequest_cache_writing_queue進行)
    if (self.writeCacheAsynchronously) {
        
        dispatch_async(ytkrequest_cache_writing_queue(), ^{
            //儲存響應資料到快取
            [self saveResponseDataToCacheFile:[super responseData]];
        });
        
    } else {
        //儲存響應資料到快取
        [self saveResponseDataToCacheFile:[super responseData]];
    }
}
複製程式碼
//YTKRequest.m:
//儲存響應資料到快取
- (void)saveResponseDataToCacheFile:(NSData *)data {
    
    if ([self cacheTimeInSeconds] > 0 && ![self isDataFromCache]) {
        if (data != nil) {
            @try {
                // New data will always overwrite old data.
                [data writeToFile:[self cacheFilePath] atomically:YES];

                YTKCacheMetadata *metadata = [[YTKCacheMetadata alloc] init];
                metadata.version = [self cacheVersion];
                metadata.sensitiveDataString = ((NSObject *)[self cacheSensitiveData]).description;
                metadata.stringEncoding = [YTKNetworkUtils stringEncodingWithRequest:self];
                metadata.creationDate = [NSDate date];
                metadata.appVersionString = [YTKNetworkUtils appVersionString];
                [NSKeyedArchiver archiveRootObject:metadata toFile:[self cacheMetadataFilePath]];
                
            } @catch (NSException *exception) {
                YTKLog(@"Save cache failed, reason = %@", exception.reason);
            }
        }
    }
}
複製程式碼

我們可以看到, requestCompletePreprocessor方法的任務是將響應資料儲存起來,也就是做快取。但是,快取的儲存有兩個條件,一個是需要cacheTimeInSeconds方法返回正整數(快取時間,單位是秒,後續會詳細說明);另一個條件是isDataFromCache方法返回NO。 但是我們知道,如果快取可用,就會將這個屬性設定為YES,所以走到這裡的時候,就不做快取了。

接著看下5.2:requestCompleteFilter方法則是需要使用者自己提供具體實現的,專門作為回撥成功之前的一些處理:

//YTKBaseRequest.m
- (void)requestCompleteFilter {
}

複製程式碼

到這裡,回撥之前的處理都結束了,下面來看一下在快取可用的情況下的回撥:

//YTKRequest.m
- (void)start{
   
    ...

    YTKRequest *strongSelf = self;
        
    //6. 執行回撥
    //6.1 請求完成的代理
    [strongSelf.delegate requestFinished:strongSelf];
        
    //6.2 請求成功的block
    if (strongSelf.successCompletionBlock) {
         strongSelf.successCompletionBlock(strongSelf);
    }
        
    //7. 把成功和失敗的block都設定為nil,避免迴圈引用
    [strongSelf clearCompletionBlock];
}

複製程式碼

我們可以看到 ,這裡面同時存在兩種回撥:代理的回撥和block的回撥。先執行的是代理的回撥,然後執行的是block的回撥。而且在回撥結束之後,YTKNetwork會幫助我們清空回撥的block:

//YTKBaseRequest.m
- (void)clearCompletionBlock {
    // 清空請求結束的block,避免迴圈引用
    self.successCompletionBlock = nil;
    self.failureCompletionBlock = nil;
}
複製程式碼

注意,在使用者同時實現了代理和block的情況下,二者都會被呼叫。

到這裡,我們瞭解了YTKNetwork在網路請求之前是如何驗證快取,以及在快取有效的情況下是如何回撥的。

反過來,如果快取無效(或忽略快取)時,需要立即請求網路。那麼我們現在來看一看在這個時候YTKNetwork都做了什麼:

仔細看一下上面的start方法,我們會發現,如果快取不滿足條件時,會直接呼叫startWithoutCache方法:

//YTKRequest.m
- (void)start{

    //1. 如果忽略快取 -> 請求
    if (self.ignoreCache) {
        [self startWithoutCache];
        return;
    }

    //2. 如果存在下載未完成的檔案 -> 請求
    if (self.resumableDownloadPath) {
        [self startWithoutCache];
        return;
    }

    //3. 獲取快取失敗 -> 請求
    if (![self loadCacheWithError:nil]) {
        [self startWithoutCache];
        return;
    }
 
    ......
}
複製程式碼

那麼在startWithoutCache方法裡都做了什麼呢?

//YTKRequest.m
- (void)startWithoutCache {
    
    //1. 清除快取
    [self clearCacheVariables];
    
    //2. 呼叫父類的發起請求
    [super start];
}

//清除當前請求對應的所有快取
- (void)clearCacheVariables {
    _cacheData = nil;
    _cacheXML = nil;
    _cacheJSON = nil;
    _cacheString = nil;
    _cacheMetadata = nil;
    _dataFromCache = NO;
}
複製程式碼

在這裡,首先清除了關於快取的所有資料,然後呼叫父類的start方法:

//YTKBaseRequest.m:
- (void)start {
    
    //1. 告訴Accessories即將回撥了(其實是即將發起請求)
    [self toggleAccessoriesWillStartCallBack];

    //2. 令agent新增請求併發起請求,在這裡並不是組合關係,agent只是一個單例
    [[YTKNetworkAgent sharedAgent] addRequest:self];
}
複製程式碼

第一步裡的Accessories是一些遵從代理的物件。這個代理定義了一些用來追蹤請求狀況的方法。它被定義在了YTKBaseRequest.h檔案裡:

//用來跟蹤請求的狀態的代理。
@protocol YTKRequestAccessory <NSObject>

@optional

///  Inform the accessory that the request is about to start.
///
///  @param request The corresponding request.
- (void)requestWillStart:(id)request;

///  Inform the accessory that the request is about to stop. This method is called
///  before executing `requestFinished` and `successCompletionBlock`.
///
///  @param request The corresponding request.
- (void)requestWillStop:(id)request;

///  Inform the accessory that the request has already stoped. This method is called
///  after executing `requestFinished` and `successCompletionBlock`.
///
///  @param request The corresponding request.
- (void)requestDidStop:(id)request;

@end
複製程式碼

所以只要某個物件遵從了這個代理,就可以追蹤到請求將要開始,將要結束,已經結束的狀態。

接著看一下第二步:YTKNetworkAgent把當前的請求物件新增到了自己身上併傳送請求。來看一下它的具體實現:

//YTKNetworkAgent.m
- (void)addRequest:(YTKBaseRequest *)request {
    
    //1. 獲取task
    NSParameterAssert(request != nil);

    NSError * __autoreleasing requestSerializationError = nil;

    //獲取使用者自定義的requestURL
    NSURLRequest *customUrlRequest= [request buildCustomUrlRequest];
    
    if (customUrlRequest) {
        
        __block NSURLSessionDataTask *dataTask = nil;
        //如果存在使用者自定義request,則直接走AFNetworking的dataTaskWithRequest:方法
        dataTask = [_manager dataTaskWithRequest:customUrlRequest completionHandler:^(NSURLResponse * _Nonnull response, id  _Nullable responseObject, NSError * _Nullable error) {
            //響應的統一處理
            [self handleRequestResult:dataTask responseObject:responseObject error:error];
        }];
        request.requestTask = dataTask;
        
    } else {
        
        //如果使用者沒有自定義url,則直接走這裡
        request.requestTask = [self sessionTaskForRequest:request error:&requestSerializationError];
        
    }

    //序列化失敗,則認定為請求失敗
    if (requestSerializationError) {
        //請求失敗的處理
        [self requestDidFailWithRequest:request error:requestSerializationError];
        return;
    }

    NSAssert(request.requestTask != nil, @"requestTask should not be nil");

    // 優先順序的對映
    // !!Available on iOS 8 +
    if ([request.requestTask respondsToSelector:@selector(priority)]) {
        switch (request.requestPriority) {
            case YTKRequestPriorityHigh:
                request.requestTask.priority = NSURLSessionTaskPriorityHigh;
                break;
            case YTKRequestPriorityLow:
                request.requestTask.priority = NSURLSessionTaskPriorityLow;
                break;
            case YTKRequestPriorityDefault:
                /*!!fall through*/
            default:
                request.requestTask.priority = NSURLSessionTaskPriorityDefault;
                break;
        }
    }

    // Retain request
    YTKLog(@"Add request: %@", NSStringFromClass([request class]));
    
    //2. 將request放入儲存請求的字典中,taskIdentifier為key,request為值
    [self addRequestToRecord:request];
    
    //3. 開始task
    [request.requestTask resume];
}
複製程式碼

這個方法挺長的,但是請不要被嚇到,它總共分為三個部分:

  • 第一部分是獲取當前請求對應的task並賦給request的requestTask屬性(以後提到的request,都為使用者自定義的當前請求類的例項)。
  • 第二部分是把request放入專門用來儲存請求的字典中,key為taskIdentifier。
  • 第三部分是啟動task。

下面我來依次講解每個部分:

第一部分:獲取當前請求對應的task並賦給request

//YTKNetworkAgent.m
- (void)addRequest:(YTKBaseRequest *)request {

  ...

  if (customUrlRequest) {
        
        __block NSURLSessionDataTask *dataTask = nil;
        //如果存在使用者自定義request,則直接走AFNetworking的dataTaskWithRequest:方法
        dataTask = [_manager dataTaskWithRequest:customUrlRequest completionHandler:^(NSURLResponse * _Nonnull response, id  _Nullable responseObject, NSError * _Nullable error) {
            //統一處理請求響應
            [self handleRequestResult:dataTask responseObject:responseObject error:error];
        }];
        request.requestTask = dataTask;
        
    } else {
        
        //如果使用者沒有自定義url,則直接走這裡
        request.requestTask = [self sessionTaskForRequest:request error:&requestSerializationError];
        
    }

  ...
}
複製程式碼

在這裡判斷了使用者是否自定義了request:

  1. 如果是,則直接呼叫AFNetworking的dataTaskWithRequest:方法。
  2. 如果不是,則呼叫YTKRequest自己的生成task的方法。

第一種情況就不說了,因為AF幫我們做好了。在這裡看一下第二種情況,sessionTaskForRequest: error :方法內部:

//YTKNetworkAgent.m
//根據不同請求型別,序列化型別,和請求引數來返回NSURLSessionTask
- (NSURLSessionTask *)sessionTaskForRequest:(YTKBaseRequest *)request error:(NSError * _Nullable __autoreleasing *)error {
    
    //1. 獲得請求型別(GET,POST等)
    YTKRequestMethod method = [request requestMethod];

    //2. 獲得請求url
    NSString *url = [self buildRequestUrl:request];

    //3. 獲得請求引數
    id param = request.requestArgument;
    AFConstructingBlock constructingBlock = [request constructingBodyBlock];
    
    //4. 獲得request serializer
    AFHTTPRequestSerializer *requestSerializer = [self requestSerializerForRequest:request];

    //5. 根據不同的請求型別來返回對應的task
    switch (method) {
            
        case YTKRequestMethodGET:
            
            if (request.resumableDownloadPath) {
                //下載任務
                return [self downloadTaskWithDownloadPath:request.resumableDownloadPath requestSerializer:requestSerializer URLString:url parameters:param progress:request.resumableDownloadProgressBlock error:error];
                
            } else {
                //普通get請求
                return [self dataTaskWithHTTPMethod:@"GET" requestSerializer:requestSerializer URLString:url parameters:param error:error];
            }
            
        case YTKRequestMethodPOST:
            //POST請求
            return [self dataTaskWithHTTPMethod:@"POST" requestSerializer:requestSerializer URLString:url parameters:param constructingBodyWithBlock:constructingBlock error:error];
            
        case YTKRequestMethodHEAD:
            //HEAD請求
            return [self dataTaskWithHTTPMethod:@"HEAD" requestSerializer:requestSerializer URLString:url parameters:param error:error];
            
        case YTKRequestMethodPUT:
            //PUT請求
            return [self dataTaskWithHTTPMethod:@"PUT" requestSerializer:requestSerializer URLString:url parameters:param error:error];
            
        case YTKRequestMethodDELETE:
            //DELETE請求
            return [self dataTaskWithHTTPMethod:@"DELETE" requestSerializer:requestSerializer URLString:url parameters:param error:error];
            
        case YTKRequestMethodPATCH:
            //PATCH請求
            return [self dataTaskWithHTTPMethod:@"PATCH" requestSerializer:requestSerializer URLString:url parameters:param error:error];
    }
}

複製程式碼

從這個方法最後的switch語句可以看出,這個方法的作用是返回當前request的NSURLSessionTask的例項。而且最終生成NSURLSessionTask例項的方法都是通過dataTaskWithHTTPMethod:requestSerializer:URLString:parameters:error:這個私有方法來實現的。在講解這個關鍵的私有方法之前,先來逐步講解一下這個私有方法需要的每個引數的獲取方法:

  1. 獲得請求型別(GET,POST等):
//YTKNetworkAgent.m
- (NSURLSessionTask *)sessionTaskForRequest:(YTKBaseRequest *)request error:(NSError * _Nullable __autoreleasing *)error {

  ...
  YTKRequestMethod method = [request requestMethod];
  ...

}
複製程式碼

requestMethod方法最初在YTKBaseRequest裡面已經實現了,預設返回了YTKRequestMethodGET。

它的列舉型別在YTKBaseRequest.h裡面定義:

//YTKBaseRequest.h
///  HTTP Request method.
typedef NS_ENUM(NSInteger, YTKRequestMethod) {
    YTKRequestMethodGET = 0,
    YTKRequestMethodPOST,
    YTKRequestMethodHEAD,
    YTKRequestMethodPUT,
    YTKRequestMethodDELETE,
    YTKRequestMethodPATCH,
};
複製程式碼

使用者可以根據實際的需求在自定義request類裡面重寫這個方法:

//RegisterAPI.m
- (YTKRequestMethod)requestMethod {
    return YTKRequestMethodPOST;
}
複製程式碼

2.獲得請求url:

//YTKNetworkAgent.m
- (NSURLSessionTask *)sessionTaskForRequest:(YTKBaseRequest *)request error:(NSError * _Nullable __autoreleasing *)error {

  ...
  NSString *url = [self buildRequestUrl:request];
  ...

}


//返回當前請求url
- (NSString *)buildRequestUrl:(YTKBaseRequest *)request {
   
    NSParameterAssert(request != nil);

    //使用者自定義的url(不包括在YTKConfig裡面設定的base_url)
    NSString *detailUrl = [request requestUrl];
    NSURL *temp = [NSURL URLWithString:detailUrl];
    
    // 存在host和scheme的url立即返回正確
    if (temp && temp.host && temp.scheme) {
        return detailUrl;
    }
    
    // 如果需要過濾url,則過濾
    NSArray *filters = [_config urlFilters];
    for (id<YTKUrlFilterProtocol> f in filters) {
        detailUrl = [f filterUrl:detailUrl withRequest:request];
    }

    NSString *baseUrl;
    if ([request useCDN]) {
        //如果使用CDN,在當前請求沒有配置CDN地址的情況下,返回全域性配置的CDN
        if ([request cdnUrl].length > 0) {
            baseUrl = [request cdnUrl];
        } else {
            baseUrl = [_config cdnUrl];
        }
    } else {
        //如果使用baseUrl,在當前請求沒有配置baseUrl,返回全域性配置的baseUrl
        if ([request baseUrl].length > 0) {
            baseUrl = [request baseUrl];
        } else {
            baseUrl = [_config baseUrl];
        }
    }
    // 如果末尾沒有/,則在末尾新增一個/
    NSURL *url = [NSURL URLWithString:baseUrl];

    if (baseUrl.length > 0 && ![baseUrl hasSuffix:@"/"]) {
        url = [url URLByAppendingPathComponent:@""];
    }

    return [NSURL URLWithString:detailUrl relativeToURL:url].absoluteString;
}
複製程式碼

3.獲得請求引數

//YTKNetworkAgent.m
- (NSURLSessionTask *)sessionTaskForRequest:(YTKBaseRequest *)request error:(NSError * _Nullable __autoreleasing *)error {

   ...
      //獲取使用者提供的請求引數
    id param = request.requestArgument;

    //獲取使用者提供的構造請求體的block(預設是沒有的)
    AFConstructingBlock constructingBlock = [request constructingBodyBlock];

   ...

}

複製程式碼

在這裡,requestArgument是一個get方法,需要使用者自己定義請求體,例如在RegisterAPI裡面就定義了兩個請求引數:

//RegisterApi.m
- (id)requestArgument {
    return @{
        @"username": _username,
        @"password": _password
    };
}
複製程式碼

4.獲得request serializer

//YTKNetworkAgent.m
- (NSURLSessionTask *)sessionTaskForRequest:(YTKBaseRequest *)request error:(NSError * _Nullable __autoreleasing *)error {

   ...
   
   //4. 獲得request serializer
   AFHTTPRequestSerializer *requestSerializer = [self requestSerializerForRequest:request];

   ...

}


- (AFHTTPRequestSerializer *)requestSerializerForRequest:(YTKBaseRequest *)request {
    
    AFHTTPRequestSerializer *requestSerializer = nil;
    
    //HTTP or JSON
    if (request.requestSerializerType == YTKRequestSerializerTypeHTTP) {
        requestSerializer = [AFHTTPRequestSerializer serializer];
    } else if (request.requestSerializerType == YTKRequestSerializerTypeJSON) {
        requestSerializer = [AFJSONRequestSerializer serializer];
    }

    //超時時間
    requestSerializer.timeoutInterval = [request requestTimeoutInterval];
    
    //是否允許資料服務
    requestSerializer.allowsCellularAccess = [request allowsCellularAccess];

    //如果當前請求需要驗證
    NSArray<NSString *> *authorizationHeaderFieldArray = [request requestAuthorizationHeaderFieldArray];
    if (authorizationHeaderFieldArray != nil) {
        [requestSerializer setAuthorizationHeaderFieldWithUsername:authorizationHeaderFieldArray.firstObject
                                                          password:authorizationHeaderFieldArray.lastObject];
    }

    //如果當前請求需要自定義 HTTPHeaderField
    NSDictionary<NSString *, NSString *> *headerFieldValueDictionary = [request requestHeaderFieldValueDictionary];
    if (headerFieldValueDictionary != nil) {
        for (NSString *httpHeaderField in headerFieldValueDictionary.allKeys) {
            NSString *value = headerFieldValueDictionary[httpHeaderField];
            [requestSerializer setValue:value forHTTPHeaderField:httpHeaderField];
        }
    }
    return requestSerializer;
}
複製程式碼

上面這個方法通過傳入的request例項,根據它的一些配置(使用者提供)來獲取AFHTTPRequestSerializer的例項。

到現在為止,獲取NSURLSessionTask例項的幾個引數都拿到了,剩下的就是呼叫dataTaskWithHTTPMethod:requestSerializer:URLString:parameters:error:方法來獲取NSURLSessionTask例項了。我們來看一下這個方法的具體實現:

//YTKNetworkAgent.m
- (NSURLSessionDataTask *)dataTaskWithHTTPMethod:(NSString *)method
                               requestSerializer:(AFHTTPRequestSerializer *)requestSerializer
                                       URLString:(NSString *)URLString
                                      parameters:(id)parameters
                                           error:(NSError * _Nullable __autoreleasing *)error {
    return [self dataTaskWithHTTPMethod:method requestSerializer:requestSerializer URLString:URLString parameters:parameters constructingBodyWithBlock:nil error:error];
}

//最終返回NSURLSessionDataTask例項
- (NSURLSessionDataTask *)dataTaskWithHTTPMethod:(NSString *)method
                               requestSerializer:(AFHTTPRequestSerializer *)requestSerializer
                                       URLString:(NSString *)URLString
                                      parameters:(id)parameters
                       constructingBodyWithBlock:(nullable void (^)(id <AFMultipartFormData> formData))block
                                           error:(NSError * _Nullable __autoreleasing *)error {
    NSMutableURLRequest *request = nil;

    //根據有無構造請求體的block的情況來獲取request
    if (block) {
        request = [requestSerializer multipartFormRequestWithMethod:method URLString:URLString parameters:parameters constructingBodyWithBlock:block error:error];
    } else {
        request = [requestSerializer requestWithMethod:method URLString:URLString parameters:parameters error:error];
    }

    //獲得request以後來獲取dataTask
    __block NSURLSessionDataTask *dataTask = nil;
    dataTask = [_manager dataTaskWithRequest:request
                           completionHandler:^(NSURLResponse * __unused response, id responseObject, NSError *_error) {
                               //響應的統一處理
                               [self handleRequestResult:dataTask responseObject:responseObject error:_error];
                           }];

    return dataTask;
}
複製程式碼

這兩個方法,上面的方法呼叫了下面的來獲取最終的NSURLSessionDataTask例項。

OK,現在我們已經知道了NSURLSessionDataTask例項是如何獲取的,再來看一下在addRequest:方法裡接下來做的是對序列化失敗的處理:

//YTKNetworkAgent.m
- (void)addRequest:(YTKBaseRequest *)request {

   ...
   
  //序列化失敗
    if (requestSerializationError) {
        //請求失敗的處理
        [self requestDidFailWithRequest:request error:requestSerializationError];
        return;
    }

   ...

}
   
複製程式碼

requestDidFailWithRequest:方法專門處理請求失敗的情況,因為它被包含在統一處理請求回撥的方法中,所以在稍後會在講解統一處理請求回撥的方法的時候再詳細講解這個方法。

繼續往下走,到了優先順序的對映部分:

//YTKNetworkAgent.m
- (void)addRequest:(YTKBaseRequest *)request {

   ...
   
    // 優先順序的對映
    // !!Available on iOS 8 +
    if ([request.requestTask respondsToSelector:@selector(priority)]) {
        switch (request.requestPriority) {
            case YTKRequestPriorityHigh:
                request.requestTask.priority = NSURLSessionTaskPriorityHigh;
                break;
            case YTKRequestPriorityLow:
                request.requestTask.priority = NSURLSessionTaskPriorityLow;
                break;
            case YTKRequestPriorityDefault:
                /*!!fall through*/
            default:
                request.requestTask.priority = NSURLSessionTaskPriorityDefault;
                break;
        }
    }

   ...

}
   
複製程式碼

requestPriority是YTKBaseRequest的一個列舉屬性,它的列舉在YTKBaseRequest.h裡面被定義:

typedef NS_ENUM(NSInteger, YTKRequestPriority) {
    YTKRequestPriorityLow = -4L,
    YTKRequestPriorityDefault = 0,
    YTKRequestPriorityHigh = 4,
};
複製程式碼

在這裡,將使用者設定的YTKRequestPriority對映到NSURLSessionTask的priority上。

到這裡,我們拿到了task的例項並設定好了優先順序,緊接著就是addRequest:方法裡的第二個部分: YTKNetworkAgent將request例項放在了一個字典中,儲存起來:

第二部分:把request放入專門用來儲存請求的字典中,key為taskIdentifier:

//YTKNetworkAgent.m
- (void)addRequest:(YTKBaseRequest *)request {

   ...

   ...
   
  //將request例項放入儲存請求的字典中,taskIdentifier為key,request為值
  [self addRequestToRecord:request];

   ...

}


- (void)addRequestToRecord:(YTKBaseRequest *)request {
    
    //加鎖
    Lock();
    _requestsRecord[@(request.requestTask.taskIdentifier)] = request;
    Unlock();
}

#define Lock() pthread_mutex_lock(&_lock)
#define Unlock() pthread_mutex_unlock(&_lock)
複製程式碼

可以看到,在新增前和新增後是進行了加鎖和解鎖的處理的。而且request例項被儲存的時候,將其task的identifier作為key來儲存。

在當前的request被儲存以後,就到了最後一步,正式發起請求:

第三部分:啟動task

//YTKNetworkAgent.m
- (NSURLSessionTask *)sessionTaskForRequest:(YTKBaseRequest *)request error:(NSError * _Nullable __autoreleasing *)error {

   ...
   
   [request.requestTask resume];

   ...

}


複製程式碼

到現在為止,我們瞭解了YTKNetwork裡面,一個請求開始之前做的事情:查詢可用快取,生成NSURLSessionTask例項,獲取url,requestSerializer,將request放到YTKNetworkAgent的一個字典裡等等(詳細流程會在稍後給出)。

那麼接下來我們看一下YTKNetwork是如何處理請求的回撥的。

眼尖的同學們可能會注意到,在獲取NSURLSessionTask例項的時候,出現了兩次“響應的統一處理”的註釋,大家可以搜尋這個註釋就可以找到這個方法:handleRequestResult:responseObject:error:。這個方法負責的是對請求回撥的處理,當然包括了成功和失敗的情況。我們來看一下在這個方法裡都做了什麼:

//YTKNetworkAgent.m
//統一處理請求結果,包括成功和失敗的情況
- (void)handleRequestResult:(NSURLSessionTask *)task responseObject:(id)responseObject error:(NSError *)error {
    
    //1. 獲取task對應的request
    Lock();
    YTKBaseRequest *request = _requestsRecord[@(task.taskIdentifier)];
    Unlock();

    //如果不存在對應的request,則立即返回
    if (!request) {
        return;
    }

    。。。

    //2. 獲取request對應的response
    request.responseObject = responseObject;
    
    //3. 獲取responseObject,responseData和responseString
    if ([request.responseObject isKindOfClass:[NSData class]]) {
        
       //3.1 獲取 responseData
        request.responseData = responseObject;
        
        //3.2 獲取responseString
        request.responseString = [[NSString alloc] initWithData:responseObject encoding:[YTKNetworkUtils stringEncodingWithRequest:request]];

         //3.3 獲取responseObject(或responseJSONObject)
        //根據返回的響應的序列化的型別來得到對應型別的響應
        switch (request.responseSerializerType)
        {
            case YTKResponseSerializerTypeHTTP:
                // Default serializer. Do nothing.
                break;
                
            case YTKResponseSerializerTypeJSON:
                request.responseObject = [self.jsonResponseSerializer responseObjectForResponse:task.response data:request.responseData error:&serializationError];
                request.responseJSONObject = request.responseObject;
                break;
                
            case YTKResponseSerializerTypeXMLParser:
                request.responseObject = [self.xmlParserResponseSerialzier responseObjectForResponse:task.response data:request.responseData error:&serializationError];
                break;
        }
    }
    
    //4. 判斷是否有錯誤,將錯誤物件賦值給requestError,改變succeed的布林值。目的是根據succeed的值來判斷到底是進行成功的回撥還是失敗的回撥
    if (error) {
        //如果該方法傳入的error不為nil
        succeed = NO;
        requestError = error;
        
    } else if (serializationError) {
        //如果序列化失敗了
        succeed = NO;
        requestError = serializationError;
        
    } else {
        
        //即使沒有error而且序列化通過,也要驗證request是否有效
        succeed = [self validateResult:request error:&validationError];
        requestError = validationError;
    }

    //5. 根據succeed的布林值來呼叫相應的處理
    if (succeed) {
        //請求成功的處理
        [self requestDidSucceedWithRequest:request];
    } else {
        
        //請求失敗的處理
        [self requestDidFailWithRequest:request error:requestError];
    }

     //6. 回撥完成的處理
    dispatch_async(dispatch_get_main_queue(), ^{
        //6.1 在字典裡移除當前request
        [self removeRequestFromRecord:request];
         //6.2 清除所有block
        [request clearCompletionBlock];
    });
}
複製程式碼

簡單講解一下上面的程式碼:

  • 首先通過task的identifier值從YTKNetworkAgent儲存的字典裡獲取對應的請求。
  • 然後將獲得的responseObject進行處理,將處理後獲得的responseObject,responseData和responseString賦值給當前的請求例項request。
  • 再根據這些值的獲取情況來判斷最終回撥的成敗(改變succeed的值)。
  • 最後根據succeed的值來進行成功和失敗的回撥。

這裡先重點介紹一下是如何判斷json的有效性的:

//YTKNetworkAgent.m
//判斷code是否符合範圍和json的有效性
- (BOOL)validateResult:(YTKBaseRequest *)request error:(NSError * _Nullable __autoreleasing *)error {
    
    //1. 判斷code是否在200~299之間
    BOOL result = [request statusCodeValidator];
    
    if (!result) {
        if (error) {
            *error = [NSError errorWithDomain:YTKRequestValidationErrorDomain code:YTKRequestValidationErrorInvalidStatusCode userInfo:@{NSLocalizedDescriptionKey:@"Invalid status code"}];
        }
        return result;
    }
    
    //2. result 存在的情況判斷json是否有效
    id json = [request responseJSONObject];
    id validator = [request jsonValidator];
    
    if (json && validator) {
        //通過json和validator來判斷json是否有效
        result = [YTKNetworkUtils validateJSON:json withValidator:validator];
        
        //如果json無效
        if (!result) {
            if (error) {
                *error = [NSError errorWithDomain:YTKRequestValidationErrorDomain code:YTKRequestValidationErrorInvalidJSONFormat userInfo:@{NSLocalizedDescriptionKey:@"Invalid JSON format"}];
            }
            return result;
        }
    }
    return YES;
}
複製程式碼

在這裡,首先,用statusCodeValidator方法判斷響應的code是否在正確的範圍:

//YTKBaseReqiest.m
- (BOOL)statusCodeValidator {
    NSInteger statusCode = [self responseStatusCode];
    return (statusCode >= 200 && statusCode <= 299);
}
- (NSInteger)responseStatusCode {
    return self.response.statusCode;
}
複製程式碼

然後再判斷json的有效性:

//YTKNetworkUtils.m
//判斷json的有效性
+ (BOOL)validateJSON:(id)json withValidator:(id)jsonValidator {
    if ([json isKindOfClass:[NSDictionary class]] &&
        [jsonValidator isKindOfClass:[NSDictionary class]]) {
        NSDictionary * dict = json;
        NSDictionary * validator = jsonValidator;
        BOOL result = YES;
        NSEnumerator * enumerator = [validator keyEnumerator];
        NSString * key;
        while ((key = [enumerator nextObject]) != nil) {
            id value = dict[key];
            id format = validator[key];
            if ([value isKindOfClass:[NSDictionary class]]
                || [value isKindOfClass:[NSArray class]]) {
                result = [self validateJSON:value withValidator:format];
                if (!result) {
                    break;
                }
            } else {
                if ([value isKindOfClass:format] == NO &&
                    [value isKindOfClass:[NSNull class]] == NO) {
                    result = NO;
                    break;
                }
            }
        }
        return result;
    } else if ([json isKindOfClass:[NSArray class]] &&
               [jsonValidator isKindOfClass:[NSArray class]]) {
        NSArray * validatorArray = (NSArray *)jsonValidator;
        if (validatorArray.count > 0) {
            NSArray * array = json;
            NSDictionary * validator = jsonValidator[0];
            for (id item in array) {
                BOOL result = [self validateJSON:item withValidator:validator];
                if (!result) {
                    return NO;
                }
            }
        }
        return YES;
    } else if ([json isKindOfClass:jsonValidator]) {
        return YES;
    } else {
        return NO;
    }
}
複製程式碼

注意,YTKNetworkUtils這個類是在YTKNetworkPirvate裡面定義的,YTKNetworkPirvate裡面有一些工具類的方法,在後面還會遇到。

在驗證返回的JSON資料是否有效以後,就可以進行回撥了:

//YTKNetworkAgent.m
- (void)handleRequestResult:(NSURLSessionTask *)task responseObject:(id)responseObject error:(NSError *)error {

    ...
    //5. 根據succeed的布林值來呼叫相應的處理
    if (succeed) {
        //請求成功的處理
        [self requestDidSucceedWithRequest:request];
    } else {
        
        //請求失敗的處理
        [self requestDidFailWithRequest:request error:requestError];
    }

    //6. 回撥完成的處理
    dispatch_async(dispatch_get_main_queue(), ^{
        //6.1 在字典裡移除當前request
        [self removeRequestFromRecord:request];
         //6.2 清除所有block
        [request clearCompletionBlock];
    });

    ...

}
複製程式碼

我們先來分別看一下請求成功的處理和失敗的處理:

請求成功的處理:

//YTKNetworkAgent.m
//請求成功:主要負責將結果寫入快取&回撥成功的代理和block
- (void)requestDidSucceedWithRequest:(YTKBaseRequest *)request {
    
    @autoreleasepool {
        //寫入快取 
        [request requestCompletePreprocessor];
    }
    
    dispatch_async(dispatch_get_main_queue(), ^{
        
        //告訴Accessories請求就要停止了
        [request toggleAccessoriesWillStopCallBack];
        
        //在真正的回撥之前做的處理,使用者自定義
        [request requestCompleteFilter];

        //如果有代理,則呼叫成功的代理
        if (request.delegate != nil) {
            [request.delegate requestFinished:request];
        }
        
        //如果傳入了成功回撥的程式碼,則呼叫
        if (request.successCompletionBlock) {
            request.successCompletionBlock(request);
        }
        
        //告訴Accessories請求已經結束了
        [request toggleAccessoriesDidStopCallBack];
    });
}
複製程式碼

我麼可以看到,在請求成功以後,第一個做的是寫入快取,我們來看一下requestCompletePreprocessor方法的實現:

//YTKRequest.m
- (void)requestCompletePreprocessor {
    
    [super requestCompletePreprocessor];

    //是否非同步將responseData寫入快取(寫入快取的任務放在專門的佇列進行)
    if (self.writeCacheAsynchronously) {
        
        dispatch_async(ytkrequest_cache_writing_queue(), ^{
            //寫入快取檔案
            [self saveResponseDataToCacheFile:[super responseData]];
        });
        
    } else {
         //寫入快取檔案
        [self saveResponseDataToCacheFile:[super responseData]];
    }
}

//寫入快取檔案
- (void)saveResponseDataToCacheFile:(NSData *)data {
    
    if ([self cacheTimeInSeconds] > 0 && ![self isDataFromCache]) {
        if (data != nil) {
            @try {
                // 1. 儲存request的responseData到cacheFilePath
                [data writeToFile:[self cacheFilePath] atomically:YES];

                // 2. 儲存request的metadata到cacheMetadataFilePath
                YTKCacheMetadata *metadata = [[YTKCacheMetadata alloc] init];
                metadata.version = [self cacheVersion];
                metadata.sensitiveDataString = ((NSObject *)[self cacheSensitiveData]).description;
                metadata.stringEncoding = [YTKNetworkUtils stringEncodingWithRequest:self];
                metadata.creationDate = [NSDate date];
                metadata.appVersionString = [YTKNetworkUtils appVersionString];
                [NSKeyedArchiver archiveRootObject:metadata toFile:[self cacheMetadataFilePath]];
                
            } @catch (NSException *exception) {
                YTKLog(@"Save cache failed, reason = %@", exception.reason);
            }
        }
    }
}
複製程式碼

首先看一下寫入快取操作的執行條件:當cacheTimeInSeconds方法返回大於0並且isDataFromCache為NO的時候會進行寫入快取。

cacheTimeInSeconds方法返回的是快取儲存的時間,它最初定義在YTKBaseRquest裡面,預設返回是-1:

//YTKBaseRequest.m
- (NSInteger)cacheTimeInSeconds {
    return -1;
}
複製程式碼

所以說YTKNetwork預設是不進行快取的,如果使用者需要做快取,則需要在自定義的request類裡面返回一個大於0的整數,這個整數的單位是秒。

isDataFromCache屬性在上面講解傳送請求部分裡的查詢快取的步驟裡有介紹。在這裡再強調一下:isDataFromCache的預設值是NO。在請求發起之前,- 查詢快取的時候:

  • 如果發現快取不可用(或忽略快取),則立即傳送請求,這個時候,isDataFromCache的值不做更改,仍然是NO。
  • 如果發現快取可用(在不忽略快取的情況下),就要將isDataFromCache屬性設定為YES,說明將不需要傳送請求,直接在裡獲取資料了。

即是說,如果傳送了請求,則isDataFromCache一定是NO的,那麼在上面這個判斷裡面,(!isDataFromCache)就一定為YES了。

因此,如果使用者設定了快取儲存的時間,在請求返回成功後,就會寫入快取。

我們接著往下看,對於快取,YTKNetwork儲存的是兩種快取: 第一種是純粹的NSData型別的例項。第二種是描述當前NSData例項的後設資料YTKCacheMetadata的例項,從它的屬性來看,分為這幾種:

  1. 快取的版本,預設返回為0,使用者可以自定義。
  2. 敏感資料,型別為id,預設返回nil,使用者可以自定義。
  3. NSString的編碼格式,在YTKNetworkPrivate內的YTKNetworkUtils實現。
  4. 後設資料的建立時間。
  5. app的版本號,在YTKNetworkPrivate內的YTKNetworkUtils實現。

在將後設資料的例項的這些屬性都被賦值以後,將後設資料例項序列化寫入磁碟中。儲存的路徑通過cacheMetadataFilePath方法獲取。

現在知道了YTKRequest的快取內容,我們來看一下這兩種快取的位置:

//YTKRequest.m
//純NSData資料快取的檔名
- (NSString *)cacheFileName {
    NSString *requestUrl = [self requestUrl];
    NSString *baseUrl = [YTKNetworkConfig sharedConfig].baseUrl;
    id argument = [self cacheFileNameFilterForRequestArgument:[self requestArgument]];
    NSString *requestInfo = [NSString stringWithFormat:@"Method:%ld Host:%@ Url:%@ Argument:%@",
                             (long)[self requestMethod], baseUrl, requestUrl, argument];
    NSString *cacheFileName = [YTKNetworkUtils md5StringFromString:requestInfo];
    return cacheFileName;
}

//純NSData資料的快取位置
- (NSString *)cacheFilePath {
    NSString *cacheFileName = [self cacheFileName];
    NSString *path = [self cacheBasePath];
    path = [path stringByAppendingPathComponent:cacheFileName];
    return path;
}

//後設資料的快取位置
- (NSString *)cacheMetadataFilePath {
    NSString *cacheMetadataFileName = [NSString stringWithFormat:@"%@.metadata", [self cacheFileName]];
    NSString *path = [self cacheBasePath];
    path = [path stringByAppendingPathComponent:cacheMetadataFileName];
    return path;
}

//建立使用者儲存所有YTKNetwork快取的資料夾
- (NSString *)cacheBasePath {
    
    //獲取全路徑
    NSString *pathOfLibrary = [NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES) objectAtIndex:0];
    NSString *path = [pathOfLibrary stringByAppendingPathComponent:@"LazyRequestCache"];

    // YTKCacheDirPathFilterProtocol定義了使用者可以自定義儲存位置的代理方法
    NSArray<id<YTKCacheDirPathFilterProtocol>> *filters = [[YTKNetworkConfig sharedConfig] cacheDirPathFilters];
    if (filters.count > 0) {
        for (id<YTKCacheDirPathFilterProtocol> f in filters) {
            path = [f filterCacheDirPath:path withRequest:self];
        }
    }
    
    //建立資料夾
    [self createDirectoryIfNeeded:path];
    return path;
}
複製程式碼

可以看出,純NSData資料快取的檔名包含了請求方法(GET,POST..),baseURL,requestURL,請求引數拼接的字串再進行md5加密而成。

而後設資料的的檔名則在純NSData資料快取的檔名後面加上了.metadata字尾。

為了更形象地看到這兩種快取,我將快取的儲存時間設定為200秒之後再請求一次,然後開啟資料夾找到了它們:

快取和快取後設資料的檔案

而且我們也確認了儲存所有YTKNetwork快取的資料夾的名字為LazyRequestCache。

OK,現在我們知道了在請求成功回撥後的快取寫入,接下來看一下是如何回撥的:

//YTKNetworkAgent.m
- (void)handleRequestResult:(NSURLSessionTask *)task responseObject:(id)responseObject error:(NSError *)error {

    ...
    YTKRequest *strongSelf = self;

    //6. 執行回撥
    //6.1 請求完成的代理
    [strongSelf.delegate requestFinished:strongSelf];

    //6.2 請求成功的block
    if (strongSelf.successCompletionBlock) {
        strongSelf.successCompletionBlock(strongSelf);
    }

    //7. 把成功和失敗的block都設定為nil,避免迴圈引用
    [strongSelf clearCompletionBlock];
}
複製程式碼

我們可以看到,代理的回撥是先於block的回撥的。而且在block回撥結束以後,會立即呼叫clearCompletionBlock方法將block清空。該方法的實現是在YTKBaseRequest裡:

//YTKBaseRequest.m
- (void)clearCompletionBlock {
    // 清空請求結束的block,避免迴圈引用
    self.successCompletionBlock = nil;
    self.failureCompletionBlock = nil;
}
複製程式碼

現在我們知道了請求成功的處理,那麼再來看一下請求失敗時的處理:

//YTKNetworkAgent.m
//請求失敗
- (void)requestDidFailWithRequest:(YTKBaseRequest *)request error:(NSError *)error {
    
    request.error = error;
    YTKLog(@"Request %@ failed, status code = %ld, error = %@",
           NSStringFromClass([request class]), (long)request.responseStatusCode, error.localizedDescription);

    // 儲存未完成的下載資料
    NSData *incompleteDownloadData = error.userInfo[NSURLSessionDownloadTaskResumeData];
    if (incompleteDownloadData) {
        [incompleteDownloadData writeToURL:[self incompleteDownloadTempPathForDownloadPath:request.resumableDownloadPath] atomically:YES];
    }

    // Load response from file and clean up if download task failed.
    //如果下載任務失敗,則取出對應的響應檔案並清空
    if ([request.responseObject isKindOfClass:[NSURL class]]) {
        NSURL *url = request.responseObject;
        
        //isFileURL:是否是檔案,如果是,則可以再isFileURL獲取;&&後面是再次確認是否存在改url對應的檔案
        if (url.isFileURL && [[NSFileManager defaultManager] fileExistsAtPath:url.path]) {
            
            //將url的data和string賦給request
            request.responseData = [NSData dataWithContentsOfURL:url];
            request.responseString = [[NSString alloc] initWithData:request.responseData encoding:[YTKNetworkUtils stringEncodingWithRequest:request]];

            [[NSFileManager defaultManager] removeItemAtURL:url error:nil];
        }
        
        //清空request
        request.responseObject = nil;
    }

    
    @autoreleasepool {
        //請求失敗的預處理,YTK沒有定義,需要使用者定義
        [request requestFailedPreprocessor];
    }
    
    dispatch_async(dispatch_get_main_queue(), ^{
        
        //告訴Accessories請求就要停止了
        [request toggleAccessoriesWillStopCallBack];
        
        //在真正的回撥之前做的處理
        [request requestFailedFilter];

        //如果有代理,就呼叫代理
        if (request.delegate != nil) {
            [request.delegate requestFailed:request];
        }
        
        //如果傳入了失敗回撥的block程式碼,就呼叫block
        if (request.failureCompletionBlock) {
            request.failureCompletionBlock(request);
        }
        
        //告訴Accessories請求已經停止了
        [request toggleAccessoriesDidStopCallBack];
    });
}

複製程式碼

在這個方法裡,首先判斷了當前任務是否為下載任務,如果是,則儲存當前已經下載好的data到resumableDownloadPath裡面。而如果下載任務失敗,則將其對應的在本地儲存的路徑上的檔案清空。

到這裡,我已經把單個請求從配置,傳送,響應,回撥的步驟都講解完了。為了幫助大家理解整個過程,這裡提供了整個的流程圖:

YTKNetwork流程圖

我們說YTKNetworkAgent是請求的傳送者,既然有傳送,也就會有取消等操作,這就不得不提它的另外兩個介面:

//YTKNetworkAgent.h
///  取消某個request
- (void)cancelRequest:(YTKBaseRequest *)request;

///  取消所有新增的request
- (void)cancelAllRequests;
複製程式碼

首先我們看下取消某個request這個方法的實現:

//YTKNetworkAgent.m
///  取消某個request
- (void)cancelRequest:(YTKBaseRequest *)request {

    NSParameterAssert(request != nil);
    //獲取request的task,並取消
    [request.requestTask cancel];
    //從字典裡移除當前request
    [self removeRequestFromRecord:request];
    //清理所有block
    [request clearCompletionBlock];
}

//從字典裡移除某request
- (void)removeRequestFromRecord:(YTKBaseRequest *)request {
    
    //加鎖
    Lock();
    [_requestsRecord removeObjectForKey:@(request.requestTask.taskIdentifier)];
    YTKLog(@"Request queue size = %zd", [_requestsRecord count]);
    Unlock();
}
複製程式碼

取消所有在字典裡新增的request:

//YTKNetworkAgent.m
- (void)cancelAllRequests {
    Lock();
    NSArray *allKeys = [_requestsRecord allKeys];
    Unlock();
    if (allKeys && allKeys.count > 0) {
        NSArray *copiedKeys = [allKeys copy];
        for (NSNumber *key in copiedKeys) {
            Lock();
            YTKBaseRequest *request = _requestsRecord[key];
            Unlock();
            //stop每個請求
            [request stop];
        }
    }
}
複製程式碼

這個stop方法是在YTKBaseRequest裡面定義的:

//YTKBaseRequest.m
- (void)stop {
    
    //告訴Accessories將要回撥了
    [self toggleAccessoriesWillStopCallBack];
    
    //清空代理
    self.delegate = nil;
    
    //呼叫agent的取消某個request的方法
    [[YTKNetworkAgent sharedAgent] cancelRequest:self];
    
    //告訴Accessories回撥完成了
    [self toggleAccessoriesDidStopCallBack];
}
複製程式碼

OK,看到這裡,相信你對YTKNetwork單個請求的流程有了比較好的瞭解了,下面我們來看一下YTKNetwork的高階功能:批量請求和鏈式請求。

3.3 批量請求和鏈式請求

YTKNetwork支援的批量請求有兩種:

  1. 批量請求:多個請求幾乎同時發起。
  2. 鏈式請求:當前個請求結束後才能發起下一個請求。

其實無論是批量請求,還是鏈式請求,我們都可以想到很可能是用一個陣列將這些請求管理了起來。那麼具體是如何實現的呢?

我們首先來看一下YTKNetwork是如何實現批量請求的。

3.31批量請求

YTKNetwork 使用YTKBatchRequest類來傳送無序的批量請求,它需要用一個含有YTKRequest子類的陣列來初始化,並將這個陣列儲存起來賦給它的_requestArray例項變數:

//YTKBatchRequest.m
- (instancetype)initWithRequestArray:(NSArray<YTKRequest *> *)requestArray {
    self = [super init];
    if (self) {
        
        //儲存為屬性
        _requestArray = [requestArray copy];

        //批量請求完成的數量初始化為0
        _finishedCount = 0;
        
        //型別檢查,所有元素都必須為YTKRequest或的它的子類,否則強制初始化失敗
        for (YTKRequest * req in _requestArray) {
            if (![req isKindOfClass:[YTKRequest class]]) {
                YTKLog(@"Error, request item must be YTKRequest instance.");
                return nil;
            }
        }
    }
    return self;
}
複製程式碼

初始化以後,我們就可以呼叫start方法來發起當前YTKBatchRequest例項所管理的所有請求了:

//YTKBatchRequest.m
//batch請求開始
- (void)startWithCompletionBlockWithSuccess:(void (^)(YTKBatchRequest *batchRequest))success
                                    failure:(void (^)(YTKBatchRequest *batchRequest))failure {
    [self setCompletionBlockWithSuccess:success failure:failure];
    [self start];
}

//設定成功和失敗的block
- (void)setCompletionBlockWithSuccess:(void (^)(YTKBatchRequest *batchRequest))success
                              failure:(void (^)(YTKBatchRequest *batchRequest))failure {
    self.successCompletionBlock = success;
    self.failureCompletionBlock = failure;
}

- (void)start {
    
    //如果batch裡第一個請求已經成功結束,則不能再start
    if (_finishedCount > 0) {
        YTKLog(@"Error! Batch request has already started.");
        return;
    }
    
    //最開始設定失敗的request為nil
    _failedRequest = nil;
    
    //使用YTKBatchRequestAgent來管理當前的批量請求
    [[YTKBatchRequestAgent sharedAgent] addBatchRequest:self];
    [self toggleAccessoriesWillStartCallBack];
    
    //遍歷所有request,並開始請求
    for (YTKRequest * req in _requestArray) {
        req.delegate = self;
        [req clearCompletionBlock];
        [req start];
    }
}
複製程式碼

在這裡,我們可以看出: 1.在至少完成了其中一個請求以後,呼叫當前YTKBatchRequest例項的start方法會立即返回,否則可以無限制start。 2.YTKBatchRequest的例項是需要在發起請求之前,要被新增在YTKBatchRequestAgent裡的陣列裡:

//YTKBatchRequestAgent.m
- (void)addBatchRequest:(YTKBatchRequest *)request {
    @synchronized(self) {
        [_requestArray addObject:request];
    }
}
複製程式碼

3.因為是批量傳送請求,所以在這裡是遍歷YTKBatchRequest例項的_requestArray並逐一傳送請求。因為已經封裝好了單個的請求,所以在這裡直接start就好了。

發起請求以後,在每個請求回撥的代理方法裡,來判斷這次批量請求是否成功。

YTKRequest子類成功的回撥:

//YTKBatchRequest.m
#pragma mark - Network Request Delegate
- (void)requestFinished:(YTKRequest *)request {
    
    //某個request成功後,首先讓_finishedCount + 1
    _finishedCount++;
    
    //如果_finishedCount等於_requestArray的個數,則判定當前batch請求成功
    if (_finishedCount == _requestArray.count) {
        
        //呼叫即將結束的代理
        [self toggleAccessoriesWillStopCallBack];
        
        //呼叫請求成功的代理
        if ([_delegate respondsToSelector:@selector(batchRequestFinished:)]) {
            [_delegate batchRequestFinished:self];
        }
        
        //呼叫批量請求成功的block
        if (_successCompletionBlock) {
            _successCompletionBlock(self);
        }
        
        //清空成功和失敗的block
        [self clearCompletionBlock];
        
        //呼叫請求結束的代理
        [self toggleAccessoriesDidStopCallBack];
        
        //從YTKBatchRequestAgent裡移除當前的batch
        [[YTKBatchRequestAgent sharedAgent] removeBatchRequest:self];
    }
}
複製程式碼

我們可以看到,在某個請求的回撥成功以後,會讓成功計數+1。在+1以後,如果成功計數和當前批量請求陣列裡元素的個數相等,則判定當前批量請求成功,並進行當前批量請求的成功回撥。

接下來我們看一下某個請求失敗的處理:

YTKReques子類失敗的回撥:

//YTKBatchRequest.m
- (void)requestFailed:(YTKRequest *)request {
    
    _failedRequest = request;
    
    //呼叫即將結束的代理
    [self toggleAccessoriesWillStopCallBack];
    
    //停止batch裡所有的請求
    for (YTKRequest *req in _requestArray) {
        [req stop];
    }
    
    //呼叫請求失敗的代理
    if ([_delegate respondsToSelector:@selector(batchRequestFailed:)]) {
        [_delegate batchRequestFailed:self];
    }
    
    //呼叫請求失敗的block
    if (_failureCompletionBlock) {
        _failureCompletionBlock(self);
    }
    
    //清空成功和失敗的block
    [self clearCompletionBlock];

    //呼叫請求結束的代理
    [self toggleAccessoriesDidStopCallBack];
    
    //從YTKBatchRequestAgent裡移除當前的batch
    [[YTKBatchRequestAgent sharedAgent] removeBatchRequest:self];
}
複製程式碼

在這裡不難看出,當前批量請求裡面只要有一個request失敗了,則判定當前批量請求失敗。 而當前批量請求失敗的回撥(代理和block)會傳入這個失敗的request的例項。而且這個失敗的request會先被賦給_failedRequest這個例項變數裡。

總的來說,YTKBatchRequest類用一個陣列來儲存當前批量請求所要處理的所有request例項。而且用一個成功計數來判定當前批量請求整體是否成功。而當前批量請求的失敗則是由這些request例項裡面第一個失敗的例項導致的:只要有一個request回撥失敗了,則立即停止其他的所有請求並呼叫當前批量請求的失敗回撥。

現在講完了批量請求的處理,我們接下來看一下鏈式請求的處理。

3.32鏈式請求

和批量請求類似,處理鏈式請求的類是YTKChainRequest,並且用YTKChainRequestAgent單例來管理YTKChainRequest的例項。

但是和批量請求不同的是,YTKChainRequest例項的初始化是不需要傳入一個含有request的陣列的:

//YTKChainRequest.m
- (instancetype)init {
    
    self = [super init];
    if (self) {
        
        //下一個請求的index
        _nextRequestIndex = 0;
        
        //儲存鏈式請求的陣列
        _requestArray = [NSMutableArray array];
        
        //儲存回撥的陣列
        _requestCallbackArray = [NSMutableArray array];
        
        //空回撥,用來填充使用者沒有定義的回撥block
        _emptyCallback = ^(YTKChainRequest *chainRequest, YTKBaseRequest *baseRequest) {
            // do nothing
        };
    }
    return self;
}
複製程式碼

但是它提供了新增和刪除request的介面:

//YTKChainRequest.m
//在當前chain新增request和callback
- (void)addRequest:(YTKBaseRequest *)request callback:(YTKChainCallback)callback {
    
    //儲存當前請求
    [_requestArray addObject:request];
    
    if (callback != nil) {
        [_requestCallbackArray addObject:callback];
    } else {
        //之所以特意弄一個空的callback,是為了避免在使用者沒有給當前request的callback傳值的情況下,造成request陣列和callback陣列的不對稱
        [_requestCallbackArray addObject:_emptyCallback];
    }
}
複製程式碼

注意,在給YTKChainRequest例項新增request例項的同時,還可以傳入回撥的block。當然也可以不傳,但是為了保持request陣列和callback陣列的對稱性(因為回撥的時候是需要根據request陣列裡的index來獲取callback陣列裡對應的callback的),YTKNetwork給我們提供了一個空的block。

我們接著看一下鏈式請求的發起:

//YTKChainRequest.m
- (void)start {
    //如果第1個請求已經結束,就不再重複start了
    if (_nextRequestIndex > 0) {
        YTKLog(@"Error! Chain request has already started.");
        return;
    }
    //如果請求佇列陣列裡面還有request,則取出並start
    if ([_requestArray count] > 0) {
        [self toggleAccessoriesWillStartCallBack];
        //取出當前request並start
        [self startNextRequest];
        //在當前的_requestArray新增當前的chain(YTKChainRequestAgent允許有多個chain)
        [[YTKChainRequestAgent sharedAgent] addChainRequest:self];
    } else {
        YTKLog(@"Error! Chain request array is empty.");
    }
}
複製程式碼

我們可以看到,YTKChainRequest用_nextRequestIndex來儲存下一個請求的index,它的預設值是0。而它的值的累加是在當前請求結束後,發起下面的請求之前進行的。所以說,如果已經完成了請求佇列裡的第一個請求,就無法在啟動當前的請求佇列了,會立即返回。

這裡startNextRequest方法比較重要:在判斷請求佇列陣列裡面還有request的話,就會呼叫這個方法:

//YTKChainRequest.m
- (BOOL)startNextRequest {
    if (_nextRequestIndex < [_requestArray count]) {
        YTKBaseRequest *request = _requestArray[_nextRequestIndex];
        _nextRequestIndex++;
        request.delegate = self;
        [request clearCompletionBlock];
        [request start];
        return YES;
    } else {
        return NO;
    }
}
複製程式碼

這個方法有兩個作用:

  1. 第一個作用是判斷是否能進行下一個request(如果index 大於或等於 request陣列的count的話就不能在request陣列裡取出request,因為會造成陣列越界)
  2. 第二個作用是如果可以進行下一個request,則發起該request。並將_nextRequestIndex+1。

所以和批量請求不同的是,鏈式請求的請求佇列是可以變動的,使用者可以無限制地新增請求。只要請求佇列裡面有請求存在,則YTKChainRequest就會繼續傳送它們。

現在我們知道了YTKChainRequest的傳送,接下來看一下回撥部分:

和YTKBatchRequest相同的是,YTKChainRequest也實現了YTKRequest的代理:

//某個request請求成功的代理的實現
//YTKChainRequest.m
- (void)requestFinished:(YTKBaseRequest *)request {
    
    //1. 取出當前的request和callback,進行回撥
    NSUInteger currentRequestIndex = _nextRequestIndex - 1;
    YTKChainCallback callback = _requestCallbackArray[currentRequestIndex];
    callback(self, request);//注意:這個回撥只是當前request的回撥,而不是當前chain全部完成的回撥。當前chain的回撥在下面
    
    //2. 如果不能再繼續請求了,說明當前成功的request已經是chain裡最後一個request,也就是說當前chain裡所有的回撥都成功了,即這個chain請求成功了。
    if (![self startNextRequest]) {
        [self toggleAccessoriesWillStopCallBack];
        if ([_delegate respondsToSelector:@selector(chainRequestFinished:)]) {
            [_delegate chainRequestFinished:self];
            [[YTKChainRequestAgent sharedAgent] removeChainRequest:self];
        }
        [self toggleAccessoriesDidStopCallBack];
    }
}

複製程式碼

我們可以看到,在某個request回撥成功以後,會根據當前請求的index(_nextRequestIndex-1)來獲取其對應的block並呼叫。接著,再呼叫startNextRequest方法來判斷當前的YTKChainRequest的請求佇列裡面是否還有其他的請求了:

  • 如果沒有了,則呼叫當前YTKChainRequest的最終成功的回撥。
  • 如果還有,則發起接下來的request(按順序)。

接下來我們再看一下某個request失敗的代理的實現:

//YTKChainRequest.m
//某個reqeust請求失敗的代理
- (void)requestFailed:(YTKBaseRequest *)request {
    
    //如果當前 chain裡的某個request失敗了,則判定當前chain失敗。呼叫當前chain失敗的回撥
    [self toggleAccessoriesWillStopCallBack];
    if ([_delegate respondsToSelector:@selector(chainRequestFailed:failedBaseRequest:)]) {
        [_delegate chainRequestFailed:self failedBaseRequest:request];
        [[YTKChainRequestAgent sharedAgent] removeChainRequest:self];
    }
    [self toggleAccessoriesDidStopCallBack];
}
複製程式碼

如果當前的request請求失敗了,則判定當前鏈式請求是失敗的,則立即呼叫當前鏈式請求的失敗回撥。

現在我們知道了鏈式請求的請求和回撥,再來看一下鏈式請求的終止:

//YTKChainRequest.m
//終止當前的chain
- (void)stop {

    //首先呼叫即將停止的callback
    [self toggleAccessoriesWillStopCallBack];

    //然後stop當前的請求,再清空chain裡所有的請求和回掉block
    [self clearRequest];

    //在YTKChainRequestAgent裡移除當前的chain
    [[YTKChainRequestAgent sharedAgent] removeChainRequest:self];

    //最後呼叫已經結束的callback
    [self toggleAccessoriesDidStopCallBack];
}
複製程式碼

這個stop方法是可以在外部呼叫的,所以使用者可以隨時終止當前鏈式請求的進行。它首先呼叫clearReuqest方法,將當前request停止,再將請求佇列陣列和callback陣列清空。

//YTKChainRequest.m
- (void)clearRequest {
    //獲取當前請求的index
    NSUInteger currentRequestIndex = _nextRequestIndex - 1;
    if (currentRequestIndex < [_requestArray count]) {
        YTKBaseRequest *request = _requestArray[currentRequestIndex];
        [request stop];
    }
    [_requestArray removeAllObjects];
    [_requestCallbackArray removeAllObjects];
}
複製程式碼

然後在YTKChainRequestAgent單例裡面,將自己移除掉。

4. 最後的話


不知不覺寫了好多,請原諒我一如既往囉嗦的風格~

閱讀這個框架的原始碼我的收穫是:加深了對命令模式,對Block的理解,知道了一個網路請求都需要什麼元素組成,知道了網路快取該怎麼設計,也知道了鏈式請求怎麼設計等等。

我還記得當初聽說YTKNetwork能發起鏈式請求的時候覺得毫無思路的感覺,不過現在應該沒什麼問題了。

所以說多閱讀原始碼對技術水平的提升是很有幫助的,除了能增多對本語言API的瞭解,其實更有意義的是它能讓你接觸到一些新的設計和解決問題的辦法,這些都是脫離某個語言本身的東西,也是作為一名程式設計師所必不可少的東西。

希望這篇文章能對讀者們有所幫助~


本文已經同步到我的個人部落格:YTKNetwork原始碼解析

---------------------------- 2018年7月17日更新 ----------------------------

注意注意!!!

筆者在近期開通了個人公眾號,主要分享程式設計,讀書筆記,思考類的文章。

  • 程式設計類文章:包括筆者以前釋出的精選技術文章,以及後續釋出的技術文章(以原創為主),並且逐漸脫離 iOS 的內容,將側重點會轉移到提高程式設計能力的方向上。
  • 讀書筆記類文章:分享程式設計類思考類心理類職場類書籍的讀書筆記。
  • 思考類文章:分享筆者平時在技術上生活上的思考。

因為公眾號每天釋出的訊息數有限制,所以到目前為止還沒有將所有過去的精選文章都發布在公眾號上,後續會逐步釋出的。

而且因為各大部落格平臺的各種限制,後面還會在公眾號上釋出一些短小精幹,以小見大的乾貨文章哦~

掃下方的公眾號二維碼並點選關注,期待與您的共同成長~

公眾號:程式設計師維他命

相關文章