iOS 开发中使用 NSURLProtocol 拦截 HTTP 请求

这篇文章会提供一种在 Cocoa 层拦截所有 HTTP 请求的方法,其实标题已经说明了拦截 HTTP 请求需要的了解的就是 NSURLProtocol。
由于文章的内容较长,会分成两部分,这篇文章介绍 NSURLProtocol 拦截 HTTP 请求的原理,另一篇文章如何进行 HTTP Mock 介绍这个原理在 OHHTTPStubs 中的应用,它是如何 Mock(伪造)某个 HTTP 请求对应的响应的。
NSURLProtocol
NSURLProtocol 是苹果为我们提供的 URL Loading System 的一部分,这是一张从官方文档贴过来的图片:

URL-loading-syste
官方文档对 NSURLProtocol 的描述是这样的:
An NSURLProtocol object handles the loading of protocol-specific URL data. The NSURLProtocol class itself is an abstract class that provides the infrastructure for processing URLs with a specific URL scheme. You create subclasses for any custom protocols or URL schemes that your app supports.
在每一个 HTTP 请求开始时,URL 加载系统创建一个合适的 NSURLProtocol 对象处理对应的 URL 请求,而我们需要做的就是写一个继承自 NSURLProtocol 的类,并通过 - registerClass: 方法注册我们的协议类,然后 URL 加载系统就会在请求发出时使用我们创建的协议对象对该请求进行处理。
这样,我们需要解决的核心问题就变成了如何使用 NSURLProtocol 来处理所有的网络请求,这里使用苹果官方文档中的 CustomHTTPProtocol 进行介绍,你可以点击这里下载源代码。
https://developer.apple.com/library/ios/samplecode/CustomHTTPProtocol/CustomHTTPProtocol.zip
在这个工程中 CustomHTTPProtocol.m 是需要重点关注的文件,CustomHTTPProtocol 就是 NSURLProtocol 的子类:
@interface CustomHTTPProtocol : NSURLProtocol
...
@end
现在重新回到需要解决的问题,也就是 如何使用 NSURLProtocol 拦截 HTTP 请求?,有这个么几个问题需要去解决:
如何决定哪些请求需要当前协议对象处理?
对当前的请求对象需要进行哪些处理?
NSURLProtocol 如何实例化?
如何发出 HTTP 请求并且将响应传递给调用者?
上面的这几个问题其实都可以通过 NSURLProtocol 为我们提供的 API 来解决,决定请求是否需要当前协议对象处理的方法是:+ canInitWithRequest:
+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
BOOL shouldAccept;
NSURL *url;
NSString *scheme;
shouldAccept = (request != nil);
if (shouldAccept) {
url = [request URL];
shouldAccept = (url != nil);
}
return shouldAccept;
}
因为项目中的这个方法是大约有 60 多行,在这里只粘贴了其中的一部分,只为了说明该方法的作用:每一次请求都会有一个 NSURLRequest 实例,上述方法会拿到所有的请求对象,我们就可以根据对应的请求选择是否处理该对象;而上面的代码只会处理所有 URL 不为空的请求。
请求经过 + canInitWithRequest: 方法过滤之后,我们得到了所有要处理的请求,接下来需要对请求进行一定的操作,而这都会在 + canonicalRequestForRequest: 中进行,虽然它与 + canInitWithRequest: 方法传入的 request 对象都是一个,但是最好不要在 + canInitWithRequest: 中操作对象,可能会有语义上的问题;所以,我们需要覆写 + canonicalRequestForRequest: 方法提供一个标准的请求对象:
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request {
return request;
}
这里对请求不做任何修改,直接返回,当然你也可以给这个请求加个 header,只要最后返回一个 NSURLRequest 对象就可以。
在得到了需要的请求对象之后,就可以初始化一个 NSURLProtocol 对象了:
- (id)initWithRequest:(NSURLRequest *)request cachedResponse:(NSCachedURLResponse *)cachedResponse client:(id )client {
return [super initWithRequest:request cachedResponse:cachedResponse client:client];
}
在这里直接调用 super 的指定构造器方法,实例化一个对象,然后就进入了发送网络请求,获取数据并返回的阶段了:
- (void)startLoading {
NSURLSession *session = [NSURLSession sessionWithConfiguration:[[NSURLSessionConfiguration alloc] init] delegate:self delegateQueue:nil];
NSURLSessionDataTask *task = [session dataTaskWithRequest:self.request];
[task resume];
}
这里使用简化了 CustomHTTPClient 中的项目代码,可以达到几乎相同的效果。
你可以在 - startLoading 中使用任何方法来对协议对象持有的 request 进行转发,包括 NSURLSession、 NSURLConnection 甚至使用 AFNetworking 等网络库,只要你能在回调方法中把数据传回 client,帮助其正确渲染就可以,比如这样:
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler {
[[self client] URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageAllowed];
completionHandler(NSURLSessionResponseAllow);
}
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
[[self client] URLProtocol:self didLoadData:data];
}
当然这里省略后的代码只会保证大多数情况下的正确执行,只是给你一个对获取响应数据粗略的认知,如果你需要更加详细的代码,我觉得最好还是查看一下
CustomHTTPProtocol中对 HTTP 响应处理的代码,也就是NSURLSessionDelegate协议实现的部分。
client 你可以理解为当前网络请求的发起者,所有的 client 都实现了 NSURLProtocolClient 协议,协议的作用就是在 HTTP 请求发出以及接受响应时向其它对象传输数据:
@protocol NSURLProtocolClient
...
- (void)URLProtocol:(NSURLProtocol *)protocol didReceiveResponse:(NSURLResponse *)response cacheStoragePolicy:(NSURLCacheStoragePolicy)policy;
- (void)URLProtocol:(NSURLProtocol *)protocol didLoadData:(NSData *)data;
- (void)URLProtocolDidFinishLoading:(NSURLProtocol *)protocol;
...
@end
当然这个协议中还有很多其他的方法,比如 HTTPS 验证、重定向以及响应缓存相关的方法,你需要在合适的时候调用这些代理方法,对信息进行传递。
如果你只是继承了 NSURLProtocol 并且实现了上述方法,依然不能达到预期的效果,完成对 HTTP 请求的拦截,你还需要在 URL 加载系统中注册当前类:
[NSURLProtocol registerClass:self];
需要注意的是
NSURLProtocol只能拦截UIURLConnection、NSURLSession和UIWebView中的请求,对于WKWebView中发出的网络请求也无能为力,如果真的要拦截来自WKWebView中的请求,还是需要实现WKWebView对应的WKNavigationDelegate,并在代理方法中获取请求。
无论是NSURLProtocol、NSURLConnection还是NSURLSession都会走底层的 socket,但是WKWebView可能由于基于 WebKit,并不会执行 C socket 相关的函数对 HTTP 请求进行处理,具体会执行什么代码暂时不是很清楚,如果对此有兴趣的读者,可以联系笔者一起讨论。
总结
如果你只想了解如何对 HTTP 请求进行拦截,其实看到这里就可以了,不过如果你想应用文章中的内容或者希望了解如何伪造 HTTP 响应,可以看下一篇文章如何进行 HTTP Mock。
References
+ NSURLProtocol
Github Repo:https://github.com/draveness/iOS-Source-Code-Analyze
Follow: https://github.com/Draveness
Source: http://draveness.me/intercept
iOS 开发中使用 NSURLProtocol 拦截 HTTP 请求的更多相关文章
- iOS进阶之使用 NSURLProtocol 拦截 HTTP 请求(转载)
这篇文章会提供一种在 Cocoa 层拦截所有 HTTP 请求的方法,其实标题已经说明了拦截 HTTP 请求需要的了解的就是 NSURLProtocol. 由于文章的内容较长,会分成两部分,这篇文章介绍 ...
- 总结iOS开发中的断点续传那些事儿
前言 断点续传概述 断点续传就是从文件赏赐中断的地方重新开始下载或者上传数据,而不是从头文件开始.当下载大文件的时候,如果没有实现断点续传功能,那么每次出现异常或者用户主动的暂停,都会从头下载,这样很 ...
- ios开发中的小技巧
在这里总结一些iOS开发中的小技巧,能大大方便我们的开发,持续更新. UITableView的Group样式下顶部空白处理 //分组列表头部空白处理 UIView *view = [[UIViewal ...
- iOS开发中常见问题集锦
在iOS开发中,会出现各种各样的问题.今天,就把这些常见的问题以及各位大牛的解决方案汇总下,方便以后查阅: 常见错误: 1. linker command failed with exit code ...
- iOS开发中获取WiFi相关信息
iOS 开发中难免会遇到很多与网络方面的判断,这里做个汇总,大多可能是与WiFi相关的. 1.Ping域名.Ping某IP 有 时候可能会遇到ping 某个域名或者ip通不通,再做下一步操作.这里的p ...
- iOS开发中打电话发短信等功能的实现
在APP开发中,可能会涉及到打电话.发短信.发邮件等功能.比如说,通常一个产品的"关于"页面,会有开发者的联系方式,理想情况下,当用户点击该电话号码时,能够自动的帮用户拨出去,就涉 ...
- iOS开发中调试小技巧
对于软件开发而言,调试是必须学会的技能,重要性不言而喻.对于调试的技能,基本上是可以迁移的,也就是说你以前在其他平台上掌握的很多调试技巧,很多也是可以用在iOS开发中.不同语言.不同IDE.不同平台的 ...
- iOS开发中的4种数据持久化方式【二、数据库 SQLite3、Core Data 的运用】
在上文,我们介绍了ios开发中的其中2种数据持久化方式:属性列表.归档解档.本节将继续介绍另外2种iOS持久化数据的方法:数据库 SQLite3.Core Data 的运 ...
- iOS开发中的MVC设计模式
我们今天谈谈cocoa程序设计中的 模型-视图-控制器(MVC)范型.我们将从两大方面来讨论MVC: 什么是MVC? M.V.C之间的交流方式是什么样子的? 理解了MVC的概念,对cocoa程序开发是 ...
随机推荐
- Android 主题动态切换框架:Prism
Prism(棱镜) 是一个全新的 Android 动态主题切换框架,虽然是头一次发布,但它所具备的基础功能已经足够强大了!本文介绍了 Prism 的各种用法,希望对你会有所帮助,你也可以对它进行扩展, ...
- JS保留两位小数 [转]
js保留2位小数toFixed(xxxx) var a = 9.39393; alert(a.toFixed()); alert(Number.toFixed(9.39393)); 返回的是9. 对于 ...
- c#里BindingFlags 筛选标志
下列 BindingFlags 筛选标志可用于定义包含在搜索中的成员: 为了获取返回值,必须指定 BindingFlags.Instance 或 BindingFlags.Static. 指定 Bin ...
- linux下socket keep alive讲解
[需求] 不影响服务器处理的前提下,检测客户端程序是否被强制终了.[现状]服务器端和客户端的Socket都设定了keepalive属性.服务器端设定了探测次数等参数,客户端.服务器只是打开了keepa ...
- 11、四大组件之二-Service高级(二)Native Service
一.Service的分类 1.1>Android Service 使用Java编写在JVM中运行的服务 1.2>Native Service 使用C/C++完成的服务,一般在系统开始时完成 ...
- C#发送简单的HTTP POST请求给传统的ASP网页。
设计思路 创建HTTPWebRequest类的一个实例,设置这个对象的Method属性为"POST",ContentType属性为"application/x-/www- ...
- Linux下配置Mysql允许远程访问
操作非常简单,就5步骤,如下: 1.进入 mysql: ? 1 /usr/local/mysql/bin/mysql -u root -p 2.使用 mysql库 : ? 1 use mysql; 3 ...
- CentOS7 安装98五笔输入法
86版的安装方式,网上找到一堆,折腾了很久才把98版的安装上,记录一下. 从这里下了这个 http://bbs.chinaunix.net/forum.php?mod=viewthread&t ...
- NOIP2005 谁拿了最多奖学金
1谁拿了最多奖学金 (scholar.pas/c/cpp) [问题描述] 某校的惯例是在每学期的期末考试之后发放奖学金.发放的奖学金共有五种,获取的条件各自不同: 1) 院士奖学金,每人800 ...
- CUDA基本概念
CUDA计算模型 CUDA中计算分为两部分,串行部分在Host上执行,即CPU,而并行部分在Device上执行,即GPU. 相比传统的C语言,CUDA增加了一些扩展,包括了库和关键字. CUDA代码提 ...