一.项目需求
项目中有个海报功能,是用UIWebView加载h5网页的形式。因为海报的使用率比较高,如果网页加载得比较慢会严重影响用户体验,因此我们想了一个方法,在用户启动APP后,如果连接了Wi-Fi,就将一些css和图片资源,先下载到本地,在加载网页内容时,优先使用本地的css和图片,以加快网页载入速度。
实现原理:
拦截所有Http请求,并伪造Http请求。我们只需要写一个继承自 NSURLProtocol 的类并注册。在Http请求开始时,系统就会通过此类对请求进行处理。
二.Demo:百度首页的logo替换为本地图片
下面demo实现:加载百度的首页,百度logo图片改用本地提前下载好的图片代替。
(一) 注册JXURLProtocol类
- 新建JXURLProtocol类,继承自NSURLProtocol
@interface JXURLProtocol : NSURLProtocol
@end
- 在加载网页前,注册JXURLProtocol类:
NSURL *url = [NSURL URLWithString:@"https://www.baidu.com"];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
[NSURLProtocol registerClass:[JXURLProtocol class]];
[self.webView loadRequest:request];
(二 )拦截请求
- 在JXURLProtocol.m中实现canInitWithRequest:方法,所有请求都会先调用此方法,返回NO表示自己需要通过自己处理,正常发出请求,否则不发出请求,因为需要经过JXURLProtocol处理。
我们需要拦截百度图片url的请求,即在canInitWithRequest:方法中,根据url判断,如果是百度logo图的url(https://m.baidu.com/static/index/plus/plus_logo.png), 返回YES,表示先不发出请求而是自己处理:
+ (BOOL)canInitWithRequest:(NSURLRequest *)request{
NSString *urlString = request.URL.absoluteString;
//此处防止重复拦截(与后面的三(3)对应)
if ([NSURLProtocol propertyForKey:@"JXProtocol" inRequest:request]) {
return NO;
}
if ([urlString isEqualToString:@"https://m.baidu.com/static/index/plus/plus_logo.png"]) {
return YES;
}
return NO;
}
- 上一步返回YES后,会继续执行方法:
- (void)startLoading{}
(三) 返回本地图片
- 根据请求的url获取本地对应的图片:
- (NSData*)imageDataWithUrl:(NSURL*)url{
if ([url.absoluteString isEqualToString:@"https://m.baidu.com/static/index/plus/plus_logo.png"]) {
NSString *filePath = [[NSBundle mainBundle] pathForResource:@"long" ofType:@"png"];
NSData *data = [NSData dataWithContentsOfFile:filePath];
return data;
}
return nil;
}
- 尝试获取本地图片,获取成功后,构建响应头,回调请求成功。
- (void)startLoading{
NSData *imageData = [self imageDataWithUrl:self.request.URL];
if (imageData) {
//获取本地图片成功
//构建请求头
NSString *mimeType = @"image/jpeg";
NSMutableDictionary *header = [NSMutableDictionary dictionary];
NSString *contentType = [mimeType stringByAppendingString:@";chartset=UTF-8"];
header[@"Content-type"] = contentType;
header[@"Content-Length"] = [NSString stringWithFormat:@"%ld",imageData.length];
NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:self.request.URL
statusCode:200 HTTPVersion:@"1.1" headerFields:header];
//回调
[self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageAllowed];
[self.client URLProtocol:self didLoadData:imageData];
[self.client URLProtocolDidFinishLoading:self];
}else{
//获取本地图片失败
}
}
- 如果获取本地图片失败,设置标志防止重复拦截,并重新构建请求来请求线上的资源:
- (void)startLoading{
NSData *imageData = [self imageDataWithUrl:self.request.URL];
if (imageData) {
//获取本地图片成功
//...
}else{
[NSURLProtocol setProperty:@(YES) forKey:@"JXProtocol" inRequest:self.request];
NSMutableURLRequest *newRequset = [self.request mutableCopy];
NSURLSessionConfiguration *config = [NSURLSessionConfiguration ephemeralSessionConfiguration];
NSURLSession *session = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:[NSOperationQueue mainQueue]];
NSURLSessionTask *task = [session dataTaskWithRequest:newRequset];
[task resume];
}
}
- 最后补上实现必须的方法:
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request {
return request;
}
- (void)stopLoading{
}
#pragma mark- NSURLConnectionDelegate
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
[self.client URLProtocol:self didFailWithError:error];
}
#pragma mark - NSURLConnectionDataDelegate
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
[self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
[self.client URLProtocol:self didLoadData:data];
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
[self.client URLProtocolDidFinishLoading:self];
}
- 运行APP,百度的logo已经换成本地的long.png,小恐龙了:
完整代码:https://github.com/dolacmeng/NSURLProtocolDemo
参考资料:
https://developer.apple.com/documentation/foundation/url_loading_system
https://github.com/draveness/analyze/blob/master/contents/OHHTTPStubs/iOS 开发中使用 NSURLProtocol 拦截 HTTP 请求.md