UIWebView (NSURLProtocol)攔截js、css
公司最近有個需求,去除h5頁面的廣告,最后實現的方式是后臺去過濾,移動端這里只需要攔截里面的一個css地址重定向就可以.開會的時候以為很簡單,畢竟 UIWebView 協議方法里面有個每次請求都會走的協議方法 - (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType 實際開發的過程當中才發現這是行不通的.
中間過程就不說了.結果肯定是可以做到的,用到了神奇的 NSURLProtocol
這里主要做下筆記:
NSURLProtocol
它是干什么的呢,是一個挺牛逼的類,它是一個抽象類,不能去實例化它,只能子類化 NSURLProtocol ,
每次在對一個 URL 進行請求的時候 URL Loading System 都會向 已經注冊的 Protocol 詢問是否可以處理該請求。這里就看出他的作用來了. 比如 : 攔截UIWebView的請求,忽略請求,重定向... ...
如何使用NSURLProtocol
-
創建
#import
@interface FilteredProtocol : NSURLProtocol
@end
-
在合適的地方注冊(demo是在 appdelegate 類中)
[NSURLProtocol registerClass:[FilteredProtocol class]];
-
取消注冊,一般在加載完成或 dealloc 方法里面取消
[NSURLProtocol unregisterClass:[FilteredProtocol class]];
-
重寫父類方法
+ (BOOL)canInitWithRequest:(NSURLRequest *)request;
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request;
+ (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b;
- (void)startLoading;
- (void)stopLoading;
一個個的說
+ (BOOL)canInitWithRequest:(NSURLRequest *)request;
這個方法是決定這個 protocol 是否可以處理傳入的 request 的如是返回 true 就代表可以處理,如果返回 false 那么就不處理這個 request 。
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request;
這個方法主要是用來返回格式化好的 request ,如果自己沒有特殊需求的話,直接返回當前的request就好了。如果你想做些其他的,比如地址重定向,或者請求頭的重新設置,你可以 cop y下這個 request 然后進行設置。
+ (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b;
該方法主要是判斷兩個請求是否為同一個請求,如果為同一個請求那么就會使用緩存數據。通常都是調用父類的該方法。
- (void)startLoading;- (void)stopLoading;
開始處理這個請求和結束處理這個請求
我們處理(攔截)好請求之后,就要開始對他經常處理,這個時候就用到了父類里面的 client 對象.
/*!
@method client
@abstract Returns the NSURLProtocolClient of the receiver.
@result The NSURLProtocolClient of the receiver.
*/
@property (nullable, readonly, retain) id
client;
他是一個協議,里面的方法和 NSURLConnection 差不多
- (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;
-
實際應用(拿我攔截css為例子)
-
需求是要去掉下面圖片上 立刻下載 的廣告:
-
我是原圖
-
這是運行后打印的log
運行ing
上圖可以看到截獲的所有的請求地址,不管是js,css還是png圖片都有
-
-
這是代碼運行后的效果
-
我是最終效果圖
-
-
代碼如下:
-
static NSString*const sourUrl = @"http://cdn.web.chelaile.net.cn/ch5/styles/main-1cb999d572.css";
static NSString*const localUrl = @"http://h5apps.scity.cn/hack/cdn.web.chelaile.net.cn/ch5/styles/main-1cb999d572.css";
static NSString*const FilteredCssKey = @"filteredCssKey";
@interface FilteredProtocol ()
@property (nonatomic, strong) NSMutableData *responseData;
@property (nonatomic, strong) NSURLConnection *connection;
@end
@implementation FilteredProtocol
+ (BOOL)canInitWithRequest:(NSURLRequest *)request
{
NSLog(@"request.URL.absoluteString = %@",request.URL.absoluteString);
//只處理http和https請求
NSString *scheme = [[request URL] scheme];
if ( ([scheme caseInsensitiveCompare:@"http"] == NSOrderedSame ||
[scheme caseInsensitiveCompare:@"https"] == NSOrderedSame ))
{
//看看是否已經處理過了,防止無限循環
if ([NSURLProtocol propertyForKey:FilteredCssKey inRequest:request])
return NO;
return YES;
}
return NO;
}
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request
{
NSMutableURLRequest *mutableReqeust = [request mutableCopy];
//截取重定向
if ([request.URL.absoluteString isEqualToString:sourUrl])
{
NSURL* url1 = [NSURL URLWithString:localUrl];
mutableReqeust = [NSMutableURLRequest requestWithURL:url1];
}
return mutableReqeust;
}
+ (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b
{
return [super requestIsCacheEquivalent:a toRequest:b];
}
- (void)startLoading
{
NSMutableURLRequest *mutableReqeust = [[self request] mutableCopy];
//給我們處理過的請求設置一個標識符, 防止無限循環,
[NSURLProtocol setProperty:@YES forKey:FilteredCssKey inRequest:mutableReqeust];
BOOL enableDebug = NO;
//這里最好加上緩存判斷
if (enableDebug)
{
NSString *str = @"寫代碼是一門藝術";
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
{
if (self.connection != nil)
{
[self.connection cancel];
self.connection = nil;
}
}
#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];
}
@end
注意點:
-
Protocols的 遍歷是反向的,也就是最后注冊的Protocol會被優先判斷。就是先注冊A再注冊B ,優先判斷B
-
一定要注意標記請求,不然你會無限的循環下去。。。因為一旦你需要處理這個請求,那么系統會創建你這個 protocol 的實例,然后你自己又開啟了 connection 進行請求的話,又會觸發 URL Loading system 的回調。系統給我們提供了 + (void)setProperty:(id)value forKey:(NSString *)key inRequest:(NSMutableURLRequest *)request; 和 + (id)propertyForKey:(NSString *)key inRequest:(NSURLRequest *)request; 這兩個方法進行標記和區分。
來自:http://www.cocoachina.com/ios/20161202/18261.html