9

iOS NSURLProtocol详解及使用陷阱

 3 years ago
source link: https://easeapi.com/blog/blog/140-nsurlprotocol.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

如果想对网络请求进行干预,使用NSURLProtocol是一个不错的选择。NSURLProtocol是iOS URL Loading System的一个功能,它提供了便捷的接口以允许开发者重新定义网络请求的行为,包括修改请求的发起和响应动作。

140-nsurlprotocol.jpg

在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 URLSession Authentication Challenge及SSL Pinning
iOS:IDFV(identifierForVendor)使用陷阱
iOS安全:iOS APP注入动态库重打包
iOS 13 Scene Delegate and multiple windows


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK