對於iOS開發者來講,就算是沒有用過YTKNetwork框架,應該也見過,聽過了。它是猿題庫技術團隊開源的一個網絡請求框架,內部封裝了AFNetworking。它把每一個請求實例化,管理它的生命週期,也能夠管理多個請求。git
在正式講解源碼以前,我會先講一下該框架所用的架構和設計模式。我總以爲對架構和設計有必定的瞭解的話,會有助於對源碼的理解。程序員
先上圖:github
在這裏簡單說明一下:編程
OK,如今咱們知道了YTKNetwork中類與類之間的關係以及關鍵類的大體職能,接下來我會告訴你YTKNetwork爲何會採用這種關係來架構,以及採用這種架構會有什麼好處。json
YTKNetwork框架採用的設計模式是命令模式(Command Pattern)。設計模式
首先看一下命令模式的定義:api
命令模式將請求封裝成對象,以便使用不一樣的請求,隊列或者日誌來參數化其餘對象。命令模式也支持可撤銷的操做。 摘自:《Head First 設計模式》數組
看一下命令模式的類圖:緩存
圖中英文的含義:網絡
英文 | 中文 |
---|---|
Command | 抽象命令類 |
ConcreteCommand | 命令類的實現類(子類) |
Invoker | 調用者 |
Receiver | 命令接收者(執行者) |
Client | 客戶端 |
詳細介紹一下:
可能仍是以爲有點抽象,在這裏舉一個《Head First 設計模式》裏的例子,一個客人在餐廳點餐的過程:
在這裏,命令就比如是訂單,而你是命令的發起者。你的命令(訂單)經過服務員(調用者)交給了命令的執行者(廚師)。 因此至於這道菜具體是誰作,怎麼作,你是不知道的,你作的只是發出命令和接受結果。並且對於餐廳來講,廚師是能夠隨便換的,而你可能對此一無所知。反過來,廚師只須要好好把菜作好,至因而誰點的菜也不須要他考慮。
結合上面命令模式的類圖以及餐廳點餐的例子,咱們來理清一下YTKNetwork內部的職能
場景 | Command | ConcreteCommand | Invoker | Receiver | Client |
---|---|---|---|---|---|
餐廳 | 空白訂單 | 填入菜名的訂單 | 服務員 | 廚師 | 客人 |
YTKNetwork | YTKBaseRequest | CustomRequest | YTKNetworkAgent | AFNetworking | ViewController/ViewModel |
能夠看到,YTKNetwork對命令模式的實現是很符合其設計標準的,它將請求的發起者和接收者分離開來(中間隔着調用者),可讓咱們隨時更換接受者。
另外,由於封裝了請求,咱們既能夠管理單個請求,也能夠同時管理多個請求,甚至實現璉式請求的發送。關於多個請求的發送,咱們也能夠想象在餐廳裏,你能夠在吃的過程當中還想起來要吃別的東西,例如點心,飲料之類的,你就能夠填多個訂單(固然也能夠寫在一塊兒)交給服務員。
相信到這裏,你們應該對YTKNetwork的設計與架構有了足夠的認識了,下面進入到真正的源碼解析,咱們結合一下它的代碼來看一下YTKNetwork是如何實現和管理網絡請求的。
在真正講解源碼以前,我先詳細說一下各個類的職責:
類名 | 職責 |
---|---|
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都作了什麼。
官方的教程建議咱們將請求的全局配置是在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
複製代碼
如今咱們知道如何配置全局的參數和針對某個請求的參數了,接下來看一下單個請求是如何發起的。
仍是剛纔的註冊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");
}
複製代碼
有兩點須要注意的是:
startWithCompletionBlockWithSuccess:failure
方法(或start
方法),才能真正發起請求。知道了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的狀況),發起請求。咱們來看一下每一步的具體實現:
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是一些聽從代理的對象。這個代理定義了一些用來追蹤請求情況的方法。它被定義在了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];
}
複製代碼
這個方法挺長的,可是請不要被嚇到,它總共分爲三個部分:
requestTask
屬性(之後提到的request,都爲用戶自定義的當前請求類的實例)。下面我來依次講解每一個部分:
第一部分:獲取當前請求對應的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:
第一種狀況就不說了,由於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:
這個私有方法來實現的。在講解這個關鍵的私有方法以前,先來逐步講解一下這個私有方法須要的每一個參數的獲取方法:
//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];
});
}
複製代碼
簡單講解一下上面的代碼:
這裏先重點介紹一下是如何判斷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了。
所以,若是用戶設置了緩存保存的時間,在請求返回成功後,就會寫入緩存。
咱們接着往下看,對於緩存,YTKNetwork保存的是兩種緩存: 第一種是純粹的NSData類型的實例。第二種是描述當前NSData實例的元數據YTKCacheMetadata的實例,從它的屬性來看,分爲這幾種:
在將元數據的實例的這些屬性都被賦值之後,將元數據實例序列化寫入磁盤中。保存的路徑經過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
裏面。而若是下載任務失敗,則將其對應的在本地保存的路徑上的文件清空。
到這裏,我已經把單個請求從配置,發送,響應,回調的步驟都講解完了。爲了幫助你們理解整個過程,這裏提供了整個的流程圖:
咱們說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的高級功能:批量請求和鏈式請求。
YTKNetwork支持的批量請求有兩種:
其實不管是批量請求,仍是鏈式請求,咱們均可以想到極可能是用一個數組將這些請求管理了起來。那麼具體是如何實現的呢?
咱們首先來看一下YTKNetwork是如何實現批量請求的。
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回調失敗了,則當即中止其餘的全部請求並調用當前批量請求的失敗回調。
如今講完了批量請求的處理,咱們接下來看一下鏈式請求的處理。
和批量請求相似,處理鏈式請求的類是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;
}
}
複製代碼
這個方法有兩個做用:
_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的請求隊列裏面是否還有其餘的請求了:
接下來咱們再看一下某個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單例裏面,將本身移除掉。
不知不覺寫了好多,請原諒我一如既往囉嗦的風格~
閱讀這個框架的源碼個人收穫是:加深了對命令模式,對Block的理解,知道了一個網絡請求都須要什麼元素組成,知道了網絡緩存該怎麼設計,也知道了鏈式請求怎麼設計等等。
我還記得當初據說YTKNetwork能發起鏈式請求的時候以爲毫無思路的感受,不過如今應該沒什麼問題了。
因此說多閱讀源碼對技術水平的提高是頗有幫助的,除了能增多對本語言API的瞭解,其實更有意義的是它能讓你接觸到一些新的設計和解決問題的辦法,這些都是脫離某個語言自己的東西,也是做爲一名程序員所必不可少的東西。
但願這篇文章能對讀者們有所幫助~
本文已經同步到個人我的博客:YTKNetwork源碼解析
---------------------------- 2018年7月17日更新 ----------------------------
注意注意!!!
筆者在近期開通了我的公衆號,主要分享編程,讀書筆記,思考類的文章。
由於公衆號天天發佈的消息數有限制,因此到目前爲止尚未將全部過去的精選文章都發布在公衆號上,後續會逐步發佈的。
並且由於各大博客平臺的各類限制,後面還會在公衆號上發佈一些短小精幹,以小見大的乾貨文章哦~
掃下方的公衆號二維碼並點擊關注,期待與您的共同成長~