最近做SDK開發(fā)的時(shí)候,為了給QA編寫一個(gè)測(cè)試工具,方便調(diào)試和記錄請(qǐng)求內(nèi)容。但是又不想改動(dòng)已經(jīng)寫好的SDK代碼。本來想到用methodSwizzle,但是發(fā)現(xiàn)SDK要開放一些私有的類出來,太麻煩,也不方便最后的打包。于是網(wǎng)上搜了下,如何黑魔法下系統(tǒng)的回調(diào)函數(shù),無意中發(fā)現(xiàn)了NSURLProtocol這個(gè)牛逼玩意。。。所有問題都被它給解決了。。。。
NSURLProtocol
NSURLProtocol是 iOS里面的URL Loading System的一部分,但是從它的名字來看,你絕對(duì)不會(huì)想到它會(huì)是一個(gè)對(duì)象,可是它偏偏是個(gè)對(duì)象。。。而且還是抽象對(duì)象(可是OC里面沒有抽象這一說)。平常我們做網(wǎng)絡(luò)相關(guān)的東西基本很少碰它,但是它的功能卻強(qiáng)大得要死。
- 可以攔截UIWebView,基于系統(tǒng)的NSUIConnection或者NSUISession進(jìn)行封裝的網(wǎng)絡(luò)請(qǐng)求。
- 忽略網(wǎng)絡(luò)請(qǐng)求,直接返回自定義的Response
- 修改request(請(qǐng)求地址,認(rèn)證信息等等)
- 返回?cái)?shù)據(jù)攔截
- 干你想干的。。。
對(duì)URL Loading System不清楚的,可以看看下面這張圖,看看里面有哪些類:

# iOS中的 NSURLProtocol
URL loading system 原生已經(jīng)支持了http,https,file,ftp,data這些常見協(xié)議,當(dāng)然也允許我們定義自己的protocol去擴(kuò)展,或者定義自己的協(xié)議。當(dāng)URL loading system通過NSURLRequest對(duì)象進(jìn)行請(qǐng)求時(shí),將會(huì)自動(dòng)創(chuàng)建NSURLProtocol的實(shí)例(可以是自定義的)。這樣我們就有機(jī)會(huì)對(duì)該請(qǐng)求進(jìn)行處理。官方文檔里面介紹得比較少,下面我們直接看如何自定義NSURLProtocol,并結(jié)合兩個(gè)簡單的demo看下如何使用。
NSURLProtocol的創(chuàng)建
首先是繼承系統(tǒng)的NSURLProtocol:
@interface CustomURLProtocol : NSURLProtocol
@end
在AppDelegate里面進(jìn)行注冊(cè)下:
[NSURLProtocol registerClass:[CustomURLProtocol class]];
這樣,我們就完成了協(xié)議的注冊(cè)。
子類NSURLProtocol必須實(shí)現(xiàn)的方法
+ (BOOL)canInitWithRequest:(NSURLRequest *)request;
這個(gè)方法是自定義protocol的入口,如果你需要對(duì)自己關(guān)注的請(qǐng)求進(jìn)行處理則返回YES,這樣,URL loading system將會(huì)把本次請(qǐng)求的操作都給了你這個(gè)protocol。
+ (BOOL)canInitWithRequest:(NSURLRequest *)request;
這個(gè)方法主要是用來返回格式化好的request,如果自己沒有特殊需求的話,直接返回當(dāng)前的request就好了。如果你想做些其他的,比如地址重定向,或者請(qǐng)求頭的重新設(shè)置,你可以copy下這個(gè)request然后進(jìn)行設(shè)置。
+ (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b;
這個(gè)方法用于判斷你的自定義reqeust是否相同,這里返回默認(rèn)實(shí)現(xiàn)即可。它的主要應(yīng)用場景是某些直接使用緩存而非再次請(qǐng)求網(wǎng)絡(luò)的地方。
- (void)startLoading;
- (void)stopLoading;
這個(gè)兩個(gè)方法很明顯是請(qǐng)求發(fā)起和結(jié)束的地方。
實(shí)現(xiàn)NSURLConnectionDelegate和NSURLConnectionDataDelegate
如果你對(duì)你關(guān)注的請(qǐng)求進(jìn)行了攔截,那么你就需要通過實(shí)現(xiàn)NSURLProtocolClient這個(gè)協(xié)議的對(duì)象將消息轉(zhuǎn)給URL loading system,也就是NSURLProtocol中的client這個(gè)對(duì)象。看看這個(gè)NSURLProtocolClient里面的方法:
- (void)URLProtocol:(NSURLProtocol *)protocol wasRedirectedToRequest:(NSURLRequest *)request redirectResponse:(NSURLResponse *)redirectResponse;
- (void)URLProtocol:(NSURLProtocol *)protocol cachedResponseIsValid:(NSCachedURLResponse *)cachedResponse;
- (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 didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge;
- (void)URLProtocol:(NSURLProtocol *)protocol didCancelAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge;
你會(huì)發(fā)現(xiàn)和NSURLConnectionDelegate很像。其實(shí)就是做了個(gè)轉(zhuǎn)發(fā)的操作。
具體的看下兩個(gè)demo
最常見的http請(qǐng)求,返回本地?cái)?shù)據(jù)進(jìn)行測(cè)試
static NSString * const hasInitKey = @"JYCustomDataProtocolKey";
@interface JYCustomDataProtocol ()
@property (nonatomic, strong) NSMutableData *responseData;
@property (nonatomic, strong) NSURLConnection *connection;
@end
@implementation JYCustomDataProtocol
+ (BOOL)canInitWithRequest:(NSURLRequest *)request
{
if ([NSURLProtocol propertyForKey:hasInitKey inRequest:request]) {
return NO;
}
return YES;
}
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request {
NSMutableURLRequest *mutableReqeust = [request mutableCopy];
//這邊可用干你想干的事情。。更改地址,或者設(shè)置里面的請(qǐng)求頭。。
return mutableReqeust;
}
- (void)startLoading
{
NSMutableURLRequest *mutableReqeust = [[self request] mutableCopy];
//做下標(biāo)記,防止遞歸調(diào)用
[NSURLProtocol setProperty:@YES forKey:hasInitKey inRequest:mutableReqeust];
//這邊就隨便你玩了。??梢灾苯臃祷乇镜氐哪M數(shù)據(jù),進(jìn)行測(cè)試
BOOL enableDebug = NO;
if (enableDebug) {
NSString *str = @"測(cè)試數(shù)據(jù)";
NSData *data = [str dataUsingEncoding:NSUTF8StringEncoding];
NSURLResponse *response = [[NSURLResponse alloc] initWithURL:mutableReqeust.URL
MIMEType:@"text/plain"
expectedContentLength:data.length
textEncodingName:nil];
[self.client URLProtocol:self
didReceiveResponse:response
cacheStoragePolicy:NSURLCacheStorageNotAllowed];
[self.client URLProtocol:self didLoadData:data];
[self.client URLProtocolDidFinishLoading:self];
}
else {
self.connection = [NSURLConnection connectionWithRequest:mutableReqeust delegate:self];
}
}
- (void)stopLoading
{
[self.connection cancel];
}
#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.responseData = [[NSMutableData alloc] init];
[self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
[self.responseData appendData:data];
[self.client URLProtocol:self didLoadData:data];
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
[self.client URLProtocolDidFinishLoading:self];
}
UIWebView圖片緩存解決方案(結(jié)合SDWebImage)
思路很簡單,就是攔截請(qǐng)求URL帶.png .jpg .gif的請(qǐng)求,首先去緩存里面取,有的話直接返回,沒有的去請(qǐng)求,并保存本地。
static NSString * const hasInitKey = @"JYCustomWebViewProtocolKey";
@interface JYCustomWebViewProtocol ()
@property (nonatomic, strong) NSMutableData *responseData;
@property (nonatomic, strong) NSURLConnection *connection;
@end
@implementation JYCustomWebViewProtocol
+ (BOOL)canInitWithRequest:(NSURLRequest *)request
{
if ([request.URL.scheme isEqualToString:@"http"]) {
NSString *str = request.URL.path;
//只處理http請(qǐng)求的圖片
if (([str hasSuffix:@".png"] || [str hasSuffix:@".jpg"] || [str hasSuffix:@".gif"])
&& ![NSURLProtocol propertyForKey:hasInitKey inRequest:request]) {
return YES;
}
}
return NO;
}
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request {
NSMutableURLRequest *mutableReqeust = [request mutableCopy];
//這邊可用干你想干的事情。。更改地址,提取里面的請(qǐng)求內(nèi)容,或者設(shè)置里面的請(qǐng)求頭。。
return mutableReqeust;
}
- (void)startLoading
{
NSMutableURLRequest *mutableReqeust = [[self request] mutableCopy];
//做下標(biāo)記,防止遞歸調(diào)用
[NSURLProtocol setProperty:@YES forKey:hasInitKey inRequest:mutableReqeust];
//查看本地是否已經(jīng)緩存了圖片
NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
NSData *data = [[SDImageCache sharedImageCache] diskImageDataBySearchingAllPathsForKey:key];
if (data) {
NSURLResponse *response = [[NSURLResponse alloc] initWithURL:mutableReqeust.URL
MIMEType:[NSData sd_contentTypeForImageData:data]
expectedContentLength:data.length
textEncodingName:nil];
[self.client URLProtocol:self
didReceiveResponse:response
cacheStoragePolicy:NSURLCacheStorageNotAllowed];
[self.client URLProtocol:self didLoadData:data];
[self.client URLProtocolDidFinishLoading:self];
}
else {
self.connection = [NSURLConnection connectionWithRequest:mutableReqeust delegate:self];
}
}
- (void)stopLoading
{
[self.connection cancel];
}
#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.responseData = [[NSMutableData alloc] init];
[self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
[self.responseData appendData:data];
[self.client URLProtocol:self didLoadData:data];
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
UIImage *cacheImage = [UIImage sd_imageWithData:self.responseData];
//利用SDWebImage提供的緩存進(jìn)行保存圖片
[[SDImageCache sharedImageCache] storeImage:cacheImage
recalculateFromImage:NO
imageData:self.responseData
forKey:[[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL]
toDisk:YES];
[self.client URLProtocolDidFinishLoading:self];
}
注意點(diǎn):
- 每次只能只有一個(gè)
protocol進(jìn)行處理,如果有多個(gè)自定義protocol,系統(tǒng)將采取你registerClass的倒序進(jìn)行調(diào)用,一旦你需要對(duì)這個(gè)請(qǐng)求進(jìn)行處理,那么接下來的所有相關(guān)操作都需要這個(gè)protocol進(jìn)行管理。 - 一定要注意標(biāo)記請(qǐng)求,不然你會(huì)無限的循環(huán)下去。。。因?yàn)橐坏┠阈枰幚磉@個(gè)請(qǐng)求,那么系統(tǒng)會(huì)創(chuàng)建你這個(gè)
protocol的實(shí)例,然后你自己又開啟了connection進(jìn)行請(qǐng)求的話,又會(huì)觸發(fā)URL Loading system的回調(diào)。系統(tǒng)給我們提供了+ (void)setProperty:(id)value forKey:(NSString *)key inRequest:(NSMutableURLRequest *)request;和+ (id)propertyForKey:(NSString *)key inRequest:(NSURLRequest *)request;這兩個(gè)方法進(jìn)行標(biāo)記和區(qū)分。
文章中的示例代碼點(diǎn)這里進(jìn)行下載JYNSURLPRotocolDemo
上面都是基于NSURLConnection的例子,iOS7之后的NSURLSession是一樣遵循的,不過里面需要改成NSURLSession相關(guān)的東西??捎每纯垂俜降睦?a target="_blank" rel="nofollow">CustomHTTPProtocol