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 许可协议进行许可,转载请注明出处!