網絡(六):AFNetworking源碼淺析

目錄
一、我們先自己用NSURLSession實現一下GET請求、POST請求、文件上傳請求、文件下載請求和支持HTTPS
 1、GET請求
 2、POST請求
 3、文件上傳請求
 4、文件下載請求
 5、支持HTTPS
二、AFNetworking源碼分析
 1、序列化器模塊
 2、session和task管理模塊
 3、支持HTTPS模塊


一、我們先自己用NSURLSession實現一下GET請求、POST請求、文件上傳、文件下載和支持HTTPS


1、GET請求

#import "GetViewController.h"

@interface GetViewController ()

@end

@implementation GetViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.navigationItem.title = @"GET請求";
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    // 第1步:創建一個HTTP請求——即NSURLRequest
    NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init];
    // 設置HTTP請求的方法
    request.HTTPMethod = @"GET";
    /*
     設置HTTP請求的URL,注意:
     ——GET請求的參數是直接放在URL裏面的
     ——使用UTF-8編碼對URL編碼一下
     */
    request.URL = [NSURL URLWithString:[@"http://api.leke.cn/auth/m/parent/homework/getHomeworkSubject.htm?_s=homework&_m=getHomeworkSubject&ticket=VFdwblBRPT07S3lvbEtpc29LaXdsTENvPTsyODI4&studentId=6090647" stringByAddingPercentEscapesUsingEncoding:(NSUTF8StringEncoding)]];
        
    // 第2步:創建一個session
    NSURLSession *session = [NSURLSession sharedSession];
    
    // 第3步:創建一個dataTask
    NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        /*
         第5步:HTTP請求結束的回調(注意block回調是在子線程,我們需要主動回到主線程去做事情)
         
         data: 響應體
         response: 響應頭
         error: 請求失敗的信息
         
         除了通過block來做HTTP請求結束的回調,我們還可以通過delegate來做HTTP請求結束的回調,這部分代碼會在POST請求裏做演示
         */
        NSLog(@"111---%@", [NSThread currentThread]);
        
        dispatch_async(dispatch_get_main_queue(), ^{
            NSLog(@"222---%@", [NSThread currentThread]);
            
            if (error != nil) {
                NSLog(@"請求失敗了");
            } else {
                // data就是我們請求到的JSON字符串的二進制格式,我們先把它轉換成JSON字符串,打印出來看看(NSData -> NSString)
                NSString *jsonString = [[NSString alloc] initWithData:data encoding:(NSUTF8StringEncoding)];
                NSLog(@"請求成功了,解析前:---%@", jsonString);
                // 我們把這個JSON字符串解析成OC字典(NSData -> NSDictionary)
                NSDictionary *dictionary = [NSJSONSerialization JSONObjectWithData:data options:(0) error:nil];
                NSLog(@"請求成功了,解析後:---%@", dictionary);
            }
        });
    }];
    
    /*
     第4步:執行dataTask
     執行dataTask後就發起了我們剛纔創建的HTTP請求,並且NSURLSession會默認把這個HTTP請求放到子線程裏去做
     */
    [dataTask resume];
}

@end

2、POST請求

#import "PostViewController.h"

// 因爲是普通的GET、POST請求,只需要遵循NSURLSessionDataDelegate協議就可以了
@interface PostViewController () <NSURLSessionDataDelegate>

/// 響應體數據
@property (nonatomic, strong) NSMutableData *responseBodyData;

@end

@implementation PostViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.navigationItem.title = @"POST請求";
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    // 第1步:創建一個HTTP請求——即NSURLRequest
    NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init];
    // 設置HTTP請求的方法
    request.HTTPMethod = @"POST";
    /*
     設置HTTP請求的URL,注意:
     ——POST請求的參數不放在URL裏,而是放在Body裏
     ——使用UTF-8編碼對URL編碼一下
     */
    request.URL = [NSURL URLWithString:[@"https://homework.leke.cn/auth/m/person/homework/getStudentNewHomeworkData.htm" stringByAddingPercentEscapesUsingEncoding:(NSUTF8StringEncoding)]];
    /*
     設置HTTP請求的請求頭:
     ——如果我們跟服務端約定採用表單提交,那麼我們就得把請求頭的Content-Type字段設置爲application/x-www-form-urlencoded; charset=utf-8
     ——如果我們跟服務端約定採用JSON提交,那麼我們就得把請求頭的Content-Type字段設置爲application/json; charset=utf-8
     */
    [request setValue:@"application/x-www-form-urlencoded; charset=utf-8" forHTTPHeaderField:@"Content-Type"];
//    [request setValue:@"application/json; charset=utf-8" forHTTPHeaderField:@"Content-Type"];
    /*
     設置HTTP請求的請求體:
     ——如果我們跟服務端約定採用表單提交,那麼請求體的格式就必須得是"username=123&password=456"這樣,同時注意對請求體使用UTF-8編碼一下
     ——如果我們跟服務端約定採用JSON提交,那麼請求體的格式就必須得是"{username:123,password=456}"這樣,同時注意對請求體使用UTF-8編碼一下
     */
    request.HTTPBody = [@"ticket=VFdwblBRPT07S3lvbEtpc29LaXdsTENvPTsyODI4&studentId=6090647&curPage=1&pageSize=10&startTime=1639497600000&endTime=1642089599000" dataUsingEncoding:NSUTF8StringEncoding];
//    request.HTTPBody = [@"{\"ticket\": \"VFdwblBRPT07S3lvbEtpc29LaXdsTENvPTsyODI4\", \"studentId\": \"6090647\", \"curPage\": \"1\", \"pageSize\": \"10\", \"startTime\": \"1639497600000\", \"endTime\": \"1642089599000\"}" dataUsingEncoding:NSUTF8StringEncoding];
    
    /*
     第2步:創建一個session
     
     configuration: 配置信息
     delegate: 代理
     delegateQueue: 設置代理方法在哪個線程裏調用,mainQueue就代表在主線程裏調用
     */
    NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:[NSOperationQueue mainQueue]];
    
    // 第3步:創建一個dataTask
    NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request];
    
    /*
     第4步:執行dataTask
     執行dataTask後就發起了我們剛纔創建的HTTP請求,並且NSURLSession會默認把這個HTTP請求放到子線程裏去做
     */
    [dataTask resume];
}


// 第5步:HTTP請求結束的回調(注意delegate回調是在主線程,我們不需要再主動回到主線程)
#pragma mark - NSURLSessionDataDelegate

/// 接收到響應頭的回調
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler {
    NSLog(@"111---%@", [NSThread currentThread]);

    /*
     NSURLSessionResponseCancel = 0, // 通過delegate來做HTTP請求結束的回調默認情況下接收到響應頭後會取消後續的請求,即內容會調用[dataTask cancel]
     NSURLSessionResponseAllow = 1, // 允許後續的請求
     NSURLSessionResponseBecomeDownload = 2, // 把當前請求轉變成一個下載請求
     NSURLSessionResponseBecomeStream = 3, // 把當前請求轉變成一個下載請求(iOS9之後可用)
     */
    completionHandler(NSURLSessionResponseAllow);
}

/// 接收到響應體的回調
///
/// data: 指得是本次接收到的數據
/// ——如果數據量很小的話,這個方法只觸發一次就接收完了,然後會觸發請求結束的回調
/// ——如果數據量很大的話,這個方法會觸發多次,一段一段地接收,全部接收完後會觸發請求結束的回調
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
    NSLog(@"222---%@", [NSThread currentThread]);
    
    [self.responseBodyData appendData:data];
}

