萌新講故事之對蘋果源碼CustomHTTPProtocol的解讀

以下為自言自語可以跳過

由于公司需求需要對WebView進行離線緩存,之前也在網上看了許多的帖子和文章下載了幾個Demo,有用JS把網頁的圖片以及內容緩存到本地的這個感覺有點麻煩放棄了,有個老外封裝的通過HTTPProtocol來實現離線緩存的功能,感覺還是屌屌的,本來緩存功能還是能使用,由于蘋果爸爸先是要讓支持ipv6 馬上又要必須使用 HTTPS,加上公司還要對接口使用 Des加密,無奈之下放棄了 AFN 轉而使用Session,但是使用Session之后 老外的那個封裝的協議緩存不使用AFN竟然不能用了,可能AFN做了內部處理, 我使用的是蘋果爸爸的Session啊。媽蛋我哪知道怎么處理的。于是在網上找到了 DKNight的作者的這篇文章iOS 開發中使用 NSURLProtocol 攔截 HTTP 請求竟然在里面發現了蘋果爸爸的源碼點擊這里下載,如果想看大神的文章還是點上門的傳送門吧。

第一天

以下是我的解讀視角

打開工程后


機智的我發現這些畫紅框的基本沒啥用 Redme啦 HTML資源啦 圖片資源啦啥的
但是我發現這個HTML中的內容還是有點用的
因為我發現storyboard中的頁面中只有一個WebView,所以加載頁面還是得看這3個頁面啊

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <title>Root</title>
</head>
<body>
<ul>
    <li><a >Apple</a></li>
    <li><a >Apple (HTTPS)</a></li>
    <li><a >Apple (HTTPS, redirect)</a></li>
    <li><a >CAcert</a></li>
    <li><a >CAcert (HTTPS)</a></li>
    <li><a >Install CAcert Anchor</a></li>
</ul>
</body>
</html>

打開Root文件就發現是幾個鏈接
運行一下工程看看啥效果


哦好簡潔,點擊鏈接上面3個能跳轉到蘋果官網,下面3個都是報錯。
那么我們看看對照著看一下,上面3個鏈接圖片上有對應的文字注釋媽蛋,忽然發現有個不認識的單詞(redirect)。我才不會承認英語不好呢。百度了一下,應該是重新定向的意思,其對應的網址是https://apple.com ,應該是把這個網址重新定向成為https://www.apple.com 好吧這個頁面已經沒啥看的了 對代碼解讀也沒啥意義。
接下來看看代碼
首先我打開了main.m 至于為啥沒有main.h 這個我哪知道。。
打開一看,滿篇的綠,我操這么多英文,不知道我有密集恐懼癥嗎。


光英文我都不說啥,還有不少純大寫的,雖然都是小寫的我也不認識!
學編程時老師就教過我一個秘訣!沒用的東西,刪! 媽蛋我發現每個頁面上面都有這么一大段,我感覺到對我深深的惡意我就是看個代碼而已竟然還得往下拉這么久,所以,我挨個把所有文件中的注釋都刪掉了。


哦,我的世界充滿了整潔。媽蛋注釋一刪掉發現一共就12行代碼。。
蘋果爸爸的代碼和我們也差不多嗎 main函數不過如此!
但是還是發現和普通工程有一些區別的比如**argv這個 大家可以和自己的main對比一下就好
接下來是AppDelegate
定義了幾個屬性目前不知道干嘛的
定義了2個靜態變量

static BOOL sAppDelegateLoggingEnabled = YES;
static NSTimeInterval sAppStartTime; 

一個是開啟日志的一個是記錄時間的。。看名字就知道了
而具體是干嘛的呢。就是在下面的協議方法中控制輸出內容的

- (void)logWithPrefix:(NSString *)prefix format:(NSString *)format arguments:(va_list)arguments

接下來看

- (BOOL)application:(UIApplication *)application willFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
//一進方法就碰到了個不認識的東西#pragma unused
//度娘告訴我主要負責遇到沒有使用的變量不報錯
    #pragma unused(application)
    #pragma unused(launchOptions)
    WebViewController *   webViewController;
// 度娘告訴我 assert這個是負責在DEBUG狀態下如果參數為NO編譯時會提示是否繼續
//如果在release狀態下不編譯這個宏    
    assert(self.window != nil);
    
    sAppStartTime = [NSDate timeIntervalSinceReferenceDate];
    //管理受信任的錨證書列表 具體干嘛目前還沒看到
    self.credentialsManager = [[CredentialsManager alloc] init];
//原文翻譯下來是下面的意思 但是也不知道干嘛的
//準備我們的日志代碼所需的全局變量。 調用-threadInfoForCurrentThread
 //設置主線程線程信息記錄,并確保它具有線程號0。
    self.threadInfoByThreadID = [[NSMutableDictionary alloc] init];
  //進入方法看了一下 主要是把所有使用過的線程信息儲存到字典中
    (void) [self threadInfoForCurrentThread];
    //上面那些代碼以及沒啥用
//核心代碼就這兩句。
//接下來進入內部看看
//CustomHTTPProtocol 不能直接從頭看,應該從調用的地方開始看
    [CustomHTTPProtocol setDelegate:self];
    if (YES) {
        [CustomHTTPProtocol start];
    }
    //下面的也沒啥用
    webViewController = [[UIStoryboard storyboardWithName:@"Main" bundle:[NSBundle bundleForClass:[self class]]] instantiateViewControllerWithIdentifier:@"webView"];
    assert(webViewController != nil);
    webViewController.delegate = self;
    if (NO) {
        webViewController.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"Test" style:UIBarButtonItemStyleBordered target:self action:@selector(testAction:)];
    }
    [((UINavigationController *) self.window.rootViewController) pushViewController:webViewController animated:NO];

    [self.window makeKeyAndVisible];
    
    return YES;
}

CustomHTTPProtocol.h中有下面幾個方法
就是重寫的setter方法和getter方法

static id<CustomHTTPProtocolDelegate> sDelegate;

+ (void)start
{
    [NSURLProtocol registerClass:self];
}

+ (id<CustomHTTPProtocolDelegate>)delegate
{
    id<CustomHTTPProtocolDelegate> result;

    @synchronized (self) {
        result = sDelegate;
    }
    return result;
}

+ (void)setDelegate:(id<CustomHTTPProtocolDelegate>)newValue
{
    @synchronized (self) {
        sDelegate = newValue;
    }
}

這句才是關鍵。
在每一個 HTTP 請求開始時,URL 加載系統創建一個合適的NSURLProtocol 對象處理對應的 URL 請求,而我們需要做的就是寫一個繼承自 NSURLProtocol 的類,并通過 - registerClass: 方法注冊我們的協議類,然后 URL 加載系統就會在請求發出時使用我們創建的協議對象對該請求進行處理。

+ (void)start
{
    [NSURLProtocol registerClass:self];
}

還有幾個協議目前先不看
在AppDelegate中注冊開始后每個請求都會按照現在這個協議方法執行了
接下來看CustomHTTPProtocol.m內部實現
上面屬性定義和一些方法先不看 沒啥用 往下看 直接到 canInitWithRequest:
下面的方法用于決定請求是否需要當前協議對象處理

//定義一個字符串用來判斷是否在請求中添加個這個標識
static NSString * kOurRecursiveRequestFlagProperty = @"com.apple.dts.CustomHTTPProtocol";

+ (BOOL)canInitWithRequest:(NSURLRequest *)request
{
    BOOL        shouldAccept;//判斷是否可以相應請求的變量
    NSURL *     url;
    NSString *  scheme;
    

    //判斷請求是否存在
    shouldAccept = (request != nil);
    if (shouldAccept) {
        url = [request URL];
        shouldAccept = (url != nil);//判斷請求中的url是否非空
    }
    if ( ! shouldAccept ) {//出現上面兩種情況通過代理輸出錯誤信息
        [self customHTTPProtocol:nil logWithFormat:@"decline request (malformed)"];
    }
    
    
    if (shouldAccept) {//請求存在后判斷這個請求是否做過標記
        shouldAccept = ([self propertyForKey:kOurRecursiveRequestFlagProperty inRequest:request] == nil);
        if ( ! shouldAccept ) {//如果做過標記返回信息而且下面的代碼都不會執行了
            [self customHTTPProtocol:nil logWithFormat:@"decline request %@ (recursive)", url];
        }
    }
        
    if (shouldAccept) {
        scheme = [[url scheme] lowercaseString];
//后面的lowercaseString是字母都變成小寫但是對scheme我不是很理解所以百度了一下
//下面有scheme講解的地址
        shouldAccept = (scheme != nil);
        //scheme是否為空,估計幾率比較小
        if ( ! shouldAccept ) {
            [self customHTTPProtocol:nil logWithFormat:@"decline request %@ (no scheme)", url];
        }
    }
    
    if (shouldAccept) {
        shouldAccept = NO && [scheme isEqual:@"http"];
//這段代碼會出現問題肯定是NO不知道為什么這么寫
        if ( ! shouldAccept ) {
            shouldAccept = YES && [scheme isEqual:@"https"];
//這段代碼如果scheme為http返回NO  https返回YES
        }

        if ( ! shouldAccept ) {
            [self customHTTPProtocol:nil logWithFormat:@"decline request %@ (scheme mismatch)", url];
        } else {
            [self customHTTPProtocol:nil logWithFormat:@"accept request %@", url];
        }
    }
    
    return shouldAccept;
}

每一次請求都會有一個 NSURLRequest 實例,上述方法會拿到所有的請求對象,我們就可以根據對應的請求選擇是否處理該對象;而上面的代碼實現的功能有,請求不能為空,請求中的URL不能為空,請求中的的自定義標記應該為空,做過標記則不再處理, scheme不能為空,HTTPS則每次都做處理而HTTP則永不做處理。

上面我提到的文章是這樣寫下面的方法的

請求經過 + canInitWithRequest: 方法過濾之后,我們得到了所有要處理的請求,接下來需要對請求進行一定的操作,而這都會在 + canonicalRequestForRequest: 中進行,雖然它與 + canInitWithRequest: 方法傳入的 request 對象都是一個,但是最好不要在 + canInitWithRequest: 中操作對象,可能會有語義上的問題;所以,我們需要覆寫 + canonicalRequestForRequest: 方法提供一個標準的請求對象

所以接下來是解讀蘋果內部是怎樣處理這個需要處理的請求的
方法中主要作用在于CanonicalRequestForRequest()函數

+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request
{
    NSURLRequest *      result;
    
    assert(request != nil);
//可以在任何線程上調用
//規范化請求是相當復雜的,所以所有的繁重工作
//被拖放到一個單獨的模塊。    
    result = CanonicalRequestForRequest(request);
    [self customHTTPProtocol:nil logWithFormat:@"canonicalized %@ to %@", [request URL], [result URL]];
    
    return result;
}

接下來根據這個規范化請求跳轉到CanonicalRequest.h類
只有一個接口

extern NSMutableURLRequest * CanonicalRequestForRequest(NSURLRequest *request);

接下來到.m查看實現方式

extern NSMutableURLRequest * CanonicalRequestForRequest(NSURLRequest *request)
{
    NSMutableURLRequest *   result;
    NSString *              scheme;

    assert(request != nil);
//請求不能為空,雖然之前的方法已經判斷過了,這里蘋果又判斷了一次    
    result = [request mutableCopy];
//將請求拷貝一份
    scheme = [[[request URL] scheme] lowercaseString];
    assert(scheme != nil);
//獲取scheme
    
    if ( ! [scheme isEqual:@"http" ] && ! [scheme isEqual:@"https"]) {
        assert(NO);//如果不是http或者https什么也不處理進行報錯
    } else {
/*
typedef CFIndex (*CanonicalRequestStepFunction)(NSURL *url, NSMutableData *urlData, CFIndex bytesInserted);
在上方有定義一個函數指針
*/
        //這三個參數用于給函數賦值
        CFIndex         bytesInserted;
        NSURL *         requestURL;
        NSMutableData * urlData;
        //通過在上面定義的函數指針 指向一個函數指針數組
        //數組中放著5個函數的實現
        static const CanonicalRequestStepFunction kStepFunctions[] = {
            FixPostSchemeSeparator, 
            LowercaseScheme, 
            LowercaseHost, 
            FixEmptyHost, 
            // DeleteDefaultPort,       -- 內置規范化程序已停止這樣做,所以我們也不這樣做。
            FixEmptyPath
        };
        size_t          stepIndex;
        size_t          stepCount;
        
       // 通過執行我們的每個步驟功能來規范化URL。
        //kCFNotFound 表示搜索操作未能成功定位目標值的常量。
        bytesInserted = kCFNotFound;
        urlData = nil;
        requestURL = [request URL];
        assert(requestURL != nil);
        //size_t 類型定義在cstddef頭文件中,該文件是C標準庫的頭文件stddef.h的C++版。它是一個與機器相關的unsigned類型,其大小足以保證存儲內存中對象的大小。
        //通過函數指針數組  除以單個數組中的元素地址 獲取個數
        stepCount = sizeof(kStepFunctions) / sizeof(*kStepFunctions);
        for (stepIndex = 0; stepIndex < stepCount; stepIndex++) {
        
            // 如果我們沒有有效的網址數據,請從網址中創建。
            assert(requestURL != nil);
            if (bytesInserted == kCFNotFound) {//每個調用一個數組中的函數返回的結果都是bytesInserted == kCFNotFound
                NSData *    urlDataImmutable;
                //下面這個方法是從請求的url訪問網址獲取一個新的URL的Data
                urlDataImmutable = CFBridgingRelease( CFURLCreateData(NULL, (CFURLRef) requestURL, kCFStringEncodingUTF8, true) );
                assert(urlDataImmutable != nil);
                
                urlData = [urlDataImmutable mutableCopy];
                assert(urlData != nil);
                
                bytesInserted = 0;
            }
            assert(urlData != nil);
            
            //通過函數調用下面數組中的函數,將返回的URL符合每一種規范
            bytesInserted = kStepFunctions[stepIndex](requestURL, urlData, bytesInserted);            
            //輸出返回的URL內容            
            if (NO) {
                fprintf(stderr, "  [%zu] %.*s\n", stepIndex, (int) [urlData length], (const char *) [urlData bytes]);
            }
            //翻譯了一下注釋
            //如果步驟使您的網址無效,請從網址資料重新建立網址。 
            //(或者我們在最后一步,因此我們需要在循環之外的URL)
            
            if ( (bytesInserted == kCFNotFound) || ((stepIndex + 1) == stepCount) ) {
                requestURL = CFBridgingRelease( CFURLCreateWithBytes(NULL, [urlData bytes], (CFIndex) [urlData length], kCFStringEncodingUTF8, NULL) );
                assert(requestURL != nil);
                
                urlData = nil;
            }
        }
        [result setURL:requestURL];        
        // 添加一些header        
        CanonicaliseHeaders(result);
    }    
    return result;
}

那個函數指針數組中的對應函數就不要看啦,都是一些網址的規范,通過這些函數把我們請求中的地址變為符合規范的地址
媽蛋這個類看了好久好久,結果讓我失望的是竟然沒什么屌用。 僅僅是自動添加標題啦各種各樣的方法 媽蛋啊媽蛋

看了這么久才看了2個方法好尷尬,不過終于到了關鍵方法了。終于可以初始化一個 NSURLProtocol 對象了

- (id)initWithRequest:(NSURLRequest *)request cachedResponse:(NSCachedURLResponse *)cachedResponse client:(id <NSURLProtocolClient>)client
{
    assert(request != nil);
    assert(client != nil);
  //這里其實可以直接返回父類的初始化方法就行了。。
    self = [super initWithRequest:request cachedResponse:cachedResponse client:client];
    if (self != nil) {
        [[self class] customHTTPProtocol:self logWithFormat:@"init for %@ from <%@ %p>", [request URL], [client class], client];
    }
    return self;
}

。。非常簡單直接調用父類方法。

第二天

接下來開始 - (void)startLoading 方法
看到了這個方法出現了卡頓。因為我有點不理解為什么在這個方法中又執行了Session的請求。網上查看了一些文章其中一篇名字為定制實現NSURLProtocol提到

使用canInitWithRequest:我們可以篩選出可以使用當前協議的request,其它的忽略掉,直接走其它協議或者默認實現。

我才好些明白了點什么原來并不是我使用協議以后所有請求都是按照我們的協議進行處理了,而是我這個協議中的canInitWithRequest:不處理也會進行默認的實現以及處理
還有一邊文章提到

NSURLProtocol在攔截NSURLSession的POST請求時不能獲取到Request中的HTTPBody,這個貌似早就國外的論壇上傳開了,但國內好像還鮮有人知,據蘋果官方的解釋是Body是NSData類型,即可能為二進制內容,而且還沒有大小限制,所以可能會很大,為了性能考慮,索性就攔截時就不拷貝了(內流滿面臉)。為了解決這個問題,我們可以通過把Body數據放到Header中,不過Header的大小好像是有限制的,我試過2M是沒有問題,不過超過10M就直接Request timeout了。。。而且當Body數據為二進制數據時這招也沒轍了,因為Header里都是文本數據,另一種方案就是用一個NSDictionary或NSCache保存沒有請求的Body數據,用URL為key,最后方法就是別用NSURLSession,老老實實用古老的NSURLConnection算了。。。

經過測試通過NSURLSession進行的網絡請求截取的Request中并沒有存儲到HTTPBody(真的是內牛滿面我還要做緩存呢這可咋整)
由于assert方法對邏輯沒有什么影響我將代碼中的assert方法都去掉了看著能方便些

- (void)startLoading
{
    NSMutableURLRequest *   recursiveRequest;//用來拷貝請求
    NSMutableArray *        calculatedModes;//用來記錄Runloop中的模式
    NSString *              currentMode;//獲取當前Runloop中的模式

    calculatedModes = [NSMutableArray array];
    [calculatedModes addObject:NSDefaultRunLoopMode];//添加默認的RunLoopMode
    currentMode = [[NSRunLoop currentRunLoop] currentMode];//獲取當前的模式
    if ( (currentMode != nil) && ! [currentMode isEqual:NSDefaultRunLoopMode] ) {
        [calculatedModes addObject:currentMode];
//這個時候肯定是添加NSRunLoopCommonModes類型因為只有兩種類型
    }
    self.modes = calculatedModes;//將Runloop類型存入Modes中 可能是1種或者2種    
    recursiveRequest = [[self request] mutableCopy];//將請求進行拷貝
    
    [[self class] setProperty:@YES forKey:kOurRecursiveRequestFlagProperty inRequest:recursiveRequest];
//在這里對我們拷貝的請求進行標記,以免陷入死循環
//我們需要在canInitWithRequest方法中進行判斷如果做過標記的請求則不進行處理,否則將死循環

//下面的時間以及判斷也沒什么用 都是用來輸出log的
    self.startTime = [NSDate timeIntervalSinceReferenceDate];
    if (currentMode == nil) {
        [[self class] customHTTPProtocol:self logWithFormat:@"start %@", [recursiveRequest URL]];
    } else {
        [[self class] customHTTPProtocol:self logWithFormat:@"start %@ (mode %@)", [recursiveRequest URL], currentMode];
    }
    
    // 獲取當前線程具體干嘛目前不知道 繼續往下看    
    self.clientThread = [NSThread currentThread];
    
    // 谷歌翻譯說 一旦一切準備就緒,請使用新請求創建數據任務。
    //那么現在分別進入接下來這段代碼的兩個方法中看看是干嘛的
    self.task = [[[self class] sharedDemux] dataTaskWithRequest:recursiveRequest delegate:self modes:self.modes];
    
    [self.task resume];
}

首先是類方法sharedDemux

+ (QNSURLSessionDemux *)sharedDemux
{
    通過GCD創建一個單例  這個單例會控制我們之后的多次請求都會在這個隊列中等待進行順序執行
    static dispatch_once_t      sOnceToken;
    static QNSURLSessionDemux * sDemux;
    dispatch_once(&sOnceToken, ^{
        NSURLSessionConfiguration *     config;
/*
 NSURLSessionConfiguration對象定義在使用NSURLSession對象上傳和下載數據時要使用的行為和策略。
 在上傳或下載數據時,創建配置對象始終是必須執行的第一步。 
 您可以使用此對象來配置超時值,緩存策略,連接要求以及要與NSURLSession對象一起使用的其他類型的信息。
 在使用NSURLSessionConfiguration對象初始化會話對象之前,必須正確配置它。
 會話對象制作您提供的配置設置的副本,并使用這些設置配置會話。
 配置后,會話對象將忽略對NSURLSessionConfiguration對象所做的任何更改。
 如果需要修改傳輸策略,則必須更新會話配置對象,并使用它來創建新的NSURLSession對象。
 API中是這樣對NSURLSessionConfiguration進行解釋的
 */
         //獲取一個默認的配置策略
        config = [NSURLSessionConfiguration defaultSessionConfiguration];
        //你必須在這里顯式配置會話使用你自己的協議子類
        //否則,您看不到重定向<rdar:// problem / 17384498>。
        // 蘋果爸爸的意思是必須配置否則重新定向時就沒內容了
        config.protocolClasses = @[ self ];//把我們自定義的協議設置成這個策略的協議
        //進行初始化
        sDemux = [[QNSURLSessionDemux alloc] initWithConfiguration:config];
    });
    return sDemux;
}

雖然剛才說要看dataTaskWithRequest: delegate: modes:方法但是發現這個類方法中還有一個初始化方法,所以需要先看初始化方法.
看到這里其實我們已經完成一半的解讀了因為這個Demo中核心的類只有4個
最重要的是CustomHTTPProtocol這個類,上面我們已經稍微解讀了一下CanonicaalRequest這個類,這個類主要是用來處理我們的Request返回一個標準的符合規定的Request.然而我覺得沒啥用啊。。 就是加一些請求頭啥的接下來就是CacheStoragePolicy和QNSURLSessionDemux兩個類了,接下來從QNSURLSessionDemux的初始化開始看

- (instancetype)init
{
//蘋果爸爸為了健壯性把init的初始化也進行調用initWithConfiguration:的初始化方法
    return [self initWithConfiguration:nil];
}

