1. 程式人生 > >YTKNetwork原始碼解析

YTKNetwork原始碼解析

對於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發起網路請求。
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是一些遵從<YTKRequestAccessory>代理的物件。這個代理定義了一些用來追蹤請求狀況的方法。它被定義在了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];
    } 
            
           

相關推薦

YTKNetwork原始碼解析

對於iOS開發者來說,就算是沒有用過YTKNetwork框架,應該也見過,聽過了。它是猿題庫技術團隊開源的一個網路請求框架,內部封裝了AFNetworking。它把每個請求例項化,管理它的生命週期,也可以管理多個請求。 在正式講解原始碼之前,我會先講一下該框架所用的架構和設計模式。我總覺得對架構和設計有一

YTKNetWork原始碼解析——針對SSL自產證書認證如何隨心所欲的遊走在IP和域名之間並開啟想要的驗證

YTKNetWork原始碼解析—— 針對SSL自產證書認證如何隨心所欲的遊走在IP和域名之間並開啟想要的驗證 最近在進行遇到個比較奇葩棘手的問題,下面進行場景介紹: 1、  只有測試環境:ip地址,之後用 地址A替換 2、  生產環境:域名,之後使用地址B替換 3、  不

Netty進階:Futrue&Promise原始碼解析

文章目錄 1. Future&Promise 2. AbstractFuture 3.Completefuture 4.Channelfuture&Completechannel

大資料基礎(1)zookeeper原始碼解析

五 原始碼解析   public enum ServerState { LOOKING, FOLLOWING, LEADING, OBSERVING;}zookeeper伺服器狀態:剛啟動LOOKING,follower是FOLLOWING,leader是LEADING,observer是

Android框架原始碼解析之(四)Picasso

這次要分析的原始碼是 Picasso 2.5.2 ,四年前的版本,用eclipse寫的,但不影響這次我們對其原始碼的分析 地址:https://github.com/square/picasso/tree/picasso-parent-2.5.2 Picasso的簡單使用

Android框架原始碼解析之(三)ButterKnife

注:所有分析基於butterknife:8.4.0 原始碼目錄:https://github.com/JakeWharton/butterknife 其中最主要的3個模組是: Butterknife註解處理器https://github.com/JakeWharton/

Android框架原始碼解析之(二)OKhttp

原始碼在:https://github.com/square/okhttp 包實在是太多了,OKhttp核心在這塊https://github.com/square/okhttp/tree/master/okhttp 直接匯入Android Studio中即可。 基本使用:

Android框架原始碼解析之(一)Volley

前幾天面試CVTE,HR面掛了。讓內部一個學長幫我查看了一下面試官評價,發現二面面試官的評價如下: 廣度OK,但缺乏深究能力,深度與實踐不足 原始碼:只能說流程,細節程式碼不清楚,retrofit和volley都是。 感覺自己一方面:自己面試技巧有待提高吧(框

HashMap原始碼解析(JDK8)

前言 這段時間有空,專門填補了下基礎,把常用的ArrayList、LinkedList、HashMap、LinkedHashMap、LruCache原始碼看了一遍,List相對比較簡單就不單獨介紹了,Map準備用兩篇的篇幅,分別介紹HashMap和(LruCache+LinkedHa

原始碼解析--Long、long型別的比較遇到的問題

Long、long型別的比較遇到的問題: 1、long 是基本型別 Long是物件型別。 public static void main(String[] args) { Long A = 127l; Long B = 127l; long C = 127; l

CopyOnWriteArrayList實現原理以及原始碼解析

CopyOnWriteArrayList實現原理以及原始碼解析 1、CopyOnWrite容器(併發容器) Copy-On-Write簡稱COW,是一種用於程式設計中的優化策略。 其基本思路是,從一開始大家都在共享同一個內容,當某個人想要修改這個內容的時候,才

LinkedList實現原理以及原始碼解析(1.7)

LinkedList實現原理以及原始碼解析(1.7) 在1.7之後,oracle將LinkedList做了一些優化, 將1.6中的環形結構優化為了直線型了連結串列結構。 1、LinkedList定義: public class LinkedList<E>

ArrayList實現原理以及原始碼解析(補充JDK1.7,1.8)

ArrayList實現原理以及原始碼解析(補充JDK1.7,1.8) ArrayList的基本知識在上一節已經討論過,這節主要看ArrayList在JDK1.6到1.8的一些實現變化。 JDK版本不一樣,ArrayList類的原始碼也不一樣。 1、ArrayList類結構:

ArrayList實現原理以及原始碼解析(JDK1.6)

ArrayList實現原理以及原始碼解析(JDK1.6) 1、ArrayList ArrayList是基於陣列實現的,是一個動態陣列,其容量能自動增長,類似於C語言中的動態申請記憶體,動態增長記憶體。 ArrayList不是執行緒安全的,只能用在單執行緒環境下。

ConcurrentHashMap實現原理以及原始碼解析

ConcurrentHashMap實現原理以及原始碼解析 ConcurrentHashMap是Java1.5中引用的一個執行緒安全的支援高併發的HashMap集合類。 1、執行緒不安全的HashMap 因為多執行緒環境下,使用Hashmap進行put操作會引起死迴圈

Java併發程式設計高階技術-高效能併發框架原始碼解析與實戰(資源同步)

第1章 課程介紹(Java併發程式設計進階課程) 什麼是Disruptor?它一個高效能的非同步處理框架,號稱“單執行緒每秒可處理600W個訂單”的神器,本課程目標:徹底精通一個如此優秀的開源框架,面試秒殺面試官。本章會帶領小夥伴們先了解課程大綱與重點,然後模擬千萬,億級資料進行壓力測試。讓大

PackageManagerService 原始碼解析

一.SystemServer建立PackageManagerService     省略 二.PackageManagerService 建構函式  2.1 Settings mSettings = new Settings(mPacka

Java中compareTo用法及原始碼解析

最近遇到一個問題,在日期比較的時候,很麻煩,因為日期比較沒有大於等於,只有大於或者小於,這就導致在比較時間的時候特別麻煩,而且還要由string轉成date格式才能比較,下面是我使用compareTo比較時間字串的程式碼: String putStartTime = Date

大資料基礎之Quartz(1)簡介、原始碼解析

一簡介 官網 http://www.quartz-scheduler.org/ What is the Quartz Job Scheduling Library? Quartz is a richly featured, open source job scheduling libra

小而美的Promise庫——promiz原始碼解析

背景 在上一篇部落格[譯]前端基礎知識儲備——Promise/A+規範中,我們介紹了Promise/A+規範的具體條目。在本文中,我們來選擇了promiz,讓大家來看下一個具體的Promise庫的內部程式碼是如何運作的。 promiz是一個體積很小的promise庫(官方介紹約為913 bytes (gz