iOS NSURLProtocol

前言:
NSURLProtocol是NSURLConnection的handle类, 它更像一套协议,如果遵守这套协议,网络请求Request都会经过这套协议里面的方法去处理.
再说简单点,就是对上层的URLRequest请求做拦截,并根据自己的需求场景做定制化响应处理
NSURLProtocol 能在系统执行 URLRequest前先去将URLRequest处理了一遍,如下图:

image.png

NSURLProtocol能够让你去重新定义苹果的URL加载系统 (URL Loading System)的行为,URL Loading System里有许多类用于处理URL请求,比如NSURL,NSURLRequest,NSURLConnection和NSURLSession等,如下图:

image.png

当URL Loading System使用NSURLRequest去获取资源的时候,它会创建一个NSURLProtocol子类的实例,你不应该直接实例化一个NSURLProtocol,NSURLProtocol看起来像是一个协议,但其实这是一个类,而且必须使用该类的子类,并且需要被注册。

URL loading system 原生已经支持了http,https,file,ftp,data这些常见协议,当然也允许我们定义自己的protocol去扩展,或者定义自己的协议。当URL loading system通过NSURLRequest对象进行请求时,将会自动创建NSURLProtocol的实例(可以是自定义的)。这样我们就有机会对该请求进行处理

当我创建多个session时,如下图:


image.png
Each session is associated with a delegate to receive periodic updates (or errors). The default delegate calls a completion handler block that you provide; if you choose to provide your own custom delegate, this block is not called.

在创建多个Session时,每个session 都会通过一个协议进行接受更新或者错误信息。

使用场景

不管你是通过UIWebView, NSURLConnection 或者第三方库 (AFNetworking, MKNetworkKit等),他们都是基于NSURLConnection或者 NSURLSession实现的,因此你可以通过NSURLProtocol做自定义的操作。

  • 重定向网络请求
  • 忽略网络请求,使用本地缓存
  • 自定义网络请求的返回结果
  • 一些全局的网络请求设置
拦截网络请求

定义协议

自定义协议类并继承NSURLProtocol,然后在application:didfinishLaunchingWithOptions:方法中注册该自定义的协议,一旦注册完毕后,它就可以来处理所有交付给URL Loading system的网络请求

@interface CustomURLProtocol : NSURLProtocol
@end

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    //注册protocol
    [NSURLProtocol registerClass:[CustomURLProtocol class]];
    return YES;
}

实现协议


+ (BOOL)canInitWithRequest:(NSURLRequest *)request;

这个方法主要是说明你是否打算处理对应的request,如果不打算处理,返回NO,URL Loading System会使用系统默认的行为去处理;如果打算处理,返回YES,然后你就需要处理该请求的所有东西,包括获取请求数据并返回给 URL Loading System。网络数据可以简单的通过NSURLConnection去获取,而且每个NSURLProtocol对象都有一个NSURLProtocolClient实例,可以通过该client将获取到的数据返回给URL Loading System

当你去加载一个URL资源的时候,URL Loading System会询问CustomeURLProtocol能否处理该请求,如果返回YES,URL Loading System会创建一个CustomeURLProtocol实例然后调用NSURLConnection去获取数据,然而这个也会调用URL Loading System,而你在+canInitWithRequest:方法中又总是返回YES,这样URL Loading System又会创建一个CustomeURLProtocol实现循环,为了保证每个request只被处理一次,应该在+canInitWithRequest:方法中查询request是否已经处理过,如果处理过,则返回NO
系统给我们提供了+ (void)setProperty:(id)value forKey:(NSString *)key inRequest:(NSMutableURLRequest *)request;和+ (id)propertyForKey:(NSString *)key inRequest:(NSURLRequest *)request;这两个方法进行标记和区分。

+ (BOOL)canInitWithRequest:(NSURLRequest *)request
{
    NSString *scheme = [[request URL] scheme];
    //只处理http和https请求
    if ([scheme caseInsensitiveCompare:@"http"] == NSOrderedSame || [scheme caseInsensitiveCompare:@"https"] == NSOrderedSame)
    {
        //看看是否已经处理过了,防止无线循环
        if ([NSURLProtocol propertyForKey:URLProtocolHandledKey inRequest:request]) {
            return NO;
        }
        
        return YES;
    }
    
    return NO;
}


+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request

