cocoa_chen

使用NSURLProtocol切换开发环境

2016-03-20

在项目开发过程中,经常需要来回的切换开发环境(正式环境+测试环境),这里可以使用NSUserDefaults来判断当前的开发环境并做对应的处理,但是如果功能分支比较多可能会部署好几套服务器,此时可以使用NSURLProtocol方便的解决开发环境的切换功能。

前言

NSURLProtocol可以让我们重定义URL加载系统(URL Loading System),register自定义的NSURLProtocol之后,我们可以对网络请求进行处理,完成下述行为:

  • 拦截NSURLConnection或NSURLSession的网络请求
  • 忽略网络请求而返回本地缓存数据
  • HTTP Mock功能
  • 其他的全局网络请求设置

这里主要介绍如何自定义NSURLProtocol来实现开发环境的切换功能

创建NSURLProtocol并注册

1
2
3
@interface CCHTTPProtocol : NSURLProtocol

@end

在- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions方法中注册我们自定义的NSURLProtocol并设置项目中的开发环境,这里使用NSUserDefaults来模拟,你也可以使用Plist文件来加载。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch.
#warning 配置开发环境,这里仅添加两种,可以更换或添加自己项目中的环境
NSDictionary *serverDict = @{@"production": @{@"host": @"www.yourAppUrl.com",@"port":@"80"},
@"dev": @{@"host": @"www.dev.com",@"port":@"8080"}};
[[NSUserDefaults standardUserDefaults] setValue:serverDict forKey:@"ccServerInfo"];
NSString *currentServer = [[NSUserDefaults standardUserDefaults] valueForKey:@"currentServer"];
if (!currentServer || ![currentServer length]) {
//默认保存正式环境
[[NSUserDefaults standardUserDefaults] setValue:@"production" forKey:@"currentServer"];
}
[[NSUserDefaults standardUserDefaults] synchronize];
//注册URLProtocol
[NSURLProtocol registerClass:[CCHTTPProtocol class]];
return YES;
}

实现自定义的NSURLProtocol

子类时必须实现的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//定义标记key和项目接口中的特定字段
static NSString *const kHTTPURLProtocolHandledKey = @"ccURLProtocolHandledKey";
static NSString *const kAPIFilterPath = @"yourProject/api";


+ (BOOL)canInitWithRequest:(NSURLRequest *)request
{
NSString *scheme = [[request URL] scheme];
NSString *urlString = [[request URL] absoluteString];
//只处理http和https请求,并且判断是否包含网络请求中的特定字符串,如果包含则是自己的服务器,替换请求
if (([scheme caseInsensitiveCompare:@"http"] == NSOrderedSame || [scheme caseInsensitiveCompare:@"https"] == NSOrderedSame) && [urlString containsString:kAPIFilterPath]) {
//看看是否已经处理过了,防止无限循环
if ([NSURLProtocol propertyForKey:kHTTPURLProtocolHandledKey inRequest:request]) {
return NO;
}
return YES;
}
return NO;
}

这个方法指明你是否要处理request,如果不打算处理的话返回NO就行,系统会使用默认的URL加载系统处理request,如果返回YES,URL Loading System会将request的处理操作交给你的urlProtocol。上面的代码我们只处理了http和https请求(你也可以拦截ftp等请求),通常我们的接口中都会包含特定的字符串,比如我这里拦截的kAPIFilterPath,将它设置为你项目中对应的接口字段即可。还有一点需要注意,我们不能总是返回YES,这样会导致无限循环,我们可以通过+ (void)setProperty:(id)value forKey:(NSString *)key inRequest:(NSMutableURLRequest *)request方法来标记request已经被处理过,如果通过+ (nullable id)propertyForKey:(NSString *)key inRequest:(NSURLRequest *)request方法获取到已经处理过该request,则返回NO。

1
2
3
4
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request
{
return request;
}

这个方法返回标准化的request,通常都是直接返回request,当然也可以在修改request,比如替换host,添加自定义的header信息等等,这里我们直接返回request不尽兴处理

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

这个方法主要是用来判断两个request是否相同,如果相同的话可以使用缓存数据,通常调用父类的实现即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (void)startLoading
{
NSMutableURLRequest *request = [[self request] mutableCopy];
//自定义request,修改Host和Port
request = [self redirectHostAndPortInRequest:request];
//标示request已经处理过了,防止无限循环
[NSURLProtocol setProperty:@(YES) forKey:kHTTPURLProtocolHandledKey inRequest:request];
self.connection = [NSURLConnection connectionWithRequest:request delegate:self];
}
- (void)stopLoading
{
[self.connection cancel];
self.connection = nil;
}

这两个方法是loading开始和停止的地方,我们可以在这里标记已处理的request并做对应的处理操作。这里我们按照下面的代码修改request的Host和Port来达到切换环境的功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#pragma mark - Helper
//修改请求的Host和Port
- (NSMutableURLRequest *)redirectHostAndPortInRequest:(NSMutableURLRequest *)request
{
if ([request.URL host].length == 0) {
return request;
}
NSString *originURLString = [request.URL absoluteString];
NSString *originHost = [request.URL host];
NSNumber *originPort = [request.URL port];
NSRange hostRange = [originURLString rangeOfString:originHost];
if (hostRange.location == NSNotFound) {
return request;
}
//读取当前的开发环境,这里暂用NSUserDefaults做效果
NSDictionary *serverInfo = [[NSUserDefaults standardUserDefaults] valueForKey:@"ccServerInfo"];
NSString *currentServer = [[NSUserDefaults standardUserDefaults] valueForKey:@"currentServer"];
NSString *host = serverInfo[currentServer][@"host"];
NSString *port = serverInfo[currentServer][@"port"];
//重定向请求Host
NSString *urlString = [originURLString stringByReplacingCharactersInRange:hostRange withString:host];
//替换端口号
if (originPort) {
urlString = [urlString stringByReplacingOccurrencesOfString:[NSString stringWithFormat:@"%@",originPort] withString:port];
}
//修改request的URL
NSURL *url = [NSURL URLWithString:urlString];
request.URL = url;
return request;
}

实现NSURLConnectionDelegate和NSURLConnectionDataDelegate

因为我们拦截了上面的request,所以需要使用NSURLProtocol中的client这个对象将网络请求的消息返回给URL Loading System

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#pragma mark - NSURLConnectionDelegate
- (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];
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
[self.client URLProtocol:self didFailWithError:error];
}

通过上面的代码,我们已经实现了开发环境切换的功能,上述功能的完整代码点这里下载Demo

上述代码是基于NSURLConnection的,NSURLSession同样支持,不过需要改成对应的NSURLSession代码。

参考链接:

Tags: iOS
使用支付宝打赏
使用微信打赏

若你觉得我的文章对你有帮助,欢迎点击上方按钮对我打赏

扫描二维码,分享此文章