/// 請求結束的回調
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
    NSLog(@"333---%@", [NSThread currentThread]);
    
    if (error != nil) {
        NSLog(@"請求失敗了");
    } else {
        // data就是我們請求到的JSON字符串的二進制格式,我們先把它轉換成JSON字符串,打印出來看看(NSData -> NSString)
        NSString *jsonString = [[NSString alloc] initWithData:self.responseBodyData encoding:(NSUTF8StringEncoding)];
        NSLog(@"請求成功了,解析前:---%@", jsonString);
        // 我們把這個JSON字符串解析成OC字典(NSData -> NSDictionary)
        NSDictionary *dictionary = [NSJSONSerialization JSONObjectWithData:self.responseBodyData options:(0) error:nil];
        NSLog(@"請求成功了,解析後:---%@", dictionary);
    }
}


#pragma mark - setter, getter

- (NSMutableData *)responseBodyData {
    if (_responseBodyData == nil) {
        _responseBodyData = [NSMutableData data];
    }
    return _responseBodyData;
}

@end

3、文件上傳請求

文件上傳請求本質上就是一個POST請求,只不過:

  • 1️⃣這個POST請求的請求頭裏必須設置Content-Type字段爲multipart/form-data; boundary=xxx
  • 2️⃣請求體也必須得嚴格按照指定的格式去拼接數據。

請求體的格式:

--分隔符
回車換行
Content-Disposition: form-data; name="file"; filename="123.jpg"
回車換行
Content-Type: image/jpg
回車換行
回車換行
數據
回車換行
--分隔符--

// 隨機生成的boundary
#define kBoundary @"----WebKitFormBoundaryjv0UfA04ED44AhWx"
// 回車換行
#define kNewLine [@"\r\n" dataUsingEncoding:NSUTF8StringEncoding]

#import "UploadViewController.h"

// 因爲uploadTask是繼承自dataTask的,只需要遵循NSURLSessionDataDelegate協議就可以了
@interface UploadViewController () <NSURLSessionDataDelegate>

@end

@implementation UploadViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.navigationItem.title = @"文件上傳請求";
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    // 第1步:創建一個HTTP請求——即NSURLRequest
    NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init];
    // 設置HTTP請求的方法
    request.HTTPMethod = @"POST";
    /*
     設置HTTP請求的URL,注意:
     ——POST請求的參數不放在URL裏,而是放在Body裏
     ——使用UTF-8編碼對URL編碼一下
     */
    request.URL = [NSURL URLWithString:[@"https://fs.leke.cn/api/w/upload/image/binary.htm?ticket=VFdwblBRPT07S3lvbEtpc29LaXdsTENvPTsyODI4" stringByAddingPercentEscapesUsingEncoding:(NSUTF8StringEncoding)]];
    /*
     設置HTTP請求的請求頭
     */
    [request setValue:[NSString stringWithFormat:@"multipart/form-data; boundary=%@", kBoundary] forHTTPHeaderField:@"Content-Type"];
    
    /*
     第2步:創建一個session
     
     configuration: 配置信息
     delegate: 代理
     delegateQueue: 設置代理方法在哪個線程裏調用,mainQueue就代表在主線程裏調用
     */
    NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:[NSOperationQueue mainQueue]];

    /*
     第3步:創建一個uploadTask
     創建uploadTask的時候,把請求體掛上去,而不是直接設置request.HTTPBody
     */
    NSURLSessionUploadTask *uploadTask = [session uploadTaskWithRequest:request fromData:[self getRequestBodyData]];

    /*
     第4步:執行uploadTask
     執行uploadTask後就發起了我們剛纔創建的HTTP請求,並且NSURLSession會默認把這個HTTP請求放到子線程裏去做
     */
    [uploadTask resume];
}


// 第5步:HTTP請求結束的回調(注意delegate回調是在主線程,我們不需要再主動回到主線程)
#pragma mark - NSURLSessionDataDelegate

/// 數據上傳中的回調
///
/// bytesSent: 本次上傳的數據大小
/// totalBytesSent: 已經上傳的數據大小
/// totalBytesExpectedToSend: 數據的總大小
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didSendBodyData:(int64_t)bytesSent totalBytesSent:(int64_t)totalBytesSent totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend {
    NSLog(@"上傳進度---%f", 1.0 * totalBytesSent / totalBytesExpectedToSend);
}

/// 請求結束的回調
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
    if (error != nil) {
        NSLog(@"上傳失敗了");
    } else {
        NSLog(@"上傳成功了");
    }
}


#pragma mark - private method

- (NSData *)getRequestBodyData {
    NSMutableData *requestBodyData = [NSMutableData data];
    
    // 開始標識
    [requestBodyData appendData:[[NSString stringWithFormat:@"--%@", kBoundary] dataUsingEncoding:NSUTF8StringEncoding]];
    
    [requestBodyData appendData:kNewLine];
    
    // name: file,服務器規定的參數
    // filename: 123.jpg,文件保存到服務器上面的名稱
    [requestBodyData appendData:[@"Content-Disposition: form-data; name=\"file\"; filename=\"123.jpg\"" dataUsingEncoding:NSUTF8StringEncoding]];
    
    [requestBodyData appendData:kNewLine];
    
    // Content-Type: 文件的類型
    [requestBodyData appendData:[@"Content-Type: image/jpg" dataUsingEncoding:NSUTF8StringEncoding]];
    
    [requestBodyData appendData:kNewLine];
    
    [requestBodyData appendData:kNewLine];
    
    // UIImage ---> NSData
    UIImage *image = [UIImage imageNamed:@"123.jpg"];
    NSData *imageData = UIImagePNGRepresentation(image);
    [requestBodyData appendData:imageData];
    
    [requestBodyData appendData:kNewLine];
    
    // 結束標識
    [requestBodyData appendData:[[NSString stringWithFormat:@"--%@--", kBoundary] dataUsingEncoding:NSUTF8StringEncoding]];
    
    return requestBodyData;
}

@end

4、文件下載請求

文件下載請求本質上來說就是一個GET請求,只不過關於下載我們需要注意五個問題:

  • 1️⃣下載進度問題:最好是能監聽下載進度,這樣方便用戶知道自己下載了多少東西。block回調的方式是無法監聽下載進度的(因爲它是等數據全部下載完一次性回調回來的),delegate回調的方式才能監聽下載的進度(因爲它是一段數據一段數據接收的),所以我們得用delegate回調的方式(NSURLSession的downloadTask已經給我們提供了一個這樣的回調,所以我們就不必像NSURLConnection那樣自己記錄數據了,直接拿來用就行);
  • 2️⃣內存暴漲問題:下載的東西最好是邊下載邊存儲到磁盤裏,而非放在應用程序內存中,因爲下載的東西可能很大(比如1個G),那如果放在內存中就會導致App內存溢出而崩潰(NSURLSession的downloadTask內部已經幫我們做了邊下載邊存儲到磁盤裏,只不過是存儲到了tmp文件夾下,App退出後就會清除掉,所以我們只需要在下載任務完成後把文件移動到我們自定義的目錄下就可以了,不必像NSURLConnection那樣自己往文件裏寫數據);
  • 3️⃣斷點下載問題:最好能支持斷點下載,因爲有可能業務允許用戶下載到一半的時候暫停下載,當恢復下載時最好是能實現客戶端通過請求頭告訴服務器我想要下載哪部分數據,以便從指定的位置處開始下載,而非從頭開始下載,這樣可以節省用戶的流量(NSURLSession的downloadTask已經給我們提供了相應的API,很方便就能實現斷點下載,就不必像NSURLConnection那樣自己記錄下載的位置、設置請求頭了);
  • 4️⃣離線斷點下載問題:但是光支持了斷點下載可能還不夠,因爲支持斷點下載僅僅是滿足了App在活躍期間用戶點擊(暫停下載就是通過取消網絡請求來實現的)、繼續下載(繼續下載就是通過重新創建一個網絡請求、但是把下載位置設置到取消下載之前的那個位置來實現的)這樣的需求或者退出界面時我們批量把下載任務暫停掉這樣的需求,但是如果用戶強制退出了App,根本不會觸發我們暫停下載的回調,我們也無從記錄斷點的信息,那怎麼辦呢?這就需要做離線斷點下載,NSURLSession的downloadTask無法實現離線斷點下載,所以我們就得用dataTask和NSFileHandle或NSOutStream像NSURLConnection那樣自己記錄下載的位置、設置請求頭了;
  • 5️⃣多線程斷點下載問題:如果一個文件很大很大(比如有1000KB),我們開一個線程下載起來有些慢,那我們就可以考慮多開幾個線程來下載這個文件,比如第一個線程通過請求頭告訴服務器要下載0 ~ 400KB的數據,第二個線程通過請求頭告訴服務器要下載401 ~ 800KB的數據,第三個線程通過請求頭告訴服務器要下載剩餘的數據,最後等三個線程都下載完了,根據順序把三個線程的數據給拼接起來就可以了。
4.1 通過downloadTask實現
/*
 關於下載我們需要注意五個問題:
 ✅1、下載進度問題
 ✅2、內存暴漲問題
 ✅3、斷點下載問題
 ❌4、離線斷點下載問題:不支持
 ❌5、多線程斷點下載問題:這裏就不實現了,可參考上面的思路
 */

#import "DownloadViewController.h"

// 因爲是個文件下載請求,只需要遵循NSURLSessionDownloadDelegate協議就可以了
@interface DownloadViewController () <NSURLSessionDownloadDelegate>

/// session
@property (nonatomic, strong) NSURLSession *session;

/// downloadTask
@property (nonatomic, strong) NSURLSessionDownloadTask *downloadTask;

/// 暫停下載時的一些位置信息(注意並非暫停下載時之前下載過的數據)
@property (nonatomic, strong) NSData *resumeData;

@end

@implementation DownloadViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.navigationItem.title = @"通過downloadTask實現";
}

- (void)dealloc {
    // 釋放session,避免內存泄漏
    [self.session invalidateAndCancel];
}

- (IBAction)start:(id)sender {
    NSLog(@"開始下載");

    // 第1步:創建一個HTTP請求——即NSURLRequest
    NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init];
    // 設置HTTP請求的方法
    request.HTTPMethod = @"GET";
    // 設置HTTP請求的URL
    request.URL = [NSURL URLWithString:[@"https://view.2amok.com/2019913/9660e7b5ef2a6fe4ea2328d91069f9eb.mp4" stringByAddingPercentEscapesUsingEncoding:(NSUTF8StringEncoding)]];

    /*
     第2步:創建一個session

     configuration: 配置信息
     delegate: 代理
     delegateQueue: 設置代理方法在哪個線程裏調用,mainQueue就代表在主線程裏調用
     */
    self.session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:[NSOperationQueue mainQueue]];

    // 第3步:創建一個downloadTask
    self.downloadTask = [self.session downloadTaskWithRequest:request];

    /*
     第4步:執行downloadTask
     執行downloadTask後就發起了我們剛纔創建的HTTP請求,並且NSURLSession會默認把這個HTTP請求放到子線程裏去做
     */
    [self.downloadTask resume];
}

- (IBAction)cancel:(id)sender {
    NSLog(@"暫停下載");

    [self.downloadTask cancelByProducingResumeData:^(NSData * _Nullable resumeData) {
        // 暫停下載時會觸發這個回調,記錄暫停下載時的一些位置信息
        self.resumeData = resumeData;
    }];
}

- (IBAction)resume:(id)sender {
    NSLog(@"繼續下載");

    if (self.resumeData != nil) { // 說明之前暫停過下載
        self.downloadTask = [self.session downloadTaskWithResumeData:self.resumeData];
    }
    [self.downloadTask resume];
}


// 第5步:HTTP請求結束的回調(注意delegate回調是在主線程,我們不需要再主動回到主線程)
#pragma mark - NSURLSessionDownloadDelegate

/// 數據下載中的回調
///
/// bytesWritten: 本次下載的數據大小
/// totalBytesWritten: 已經下載的數據大小
/// totalBytesExpectedToWrite: 數據的總大小
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite {
    NSLog(@"下載進度---%f", 1.0 * totalBytesWritten / totalBytesExpectedToWrite);
}

/// 數據下載完成的回調
///
/// location: 文件的臨時存儲路徑
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location {
    // 這裏我們把文件存儲到Library/Cache目錄下
    NSString *cachePath = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"test.mp4"];
    [[NSFileManager defaultManager] moveItemAtURL:location toURL:[NSURL fileURLWithPath:cachePath] error:nil];
    NSLog(@"文件路徑---%@", cachePath);
}

/// 數據下載結束的回調
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
    if (error != nil) {
        NSLog(@"請求失敗了");
    } else {
        NSLog(@"請求成功了");
    }
}

@end
4.2 通過dataTask實現
/*
 關於下載我們需要注意五個問題:
 ✅1、下載進度問題
 ✅2、內存暴漲問題
 ✅3、斷點下載問題
 ✅4、離線斷點下載問題
 ❌5、多線程斷點下載問題:這裏就不實現了,可參考上面的思路
 */

#import "DownloadViewController1.h"

// 因爲是普通的GET、POST請求,只需要遵循NSURLSessionDataDelegate協議就可以了
@interface DownloadViewController1 () <NSURLSessionDataDelegate>

/// request
@property (nonatomic, strong) NSMutableURLRequest *request;

/// session
@property (nonatomic, strong) NSURLSession *session;

/// dataTask
@property (nonatomic, strong) NSURLSessionDataTask *dataTask;

/// 數據的總大小
@property (nonatomic, assign) NSInteger totalSize;

/// 已經下載的數據大小
@property (nonatomic, assign) NSInteger currentSize;

/// 要把文件下載到的路徑
@property (nonatomic, copy) NSString *cachePath;

/// fileHandle
@property (nonatomic, strong) NSFileHandle *fileHandle;

@end

@implementation DownloadViewController1

- (void)viewDidLoad {
    [super viewDidLoad];
    self.navigationItem.title = @"通過dataTask實現";
}

- (void)dealloc {
    // 釋放session,避免內存泄漏
    [self.session invalidateAndCancel];
}

- (IBAction)start:(id)sender {
    NSLog(@"開始下載");
    
    /*
     第4步:執行dataTask
     執行dataTask後就發起了我們剛纔創建的HTTP請求,並且NSURLSession會默認把這個HTTP請求放到子線程裏去做
     */
    [self.dataTask resume];
}

- (IBAction)cancel:(id)sender {
    NSLog(@"暫停下載");
    
    [self.dataTask cancel];
    self.request = nil;
    self.session = nil;
    self.dataTask = nil;
}

- (IBAction)resume:(id)sender {
    NSLog(@"繼續下載");
    
    [self.dataTask resume];
}


// 第5步:HTTP請求結束的回調(注意delegate回調是在主線程,我們不需要再主動回到主線程)
#pragma mark - NSURLSessionDataDelegate

/// 接收到響應頭的回調
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler {
    NSLog(@"111---%@", [NSThread currentThread]);
    
    /*
     NSURLSessionResponseCancel = 0, // 通過delegate來做HTTP請求結束的回調默認情況下接收到響應頭後會取消後續的請求,即內容會調用[dataTask cancel]
     NSURLSessionResponseAllow = 1, // 允許後續的請求
     NSURLSessionResponseBecomeDownload = 2, // 把當前請求轉變成一個下載請求
     NSURLSessionResponseBecomeStream = 3, // 把當前請求轉變成一個下載請求(iOS9之後可用)
     */
    completionHandler(NSURLSessionResponseAllow);
    
    // 獲取到數據的總大小(注意是獲得本次請求數據的總大小,如果我們使用了斷點下載,那麼這個值很可能就是某一段數據的大小,而非整個數據的大小,所以得加上之前的數據大小)
    self.totalSize = response.expectedContentLength + self.currentSize;
    
    if (self.currentSize <= 0) { // 如果之前沒創建過再創建
        [[NSFileManager defaultManager] createFileAtPath:self.cachePath contents:nil attributes:nil];
    }
    
    // 創建fileHandle,以便往文件裏寫數據
    self.fileHandle = [NSFileHandle fileHandleForWritingAtPath:self.cachePath];
    // 先把fileHandle的指針移動到末尾
    [self.fileHandle seekToEndOfFile];
}

/// 接收到響應體的回調
///
/// data: 指得是本次接收到的數據
/// ——如果數據量很小的話,這個方法只觸發一次就接收完了,然後會觸發請求結束的回調
/// ——如果數據量很大的話,這個方法會觸發多次,一段一段地接收,全部接收完後會觸發請求結束的回調
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
    NSLog(@"222---%@", [NSThread currentThread]);
        
    // 獲取到已經下載的數據大小,並計算下載進度
    self.currentSize += data.length;
    
    // 然後再通過fileHandle往文件裏寫數據
    [self.fileHandle writeData:data];
    
    NSLog(@"下載進度---%f", 1.0 * self.currentSize / self.totalSize);
}

/// 請求結束的回調
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
    NSLog(@"333---%@", [NSThread currentThread]);
    
    if (error != nil) {
        NSLog(@"請求失敗了");
    } else {
        NSLog(@"請求成功了");
    }
    
    // 釋放fileHandle
    [self.fileHandle closeFile];
    self.fileHandle = nil;
    
    // 釋放dataTask
    if (_dataTask != nil) {
        self.currentSize = 0;
        self.request = nil;
        self.session = nil;
        self.dataTask = nil;
    }
}


#pragma mark - setter, getter

// 第1步:創建一個HTTP請求——即NSURLRequest
- (NSMutableURLRequest *)request {
    if (_request == nil) {
        _request = [[NSMutableURLRequest alloc] init];
        // 設置HTTP請求的方法
        _request.HTTPMethod = @"GET";
        // 設置HTTP請求的URL
        _request.URL = [NSURL URLWithString:[@"https://view.2amok.com/2019913/9660e7b5ef2a6fe4ea2328d91069f9eb.mp4" stringByAddingPercentEscapesUsingEncoding:(NSUTF8StringEncoding)]];
        // 先獲取一下之前已經下載到磁盤裏數據的大小
        NSDictionary *fileInfoDictionary = [[NSFileManager defaultManager] attributesOfItemAtPath:self.cachePath error:nil];
        NSLog(@"fileInfoDictionary---%@", fileInfoDictionary);
        self.currentSize = [fileInfoDictionary[@"NSFileSize"] integerValue];
        // 設置HTTP請求的請求頭,告訴服務器要獲取哪一部分的數據
        [_request setValue:[NSString stringWithFormat:@"bytes=%ld-", self.currentSize] forHTTPHeaderField:@"Range"];
    }
    return _request;
}

/*
 第2步:創建一個session

 configuration: 配置信息
 delegate: 代理
 delegateQueue: 設置代理方法在哪個線程裏調用,mainQueue就代表在主線程裏調用
 */
- (NSURLSession *)session {
    if (_session == nil) {
        self.session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:[NSOperationQueue mainQueue]];
    }
    return _session;
}

// 第3步:創建一個dataTask
- (NSURLSessionDataTask *)dataTask {
    if (_dataTask == nil) {
        _dataTask = [self.session dataTaskWithRequest:self.request];
    }
    return _dataTask;
}

// 這裏我們把文件存儲到Library/Cache目錄下
- (NSString *)cachePath {
    if (_cachePath == nil) {
        _cachePath = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"test.mp4"];
        NSLog(@"文件路徑---%@", _cachePath);
    }
    return _cachePath;
}

@end

5、支持HTTPS

  • 1️⃣當我們用NSURLSession發起一個HTTPS請求後,其實服務器並不會立馬把真正的數據發給我們,而是先發一個證書給我們;(這部分知識可以參考下應用層:HTTPS
  • 2️⃣當我們收到這個證書後,就會觸發NSURLSession的一個收到證書的回調,我們要在這個回調裏來決定如何對待證書,所以使用NSURLSession支持HTTPS很簡單,我們只要實現它的didReceiveChallenge這個代理方法就可以了。(當然我們訪問一些大型網站時——比如蘋果官網,即便是HTTPS請求也不會觸發這個回調,這是因爲這些大型網站的證書已經被內置安裝在系統裏了)
#import "HTTPSViewController.h"

@interface HTTPSViewController () <NSURLSessionDataDelegate>

@end

@implementation HTTPSViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.navigationItem.title = @"支持HTTPS";
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init];
    request.HTTPMethod = @"GET";
    request.URL = [NSURL URLWithString:[@"https://www.12306.cn/index/" stringByAddingPercentEscapesUsingEncoding:(NSUTF8StringEncoding)]];
        
    NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:[NSOperationQueue mainQueue]];
    NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        dispatch_async(dispatch_get_main_queue(), ^{
            if (error != nil) {
                NSLog(@"請求失敗了---%@", error);
            } else {
                NSString *jsonString = [[NSString alloc] initWithData:data encoding:(NSUTF8StringEncoding)];
                NSLog(@"請求成功了---%@", jsonString);
            }
        });
    }];
    
    [dataTask resume];
}


#pragma mark - NSURLSessionDataDelegate

/// 收到證書的回調
- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
 completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * _Nullable credential))completionHandler {
    
    if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
        /*
         第一個參數:如何對待證書
         NSURLSessionAuthChallengeUseCredential = 0, // 安裝並使用該證書
         NSURLSessionAuthChallengePerformDefaultHandling = 1, // 默認,忽略該證書
         NSURLSessionAuthChallengeCancelAuthenticationChallenge = 2, // 忽略該證書,並取消請求
         NSURLSessionAuthChallengeRejectProtectionSpace = 3, // 拒絕該證書
        
         第二個參數:身份驗證
         */
        completionHandler(NSURLSessionAuthChallengeUseCredential, [[NSURLCredential alloc] initWithTrust:challenge.protectionSpace.serverTrust]);
    }
}

@end


二、AFNetworking源碼分析


其實AFN的三大核心模塊就是我們上面所實現的序列化器模塊(創建NSURLRequest + 解析NSURLResponse)、session和task管理模塊、支持HTTPS模塊,當然除此之外它還提供了非常方便易用的網絡狀態監聽模塊和UIKit分類模塊(類似於SDWebImage的UIKit分類模塊)。

1、序列化器模塊

1.1 請求序列化器

請求序列化器主要用來創建一個HTTP請求——即NSURLRequest,這其中就包含了設置HTTP請求的方法、設置HTTP請求的URL + URL編碼、設置HTTP請求的請求頭、設置HTTP請求的Body + Body編碼等內容。AFN提供了多種請求序列化器,不過常用的就兩種AFHTTPRequestSerializerAFJSONRequestSerializer

  • AFHTTPRequestSerializer:默認,主要用來創建一個GET請求 || 採用表單提交的POST請求 || 文件上傳請求。
/// 單例
+ (instancetype)serializer {
    return [[self alloc] init];
}

/// 初始化方法
- (instancetype)init {
    self = [super init];
    if (!self) {
        return nil;
    }

    // 設置默認的URL編碼方式和Body編碼方式爲UTF-8編碼
    self.stringEncoding = NSUTF8StringEncoding;

    // 存儲在這個Set裏的請求——即GET請求、HEAD請求、DELETE請求這三種請求的入參會被編碼後拼接到URL後面
    self.HTTPMethodsEncodingParametersInURI = [NSSet setWithObjects:@"GET", @"HEAD", @"DELETE", nil];

    return self;
}
/// 創建一個GET請求 || 採用表單提交的POST請求
///
/// method: 請求方法,@"GET"、@"POST"等
/// URLString: 請求的URL
/// parameters: 入參
///
/// return 一個HTTP請求——即NSURLRequest
- (NSMutableURLRequest *)requestWithMethod:(NSString *)method
                                 URLString:(NSString *)URLString
                                parameters:(id)parameters {

    // 創建一個HTTP請求——即NSURLRequest
    NSMutableURLRequest *mutableRequest = [[NSMutableURLRequest alloc] init];
    // 設置HTTP請求的方法
    mutableRequest.HTTPMethod = method;
    // 設置HTTP請求的URL
    mutableRequest.url = [NSURL URLWithString:URLString];

    // URL編碼 + Body編碼 + Body格式組織
    mutableRequest = [[self requestBySerializingRequest:mutableRequest withParameters:parameters] mutableCopy];

    return mutableRequest;
}

/// URL編碼 + Body編碼 + Body格式組織
- (NSURLRequest *)requestBySerializingRequest:(NSURLRequest *)request
                               withParameters:(id)parameters {
    
    NSMutableURLRequest *mutableRequest = [request mutableCopy];
    
    /**
     實際開發中,我們的入參主要是個字典,因此這裏主要考慮字典。
     把入參字典轉換成特定格式的字符串,並採用UTF-8編碼對轉換後的字符串進行編碼
     
     比如入參字典是這樣的:
     @{
     @"username": @"123",
     @"password": @"456",
     }
     
     那麼query最終會是這樣的字符串:
     @"username=123&password=456"
     */
    NSString *query = AFQueryStringFromParameters(parameters);;
    
    if ([self.HTTPMethodsEncodingParametersInURI containsObject:[[request HTTPMethod] uppercaseString]]) {
        // 如果是GET請求(我們主要分析GET請求和POST請求),那就把入參直接拼接到URL後面
        if (query) {
            mutableRequest.URL = [NSURL URLWithString:[[mutableRequest.URL absoluteString] stringByAppendingFormat:mutableRequest.URL.query ? @"&%@" : @"?%@", query]];
        }
    } else {
        // 如果是POST請求(我們主要分析GET請求和POST請求)
        // 那就設置請求頭的@"Content-Type"字段爲@"application/x-www-form-urlencoded",代表默認採用表單提交
        if (![mutableRequest valueForHTTPHeaderField:@"Content-Type"]) {
            [mutableRequest setValue:@"application/x-www-form-urlencoded" forHTTPHeaderField:@"Content-Type"];
        }
        // 把入參放到POST請求的請求體裏,表單提交請求體的格式正好就是@"username=123&password=456"這樣的格式
        [mutableRequest setHTTPBody:[query dataUsingEncoding:self.stringEncoding]];
    }
    
    return mutableRequest;
}

回顧一下文件上傳請求體的格式:

--分隔符
回車換行
Content-Disposition: form-data; name="file"; filename="123.jpg"
回車換行
Content-Type: image/jpg
回車換行
回車換行
數據
回車換行
--分隔符--

/// 創建一個文件上傳請求
///
/// 文件上傳請求本質來說就是一個POST請求,只不過:
/// ——這個POST請求的請求頭裏必須設置Content-Type字段爲multipart/form-data; boundary=xxx
/// ——請求體也必須得嚴格按照指定的格式去拼接數據
- (NSMutableURLRequest *)multipartFormRequestWithURLString:(NSString *)URLString
                                                parameters:(NSDictionary *)parameters
                                 constructingBodyWithBlock:(void (^)(id <AFMultipartFormData> formData))block {
    
    NSMutableURLRequest *mutableRequest = [self requestWithMethod:@"POST" URLString:URLString parameters:nil error:nil];

    // 創建一個formData
    __block AFStreamingMultipartFormData *formData = [[AFStreamingMultipartFormData alloc] initWithURLRequest:mutableRequest stringEncoding:NSUTF8StringEncoding];
    // 把formData回調出去,好讓外界往上拼接數據
    if (block) {
        block(formData);
    }

    // 把設置好請求頭並且組織好請求體格式的request給返回
    return [formData requestByFinalizingMultipartFormData];
}

// 隨機生成的Boundary
#define kBoundary @"----WebKitFormBoundaryjv0UfA04ED44AhWx"
// 回車換行
#define kNewLine @"\r\n"

/// 我們會調用AFMultipartFormData的這個方法往formData上拼接數據,下面的self就等於上面創建一個上傳任務請求時block回調出來的formData
///
/// data: 要上傳的數據
/// name: 服務器規定的參數,一般填file,和服務端商量好
/// filename: 文件保存到服務器上面的名稱
/// mimeType: 文件的類型,大類型/小類型
///
/// 僞代碼,但原理就是這樣
- (void)appendPartWithFileData:(NSData *)data
                          name:(NSString *)name
                      fileName:(NSString *)fileName
                      mimeType:(NSString *)mimeType {
    
    [self appendData:[[NSString stringWithFormat:@"--%@", kBoundary] dataUsingEncoding:NSUTF8StringEncoding]];
    
    [self appendData:kNewLine];
    
    [bodyData appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\"; filename=\"%@\"", name, fileName] dataUsingEncoding:NSUTF8StringEncoding]];
    
    [bodyData appendData:kNewLine];
        
    [bodyData appendData:[[NSString stringWithFormat:@"Content-Type: %@", mimeType] dataUsingEncoding:NSUTF8StringEncoding]];
    
    [bodyData appendData:kNewLine];
    
    [bodyData appendData:kNewLine];
    
    [bodyData appendData:data];
    
    [bodyData appendData:kNewLine];
    
    [bodyData appendData:[[NSString stringWithFormat:@"--%@--", kBoundary] dataUsingEncoding:NSUTF8StringEncoding]];
}
  • AFJSONRequestSerializer:主要用來創建一個採用JSON提交的POST請求。
// 單例
+ (instancetype)serializer {
    return [[self alloc] init];
}

/// 初始化方法
- (instancetype)init {
    // 因爲AFJSONRequestSerializer繼承自AFHTTPRequestSerializer
    // 所以這裏會調用父類AFHTTPRequestSerializer的init方法,見上面
    self = [super init];
    if (!self) {
        return nil;
    }
    return self;
}
/// 創建採用JSON提交的POST請求
///
/// method: 請求方法,@"GET"、@"POST"等
/// URLString: 請求的URL
/// parameters: 入參
///
/// return 一個HTTP請求——即NSURLRequest
- (NSMutableURLRequest *)requestWithMethod:(NSString *)method
                                 URLString:(NSString *)URLString
                                parameters:(id)parameters {

    // 直接調用父類AFHTTPRequestSerializer的同名方法,見上面
    return [super requestWithMethod:method URLString:URLString parameters:parameters];
}