通过该方法你可以简单的直接返回request,但可以在这里修改request,比如修改header,修改host等,并返回一个新的request,这是一个抽象方法,子类必须实现

+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request
{
    NSMutableURLRequest *mutableRequest = [request mutableCopy];
    mutableRequest = [self redirectHostInRequest:mutableRequest];
    
    return mutableRequest;
}

+ (NSMutableURLRequest *)redirectHostInRequest:(NSMutableURLRequest *)request
{
    if ([request.URL host].length == 0) {
        return request;
    }
    
    NSString *originUrl = [request.URL absoluteString];
    NSString *originHost = [request.URL host];
    
    NSRange hostRange = [originUrl rangeOfString:originHost];
    if (hostRange.location == NSNotFound) {
        return  request;
    }
    
    //定向到bing搜索主页
    NSString *ip = @"cn.bing.com";
    
    //替换域名
    NSString *urlString = [originUrl stringByReplacingCharactersInRange:hostRange withString:ip];
    NSURL *url = [NSURL URLWithString:urlString];
    request.URL = url;
    
    return request;
}

+ (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b

主要判断两个request是否相同,如果相同的话可以使用缓存数据,通常只需要调用父类的实现

+ (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b
{
    return  [super requestIsCacheEquivalent:a toRequest:b];
}

- (void)startLoading;
- (void)stopLoading;

这两个方法主要是开始和取消相应的request,而且需要标识哪些已经处理过的request

- (void)startLoading
{
    NSMutableURLRequest *mutableRequest = [[self request] mutableCopy];
    //标示改request已经处理过了,防止无限循环
    [NSURLProtocol setProperty:@YES forKey:URLProtocolHandledKey inRequest:mutableRequest];
    
    self.connection = [NSURLConnection connectionWithRequest:mutableRequest delegate:self];
}

- (void)stopLoading
{
    [self.connection cancel];
}

在协议的NSURLConnectionDataDelegate 方法中,处理网络请求的时候会调用到代理方法,我们需要将收到的消息通过client返回给 URL Loading System

如果注册了两个NSURLProtocol,执行顺序是怎样?###
Protocols的遍历是反向的,也就是最后注册的Protocol会被优先判断。
如下图, 先注册AAAA,再注册BBBB的话优先判断的是BBBB,

image.png

测试Demo

API

NSURLProtocol
  • (instancetype)initWithRequest:(NSURLRequest *)request cachedResponse:(NSCachedURLResponse *)cachedResponse client:(id<NSURLProtocolClient>)client

初始化并返回这个类;

request:NSURLProtocol对象的URL请求。 此request:NSURLProtocol被retain。
cachedResponse:请求响应的缓存; 如果请求没有现有的缓存,则可能为nil。
client:调用者用来与URL加载系统通信实现了的NSURLProtocolClient协议的对象。 此client被retain。

子类应该覆盖此方法以执行任何自定义初始化操作。 应用程序永远不应该显式调用此方法。
这是NSURLProtocol的指定的初始化方法。

  • (BOOL)registerClass:(Class)protocolClass

尝试注册NSURLProtocol的子类,使其对URL加载系统可见。唯一的失败情况是如果protocolClass不是NSURLProtocol的子类。

当URL加载系统开始加载请求时,依次查询每个注册的协议类,以查看是否可以使用指定的请求进行初始化。 当第一个NSURLProtocol子类的canInitWithRequest:方法返回YES时,这个子类将用于执行URL加载。不能保证所有注册的协议类都被查询。
所有的子类按照注册的相反顺序进行查询。

  • (void)unregisterClass:(Class)protocolClass

注销NSURLProtocol的指定子类。
调用此方法后,URL加载系统不再查询protocolClass。

  • (BOOL)canInitWithRequest:(NSURLRequest *)request
    返回NSURLProtocol的子类是否可以处理指定的请求。
    子类应检查请求,并确定此方法是否可以执行该请求的加载。

这是一个抽象的方法,子类必须实现此方法。

  • (id)propertyForKey:(NSString *)key inRequest:(NSURLRequest *)request

返回与指定的请求中指定的关键字关联的属性。如果没有该key,返回nil;
该方法为协议实现者提供了访问与NSURLRequest对象相关的特定于协议的信息的接口

  • (void)setProperty:(id)value forKey:(NSString *)key inRequest:(NSMutableURLRequest *)request

给指定的请求设置与指定键相关联的属性。
该方法用于为协议实现者提供一个用于定制与NSMutableURLRequest对象相关联的协议特定信息的接口。

  • (void)removePropertyForKey:(NSString *)key inRequest:(NSMutableURLRequest *)request

移除给指定的请求的指定key相关联的属性。

  • (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request

返回指定请求的规范版本。即返回通过request来自定义的请求

每个具体的协议实现都是由“规范”所指定的。 协议应该保证相同的输入请求总是产生相同的规范形式。

在实现此方法时应特别注意,因为请求的规范形式用于查找URL缓存中的对象,这是在NSURLRequest对象之间执行相等检查的进程。

这是一个抽象的方法,子类必须提供一个实现。
一般情况下我们会直接返回request或者是修改请求头部信息后再返回;

  • (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b

返回两个请求在做缓存的时候是否相同;
如果aRequest和bRequest在缓存的时候相同,则为YES,否则为NO。 当且仅当这些请求将被相同的协议处理并且该协议在执行特定于检查之后它们仍是等效的,才认为他们相等。

该方法的实现用来确定请求是否应被视为等效的。 子类可以覆盖此方法以提供协议特定的比较

@property(readonly, copy) NSCachedURLResponse *cachedResponse

调用者缓存的响应数据;
如果不在子类中覆盖,则此方法返回在初始化时存储的缓存响应

@property(readonly, retain) id<NSURLProtocolClient> client

调用者用来与URL加载系统通信的对象

  • (instancetype)initWithTask:(NSURLSessionTask *)task cachedResponse:(NSCachedURLResponse *)cachedResponse client:(id<NSURLProtocolClient>)client

这个方法是在其NSURLSessionTaskAdditions分类中定义的方法;文档和头文件中并没有介绍;与该方法类似的还有+ (BOOL)canInitWithTask:(NSURLSessionTask *)task方法和@property(readonly, copy) NSURLSessionTask *task方法;
这里我就不妄自猜测这些方法的作用,应该主要用在与session相关的操作上的,如果以后碰到了用法再回来添加进来

NSURLProtocol (NSURLSessionTaskAdditions)

NSURLProtocolClient协议提供NSURLProtocol子类与URL加载系统进行通信的接口。 应用程序永远不需要实现此协议

  • (void)URLProtocol:(NSURLProtocol *)protocol cachedResponseIsValid:(NSCachedURLResponse *)cachedResponse

向URL加载系统发送缓存响应有效的消息

  • (void)URLProtocol:(NSURLProtocol *)protocol didCancelAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge

向URL加载系统发送身份认证已被取消的消息

  • (void)URLProtocol:(NSURLProtocol *)protocol didFailWithError:(NSError *)error

发送加载任务因为error而失败

  • (void)URLProtocol:(NSURLProtocol *)protocol didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge

向URL加载系统发送指示已接收到身份验证。

  • (void)URLProtocol:(NSURLProtocol *)protocol didReceiveResponse:(NSURLResponse *)response cacheStoragePolicy:(NSURLCacheStoragePolicy)policy

向URL加载系统发送协议实现已经为请求创建了响应对象的消息。
实现中应该使用提供的缓存策略来确定是否将响应存储在高速缓存中。

  • (void)URLProtocol:(NSURLProtocol *)protocol wasRedirectedToRequest:(NSURLRequest *)request redirectResponse:(NSURLResponse *)redirectResponse

向URL加载系统发送协议实现已被重定向的消息。

  • (void)URLProtocolDidFinishLoading:(NSURLProtocol *)protocol

向URL加载系统发送协议实现已经完成加载的消息

NSURLProtocolClient

上面的NSURLProtocol定义了一系列加载的流程。而在每一个流程中,我们作为使用者该如何使用URL加载系统,则是NSURLProtocolClient中几个方法该做的事情。

//请求重定向

  • (void)URLProtocol:(NSURLProtocol *)protocol wasRedirectedToRequest:(NSURLRequest *)request redirectResponse:(NSURLResponse *)redirectResponse;

// 响应缓存是否合法

  • (void)URLProtocol:(NSURLProtocol *)protocol cachedResponseIsValid:(NSCachedURLResponse *)cachedResponse;

//刚接收到Response信息

  • (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;

参考:
NSURLProtocol官方文档阅读
iOS中的 NSURLProtocol
iOS H5容器的一些探究(二):iOS下的黑魔法NSURLProtocol
Document
.....

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335