最近做SDK開(kāi)發(fā)的時(shí)候昆码,為了給QA編寫(xiě)一個(gè)測(cè)試工具,方便調(diào)試和記錄請(qǐng)求內(nèi)容。但是又不想改動(dòng)已經(jīng)寫(xiě)好的SDK代碼。本來(lái)想到用methodSwizzle萧福,但是發(fā)現(xiàn)SDK要開(kāi)放一些私有的類出來(lái),太麻煩辈赋,也不方便最后的打包鲫忍。于是網(wǎng)上搜了下,如何黑魔法下系統(tǒng)的回調(diào)函數(shù)钥屈,無(wú)意中發(fā)現(xiàn)了NSURLProtocol這個(gè)牛逼玩意悟民。。篷就。所有問(wèn)題都被它給解決了射亏。。腻脏。鸦泳。
NSURLProtocol
NSURLProtocol
是 iOS里面的URL Loading System的一部分银锻,但是從它的名字來(lái)看永品,你絕對(duì)不會(huì)想到它會(huì)是一個(gè)對(duì)象,可是它偏偏是個(gè)對(duì)象击纬。鼎姐。。而且還是抽象對(duì)象
(可是OC里面沒(méi)有抽象這一說(shuō))。平常我們做網(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
這些常見(jiàn)協(xié)議,當(dāng)然也允許我們定義自己的protocol
去擴(kuò)展贡茅,或者定義自己的協(xié)議秘蛇。當(dāng)URL loading system
通過(guò)NSURLRequest
對(duì)象進(jìn)行請(qǐng)求時(shí),將會(huì)自動(dòng)創(chuàng)建NSURLProtocol
的實(shí)例(可以是自定義的)顶考。這樣我們就有機(jī)會(huì)對(duì)該請(qǐng)求進(jìn)行處理赁还。官方文檔里面介紹得比較少,下面我們直接看如何自定義NSURLProtocol
驹沿,并結(jié)合兩個(gè)簡(jiǎn)單的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è)方法主要是用來(lái)返回格式化好的request
,如果自己沒(méi)有特殊需求的話病涨,直接返回當(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)用場(chǎ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)行了攔截当悔,那么你就需要通過(guò)實(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
最常見(jiàn)的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];
//這邊就隨便你玩了伸但。肾请。可以直接返回本地的模擬數(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)
思路很簡(jiǎn)單铛铁,就是攔截請(qǐng)求URL帶.png
.jpg
.gif
的請(qǐng)求,首先去緩存里面取却妨,有的話直接返回饵逐,沒(méi)有的去請(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)行處理,那么接下來(lái)的所有相關(guān)操作都需要這個(gè)protocol
進(jìn)行管理竞思。 - 一定要注意標(biāo)記請(qǐng)求表谊,不然你會(huì)無(wú)限的循環(huán)下去。盖喷。爆办。因?yàn)橐坏┠阈枰幚磉@個(gè)請(qǐng)求,那么系統(tǒng)會(huì)創(chuàng)建你這個(gè)
protocol
的實(shí)例传蹈,然后你自己又開(kāi)啟了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
是一樣遵循的,不過(guò)里面需要改成NSURLSession
相關(guān)的東西沾歪∑可用看看官方的例子CustomHTTPProtocol