iOS开发技巧iOS Blog常用的第三方

NSURLProtocol全攻略

2017-02-15  本文已影响1840人  84a6eed103c0

title: NSURLProtocol 全攻略
author: 全凯
description: NSURLProtocol是URL Loading System的重要组成部分,具有非常强大的功能,本文全面介绍了NSURLProtocol的方方面面。
categories: iOS
date: 2017/02/15
tags:


一位著名的iOS大神Mattt Thompson在http://nshipster.com/nsurlprotocol/ 博客里说过,说“NSURLProtocol is both the most obscure and the most powerful part of the URL Loading System.”NSURLProtocol是URL Loading System中功能最强大也是最晦涩的部分。

这句话给了NSURLProtocol一个非常准确的定性。NSURLProtocol作为URL Loading System中的一个独立部分存在,能够拦截所有的URL Loading System发出的网络请求,拦截之后便可根据需要做各种自定义处理,是iOS网络层实现AOP(面向切面编程)的终极利器,所以功能和影响力都是非常强大的。但是关于NSURLProtocol的文档非常少,文档陈旧,包括苹果官方的文档也介绍得比较简单。而且,对于NSURLProtocol的使用,有坑的地方非常多。所以说它也是晦涩的并且是危险的。


什么是 NSURLProtocol

NSURLProtocol是URL Loading System的重要组成部分。
首先虽然名叫NSURLProtocol,但它却不是协议。它是一个抽象类。我们要使用它的时候需要创建它的一个子类。
NSURLProtocol在iOS系统中大概处于这样一个位置:

NSURLProtocol能拦截哪些网络请求

NSURLProtocol能拦截所有基于URL Loading System的网络请求。
这里先贴一张URL Loading System的图:


所以,可以拦截的网络请求包括NSURLSession,NSURLConnection以及UIWebVIew。
基于CFNetwork的网络请求,以及WKWebView的请求是无法拦截的。
现在主流的iOS网络库,例如AFNetworking,Alamofire等网络库都是基于NSURLSession或NSURLConnection的,所以这些网络库的网络请求都可以被NSURLProtocol所拦截。
还有一些年代比较久远的网络库,例如ASIHTTPRequest,MKNetwokit等网路库都是基于CFNetwork的,所以这些网络库的网络请求无法被NSURLProtocol拦截。


使用 NSURLProtocol

如上文所说,NSURLProtocol是一个抽象类。我们要使用它的时候需要创建它的一个子类。

@interface CustomURLProtocol : NSURLProtocol

使用NSURLProtocol的主要可以分为5个步骤:
注册—>拦截—>转发—>回调—>结束

注册:

对于基于NSURLConnection或者使用[NSURLSession sharedSession]创建的网络请求,调用registerClass方法即可。

[NSURLProtocol registerClass:[NSClassFromString(@"CustomURLProtocol") class]];

对于基于NSURLSession的网络请求,需要通过配置NSURLSessionConfiguration对象的protocolClasses属性。

NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration];
sessionConfiguration.protocolClasses = @[[NSClassFromString(@"CustomURLProtocol") class]];

拦截:

在拦截到网络请求后,NSURLProtocol会依次执行下列方法:

+ (BOOL)canInitWithRequest:(NSURLRequest *)request

该方法会拿到request的对象,我们可以通过该方法的返回值来筛选request是否需要被NSURLProtocol做拦截处理。
比如:

+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
    
    NSString * scheme = [[request.URL scheme] lowercaseString];
    
    if ([scheme isEqual:@"http"]) {
        return YES;
    }
    return NO;
}

这里我们就只会拦截http的请求。

+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request

在该方法中,我们可以对request进行处理。例如修改头部信息等。最后返回一个处理后的request实例。

转发:

在拦截到网络请求,并且对网络请求进行定制处理以后。我们需要将网络请求重新发送出去。

- (id)initWithRequest:(NSURLRequest *)request cachedResponse:(NSCachedURLResponse *)cachedResponse client:(id<NSURLProtocolClient>)client

该方法会创建一个NSURLProtocol实例,这里每一个网络请求都会创建一个新的实例。

- (void)startLoading

接下来就是转发的核心方法startLoading。在该方法中,我们把处理过的request重新发送出去。至于发送的形式,可以是基于NSURLConnection,NSURLSession甚至CFNetwork。

回调:

既是面向切面的编程,就不能影响到原来网络请求的逻辑。所以上一步将网络请求转发出去以后,当收到网络请求的返回,还需要再将返回值返回给原来发送网络请求的地方。
主要需要需要调用到

[self.client URLProtocol:self didFailWithError:error];
[self.client URLProtocolDidFinishLoading:self];
[self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
[self.client URLProtocol:self didLoadData:data];

这四个方法来回调给原来发送网络请求的地方。
这里假设我们在转发过程中是使用NSURLSession发送的网络请求,那么在NSURLSession的回调方法中,我们做相应的处理即可。并且我们也可以对这些返回,进行定制化处理。

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
    if (error) {
        [self.client URLProtocol:self didFailWithError:error];
    } else {
        [self.client URLProtocolDidFinishLoading:self];
    }
}

- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler {
    [self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];

    completionHandler(NSURLSessionResponseAllow);
}

- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
    [self.client URLProtocol:self didLoadData:data];
}

结束:

在一个网络请求完全结束以后,NSURLProtocol回调用到

- (void)stopLoading

在该方法里,我们完成在结束网络请求的操作。以NSURLSession为例:

- (void)stopLoading {
    [self.session invalidateAndCancel];
    self.session = nil;
}

以上便是NSURLProtocol的基本流程。


应用:

既然NSURLProtocol功能非常强大,那么在具体开发中,会有哪些应用呢?

  • 网络请求缓存
  • 网络请求mock stub,知名的库OHHTTPStubs就是基于NSURLProtocol
  • 网络相关的数据统计
  • URL重定向
  • 配合实现HTTPDNS
  • ......

坑&注意事项:

使用NSURLProtocol碰到的坑也特别多,有的是很少有文档提及所以没有注意到的,有的甚至是至今还没解释的。下面列举一些我碰到的问题:

多个NSURLProtocol嵌套使用

若一个项目中存在多个NSURLProtocol,那么NSURLProtocol的拦截顺序跟注册的方式和顺序有关。
*对于使用registerClass方法注册的情况:
多个NSURLProtocol拦截顺序为注册顺序的反序,即后注册的的NSURLProtocol先拦截。
*对于通过配置NSURLSessionConfiguration对象的protocolClasses属性来注册的情况:
protocolClasses这个数组里只有第一个NSURLProtocol会起作用。
所以我们看到OHHTTPStubs库在注册的时候进行了这样的处理:

+ (void)setEnabled:(BOOL)enable forSessionConfiguration:(NSURLSessionConfiguration*)sessionConfig
{
    // Runtime check to make sure the API is available on this version
    if (   [sessionConfig respondsToSelector:@selector(protocolClasses)]
        && [sessionConfig respondsToSelector:@selector(setProtocolClasses:)])
    {
        NSMutableArray * urlProtocolClasses = [NSMutableArray arrayWithArray:sessionConfig.protocolClasses];
        Class protoCls = OHHTTPStubsProtocol.class;
        if (enable && ![urlProtocolClasses containsObject:protoCls])
        {
            [urlProtocolClasses insertObject:protoCls atIndex:0];
        }
        else if (!enable && [urlProtocolClasses containsObject:protoCls])
        {
            [urlProtocolClasses removeObject:protoCls];
        }
        sessionConfig.protocolClasses = urlProtocolClasses;
    }
    else
    {
        NSLog(@"[OHHTTPStubs] %@ is only available when running on iOS7+/OSX9+. "
              @"Use conditions like 'if ([NSURLSessionConfiguration class])' to only call "
              @"this method if the user is running iOS7+/OSX9+.", NSStringFromSelector(_cmd));
    }
}

就是把自己的NSURLProtocol插入到protocolClasses的第一个,进行拦截。拦截完成之后,又进行移除。

关于不能拦截WKWebView

原因是WKWebView 在独立于 app 进程之外的进程中执行网络请求,请求数据不经过主进程,因此,在 WKWebView 上直接使用 NSURLProtocol 无法拦截请求。
具体可以参考 wkwebview的那些坑这篇文章。文章也给出了不算完美的解决方案。

canInitWithRequest方法多次调用

偶尔会出现canInitWithRequest方法多次调用的情况,这个问题出现非常的奇怪,目前还不清楚原因。但是因为我们在canInitWithRequest方法中会判断是否拦截过的标记。所以这个问题不会影响到正常使用。另外还发现,当我们在进行网络请求之前把缓存清除掉,也不会出现这个问题。

使用NSURLSession的坑

在NSURLProtocol中使用NSURLSession有很多莫名其妙的问题,基本上都是系统的bug。
我们可以在http://www.openradar.me/search?query=nsurlprotocol 这里看到关于NSURLProtocol的系统bug,基本都与NSURLSession有关。比较明显的就是:

  • 拦截到的Request中的HTTPBody为nil;
  • startLoading在某些特殊情况会出现死锁;
  • 关于注册registerClass方法只适用于sharedSession创建的网络请求;
  • ……

这些问题都是在使用NSURLProtocol需要特别注意的。


总结:

NSURLProtocol的强大功能,为iOS网络开发提供了非常大的可操作空间。在商业项目中,也得到了广泛的应用,但我们在应用的同时,也要注意避免NSURLProtocol存在的问题。不过好在随着iOS系统的发展,关于NSURLProtocol的系统bug已经越来越少。

上一篇下一篇

猜你喜欢

热点阅读