- (instancetype)initWithConfiguration:(NSURLSessionConfiguration *)configuration
{
    // 所以configuration也是有可能為空的
    self = [super init];
    if (self != nil) {
        if (configuration == nil) {//如果為空則再創建一個默認的配置策略。估計基本調用不到了
            configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
        }
/*
@property (atomic, copy,   readonly ) NSURLSessionConfiguration *   configuration; 
因為把屬性設置成這個鬼樣子,只讀并且是原子性的所以需要使用
self->_configuration的方式調用
*/
        self->_configuration = [configuration copy];
 //初始化一個存儲任務信息的和任務ID的字典
        self->_taskInfoByTaskID = [[NSMutableDictionary alloc] init];
//又搞了個隊列
        self->_sessionDelegateQueue = [[NSOperationQueue alloc] init];
//設置可同時執行的排隊操作的最大數量為1
        [self->_sessionDelegateQueue setMaxConcurrentOperationCount:1];
//設置隊列的名稱
        [self->_sessionDelegateQueue setName:@"QNSURLSessionDemux"];
//創建具有指定的會話配置,委派和操作隊列的會話。
        self->_session = [NSURLSession sessionWithConfiguration:self->_configuration delegate:self delegateQueue:self->_sessionDelegateQueue];
//設置個描述竟然和剛才的名字一樣 不知道干嘛
        self->_session.sessionDescription = @"QNSURLSessionDemux";
    }
    return self;
}

接下來看dataTaskWithRequest: delegate: modes:

- (NSURLSessionDataTask *)dataTaskWithRequest:(NSURLRequest *)request delegate:(id<NSURLSessionDataDelegate>)delegate modes:(NSArray *)modes
{
    NSURLSessionDataTask *          task;
    QNSURLSessionDemuxTaskInfo *    taskInfo;
    //和上面一樣如果Mode啥也沒有 給添加個默認的
    if ([modes count] == 0) {
        modes = @[ NSDefaultRunLoopMode ];
    }
    //創建根據指定的URL請求對象檢索URL內容的任務。
    task = [self.session dataTaskWithRequest:request];
    //根據Task,代理和Model創建一個Task信息
    taskInfo = [[QNSURLSessionDemuxTaskInfo alloc] initWithTask:task delegate:delegate modes:modes];    
    @synchronized (self) {
//加鎖來保護內容的安全性,將task的taskIdentifier設置為字典的Key存儲taskInfo信息
        self.taskInfoByTaskID[@(task.taskIdentifier)] = taskInfo;
    }    
    return task;
}

第三天

接下來看initWithTask:delegate:modes:方法 這個方法是在.m中創建了一個內部的類

- (instancetype)initWithTask:(NSURLSessionDataTask *)task delegate:(id<NSURLSessionDataDelegate>)delegate modes:(NSArray *)modes
{
    self = [super init];
    if (self != nil) {//都是很簡單的代碼就不寫注釋了
        self->_task = task;
        self->_delegate = delegate;
        self->_thread = [NSThread currentThread];
        self->_modes = [modes copy];
    }
    return self;
}

這個類有2個方法4個屬性,4個屬性沒啥說的

@interface QNSURLSessionDemuxTaskInfo : NSObject

- (instancetype)initWithTask:(NSURLSessionDataTask *)task delegate:(id<NSURLSessionDataDelegate>)delegate modes:(NSArray *)modes;

@property (atomic, strong, readonly ) NSURLSessionDataTask *        task;
@property (atomic, strong, readonly ) id<NSURLSessionDataDelegate>  delegate;
@property (atomic, strong, readonly ) NSThread *                    thread;
@property (atomic, copy,   readonly ) NSArray *                     modes;

- (void)performBlock:(dispatch_block_t)block;

- (void)invalidate;

@end
//制空的方法
- (void)invalidate
{
    self.delegate = nil;
    self.thread = nil;
}
//接下來看看下面2個方法是干嘛的
- (void)performBlock:(dispatch_block_t)block
{
//可以使用此方法將消息傳遞到應用程序中的其他線程。在這種情況下,消息是要在目標線程上執行的當前對象的方法。
    //參數分別為  執行方法;選擇線程;Object則代表傳遞的參數;如果當前線程和目標線程相同,并且您為此參數指定了YES,則會立即執行選擇器。如果指定NO,此方法將消息排隊并立即返回,而不管線程是相同還是不同;最后是Runloop的哪種執行模式下
    [self performSelector:@selector(performBlockOnClientThread:) onThread:self.thread withObject:[block copy] waitUntilDone:NO modes:self.modes];
}
//調用的方法
- (void)performBlockOnClientThread:(dispatch_block_t)block
{
    block();
}

接下來是一大波的方法,都是URLSession的回調

//第一個不是回調但是是每個回調都會用到的方法,這個方法主要是返回剛才存儲的TaskInfo
- (QNSURLSessionDemuxTaskInfo *)taskInfoForTask:(NSURLSessionTask *)task
{
    QNSURLSessionDemuxTaskInfo *    result;
         @synchronized (self) {
//取出字典中存儲的taskInfo類
        result = self.taskInfoByTaskID[@(task.taskIdentifier)];
    }
    return result;
}
/* 
 告訴代理遠程服務器請求HTTP重定向。
 僅對缺省和臨時會話中的任務調用此方法。 后臺會話中的任務會自動跟隨重定向。
 */
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task willPerformHTTPRedirection:(NSHTTPURLResponse *)response newRequest:(NSURLRequest *)newRequest completionHandler:(void (^)(NSURLRequest *))completionHandler
{
    QNSURLSessionDemuxTaskInfo *    taskInfo;
    
    taskInfo = [self taskInfoForTask:task];
    if ([taskInfo.delegate respondsToSelector:@selector(URLSession:task:willPerformHTTPRedirection:newRequest:completionHandler:)]) {
//這個block將其中的代碼段加到了隊列之中等待執行
        [taskInfo performBlock:^{
//主要通過代理將協議方法回調到CustomHTTPProtocol類中..
//結果代碼看起來很多結果下面的方法和這個一樣 都是將HTTPURLSession的回調
//再通過回調的方法傳遞到CustomHTTPProtocol類中
            [taskInfo.delegate URLSession:session task:task willPerformHTTPRedirection:response newRequest:newRequest completionHandler:completionHandler];
        }];
    } else {
        completionHandler(newRequest);
    }
}//接下來主要看看其他的HTTPURLSession的回調都是干嘛用的
//請從委托憑證響應來自遠程服務器的認證請求。  英語不好只能谷歌翻譯了
/** 
 此方法處理任務級身份驗證挑戰。 NSURLSessionDelegate協議還提供了會話級身份驗證委托方法。調用的方法取決于身份驗證質詢的類型:
 
 對于會話級挑戰 - NSURLAuthenticationMethodNTLM,NSURLAuthenticationMethodNegotiate,NSURLAuthenticationMethodClientCertificate或NSURLAuthenticationMethodServerTrust - NSURLSession對象調用會話委托的URLSession:didReceiveChallenge:completionHandler:方法。如果您的應用程序不提供會話委托方法,那么NSURLSession對象將調用任務委托的URLSession:task:didReceiveChallenge:completionHandler:方法來處理挑戰。
 
 對于非會話級挑戰(所有其他),NSURLSession對象調用會話委托的URLSession:task:didReceiveChallenge:completionHandler:方法來處理挑戰。如果您的應用程序提供了一個會話委托,并且您需要處理身份驗證,那么您必須在任務級別處理身份驗證或提供一個明確調用每會話處理程序的任務級處理程序。會話代理的URLSession:didReceiveChallenge:completionHandler:方法不是為非會話級的挑戰調用。
 */

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler
{
    QNSURLSessionDemuxTaskInfo *    taskInfo;
    
    taskInfo = [self taskInfoForTask:task];
    if ([taskInfo.delegate respondsToSelector:@selector(URLSession:task:didReceiveChallenge:completionHandler:)]) {
        [taskInfo performBlock:^{
            [taskInfo.delegate URLSession:session task:task didReceiveChallenge:challenge completionHandler:completionHandler];
        }];
    } else {
//對挑戰使用默認處理,就像未實現此委派方法一樣。 將忽略提供的憑據參數。
        completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
    }
}
//當任務需要新的請求主體流發送到遠程服務器時告訴代理。
/*
在兩種情況下調用此委托方法:
如果任務是使用uploadTaskWithStreamedRequest創建的,則提供初始請求正文流:
如果任務由于認證質詢或其他可恢復的服務器錯誤而需要重新發送具有正文流的請求,則提供替換請求主體流。
*/
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task needNewBodyStream:(void (^)(NSInputStream *bodyStream))completionHandler
{
    QNSURLSessionDemuxTaskInfo *    taskInfo;
    
    taskInfo = [self taskInfoForTask:task];
    if ([taskInfo.delegate respondsToSelector:@selector(URLSession:task:needNewBodyStream:)]) {
        [taskInfo performBlock:^{
            [taskInfo.delegate URLSession:session task:task needNewBodyStream:completionHandler];
        }];
    } else {
        completionHandler(nil);
    }
}
//定期向代表通知將正文內容發送到服務器的進度。
/*
 bytesSent
 自上次調用此委托方法以來發送的字節數。
 totalBytesSent
 到目前為止發送的字節總數。
 totalBytesExpectedToSend
 主體數據的預期長度。 URL加載系統可以通過三種方式確定上傳數據的長度:
 
 從作為上傳正文提供的NSData對象的長度。
 從作為上載任務(而不是下載任務)的上載主體提供的磁盤上的文件的長度。
 從請求對象中的Content-Length,如果您明確設置它。
 否則,如果您提供了流或主體數據對象,值為NSURLSessionTransferSizeUnknown(-1),如果沒有提供,則值為零。
 */
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didSendBodyData:(int64_t)bytesSent totalBytesSent:(int64_t)totalBytesSent totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend
{
    QNSURLSessionDemuxTaskInfo *    taskInfo;
    
    taskInfo = [self taskInfoForTask:task];
    if ([taskInfo.delegate respondsToSelector:@selector(URLSession:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:)]) {
        [taskInfo performBlock:^{
            [taskInfo.delegate URLSession:session task:task didSendBodyData:bytesSent totalBytesSent:totalBytesSent totalBytesExpectedToSend:totalBytesExpectedToSend];
        }];
    }
}
//告訴代理任務完成了數據傳輸。
/**
 不通過error參數報告服務器錯誤。 代理通過error參數接收的唯一錯誤是客戶端錯誤,例如無法解析主機名或連接到主機。
 */
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
{
    QNSURLSessionDemuxTaskInfo *    taskInfo;
    
    taskInfo = [self taskInfoForTask:task];

   //這是我們的最后一個委托回調,所以我們刪除了我們的任務信息記錄。
    
    @synchronized (self) {
        [self.taskInfoByTaskID removeObjectForKey:@(taskInfo.task.taskIdentifier)];
    }

     //如果需要,調用委托。 在這種情況下,我們使客戶端線程上的任務信息無效
     //在調用委托后,否則客戶端線程端的-performBlock:代碼就可以了
     //發現自己帶有無效的任務信息。
    
    if ([taskInfo.delegate respondsToSelector:@selector(URLSession:task:didCompleteWithError:)]) {
        [taskInfo performBlock:^{
            [taskInfo.delegate URLSession:session task:task didCompleteWithError:error];
            [taskInfo invalidate];
        }];
    } else {
        [taskInfo invalidate];
    }
}
//告訴代理數據任務從服務器接收到初始答復(頭)。
/**
 此方法是可選的,除非您需要支持(相對模糊的)multipart / x-mixed-replace內容類型。 使用該內容類型,服務器發送一系列部件,每個部件旨在替換以前的部件。 會話在每個部分的開始調用此方法,然后您應該根據需要顯示,放棄或以其他方式處理上一部分。
 
 如果您不提供此委派方法,則會話始終允許任務繼續。
 您的代碼調用以繼續傳輸的完成處理程序,傳遞常量以指示傳輸是作為數據任務繼續還是應成為下載任務。
 completionHandler參數
 如果通過NSURLSessionResponseAllow,任務將正常繼續。
 如果通過NSURLSessionResponseCancel,任務將被取消。
 如果您通過NSURLSessionResponseBecomeDownload作為處置,則調用代理的URLSession:dataTask:didBecomeDownloadTask:方法來為您提供取代當前任務的新下載任務。
 */
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler
{
    QNSURLSessionDemuxTaskInfo *    taskInfo;
    
    taskInfo = [self taskInfoForTask:dataTask];
    if ([taskInfo.delegate respondsToSelector:@selector(URLSession:dataTask:didReceiveResponse:completionHandler:)]) {
        [taskInfo performBlock:^{
            [taskInfo.delegate URLSession:session dataTask:dataTask didReceiveResponse:response completionHandler:completionHandler];
        }];
    } else {
        completionHandler(NSURLSessionResponseAllow);
    }
}
//告訴代理數據任務已更改為下載任務。
/**
  當代理的URLSession:dataTask:didReceiveResponse:completionHandler:方法決定將處置從數據請求更改為下載時,會話將調用此委派方法為您提供新的下載任務。 在此調用之后,會話委托不接收與原始數據任務相關的進一步委托方法調用。
 */
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didBecomeDownloadTask:(NSURLSessionDownloadTask *)downloadTask
{
    QNSURLSessionDemuxTaskInfo *    taskInfo;
    
    taskInfo = [self taskInfoForTask:dataTask];
    if ([taskInfo.delegate respondsToSelector:@selector(URLSession:dataTask:didBecomeDownloadTask:)]) {
        [taskInfo performBlock:^{
            [taskInfo.delegate URLSession:session dataTask:dataTask didBecomeDownloadTask:downloadTask];
        }];
    }
}
//告訴代理數據任務已收到一些預期的數據。
/**
 因為NSData對象通常從許多不同的數據對象拼湊而成,所以盡可能使用NSData的enumerateByteRangesUsingBlock:方法來遍歷數據,而不是使用bytes方法(將NSData對象拉平到單個內存塊中)。
 
 這個委托方法可以被多次調用,并且每個調用僅提供自從上一次調用以來接收的數據。 應用程序負責累積這些數據(如果需要)。
 */
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data
{
    QNSURLSessionDemuxTaskInfo *    taskInfo;
    
    taskInfo = [self taskInfoForTask:dataTask];
    if ([taskInfo.delegate respondsToSelector:@selector(URLSession:dataTask:didReceiveData:)]) {
        [taskInfo performBlock:^{
            [taskInfo.delegate URLSession:session dataTask:dataTask didReceiveData:data];
        }];
    }
}
//向代理詢問數據(或上傳)任務是否應將響應存儲在緩存中。
/**
 在任務完成接收所有預期數據后,會話調用此委托方法。如果不實現此方法,則默認行為是使用會話配置對象中指定的緩存策略。此方法的主要目的是防止緩存特定的URL或修改與URL響應相關聯的userInfo字典。
 
 僅當處理請求的NSURL協議決定緩存響應時,才調用此方法。通常,只有當以下所有條件都為真時,才會緩存響應:
 
 1請求是針對HTTP或HTTPS URL(或您自己的支持緩存的自定義網絡協議)。
 
 2請求已成功(狀態碼位于200-299范圍內)。
 
 3提供的響應來自服務器,而不是從緩存中。
 
 4會話配置的緩存策略允許緩存。
 
 5提供的NSURLRequest對象的緩存策略(如果適用)允許緩存。
 
 6服務器響應中與緩存相關的頭(如果存在)允許緩存。
 
 7響應大小足夠小以合理地適合緩存。 (例如,如果提供磁盤緩存,響應必須不大于磁盤緩存大小的大約5%)。
 proposedResponse
 默認緩存行為。 該行為基于當前高速緩存策略和某些所接收的報頭(例如,Pragma和高速緩存控制報頭)的值來確定。
 completionHandler
 您的處理程序必須調用的塊,提供原始的建議響應,該響應的修改版本或NULL以防止緩存響應。 如果你的委托實現這個方法,它必須調用這個完成處理程序; 否則,您的應用程序泄漏內存。
 */
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask willCacheResponse:(NSCachedURLResponse *)proposedResponse completionHandler:(void (^)(NSCachedURLResponse *cachedResponse))completionHandler
{
    QNSURLSessionDemuxTaskInfo *    taskInfo;
    
    taskInfo = [self taskInfoForTask:dataTask];
    if ([taskInfo.delegate respondsToSelector:@selector(URLSession:dataTask:willCacheResponse:completionHandler:)]) {
        [taskInfo performBlock:^{
            [taskInfo.delegate URLSession:session dataTask:dataTask willCacheResponse:proposedResponse completionHandler:completionHandler];
        }];
    } else {
        completionHandler(proposedResponse);
    }
}

好啦。這個類就全部解讀完成。全部看完感覺大概,應該,可能是把所有的Task在當前線程添加一個隊列的網絡請求。一個一個來請求。。

接下來回到我們的HTTPProtocol類中繼續看stopLoading

- (void)stopLoading
{
//我進去看了一下這個方法的實現。。我感覺是沒啥用,也可能是我比較菜。
//和這些關聯的方法我就不解讀了有好幾個 但是主要都是用來輸出log的
    [self cancelPendingChallenge];
    if (self.task != nil) {
        [self.task cancel];
        self.task = nil;
    }
}

接下來開始看那些NSURLSession的回調是怎么處理的

為了方便查看還是把上面查詢到的內容再復制一份到方法的上方

/* 
 告訴代理遠程服務器請求HTTP重定向。
 僅對缺省和臨時會話中的任務調用此方法。 后臺會話中的任務會自動跟隨重定向。
 */
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task willPerformHTTPRedirection:(NSHTTPURLResponse *)response newRequest:(NSURLRequest *)newRequest completionHandler:(void (^)(NSURLRequest *))completionHandler
{
    NSMutableURLRequest *    redirectRequest;

    redirectRequest = [newRequest mutableCopy];
 //先移除請求中的標記
    [[self class] removePropertyForKey:kOurRecursiveRequestFlagProperty inRequest:redirectRequest];
 //發送以向URL加載系統指示協議實現已重定向。
    [[self client] URLProtocol:self wasRedirectedToRequest:redirectRequest redirectResponse:response];
    [self.task cancel];
 //當加載請求由于錯誤而失敗時發送。
    [[self client] URLProtocol:self didFailWithError:[NSError errorWithDomain:NSCocoaErrorDomain code:NSUserCancelledError userInfo:nil]];
}
//請從委托憑證響應來自遠程服務器的認證請求。這是干嘛的不懂。
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential *))completionHandler
{
    BOOL        result;
    id<CustomHTTPProtocolDelegate> strongeDelegate;    
    strongeDelegate = [[self class] delegate];
    
    result = NO;
    if ([strongeDelegate respondsToSelector:@selector(customHTTPProtocol:canAuthenticateAgainstProtectionSpace:)]) {
        result = [strongeDelegate customHTTPProtocol:self canAuthenticateAgainstProtectionSpace:[challenge protectionSpace]];
    }

    if (result) {
        [self didReceiveAuthenticationChallenge:challenge completionHandler:completionHandler];
    } else {
        completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
    }
}
//告訴代理數據任務從服務器接收到初始答復
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler
{
    NSURLCacheStoragePolicy cacheStoragePolicy;

    if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
//CacheStoragePolicy中實現了CacheStoragePolicyForRequestAndResponse這個函數主要是返回一個緩沖方式
//這個函數的判斷方式基本和上面canInitWithRequest一樣的判斷方式
//一個變量反復判斷最后給出一個類型
        cacheStoragePolicy = CacheStoragePolicyForRequestAndResponse(self.task.originalRequest, (NSHTTPURLResponse *) response);
    } else {//如果不是NSHTTPURLResponse類型則報錯啦 所以基本下面的else不會執行
        assert(NO);
        cacheStoragePolicy = NSURLCacheStorageNotAllowed;
    }
    //發送以向URL加載系統指示協議實現已為請求創建了響應對象。
    //實現應使用提供的高速緩存存儲策略來確定是否將響應存儲在高速緩存中。
    [[self client] URLProtocol:self didReceiveResponse:response cacheStoragePolicy:cacheStoragePolicy];    
    completionHandler(NSURLSessionResponseAllow);
}
//告訴代理數據任務已收到一些預期的數據。
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data
{
//NSURLProtocol子類實例協議在加載數據時將此消息發送到[協議客戶端]。
    [[self client] URLProtocol:self didLoadData:data];
}
//向代理詢問數據(或上傳)任務是否應將響應存儲在緩存中。
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask willCacheResponse:(NSCachedURLResponse *)proposedResponse completionHandler:(void (^)(NSCachedURLResponse *))completionHandler
{

    completionHandler(proposedResponse);
}
//告訴代理任務完成了數據傳輸。
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
   // NSURLSession委托回調。 我們把它傳遞給客戶。
{

    if (error == nil) {
  //發送以向URL加載系統指示協議實現已完成加載。
        [[self client] URLProtocolDidFinishLoading:self];
    } else if ( [[error domain] isEqual:NSURLErrorDomain] && ([error code] == NSURLErrorCancelled) ) {
          //蘋果解釋如下
          //這發生在兩種情況:
         // 在重定向期間,在這種情況下重定向代碼已經告訴客戶端失敗
         // 如果請求被調用-stopLoading取消,在這種情況下客戶端不會想知道失敗
    } else {
        //當加載請求由于錯誤而失敗時發送。
        [[self client] URLProtocol:self didFailWithError:error];
    }

    // We don't need to clean up the connection here; the system will call, or has already called, 
    // -stopLoading to do that.
}

好吧到這里基本上核心代碼都解讀完了。。不知道大家有沒有看懂。。反正我自己是沒太懂。

總結

CustomHTTPProtocol這個Demo中有4個核心類,其中CustomHTTPProtocol為主要控制的類,首先通過canInitWithRequest:來判斷這次Request是否通過我們寫的這個協議進行處理和判斷,并且,我們攔截的Request需要打上標記 以免沖重復調用變成死循環,因為我們發起的請求會再次調用我們這次的方法,之后調用canonicalRequestForRequest:來設置我們的請求頭或者更標準化我們的請求(其中的函數封裝到了CanonicalRequest類中),之后進行初始initWithRequest:cachedResponse:client:初始化過后進行startLoading開始請求后,通過當前線程和當前Runloop進行初始化了QNSURLSessionDemux這個類,我覺得應該是把這些任務排列在了線程里面,通過QNSURLSessionDemux的代理方法再把Session的代理返回到CustomHTTPProtocol類中進行處理,處理時用到了CacheStoragePolicy類 主要作用是一個函數用來判斷什么樣的內容進行怎樣的緩存機制。。好了大概這些。。謝謝大家看自言自語到這里。。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,868評論 18 139
  • 從三月份找實習到現在,面了一些公司,掛了不少,但最終還是拿到小米、百度、阿里、京東、新浪、CVTE、樂視家的研發崗...
    時芥藍閱讀 42,356評論 11 349
  • AFHTTPRequestOperationManager 網絡傳輸協議UDP、TCP、Http、Socket、X...
    Carden閱讀 4,377評論 0 12
  • 國家電網公司企業標準(Q/GDW)- 面向對象的用電信息數據交換協議 - 報批稿:20170802 前言: 排版 ...
    庭說閱讀 11,121評論 6 13
  • 文章轉載:https://onevcat.com/2016/11/pop-cocoa-1/ (作者非常棒,建議大家...
    Buddha_like閱讀 487評論 0 0