在项目开发过程中,经常需要来回的切换开发环境(正式环境+测试环境),这里可以使用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代码。
参考链接: