NSURLProtocol 是 URL Loading System 中的一部分, 但是它十分的强大和牛逼, 可以说是苹果又一大黑魔法了, 它能够拦截所有的
URL Loading System发出的网络请求, 拦截之后就可以做各种自定义处理. 下面就来看下这个神奇的东西。。。
什么是NSURLProtocol
NSURLProtocol是URL Loading System的一个重要组成部分, 看下官方给的结构图:

URL加载系统包括URL加载、认证&证书、配置管理、缓存管理、Cookie存储和协议支持这几个部分. NSURLProtocol看起来像是一个协议, 但是其并不是一个Protocol, 而是一个Class, 而且是一个Abstract Class. 来看下官方对它的解释:
An NSURLProtocol object handles the loading of protocol-specific URL data. The NSURLProtocol class itself is an abstract class that provides the infrastructure for processing URLs with a specific URL scheme. You create subclasses for any custom protocols or URL schemes that your app supports.
每一个HTTP请求开始时,URL Loading System就会创建一个合适的 NSURLProtocol对象处理对应的URL请求,我们不需要直接实例化一个NSURLProtocol子类, 而我们需要做的就是写一个继承自NSURLProtocol的类,并实现合适的抽象方法, 通过-registerClass:方法注册我们的协议类,然后URL Loading System就会在请求发出时使用我们创建的协议对象对该请求进行处理, 也就是说, 最后我们只需要写好继承自NSURLProtocol的子类即可.
NSURLProtocol的使用
NSURLProtocol是如何实例化的?
那么
NSURLProtocol是如何拦截HTTP请求的 ?
如何决定哪些请求需要处理哪些请求不需要处理?
对需要处理的请求做哪些操作?
如何发出
HTTP请求后并且将response传递给调用者?
首先来看下, NSURLProtocol都提供有哪些API.
1 | - (instancetype)initWithRequest:(NSURLRequest *)request cachedResponse:(nullable NSCachedURLResponse *)cachedResponse client:(nullable id <NSURLProtocolClient>)client; |
之前说到过 NSURLProtocol 是一个抽象类,所以不能够直接使用必须被子类化之后才能使用.
1 | @interface CustomURLProtocol : NSURLProtocol |
之后需要注册它
[NSURLProtocol registerClass:[CustomURLProtocol class]]
注册完成之后我们需要在子类中实现一些方法
+ (BOOL)canInitWithRequest:(NSURLRequest *)request
每一次请求都会有一个NSURLRequest实例,通过拿到请求对象,我们就可以根据对应的请求选择是否处理该对象. 这个方法主要是说明你是否打算处理对应的request,如果不处理,返回NO,URL Loading System会使用系统默认的行为去处理;如果打算处理,则返回YES
1 | + (BOOL)canInitWithRequest:(NSURLRequest *)request { |
这表示我们只处理http和https请求, 发现有一个[NSURLProtocol propertyForKey:protocolKey inRequest:request]判断, 这个方法表示这个请求是否已经被请求过了, 为了防止死循环, 这个接下来会说到
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request
在这个方法中,我们可以对request对象进行处理, 如修改头部信息、URL重定向、修改host等,最后返回一个处理后的request实例, 如果不处理我们也可以直接返回request
1 | + (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request { |
+ (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b
用于判断两个request是否相同,如果相同的话可以使用缓存数据,通常只需要调用父类的实现即可
1 | + (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b { |
- (instancetype)initWithRequest:(NSURLRequest *)request cachedResponse:(nullable NSCachedURLResponse *)cachedResponse client:(nullable id <NSURLProtocolClient>)client
该方法会创建一个NSURLProtocol实例,每一个网络请求都会创建一个新的实例, 在这里直接调用父类的方法,实例化一个对象并返回
1 | - (id)initWithRequest:(NSURLRequest *)request cachedResponse:(NSCachedURLResponse *)cachedResponse client:(id <NSURLProtocolClient>)client { |
- (void)startLoading
拦截到网络请求,并且对网络请求进行定制处理以后, 我们需要将网络请求重新发送出去, 该方法就是转发的核心方法, 在该方法中, 我们把处理过的request重新发送出去, 可以是基于NSURLConnection、NSURLSession甚至CFNetwork.
1 | - (void)startLoading { |
在这里我们又看到了[NSURLProtocol setProperty:@YES forKey:URLProtocolKey inRequest:mutableReqeust],将这个Property设为了YES, 在canInitWithRequest需要判断, 为什么我们需要做这些个操作?
因为URL Loading System在询问是否处理该请求的时候, 没加判断并返回了YES, 然后URL Loading System会创建一个CustomURLProtocol实例然后调用startLoading去获取数据, 重新发起一个请求, 然后又会走到canInitWithRequest,而在这个方法中又返回了YES,之后URL Loading System又会创建一个CustomURLProtocol实例, 然后就会出现无限循环下去. 所以我们应该保证每个request只被处理一次,所以需要通过setProperty:forKey:inRequest:标记那些已经处理过的request,然后在canInitWithRequest:中判断该request是否已经处理过了, 如果是则返回NO
如果我们拦截图片加载请求,本地有缓存的话就直接从本地加载没有再去请求, 我们可以这么做:
1 | - (void)startLoading { |
- (void)stopLoading
在请求完全结束之后取消对应的request
1 | - (void)stopLoading { |
NSURLProtocolClient
当我们把request发送出去之后, 当收到网络请求的响应,我们怎么把返回值返回给原来发送网络请求的地方呢?
这时我们需要用到client这个对象, 每一个client都实现了NSURLProtocolClient协议, 我们把response告诉client,也就是URL Loading System,让他来继续处理这个response,因为一切都是基于URL Loading System发生的,所以把response交给他,他会自动处理这个response并返回给发送request的地方
那么这个协议都有一些什么方法呢?
1 | - (void)URLProtocol:(NSURLProtocol *)protocol wasRedirectedToRequest:(NSURLRequest *)request redirectResponse:(NSURLResponse *)redirectResponse; |
每一个NSURLProtocol的子类都有一个client对象来处理请求得到的response, 我们通常这样做, 就能将收到的消息通过client返回给URL Loading System:
1 | - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response { |
因为我们是通过NSURLConnection请求的,所以需要通过其代理方法获取到数据, 在代理方法中将数据交给client,让其内部处理即可.
需要注意的地方
在使用NSURLProtocol的时候, 有几个坑是需要我们注意的:
NSURLProtocol可以拦截的网络请求包括NSURLSession,NSURLConnection以及UIWebVIew,基于CFNetwork的网络请求,以及WKWebView的请求是无法拦截的,详细可以查看这个网站.AFNetworking,Alamofire等这些第三方网络库都是基于NSURLSession或NSURLConnection的,所以这些网络库的网络请求都可以被NSURLProtocol拦截.ASIHTTPRequest等网路库都是基于CFNetwork的,所以这些网络库的网络请求都无法被NSURLProtocol拦截如果需要
NSURLProtocol来截获NSURLSession发出的请求,需要每一个NSURLSession在创建时配置的NSURLSessionConfiguration类的protocolClasses属性设置为自定义的NSURLProtocol
1 | + (NSURLSessionConfiguration *)defaultSessionConfiguration { |
苹果文档也说了You cannot use custom NSURLProtocol subclasses in conjunction with background sessions.对于后台Sessions,是不支持自定义的NSURLProtocol的
记得在
canInitWithRequest方法里判断if ([NSURLProtocol propertyForKey:protocolKey inRequest:request]) { return NO; }, 在startLoading方法里设置[NSURLProtocol setProperty:@(YES) forKey:protocolKey inRequest:request];
防止无限循环若一个项目中存在多个
NSURLProtocol,那么NSURLProtocol的拦截顺序跟注册顺序有关,多个NSURLProtocol拦截顺序为注册顺序的倒序,也就是后注册的NSURLProtocol会先被拦截. 对于通过配置NSURLSessionConfiguration对象的protocolClasses属性来注册的,protocolClasses这个数组里只有第一个NSURLProtocol会起作用,其他的都无效, 我们看下OHHTTPStubs库在注册时是如何处理的:
1 | NSMutableArray * urlProtocolClasses = [NSMutableArray arrayWithArray:sessionConfig.protocolClasses]; |
把自己的NSURLProtocol插入到protocolClasses的第一个,进行拦截, 拦截完成之后,又对其进行移除, 保证需要拦截的时候, 自己是在数组的第一个.
到这里, 对HTTP请求进行拦截就结束了, 我们通过NSURLProtocol对request做了各种神奇的事情, 但是它的强大还不止这些.
NSURLProtocol实现
了解了NSURLProtocol的使用之后, 我们再来看下它的内部实现. 这次依然用的是GNU源码.
1 |
|
在类初始化的时候会初始化一些类, 发现会默认注册了几个XXXURLProtocol, 这些类都是什么?
1 | The URL loading system provides support for accessing resources using the following protocols: |
这就是URL loading system支持的这几个协议, 但是还有一个_NSAboutURLProtocol, about协议, 这个协议应该是web浏览器一个内部的协议, 这可以显示关于浏览器的信息.
1 | + (BOOL)registerClass:(Class)protocolClass { |
这就是注册函数的实现, 其内部很简单, 如果protocolClass是NSURLProtocol的子类的话, 就往registered数组里添加, registered是一个静态全局数组. 用于存放注册的类. 而反注册则是将注册过的类直接从registered中移除.
1 | + (BOOL)canInitWithRequest:(NSURLRequest *)request { |
这三个都是抽象方法, 内部什么都没做, 需要子类覆盖实现的. 如果没有实现, - subclassResponsibility: 则会以exception形式告诉Dev子类忘记覆盖实现了
1 | + (NSURLRequest *)canonicalRequestForRequest: (NSURLRequest *)request { |
这个方法则是默认直接返回request对象, 如果子类不需要特殊处理request的话可以不用覆盖.
1 | + (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b { |
用isEqual判断, 两个request对象是不是同一个
1 | + (void)setProperty:(id)value forKey:(NSString *)key inRequest:(NSMutableURLRequest *)request { |
这个用于标记是否已经处理过请求的方法, 将value和key赋值给request, request是一个NSURLRequest对象, 其内部有一个properties可变字典, 用于存放标记过的request.
1 | - (id)initWithRequest:(NSURLRequest *)request cachedResponse:(NSCachedURLResponse *)cachedResponse client: (id <NSURLProtocolClient>)client { |
这个NSURLProtocol的初始化方法, 如果当前类是NSURLProtocol或者是NSURLProtocolPlaceholder, 则将registered注册过的类从最后一个依次往前取出来(这就是之前说的为什么后注册的会先被拦截), 如果canInitWithRequest:返回YES, 则初始化self, 如果self不为空, 将赋值reques、cachedResponse和client , 并返回.
PS: 通过对NSURLProtocol的使用和内部实现进行了学习,又get到了一个黑魔法~~~
最后更新: 2023年03月25日 22:39:54
本文链接: http://aeronxie.github.io/post/fd52cee9.html
版权声明: 本作品采用 CC BY-NC-SA 4.0 许可协议进行许可,转载请注明出处!