/// Body編碼 + Body格式組織
- (NSURLRequest *)requestBySerializingRequest:(NSURLRequest *)request
                               withParameters:(id)parameters {
    
    NSMutableURLRequest *mutableRequest = [request mutableCopy];
    
    if (parameters) {
        // 設置請求頭的@"Content-Type"字段爲@"application/json",代表採用JSON提交
        if (![mutableRequest valueForHTTPHeaderField:@"Content-Type"]) {
            [mutableRequest setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
        }
        
        /**
         把入參字典轉換成特定格式的字符串,並採用UTF-8編碼對轉換後的字符串進行編碼
         
         比如入參字典是這樣的:
         @{
         @"username": @"123",
         @"password": @"456",
         }
         
         那麼httpBody最終會是這樣的字符串:
         @"{\"username\": \"123\", \"password\": \"456\"}"
         */
        NSData *httpBody = [NSJSONSerialization dataWithJSONObject:parameters options:0 error:nil];
        
        // 把入參放到POST請求的請求體裏
        [mutableRequest setHTTPBody:httpBody];
    }
    
    return mutableRequest;
}
1.2 響應序列化器

響應序列化器主要用來決定採用什麼方式解析響應體。AFN提供了多種響應序列化器,不過常用的就兩種AFHTTPResponseSerializerAFJSONResponseSerializer

  • AFHTTPResponseSerializer:不對響應體做解析,直接把服務器返回的二進制數據暴露給我們開發者(NSData )。所以如果我們想以二進制的格式接收響應體就用這個,這樣就可以拿着二進制數據做一些自定義的處理。
/// 單例
+ (instancetype)serializer {
    return [[self alloc] init];
}

/// 初始化方法
- (instancetype)init {
    self = [super init];
    if (!self) {
        return nil;
    }

    // 設置對響應體的解碼方式,默認爲UTF-8
    self.stringEncoding = NSUTF8StringEncoding;
    // 設置可接收的MIMEType,默認爲nil
    self.acceptableContentTypes = nil;
    // 設置可接收的狀態碼,默認爲200~299
    self.acceptableStatusCodes = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(200, 100)];

    return self;
}

/// 驗證響應的有效性
- (BOOL)validateResponse:(NSHTTPURLResponse *)response
                    data:(NSData *)data
                   error:(NSError * __autoreleasing *)error {
    
    // 1、默認響應是有效的
    BOOL responseIsValid = YES;
    NSError *validationError = nil;
    
    // 2、如果響應存在 && 響應是NSHTTPURLResponse
    if (response && [response isKindOfClass:[NSHTTPURLResponse class]]) {
        
        // 2.1 如果self.acceptableContentTypes != nil && 不包含響應的MIMEType,就判定爲響應無效從而報錯(這就是爲什麼我們在開發中會遇到這個報錯的原因,設置一下acceptableContentTypes就可以了)
        if (self.acceptableContentTypes && ![self.acceptableContentTypes containsObject:[response MIMEType]]) {
            if ([data length] > 0 && [response URL]) {
                // 報@"Request failed: unacceptable content-type: %@"這麼個錯
                NSMutableDictionary *mutableUserInfo = [@{
                    NSLocalizedDescriptionKey: [NSString stringWithFormat:NSLocalizedStringFromTable(@"Request failed: unacceptable content-type: %@", @"AFNetworking", nil), [response MIMEType]],
                    NSURLErrorFailingURLErrorKey:[response URL],
                    AFNetworkingOperationFailingURLResponseErrorKey: response,
                } mutableCopy];
                if (data) {
                    mutableUserInfo[AFNetworkingOperationFailingURLResponseDataErrorKey] = data;
                }
                
                validationError = AFErrorWithUnderlyingError([NSError errorWithDomain:AFURLResponseSerializationErrorDomain code:NSURLErrorCannotDecodeContentData userInfo:mutableUserInfo], validationError);
            }
            
            // 判定爲響應無效
            responseIsValid = NO;
        }
        
        // 2.2 同上
        if (self.acceptableStatusCodes && ![self.acceptableStatusCodes containsIndex:(NSUInteger)response.statusCode] && [response URL]) {
            NSMutableDictionary *mutableUserInfo = [@{
                NSLocalizedDescriptionKey: [NSString stringWithFormat:NSLocalizedStringFromTable(@"Request failed: %@ (%ld)", @"AFNetworking", nil), [NSHTTPURLResponse localizedStringForStatusCode:response.statusCode], (long)response.statusCode],
                NSURLErrorFailingURLErrorKey:[response URL],
                AFNetworkingOperationFailingURLResponseErrorKey: response,
            } mutableCopy];
            
            if (data) {
                mutableUserInfo[AFNetworkingOperationFailingURLResponseDataErrorKey] = data;
            }
            
            validationError = AFErrorWithUnderlyingError([NSError errorWithDomain:AFURLResponseSerializationErrorDomain code:NSURLErrorBadServerResponse userInfo:mutableUserInfo], validationError);
            
            responseIsValid = NO;
        }
    }
    
    if (error && !responseIsValid) {
        *error = validationError;
    }
    
    return responseIsValid;
}

/// data: 響應體,服務器返回的二進制數據
- (id)responseObjectForResponse:(NSURLResponse *)response
                           data:(NSData *)data
                          error:(NSError *__autoreleasing *)error {

    // 驗證響應的有效性
    [self validateResponse:(NSHTTPURLResponse *)response data:data error:error];

    // 不做解析,直接返回
    return data;
}
  • AFJSONResponseSerializer:默認,以JSON格式解析響應體,也就是說AFN會把服務器返回的二進制數據當成一個JSON字符串來對待,把它解析成相應的OC字典或OC數組暴露給我們開發者(NSData -> NSDictionary、NSArray)。所以如果我們想以OC字典或OC數組的格式接收響應體就用這個,這樣我們就可以直接拿着響應體使用了,就不用自己做(NSData -> NSDictionary、NSArray)的解析了。
/// 單例
+ (instancetype)serializer {
    return [[self alloc] init];
}

/// 初始化方法
- (instancetype)init {
    // 因爲AFJSONResponseSerializer繼承自AFHTTPResponseSerializer
    // 所以這裏會調用父類AFHTTPResponseSerializer的init方法,見上面
    self = [super init];
    if (!self) {
        return nil;
    }

    // 設置可接收的MIMEType,包括[@"application/json", @"text/json", @"text/javascript"]
    self.acceptableContentTypes = [NSSet setWithObjects:@"application/json", @"text/json", @"text/javascript", nil];

    return self;
}

/// 驗證響應的有效性
- (BOOL)validateResponse:(NSHTTPURLResponse *)response
                    data:(NSData *)data
                   error:(NSError * __autoreleasing *)error {

    // 直接調用父類AFHTTPResponseSerializer的同名方法,見上面
    return [super validateResponse:response data:data error:error];
}

/// data: 響應體,服務器返回的二進制數據
- (id)responseObjectForResponse:(NSURLResponse *)response
                           data:(NSData *)data
                          error:(NSError *__autoreleasing *)error {
    
    // 驗證響應的有效性,如果驗證失敗,不做解析,直接返回
    if (![self validateResponse:(NSHTTPURLResponse *)response data:data error:error]) {
        return nil;
    }
    
    // 對響應體的解碼方式爲UTF-8
    NSStringEncoding stringEncoding = self.stringEncoding;
    
    id responseObject = nil;
    NSError *serializationError = nil;
    
    // 把服務器返回的二進制數據當成JSON字符串來對待
    // 把它解析成相應的OC字典或OC數組暴露給我們開發者(NSData -> NSDictionary、NSArray)
     if ([data length] > 0) {
          responseObject = [NSJSONSerialization JSONObjectWithData:data options:self.readingOptions error:&serializationError];
      } else {
          return nil;
      }
    
    // 刪除響應體中的NULL
    if (self.removesKeysWithNullValues && responseObject) {
        responseObject = AFJSONObjectByRemovingKeysWithNullValues(responseObject, self.readingOptions);
    }
    
    if (error) {
        *error = AFErrorWithUnderlyingError(serializationError, *error);
    }
    
    return responseObject;
}

2、session和task管理模塊

我們都知道AFN 3.0是基於NSURLSession實現的,AFURLSessionManager和AFHTTPSessionManager這兩個類就創建並管理着session和不同類型的task。其中AFHTTPSessionManager繼承自AFURLSessionManager,是專門爲HTTP請求設計的,它內部封裝了session和task的調用過程,給我們提供了一堆非常方便易用的API,實際開發中我們就是用它來發起一個來自序列化器模塊創建好的網絡請求,接下來我們就看看AFHTTPSessionManager的內部實現。

  • 創建
/// 單例
+ (instancetype)manager {
    return [[[self class] alloc] initWithBaseURL:nil];
}

/// 初始化方法
- (instancetype)init {
    self = [super init];
    if (!self) {
        return nil;
    }
    
    // 可見AFN默認的請求序列化器爲AFHTTPRequestSerializer
    self.requestSerializer = [AFHTTPRequestSerializer serializer];
    // 可見AFN默認的響應序列化器爲AFJSONResponseSerializer
    self.responseSerializer = [AFJSONResponseSerializer serializer];
    
    return self;
}
  • GET請求

可見AFN的GET請求就是通過NSURLSession的dataTask發起的,然後把各種delegate回調封裝成了block暴露給我們使用;至於session的創建代碼比較深,這裏就不貼了。

// GET請求不帶進度
- (NSURLSessionDataTask *)GET:(NSString *)URLString
                   parameters:(id)parameters
                      success:(void (^)(NSURLSessionDataTask *task, id responseObject))success
                      failure:(void (^)(NSURLSessionDataTask *task, NSError *error))failure {

    return [self GET:URLString parameters:parameters progress:nil success:success failure:failure];
}

// GET請求帶進度
- (NSURLSessionDataTask *)GET:(NSString *)URLString
                   parameters:(id)parameters
                     progress:(void (^)(NSProgress * _Nonnull))downloadProgress
                      success:(void (^)(NSURLSessionDataTask * _Nonnull, id _Nullable))success
                      failure:(void (^)(NSURLSessionDataTask * _Nullable, NSError * _Nonnull))failure {

    NSURLSessionDataTask *dataTask = [self dataTaskWithHTTPMethod:@"GET"
                                                        URLString:URLString
                                                       parameters:parameters
                                                   uploadProgress:nil
                                                 downloadProgress:downloadProgress
                                                          success:success
                                                          failure:failure];

    [dataTask resume];

    return dataTask;
}
  • POST請求

可見AFN的POST請求也是通過NSURLSession的dataTask發起的,然後把各種delegate回調封裝成了block暴露給我們使用;至於session的創建代碼比較深,這裏就不貼了。

// POST請求不帶進度
- (NSURLSessionDataTask *)POST:(NSString *)URLString
                    parameters:(id)parameters
                       success:(void (^)(NSURLSessionDataTask *task, id responseObject))success
                       failure:(void (^)(NSURLSessionDataTask *task, NSError *error))failure {

    return [self POST:URLString parameters:parameters progress:nil success:success failure:failure];
}

// POST請求帶進度
- (NSURLSessionDataTask *)POST:(NSString *)URLString
                    parameters:(id)parameters
                      progress:(void (^)(NSProgress * _Nonnull))uploadProgress
                       success:(void (^)(NSURLSessionDataTask * _Nonnull, id _Nullable))success
                       failure:(void (^)(NSURLSessionDataTask * _Nullable, NSError * _Nonnull))failure {
    
    NSURLSessionDataTask *dataTask = [self dataTaskWithHTTPMethod:@"POST"
                                                        URLString:URLString
                                                       parameters:parameters
                                                   uploadProgress:uploadProgress
                                                 downloadProgress:nil
                                                          success:success
                                                          failure:failure];

    [dataTask resume];

    return dataTask;
}
  • 文件上傳請求

可見AFN的文件上傳請求就是通過NSURLSession的uploadTask發起的,然後把各種delegate回調封裝成了block暴露給我們使用;至於session的創建代碼比較深,這裏就不貼了。

跟我們自己寫的不同的地方在於,我們是把請求體放進了uploadTask裏,而AFN是暴露了一個block出來,讓我們把請求體拼接到formData上,然後它把請求體通過流的方式放進了request裏。

// 文件上傳請求不帶進度
- (NSURLSessionDataTask *)POST:(NSString *)URLString
                    parameters:(nullable id)parameters
     constructingBodyWithBlock:(nullable void (^)(id<AFMultipartFormData> _Nonnull))block
                       success:(nullable void (^)(NSURLSessionDataTask * _Nonnull, id _Nullable))success
                       failure:(nullable void (^)(NSURLSessionDataTask * _Nullable, NSError * _Nonnull))failure {
    
    return [self POST:URLString parameters:parameters constructingBodyWithBlock:block progress:nil success:success failure:failure];
}

// 文件上傳請求帶進度
- (NSURLSessionDataTask *)POST:(NSString *)URLString
                    parameters:(id)parameters
     constructingBodyWithBlock:(void (^)(id <AFMultipartFormData> formData))block
                      progress:(nullable void (^)(NSProgress * _Nonnull))uploadProgress
                       success:(void (^)(NSURLSessionDataTask *task, id responseObject))success
                       failure:(void (^)(NSURLSessionDataTask *task, NSError *error))failure {
    
    NSError *serializationError = nil;
    NSMutableURLRequest *request = [self.requestSerializer multipartFormRequestWithMethod:@"POST" URLString:[[NSURL URLWithString:URLString relativeToURL:self.baseURL] absoluteString] parameters:parameters constructingBodyWithBlock:block error:&serializationError];
    if (serializationError) {
        if (failure) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wgnu"
            dispatch_async(self.completionQueue ?: dispatch_get_main_queue(), ^{
                failure(nil, serializationError);
            });
#pragma clang diagnostic pop
        }

        return nil;
    }

    __block NSURLSessionDataTask *task = [self uploadTaskWithStreamedRequest:request progress:uploadProgress completionHandler:^(NSURLResponse * __unused response, id responseObject, NSError *error) {
        if (error) {
            if (failure) {
                failure(task, error);
            }
        } else {
            if (success) {
                success(task, responseObject);
            }
        }
    }];

    [task resume];

    return task;
}
  • 文件下載請求

可見AFN的文件下載請求就是通過NSURLSession的downloadTask發起的,然後把各種delegate回調封裝成了block暴露給我們使用;至於session的創建代碼比較深,這裏就不貼了。

使用AFN做文件下載,我們得自己創建NSURLRequest,再調用它提供的API。同時針對下載需要注意的幾個問題,AFN提供了下載進度的回調,AFN提供了邊下載邊存儲到磁盤裏、我們只需要告訴它下載到哪裏即可,AFN支持斷點下載但不支持離線斷點下載。

// 不搞斷點下載就用這個
- (NSURLSessionDownloadTask *)downloadTaskWithRequest:(NSURLRequest *)request
                                             progress:(void (^)(NSProgress *downloadProgress)) downloadProgressBlock
                                          destination:(NSURL * (^)(NSURL *targetPath, NSURLResponse *response))destination
                                    completionHandler:(void (^)(NSURLResponse *response, NSURL *filePath, NSError *error))completionHandler {
    
    __block NSURLSessionDownloadTask *downloadTask = nil;
    url_session_manager_create_task_safely(^{
        downloadTask = [self.session downloadTaskWithRequest:request];
    });

    [self addDelegateForDownloadTask:downloadTask progress:downloadProgressBlock destination:destination completionHandler:completionHandler];

    return downloadTask;
}

// 想搞斷點下載就用這個
- (NSURLSessionDownloadTask *)downloadTaskWithResumeData:(NSData *)resumeData
                                                progress:(void (^)(NSProgress *downloadProgress)) downloadProgressBlock
                                             destination:(NSURL * (^)(NSURL *targetPath, NSURLResponse *response))destination
                                       completionHandler:(void (^)(NSURLResponse *response, NSURL *filePath, NSError *error))completionHandler {
    
    __block NSURLSessionDownloadTask *downloadTask = nil;
    url_session_manager_create_task_safely(^{
        downloadTask = [self.session downloadTaskWithResumeData:resumeData];
    });

    [self addDelegateForDownloadTask:downloadTask progress:downloadProgressBlock destination:destination completionHandler:completionHandler];

    return downloadTask;
}

3、支持HTTPS模塊

/// 僞代碼
- (void)URLSession:(NSURLSession *)session
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
 completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler {
    
    NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
    __block NSURLCredential *credential = nil;

    if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
        if ([self.securityPolicy evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:challenge.protectionSpace.host]) {
            // 如果證書有效,則安裝並使用該證書
            disposition = NSURLSessionAuthChallengeUseCredential;
        } else {
            // 如果證書無效,則拒絕該證書
            disposition = NSURLSessionAuthChallengeRejectProtectionSpace;
        }
    }

    if (completionHandler) {
        completionHandler(disposition, credential);
    }
}

可見AFN支持HTTPS也是通過NSURLSession收到證書的回調來實現的,只不過它比我們自己實現的多了一步驗證證書有效性的操作(AFSecurityPolicy這個類就是用來驗證證書有效性的),接下來我們就看看它是怎麼驗證的。

/**
 是否允許無效證書(即自建證書,大多數網站的證書都是權威機構頒發的,但是12306以前的證書就是自建的)或過期證書
 默認爲NO
 */
@property (nonatomic, assign) BOOL allowInvalidCertificates;

/**
 是否驗證證書中的域名
 默認爲YES
 
 假如證書的域名與你請求的域名不一致,需把該項設置爲NO;如設成NO的話,即服務器使用其他可信任機構頒發的證書,也可以建立連接,這個非常危險,建議保持爲YES
 
 置爲NO,主要用於這種情況:客戶端請求的是子域名,而證書上的是另外一個域名。因爲SSL證書上的域名是獨立的,假如證書上註冊的域名是www.google.com,那麼mail.google.com是無法驗證通過的;當然,有錢可以註冊通配符的域名*.google.com,但這個還是比較貴的
 
 如置爲NO,建議自己添加對應域名的校驗邏輯
 */
@property (nonatomic, assign) BOOL validatesDomainName;

// 對服務器發過來的證書採取什麼驗證策略
typedef NS_ENUM(NSUInteger, AFSSLPinningMode) {
    AFSSLPinningModeNone, // 默認,不驗證證書
    AFSSLPinningModePublicKey, // 驗證證書中的公鑰,通過就代表證書有效,不通過就代表證書無效
    AFSSLPinningModeCertificate, // 把服務器發過來的證書和本地證書做完全對比,通過就代表證書有效,不通過就代表證書無效
};
- (BOOL)evaluateServerTrust:(SecTrustRef)serverTrust
                  forDomain:(NSString *)domain {
    
    if (domain && self.allowInvalidCertificates && self.validatesDomainName && (self.SSLPinningMode == AFSSLPinningModeNone || [self.pinnedCertificates count] == 0)) {
        return NO;
    }

    // 驗證證書中的域名處理
    NSMutableArray *policies = [NSMutableArray array];
    if (self.validatesDomainName) {
        [policies addObject:(__bridge_transfer id)SecPolicyCreateSSL(true, (__bridge CFStringRef)domain)];
    } else {
        [policies addObject:(__bridge_transfer id)SecPolicyCreateBasicX509()];
    }
    SecTrustSetPolicies(serverTrust, (__bridge CFArrayRef)policies);

    // 無效證書或自建證書處理
    if (self.SSLPinningMode == AFSSLPinningModeNone) {
        if (self.allowInvalidCertificates || AFServerTrustIsValid(serverTrust)) {
            return YES;
        } else {
            return NO;
        }
    } else if (!self.allowInvalidCertificates && !AFServerTrustIsValid(serverTrust)) {
        return NO;
    }

    // 對服務器發過來的證書採取什麼驗證策略處理
    switch (self.SSLPinningMode) {
        case AFSSLPinningModeCertificate: { // 把服務器發過來的證書和本地證書做完全對比
            /*
             self.pinnedCertificates:這個屬性保存着所有可用於跟服務器證書做完全對比的本地證書集合
            
             AFNetworking會自動搜索工程中所有.cer的證書文件,並把它們添加到這個屬性中
             所以我們在使用AFN的做HTTPS的時候就可能需要把服務器的證書轉換成cer文件放到項目裏,但很多時候我們都採用了默認驗證策略——不驗證證書,所以不放進來也沒事
             */
            NSMutableArray *pinnedCertificates = [NSMutableArray array];
            for (NSData *certificateData in self.pinnedCertificates) {
                [pinnedCertificates addObject:(__bridge_transfer id)SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certificateData)];
            }
            SecTrustSetAnchorCertificates(serverTrust, (__bridge CFArrayRef)pinnedCertificates);

            if (!AFServerTrustIsValid(serverTrust)) {
                return NO;
            }

            // obtain the chain after being validated, which *should* contain the pinned certificate in the last position (if it's the Root CA)
            NSArray *serverCertificates = AFCertificateTrustChainForServerTrust(serverTrust);
            
            for (NSData *trustChainCertificate in [serverCertificates reverseObjectEnumerator]) {
                // 判斷本地證書和服務器證書是否相同
                if ([self.pinnedCertificates containsObject:trustChainCertificate]) {
                    return YES;
                }
            }
            
            return NO;
        }
        case AFSSLPinningModePublicKey: { // 驗證證書中的公鑰
            NSUInteger trustedPublicKeyCount = 0;
            NSArray *publicKeys = AFPublicKeyTrustChainForServerTrust(serverTrust);

            // 如果包含這個公鑰就通過
            for (id trustChainPublicKey in publicKeys) {
                for (id pinnedPublicKey in self.pinnedPublicKeys) {
                    if (AFSecKeyIsEqualToKey((__bridge SecKeyRef)trustChainPublicKey, (__bridge SecKeyRef)pinnedPublicKey)) {
                        trustedPublicKeyCount += 1;
                    }
                }
            }
            return trustedPublicKeyCount > 0;
        }
            
        default:
            return NO;
    }
    
    return NO;
}

可見證書的驗證主要分三步:是否允許無效證書或自建證書、是否驗證證書中的域名和對服務器發過來的證書採取什麼驗證策略。


後語


  • JSON是什麼

JSON就是一個字符串,所以完整的叫法應該是JSON字符串,只不過我們通常的叫法是JSON。網絡傳輸過程中傳輸的就是JSON字符串、當然肯定是JSON字符串的二進制數據。

這個字符串有特定的格式,它的格式很像我們OC裏的NSDictionary、NSArray、NSString、NSNumber等,也很像Dart裏的Map、List、String、Number等,像歸像,但本質來說它還是一個字符串,我們經常通過相應的API來完成JSON字符串和NSDictionary、NSArray、Map、List等的相互轉化。

  • JSON解析是什麼

JSON解析就是指我們通過相應的API把服務器返回的JSON字符串二進制數據轉換成OC裏的NSDictionary、NSArray、NSString、NSNumber等。

  • JSON解析怎麼做

我們可以用一些三方框架,但是蘋果原生的 NSJSONSerialization性能最好,所以我們用蘋果原生的就可以了,它就可以完成JSON字符串二進制數據和NSDictionary、NSArray、NSString、NSNumber等的相互轉換。

/// data: JSON字符串的二進制數據
/// options:
/// ——NSJSONReadingMutableContainers,當服務器返回的JSON字符串是字典或數組格式時,就用這個參數
/// ——NSJSONReadingAllowFragments,當服務器返回的JSON字符串不是字典或數組格式時,就用這個參數,比如直接是個字符串或數字
id obj = [NSJSONSerialization JSONObjectWithData:data options:options error:nil];

注意:數據在傳輸過程中肯定是二進制數據,也就是NSData,但是這個NSData的內容現在一般就是一個JSON字符串了(很少有公司用XML了),所以我們從服務器拿到的數據肯定就是個NSData,我們把這個NSData轉換成OC對象就是在把JSON字符串轉換成OC對象(進而再把OC對象轉換成Model使用)。

參考
1、AFNetworking源碼分析
2、AFNetworking到底做了什麼

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章