iOS NSURLProtocol详解及使用陷阱
原创 2021-01-05
如果想对网络请求进行干预,使用NSURLProtocol是一个不错的选择。NSURLProtocol是iOS URL Loading System的一个功能,它提供了便捷的接口以允许开发者重新定义网络请求的行为,包括修改请求的发起和响应动作。
在iOS网络开发中,如果有类似以下需求:
- 对全局请求增加特定的header或参数;
- 对某些资源的请求重定向、MOCK请求或使用本地缓存;
- 对请求的正常响应数据进行处理,如过滤关键字等。
使用NSURLProtocol是一个不错的方案。NSURLProtocol是iOS URL Loading System的一个功能,它提供了便捷的接口以允许开发者重新定义网络请求的行为,包括修改请求的发起和响应动作。
新建NSURLProtocol
NSURLProtocol是个抽象类,使用时必须子类化。
@interface EaseapiURLProtocol: NSURLProtocol
@end
不需要开发者自己实例化NSURLProtocol子类,而要将NSURLProtocol子类在系统中注册。有两种方式注册NSURLProtocol子类。
- 使用NSURLProtocol的registerClass接口
//注册
[NSURLProtocol registerClass:[EaseapiURLProtocol class]]
//卸载
[NSURLProtocol unregisterClass:[EaseapiURLProtocol class]];
适用于使用[NSURLSession sharedSession]创建的网络请求。
- 使用NSURLSessionConfiguration的protocolClasses接口
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration ephemeralSessionConfiguration];
configuration.protocolClasses = @[[EaseapiURLProtocol class]];
适用于自定义NSURLSession的Configuration的请求。
注册成功之后,在使用NSURLSessionTask等方式发起请求之前,URL loading system会检查所有注册过的NSURLProtocol子类,直到找到一个可以处理的NSURLProtocol子类并将请求交由该类处理。
同一个NSURLSessionConfiguration可以注册多个NSURLProtocol子类,URL loading system遍历的顺序和注册顺序相反。当一个NSURLProtocol子类能处理后,就接管了该请求,后续的NSURLProtocol子类不再执行。也就是不能保证所有注册的NSURLProtocol子类都能被执行。
NSURLProtocol的内部逻辑
NSURLProtocol开发的主要工作就是实现NSURLProtocol的接口方法,包括以下几个核心接口:
判断是否需要接管请求
+ (BOOL)canInitWithRequest:(NSURLRequest *)request;
如果需要接管请求返回YES,否则返回NO。返回NO后,URL loading system会继续查询下一个NSURLProtocol或采用原有的方式发起请求。
编辑请求
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request;
接管请求后,可以对原有请求进行修改,比如增加参数、添加header等。canonicalRequestForRequest方法允许对原有请求进行修改编辑,并返回修改后的NSURLRequest对象。利用这个方法,可以轻松实现请求重定向,映射本地资源等操作。
发起/停止请求
- (void)startLoading;
- (void)stopLoading;
由于已经接管了原来的请求,在对新请求编辑完成后,还需要负责新请求的执行。在startLoading方法中,可以使用NSURLSession等方式发起请求。示例:
- (void)startLoading {
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration];
self.task = [session dataTaskWithRequest:self.request];
[self.task resume];
}
这里可以使用任何网络请求方式,包括AFNetworking、Alamofire等。
NSURLProtocol和URL loading system通信
NSURLProtocol接管原有请求之后,需要一种方式实现和原有请求的无缝衔接,才能达到上层应用无感知的效果。
@property (nullable, readonly, retain) id <NSURLProtocolClient> client;
NSURLProtocol的client属性即是和URL loading system通信的对象。NSURLProtocolClient协议声明如下:
@protocol NSURLProtocolClient <NSObject>
- (void)URLProtocol:(NSURLProtocol *)protocol wasRedirectedToRequest:(NSURLRequest *)request redirectResponse:(NSURLResponse *)redirectResponse;
- (void)URLProtocol:(NSURLProtocol *)protocol cachedResponseIsValid:(NSCachedURLResponse *)cachedResponse;
- (void)URLProtocol:(NSURLProtocol *)protocol didReceiveResponse:(NSURLResponse *)response cacheStoragePolicy:(NSURLCacheStoragePolicy)policy;
- (void)URLProtocol:(NSURLProtocol *)protocol didLoadData:(NSData *)data;
- (void)URLProtocolDidFinishLoading:(NSURLProtocol *)protocol;
- (void)URLProtocol:(NSURLProtocol *)protocol didFailWithError:(NSError *)error;
- (void)URLProtocol:(NSURLProtocol *)protocol didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge;
- (void)URLProtocol:(NSURLProtocol *)protocol didCancelAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge;
@end
NSURLProtocolClient协议定义了接收数据、请求成功/失败、以及Authentication Challeng处理,需要在适当的时候调用。以使用NSURLSessionDataTask请求为例,在响应的回调方法中调用self.client方法。
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
{
if (error != nil) {
[self.client URLProtocol:self didFailWithError:error];
} else {
[self.client URLProtocolDidFinishLoading:self];
}
}
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data
{
[self.client URLProtocol:self didLoadData:data];
}
如何避免死循环
你也许已经意识到一个问题:既然在startLoading中也可以使用NSURLSession请求,这个拦截的请求也可能会受到NSURLProtocol的影响。这样可能造成死循环:在NSURLProtocol中发起的请求又被拦截。
为了避免死循环,可以对已经拦截的请求增加一个已处理的标记:
[NSURLProtocol setProperty:@YES forKey:URLEaseapiHandledKey inRequest:self.request];
在canInitWithRequest方法中检测到有这个标记,则不拦截:
+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
if ([NSURLProtocol propertyForKey:URLEaseapiHandledKey inRequest:request]) {
return NO;
}
return YES;
}
使用陷阱
POST请求body为空
对一个POST请求拦截时,canonicalRequestForRequest方法的入参request的HTTPBody属性为空。如果需要body内容,要使用HTTPBodyStream属性获取。
对原有请求的影响
NSURLProtocol从原理上来讲就是对原有请求拦截并反馈响应,则原有请求的缓存策略、超时等设置可能不启作用。
无法保证自己的NSURLProtocol一定会被执行
当有多个NSURLProtocol子类时,后注册至系统的会被优先执行,这就无法保证自己的NSURLProtocol子类就一定能执行。如果需求是实现网络拦截器的功能,则NSURLProtocol的功能还是有所欠缺。特别是当需要多个拦截器处理不同的业务时,NSURLProtocol就更难胜任了。
NSURLProtocol | Apple Developer Documentation
相关文章:
iOS Flutter 开发环境部署
iOS 13 Scene Delegate and multiple windows
iOS DeviceCheck详解
iOS安全:使用dumpdecrypted/Clutch 砸壳
iOS TestFlight的局限性及改进措施