这篇文章介绍了些什么?
通过这篇文章,你将会了解到一种对原代码毫无入侵的网络请求性能监控方案NSURLProtocol
以及:
1.NSURLProtocol是什么和其在URL Loading System中的作用
2.NSURLProtocol中最重要的几个API介绍
3.如何在集成AFNetworking等第三方网络库的项目中使用NSURLProtocol
4.如何通过NSURLProtocol处理自定义的scheme,而不发送真正的网络请求
一、什么是NSURLProtocol?
1.URL Loading System
援引一段官网介绍:
注:本文出现的中文版Apple文档均为Google翻译结果,可能有些语病,但基本上不影响阅读和理解,想阅读原文,可以点击本文提供的超链
简而言之:URL Loading System的作用就是与服务器进行通信
2.URL Loading System中的Protocol
NSURLProtocol作为Client和Server的中间层,接收Client发送的Request,将其发送至Server端,并接收Server端发送的Response,将数据传回Client端
2.NSURLProtocol官方文档
概要:NSURLProtocol虽然命名为Protocol但其实它是一个抽象类,正如文档介绍所说,不要直接实例化NSURLProtocol,正确的使用姿势是,创建NSURLProtocol的子类,通过
registerClass:
方法将其注册到URL Loading System中,系统会创建协议对象来处理相应的URL请求,我们可以通过NSURLProtocol提供的API接口,来达到存储和检索特定协议的请求数据的目的。
3.NSURLProtocol适用的场景
①基于URL Loading System的网络请求
②UIWebView
二、NSURLProtocol的工作流程
1.核心API介绍
NSURLProtocol.h
①拦截请求并发送
在URL Loading System发送网络请求后,通过protocol中拦截请求并处理,再手动的将请求发送出去
@interface NSURLProtocol : NSObject
//所有注册此Protocol的请求都会经过这个方法的判断
+ (BOOL)canInitWithRequest:(NSURLRequest *)request;
//对需要拦截的请求进行自定的处理
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request;
/**
初始化protocol实例,所有来源的请求都以NSURLRequest形式接收
@param client The NSURLProtocolClient object that serves as the
interface the protocol implementation can use to report results back
to the URL loading system
*/
- (id)initWithRequest:(NSURLRequest *)request cachedResponse:(NSCachedURLResponse *)cachedResponse client:(id <NSURLProtocolClient>)client;
/**
开始请求
在这里需要我们手动的把请求发出去,可以使用原生的NSURLSessionDataTask,也可以使用的第三方网络库
同时设置"NSURLSessionDataDelegate"协议,接收Server端的响应
*/
- (void)startLoading;
//请求被停止
- (void)stopLoading;
②接收响应并转发
当接收到Server端的响应时,将其通过"NSURLProtocolClient"协议,转发给URL Loading System
@protocol NSURLProtocolClient <NSObject>
//发生重定向时
- (void)URLProtocol:(NSURLProtocol *)protocol wasRedirectedToRequest:(NSURLRequest *)request redirectResponse:(NSURLResponse *)redirectResponse;
//接收到响应时
- (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;
"NSURLProtocolClient"的协议方法与"NSURLSessionDataDelegate"的协议方法基本上是一一对应的
以- (void)URLProtocol:(NSURLProtocol *)protocol didLoadData:(NSData *)data;
为例:
/**
1️⃣我们在`- (void)startLoading`方法中,实现了"NSURLSessionDataTask"的协议
在接收到Sever端的响应时,首先响应"NSURLSessionDataTask"的协议方法
*/
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data
{
//2️⃣通过NSURLProtocolClient转发给URL Loading System
[[self client] URLProtocol:self didLoadData:data];
}
③注册Protocol到URL loading system
在实现上述方式后,通过[NSURLProtocol registerClass:self]
将protocol注册到URL loading system中
2.注意事项
①线程同步问题
URL Loading System发出请求与接收响应要在同一线程
//创建NSURLSessionDataTask实例
- (instancetype)initWithTask:(NSURLSessionDataTask *)task delegate:(id<NSURLSessionDataDelegate>)delegate modes:(NSArray *)modes
{
self = [super init];
if (self != nil) {
//在开始请求前记录当前线程
self->_thread = [NSThread currentThread];
}
return self;
}
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data
{
//在接收响应时,同步到发送请求时线程
[self performSelector:@selector(performBlockOnClientThread:) onThread:self.thread withObject:[block copy] waitUntilDone:NO modes:self.modes];
}
②AFNetworking、Alamofire等第三方网络库发出的网络请求无法进入到自定义NSURLProtocol的问题
以AFNetworking举例:
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration ephemeralSessionConfiguration];
NSURLSession * session = [NSURLSession sessionWithConfiguration:configuration];
以NSURLSessionConfiguration
作为参数初始化的NSURLSession
,在configuration
对象中有属性protocolClasses
@property (nullable, copy) NSArray<Class> *protocolClasses;
也就是说,我们监控网络是通过注册NSURLProtocol来实现的,但是通过sessionWithConfiguration:
得到的session
,它的configuration中已经有一个NSURLProtocol,所以它不会走我们的protocol中来!
解决方案:
创建NSURLSessionConfiguration的子类,hook
protocolClasses
get方法,将我们的protocol返回
- (NSArray *)protocolClasses {
// 如果还有其他的监控protocol,也可以在这里加进去
return @[[CustomHTTPProtocol class]];
}
3.两种场景
①真正的网络请求
②模拟的网络请求
在
- (void)startLoading
中不发送真正的网络请求,而是对自定义的请求进行解析!
比如通过自定协议实现页面的跳转。在实际业务中,会有这样的需求,前端页调起APP,回传给客户端一个自定的协议,客户端解析协议,并跳转至对应页,假如自定协议:gcsdeveloper://handoff/openControllerA
我们可以通过Protocol实现类似URL请求的方式来跳转至ControllerA
只需要两步:
1.在canInitWithRequest
中判断Scheme == gcsdeveloper
时返回YES,拦截请求
2.在startLoading
中发现是自定义协议,不发送真正的网络请求,而且走自定协议的解析流程
三、NSURLProtocol的应用
统计APP内所有网络请求的失败率、响应时间等数据
通过拦截后统一重发,可以在对应时机添加方法,在这个过程中记录各类数据
其他应用
1.防止DNS劫持
2.自定义请求和响应
3.本地Mock数据
4.网络的缓存处理
5.重定向网络请求
6.过滤掉一些非法请求
7.使UIWebView的网络图片也享受XXWebImage的图片缓存池
四、NSURLProtocol的不足
1.并不是真正的拦截了所有的网络请求,比如WKWebview、AVPlayer等发出的网络请求并不能被拦截
2.系统没有提供对个性化配置的网络请求的兼容,需要手动处理(Hook)
3.增加一定的网络延迟,主要在于方法间的调用
五、参考内容
官方文档
相关文章
iOS H5容器的一些探究(二):iOS下的黑魔法NSURLProtocol
iOS应用内抓包、NSURLProtocol 拦截 APP 内的网络请求
iOS 开发中使用 NSURLProtocol 拦截 HTTP 请求
特别鸣谢:谷歌翻译