說一說NSURLProtocol

最早接觸到NSURLProtocol應該是在三四年前,當時有了解到微信讀書好像出了一個框架,是可以實現單個接口的mock,自己研究了一下加了一點東西,通過實現匹配網絡請求,來達到,網絡請求內容讀取指定路徑的json文件,實現mock接口的操作。源碼地址:https://github.com/xindizhiyin2014/JKAPIMock

  雖說知道了當時NSURLProtocol可以實現網絡請求的攔截。但其實瞭解並不是特別多。由於最近在推動並行開發,因此對全鏈路的mock操作都進行了梳理,其中使用charles進行mock可以參考下面這篇文章《使用Charles進行mock的三種方式》 但是有的時候mock需要擺脫局域網的限制,因此charles的使用就不能夠滿足我們的需求。

   NSURLProtocol可以攔截UIwebView,NSURLConnection,NSURLSession發出的網絡請求。由於UIWebView目前已經廢棄,普通接口發送網絡請求也不再使用NSURLConnection。我這裏就不再一一多說。下面就重點說一下攔截使用NSURLSession發送網絡請求的情況。
使用NSURLSession發送網絡請求前需要進行額外的配置纔可以,具體配置如下:

[JKNetworkConfig sharedConfig].mockBaseUrl = @"https://123.com";
    [JKNetworkConfig sharedConfig].isMock = YES;
    NSDictionary *config = @{@"GET,/a1":@{}};
    [JKMockManager initMockConfig:config];
    NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
    configuration.protocolClasses = @[[JKMockURLProtocol class]];
    AFHTTPSessionManager *sessionManager = [[AFHTTPSessionManager alloc] initWithSessionConfiguration:configuration];
    AFHTTPRequestSerializer *requestSerializer = [AFHTTPRequestSerializer serializer];
    sessionManager.securityPolicy = [AFSecurityPolicy defaultPolicy];
    sessionManager.responseSerializer = [AFHTTPResponseSerializer serializer];
    NSMutableURLRequest *request = [requestSerializer requestWithMethod:@"GET" URLString:@"https://www.baidu.com/a1" parameters:nil error:nil];
   NSURLSessionTask *dataTask = [sessionManager dataTaskWithRequest:request uploadProgress:nil downloadProgress:nil completionHandler:^(NSURLResponse * _Nonnull response, id  _Nullable responseObject, NSError * _Nullable error) {
        NSLog(@"AAA %@",responseObject);
    }];
    [dataTask resume];

由於AFNetworking網絡請求底層是使用NSURLSession來實現的,這裏就使用AFnetworking框架來進行說明。來發送網絡請求之前需要配置NSURLSession的sessionConfiguration,而這個sessionConfiguration有一個屬性protocolClasses用來保存NSURLProtocol相關的子類。我這邊用了我自己實現的子類。配置好以後,JKMockURLProtocol會被自動註冊。此時發送網絡請求可以發現網絡請求已經被攔截到了。下面是JKMockURLProtocol的源碼,大家可以看看

@interface JKMockURLProtocol()

@property (nonatomic, strong) AFHTTPSessionManager *sessionManager;

@end

@implementation JKMockURLProtocol

static AFHTTPSessionManager *_jkSessionManager = nil;

- (AFHTTPSessionManager *)sessionManager
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _jkSessionManager = [[AFHTTPSessionManager alloc] initWithSessionConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];
    });
    return _jkSessionManager;
}

+ (BOOL)canInitWithRequest:(NSURLRequest *)request
{
    
    if ([self matchMethod:request]
        && [self matchQueryKeyParams:request]
        && [self matchesHeaders:request]
        && [self matchBody:request]
        ) {
        return YES;
    }
    return NO;
}

+ (NSURLRequest *) canonicalRequestForRequest:(NSURLRequest *)request {
    NSMutableURLRequest *newRequest = [request mutableCopy];
    NSString *host = newRequest.URL.host;
    NSString *url = newRequest.URL.absoluteString;
    NSNumber *port = newRequest.URL.port;
    NSString *scheme = newRequest.URL.scheme;
    NSString *baseUrl = nil;
    if (port) {
        baseUrl = [NSString stringWithFormat:@"%@://%@:%@",scheme,host,port];
    } else {
        baseUrl = [NSString stringWithFormat:@"%@://%@",scheme,host];
    }
    url = [url stringByReplacingOccurrencesOfString:baseUrl withString:[JKNetworkConfig sharedConfig].mockBaseUrl];
    NSURL *mockUrl = [NSURL URLWithString:url];
    [newRequest setURL:mockUrl];
    return newRequest;
}

- (void)startLoading
{
   NSURLSessionTask *dataTask = [[self sessionManager] dataTaskWithRequest:self.request uploadProgress:nil downloadProgress:nil completionHandler:^(NSURLResponse * _Nonnull response, id  _Nullable responseObject, NSError * _Nullable error) {
        [self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
        [self.client URLProtocolDidFinishLoading:self];
        [self.client URLProtocol:self didFailWithError:error];

    }];
    [dataTask resume];
}

- (void)stopLoading
{

}

/**
 判斷方法是否相同
 */
+ (BOOL)matchMethod:(NSURLRequest *)request
{
    NSString *httpMethod = [self getMockHttpMethodWithRequest:request];
    if (httpMethod && [httpMethod caseInsensitiveCompare:request.HTTPMethod] == NSOrderedSame) {
        return YES;
    }
    return NO;
}

/**
 匹配query參數:關鍵參數是否相同
 */
+ (BOOL)matchQueryKeyParams:(NSURLRequest *)request
{
    NSDictionary *params = [JKMockManager paramsWithURL:request.URL.absoluteString];
    NSDictionary *mockQueryParams = [self getMockQueryParamsWithRequest:request];
    for (NSDictionary *dic in mockQueryParams) {
        BOOL status = NO;
        for (NSDictionary *tmpDic in params) {
            if ([tmpDic isEqualToDictionary:dic]) {
                status = YES;
                break;
            }
        }
        if (!status) {
            return NO;
        }
    }
    return YES;
}

/**
 判斷頭關鍵參數是否相同
 */
+ (BOOL)matchesHeaders:(NSURLRequest *)request
{
    NSDictionary *mockHeaders = [self getMockHeadersWithRequest:request];
    NSDictionary *headers = request.allHTTPHeaderFields;
    for (NSDictionary *dic in mockHeaders) {
        BOOL status = NO;
        for (NSDictionary *tmpDic in headers) {
            if ([tmpDic isEqualToDictionary:dic]) {
                status = YES;
                break;
            }
        }
        if (!status) {
            return NO;
        }
    }
    return YES;
}

/**
 判斷body 只匹配關鍵參數
 */
+ (BOOL)matchBody:(NSURLRequest *)request
{
    NSDictionary *mockParams = [self getMockBodyParamsWithRequest:request];
    if (!mockParams) {
        return YES;
    }
    NSData *reqBody = request.HTTPBody;
    NSString *reqBodyString = [[NSString alloc] initWithData:reqBody encoding:NSUTF8StringEncoding];
    NSDictionary *params = [JKMockManager convertDictionaryWithURLParams:reqBodyString];
    for (NSDictionary *dic in mockParams) {
        BOOL status = NO;
        for (NSDictionary *tmpDic in params) {
            if ([tmpDic isEqualToDictionary:dic]) {
                status = YES;
                break;
            }
        }
        if (!status) {
            return NO;
        }
    }
    return YES;
}

/// 根據request獲取本地配置的需要mock的請求的請求方法
/// @param request request
+ (NSString *)getMockHttpMethodWithRequest:(NSURLRequest *)request
{
    NSString *apiName = [self getAPINameWithRequest:request];
    NSString *method = [request.HTTPMethod uppercaseString];
    return [JKMockManager mockHttpMethodWithApiName:apiName method:method];
}

/// 根據request按照制定規則解析獲取APIName
/// @param request request
+ (NSString *)getAPINameWithRequest:(NSURLRequest *)request
{
    NSString *path = [request.URL path];
    NSString *apiName = path;
    return apiName;
}

+ (NSDictionary *)getMockQueryParamsWithRequest:(NSURLRequest *)request
{
    NSString *apiName = [self getAPINameWithRequest:request];
    NSString *method = [request.HTTPMethod uppercaseString];
    return [JKMockManager mockQueryParamsWithApiName:apiName method:method];
}

+ (NSDictionary *)getMockHeadersWithRequest:(NSURLRequest *)request
{
    NSString *apiName = [self getAPINameWithRequest:request];
    NSString *method = [request.HTTPMethod uppercaseString];
    return [JKMockManager mockHeadersWithApiName:apiName method:method];
}

+ (NSDictionary *)getMockBodyParamsWithRequest:(NSURLRequest *)request
{
    NSString *apiName = [self getAPINameWithRequest:request];
    NSString *method = [request.HTTPMethod uppercaseString];
    return [JKMockManager mockBodyParamsWithApiName:apiName method:method];
}

@end

由於我這邊主要是進行攔截然後重定向到指定域名,大家也可以用來進行接口的數據緩存操作。
源碼下載地址:https://github.com/xindizhiyin2014/JKNetworking.git
更多技術乾貨文章可以掃描下方二維碼:
在這裏插入圖片描述

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