理解NSURLProtocol

什么是NSURLProtocol?一句话概括:NSURLProtocol就是一个苹果允许的中间人攻击。使用NSURLProtocol,你不必改动应用在网络调用上的其他部分,即可改变URL加载行为的全部细节,有如下应用场景:

  • 拦截图片加载请求,转为从本地文件加载
  • 为了测试对HTTP返回内容进行mock和stub
  • 对发出请求的header进行格式化
  • 对发出的媒体请求进行签名
  • 创建本地代理服务,用于数据变化时对URL请求的更改
  • 故意制造畸形或非法返回数据来测试程序的鲁棒性
  • 在既有协议基础上完成对NSURLConnection的实现且与原逻辑不产生矛盾

NSURLProtocol是一个抽象类,用户需要自己使用并继承,对它定义的几个重要方法进行说明:

// `canInitWithRequest:`是整个逻辑的入口,
// 当它返回`YES`时,URL Loading System会创建一个对应的实例,意味着该请求就会被该Protocol实例控制
// 当它返回`NO`时,则直接跳入下一个Protocol
+ (BOOL)canInitWithRequest:(NSURLRequest *)request;
// 如果想要用特定的某个方式来修改一个请求,应该使用该方法。
// 一般来说,每一个subclass都应该依据某一个规范,一个protocol应该保证只有唯一的规范范式
//
// 补充:具体来说,`canonicalRequestForRequest`返回值会作为Protocol实例
// 构造器`initWithRequest:cachedResponse:client:`的参数
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request;
// 这几个方法允许添加、获取、删除一个request对象的任意metedata,而不需要私有扩展或者method swizzling。
// Objective-C中,通过extension可以为NSURLRequest新增方法,但是不能直接新增属性;如何解决这个问题?
// 一种很自然能想到的方案是:关联对象。
// 而这几个方法提供了另外一种方案。相较于关联对象,这种方式更干净,且更简单,且与Runtime无关。
+ (id)propertyForKey:(NSString *)key inRequest:(NSURLRequest *)request;
+ (void)setProperty:(id)value forKey:(NSString *)key inRequest:(NSMutableURLRequest *)request;
+ (void)removePropertyForKey:(NSString *)key inRequest:(NSMutableURLRequest *)request;
// Designated Initializer
// URL Loading System使用该构造器来构建Protocol实例,关于这几个参数:
// * 每一个Protocol实例对应一个request
// * 每一个Protocol实例对应一个client,该client用于和URL Loading System进行通信
- (instancetype)initWithRequest:(NSURLRequest *)request
cachedResponse:(NSCachedURLResponse *)cachedResponse
client:(id <NSURLProtocolClient>)client;
// 这两个方法顾名思义是与loading request有关,前者开启一个request请求,后者则结束/取消。
// 在自定义URLProtocol时,这两个方法是必须要实现的。简单来说,在实现时:
// 1. `startLoading`的实现逻辑里,要启动一个request,对于`NSURLConnection`而言,要创建一个`NSURLConnection`对象,譬如:
// self.connection = [[NSURLConnection alloc] initWithRequest:request delegate:self];
// 2. `stopLoading`的实现逻辑里,一般都是cancel掉request,譬如:
// [self.connection cancel];
//
// `startLoading`中构建connection的request从哪里来?在构建protocol实例时,URL Loading System会传入一个request...
- (void)startLoading;
- (void)stopLoading;
// 创建一个NSURLProtocol子类后,需要使用`registerClass:`注册到URL Loading System,
// 当URL Loading System开始load request时,会询问已注册的Protocol是否要处理对应的request,
// 如果处理(`canInitWithRequest:`的返回值),则会初始化该Protocol对应的实例,以便后续处理。
// 需要注意的是:并不是所有已注册的Protocol都会被询问,URL Loading System根据Protocols的注册
// 顺序逆序询问,当某个Protocol的`canInitWithRequest:`返回`YES`时,后续的Protocol便不再被询问。
//
// P.S: 关于逆序询问,Doc的描述:Classes are consulted in the reverse order of their registration。
// P.S: 如何理解「当URL Loading System开始load request时」呢?对于`NSURLConnection`框架而言,
// 这指的是一个`NSURLConnection`实例被创建了。
+ (BOOL)registerClunregisterClassass:(Class)protocolClass;
+ (void)unregisterClass:(Class)protocolClass;
/**************** NSURLProtocol实例的常用属性 ****************/
// 上文已有描述,此处不再赘述 */
@property (readonly, copy) NSURLRequest *request;
// client用来干嘛?其作用主要是与URL Loading System进行通信,它的方法和`NSURLConnectionDelegate`非常类似
// 用户需要做的事情是在合适的时候调用其中的方法...
@property (nullable, readonly, retain) id <NSURLProtocolClient> client;

一些注意事项:

  • 当某个URL Protocol接收对某request的处理(即+canInitWithRequest:返回YES)时,它通常会创建一个新的NSURLConnection实例,这可能又会触发该URL Protocol的+canInitWithRequest:被调用,如果仍然返回YES,则会陷入死循环,所以通常的处理是在startLoading里,对request附上一个flag,以确保在进入+canInitWithRequest:时能够被甄别出来并进行过滤处理。

参考: