YTKNetwork原始碼解析
對於iOS開發者來說,就算是沒有用過YTKNetwork框架,應該也見過,聽過了。它是猿題庫技術團隊開源的一個網路請求框架,內部封裝了AFNetworking。它把每個請求例項化,管理它的生命週期,也可以管理多個請求。
在正式講解原始碼之前,我會先講一下該框架所用的架構和設計模式。我總覺得對架構和設計有一定的瞭解的話,會有助於對原始碼的理解。
1. 架構
先上圖:
YTKRequest架構圖
在這裡簡單說明一下:
- YTKNetwork框架將每一個請求例項化,YTKBaseRequest是所有請求類的基類,YTKRequest是它的子類。所以如果我們想要傳送一個請求,則需要建立並例項化一個繼承於YTKRequest的自定義的請求類(CustomRequest)併發送請求。
- YTKNetworkAgent是一個單例,負責管理所有的請求類(例如CustomRequest)。當CustomRequest傳送請求以後,會把自己放在YTKNetworkAgent持有的一個字典裡,讓其管理自己。
- 我們說YTKNetwork封裝了AFNetworking,實際上是YTKNetworkAgent封裝了AFNetworking,由它負責AFNetworking請求的傳送和AFNetworking的回撥處理。所以如果我們想更換一個第三方網路請求庫,就可以在這裡更換一下。而YTKRequest更多的是隻是負責快取的處理。
- YTKNetworkConfig與YTKPriviate的具體職能現在不做介紹,會在後文給出。
OK,現在我們知道了YTKNetwork中類與類之間的關係以及關鍵類的大致職能,接下來我會告訴你YTKNetwork為什麼會採用這種關係來架構,以及採用這種架構會有什麼好處。
2. 設計模式
YTKNetwork框架採用的設計模式是命令模式(Command Pattern)。
首先看一下命令模式的定義:
命令模式將請求封裝成物件,以便使用不同的請求,佇列或者日誌來引數化其他物件。命令模式也支援可撤銷的操作。
摘自:《Head First 設計模式》
看一下命令模式的類圖:
命令模式類圖.png
圖中英文的含義:
英文 | 中文 |
---|---|
Command | 抽象命令類 |
ConcreteCommand | 命令類的實現類(子類) |
Invoker | 呼叫者 |
Receiver | 命令接收者(執行者) |
Client | 客戶端 |
詳細介紹一下:
- 命令模式的本質是對命令的封裝,將發出命令的責任和執行命令的責任分割開。
- 命令模式允許請求的一方和接收的一方獨立開來,使得請求的一方不必知道接收請求的一方的介面,更不必知道請求是怎麼被接收,以及操作是否被執行、何時被執行,以及是怎麼被執行的。
可能還是覺得有點抽象,在這裡舉一個《Head First 設計模式》裡的例子,一個客人在餐廳點餐的過程:
- 你將點的菜寫在訂單裡,交給了服務員。
- 服務員將訂單交給廚師。
- 廚師做好菜之後將做好的菜交給服務員。
- 最後服務員把菜遞給你。
在這裡,命令就好比是訂單,而你是命令的發起者。你的命令(訂單)通過服務員(呼叫者)交給了命令的執行者(廚師)。
所以至於這道菜具體是誰做,怎麼做,你是不知道的,你做的只是發出命令和接受結果。而且對於餐廳來說,廚師是可以隨便換的,而你可能對此一無所知。反過來,廚師只需要好好把菜做好,至於是誰點的菜也不需要他考慮。
結合上面命令模式的類圖以及餐廳點餐的例子,我們來理清一下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");
}
有兩點需要注意的是:
- 必須給自定義請求類(RegisterApi)呼叫
startWithCompletionBlockWithSuccess:failure
方法(或start
方法),才能真正發起請求。 - 在同時設定了回撥代理和回撥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的情況)。
我們來看一下每一步的具體實現:
ignoreCache
屬性是使用者手動設定的,如果使用者強制忽略快取,則無論是否快取是否存在,直接傳送請求。resumableDownloadPath
是斷點下載路徑,如果該路徑不為空,說明有未完成的下載任務,則直接傳送請求繼續下載。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:
- 如果是,則直接呼叫AFNetworking的dataTaskWithRequest:方法。
- 如果不是,則呼叫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:
這個私有方法來實現的。在講解這個關鍵的私有方法之前,先來逐步講解一下這個私有方法需要的每個引數的獲取方法:
- 獲得請求型別(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