iOS UIWebView 廣告攔截 js,css 攔截方法 (NSURLProtocol)

剛剛開始寫博客,可能描述的思路不是很清晰,還請(qǐng)各位看官擔(dān)待,寫的不好,如有紕漏,望請(qǐng)斧正,感覺不盡.

當(dāng)我們做混合APP 或者是 純網(wǎng)頁的APP(就是只加一個(gè)UIWebView的套殼APP)時(shí),經(jīng)常會(huì)遇到討厭的廣告劫持!!!
特別是那些 營運(yùn)商 的 DNS劫持,植入廣告,真的是太惡心了.

來現(xiàn)在過濾DNS廣告好像有2種:
  1. HTTPS協(xié)議 --- 這個(gè)安全,可靠,推薦!!! 但是如果都能用這個(gè)那就沒這篇文章什么事了.
    2.設(shè)置過濾規(guī)則 ---這個(gè)也是Adblock (最強(qiáng)的廣告攔截插件,沒用的,我也不知道說什么了) 的攔截原理.
    現(xiàn)在我們是要模仿它,為我們的UIWebView 加載的網(wǎng)頁也加上攔截機(jī)制,過濾這些廣告.
    這個(gè)方法缺點(diǎn)也是很大的,需要維護(hù)過濾規(guī)則 比較麻煩.
  1. 如果你需要加載的網(wǎng)頁都是從外網(wǎng)上拉下來的,那這個(gè)目前好像沒有什么解決辦法.(如果讀者還有什么高招,還請(qǐng)留言告知!)只能 保持最新的過濾規(guī)則,哈哈哈~~
  2. 如果你加載的網(wǎng)頁全部都是在你自己的服務(wù)器上,那就好辦多了,直接不是你域名和你合作的域名內(nèi)的請(qǐng)求都不通過就行了. 注意!! 像第三方登錄之類的都會(huì)被攔截的,因?yàn)樗械南到y(tǒng)請(qǐng)求都會(huì)經(jīng)過你的過濾規(guī)則.(筆者就被坑過,哈哈哈哈)

眾所周知,我們?cè)鷌OS的UIWebView 要于html網(wǎng)頁進(jìn)行交互 有兩個(gè)途徑

1: 通過UIWebView 的 stringByEvaluatingJavaScriptFromString: 方法實(shí)現(xiàn)與HTML網(wǎng)頁的交互.

注意: 這個(gè)方法必須在網(wǎng)頁加載完成之后才會(huì)有效,也就是再 delegate 中的 webViewDidFinishLoad: 方法執(zhí)行過之后

- (void)webViewDidFinishLoad:(UIWebView *)webView
{
     //獲取某個(gè)id的標(biāo)簽中的內(nèi)容 
    NSString *content = [webView stringByEvaluatingJavaScriptFromString:
        @"document.getElementById('你的某個(gè)標(biāo)簽的id').innerHTML"];
}
2: ios7 以上有了這個(gè) 神器!!<JavaScriptCore/JavaScriptCore.h> 框架.

使用這個(gè)框架進(jìn)行交互的網(wǎng)上有這相當(dāng)多的教程,我這就放幾個(gè)傳送門就好了.

這個(gè)有Swift 版 和 OC版的,協(xié)議模型注入 (JSExport )

這個(gè)是使用 stringByEvaluatingJavaScriptFromString 方法的

NSURLProtocol

概念
NSURLProtocol :它可以輕松地重定義整個(gè)URL Loading System。當(dāng)你注冊(cè)自定義NSURLProtocol后,就有機(jī)會(huì)對(duì)所有的請(qǐng)求進(jìn)行統(tǒng)一的處理,基于這一點(diǎn)它可以讓你實(shí)現(xiàn)以下的功能

·自定義請(qǐng)求和響應(yīng)
·提供自定義的全局緩存支持
·重定向網(wǎng)絡(luò)請(qǐng)求
·提供HTTP Mocking (方便前期測(cè)試)
·其他一些全局的網(wǎng)絡(luò)請(qǐng)求修改需求

使用方法

繼承NSURLPorotocl,并注冊(cè)你的NSURLProtocol
完整源代碼最后附上

[NSURLProtocol registerClass:[CCURLProtocol class]];

實(shí)現(xiàn)NSURLProtocol的相關(guān)方法
當(dāng)遍歷到我們自定義的NSURLProtocol時(shí),系統(tǒng)先會(huì)調(diào)用canInitWithRequest:這個(gè)方法。顧名思義,這是整個(gè)流程的入口,只有這個(gè)方法返回YES我們才能夠繼續(xù)后續(xù)的處理。我們可以在這個(gè)方法的實(shí)現(xiàn)里面進(jìn)行請(qǐng)求的過濾,篩選出需要進(jìn)行處理的請(qǐng)求。

+ (BOOL)canInitWithRequest:(NSURLRequest *)request
{
  //只處理http和https請(qǐng)求
    NSString *scheme = [[request URL] scheme];
    if (  ([scheme caseInsensitiveCompare:@"http"] == NSOrderedSame )||
          ([scheme caseInsensitiveCompare:@"https"] == NSOrderedSame )   )
    {
        //看看是否已經(jīng)處理過了,防止無限循環(huán)
        if ([NSURLProtocol propertyForKey:URLProtocolHandledKey inRequest:request]) {
            return NO;
        }
        
        return YES;//處理
        
    }
    return NO;

}

當(dāng)篩選出需要處理的請(qǐng)求后,就可以進(jìn)行后續(xù)的處理,需要至少實(shí)現(xiàn)如下4個(gè)方法

+ (NSURLRequest *) canonicalRequestForRequest:(NSURLRequest *)request
{
 //在這里網(wǎng)頁出現(xiàn)任何變動(dòng)(加載JS ,CSS 什么的都能攔截得到) ,發(fā)送個(gè)通知 do something
    //[[NSNotificationCenter defaultCenter] postNotificationName:PageChangeNotification object:self userInfo:nil]; 

    NSLog(@"canonicalRequestForRequest:%@",request.URL.absoluteString);
    NSMutableURLRequest *mutableReqeust = [request mutableCopy];
#warning --- 攔截URL 進(jìn)行規(guī)則過濾 
   //在這個(gè)方法  redirectHostInRequset 里面去過濾你要過濾的東西
    mutableReqeust = [self redirectHostInRequset:mutableReqeust];
    return mutableReqeust;
}

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

- (void)startLoading
{   
    NSMutableURLRequest *mutableReqeust = [[self request] mutableCopy];
    
    //打標(biāo)簽,防止無限循環(huán)
    [NSURLProtocol setProperty:@YES forKey:URLProtocolHandledKey inRequest:mutableReqeust];
//    
    self.connection = [NSURLConnection connectionWithRequest:mutableReqeust delegate:self];
}
//
- (void)stopLoading
{
    [self.connection cancel];
}

canonicalRequestForRequest: 進(jìn)行過濾 ,返回規(guī)范化后的request

**requestIsCacheEquivalent:toRequest: **用于判斷你的自定義reqeust是否相同,這里返回默認(rèn)實(shí)現(xiàn)即可。它的主要應(yīng)用場(chǎng)景是某些直接使用緩存而非再次請(qǐng)求網(wǎng)絡(luò)的地方。

startLoadingstopLoading 實(shí)現(xiàn)請(qǐng)求和取消流程。

redirectHostInRequset: 方法的實(shí)現(xiàn)

+(NSMutableURLRequest*)redirectHostInRequset:(NSMutableURLRequest*)request
{
  //沒有域名的URL請(qǐng)求就原路返回,不能返回nil ,不然在跳轉(zhuǎn)APP的時(shí)候會(huì)被攔截返回空出錯(cuò)(或者其他情況).
  //eg:  mqq://im/chat?chat_type=wpa&uin=1299101858&version=1&src_type=web  跳轉(zhuǎn)到指定QQ用戶的聊天窗口
    if ([request.URL host].length == 0) {
        return request;
    }
    NSString *originUrlString = request.URL.absoluteString;
    
    //獲取主機(jī)名字,在這里執(zhí)行正則匹配
    NSString *originHostString = [request.URL host];
    NSRange hostRange = [originUrlString rangeOfString:originHostString];
    //找不到主機(jī)名,返回
    if (hostRange.location == NSNotFound) {
        return request;
    }
    
    if (originUrlString != nil) {
        //獲取攔截的黑白名單數(shù)據(jù)(過濾名單)
  //這個(gè)是自定義方法,你們自己隨意發(fā)揮,哈哈哈.
#warning --- 思路實(shí)現(xiàn)
/*

這里的匹配黑白名單一般只是**匹配域名** 
思路 1:匹配白名單->匹配黑名單-> 如果兩個(gè)都沒有,就向服務(wù)器打印日志. (拉外網(wǎng))
思路 2:匹配白名單 

以下代碼運(yùn)用思路1 實(shí)現(xiàn)

eg: 這個(gè)是過濾的規(guī)則的例子格式
.*(.qq.com|api.weibo.com|.weibo.com|.baidu.com|.weixin.qq.com|.sina.com|.sina.cn).*

*/
        NSDictionary *dic = [self getHoldUpDic];
        if (!dic)//如果為空不處理黑白名單
        {
            return request;
        }
        
        //白名單
        NSString *whiteList = dic[@"whiteList"];
        //黑名單
        NSString * blackList = dic[@"blackList"];
        
#pragma mark - 白名單匹配
        
        //1.1將正則表達(dá)式設(shè)置為OC規(guī)則
        if (![whiteList isEqualToString:@""])
        {
            NSRegularExpression *regular1 = [[NSRegularExpression alloc] initWithPattern:whiteList options:NSRegularExpressionCaseInsensitive error:nil];
            //2.利用規(guī)則測(cè)試字符串獲取匹配結(jié)果
            NSArray *results1 = [regular1 matchesInString:originUrlString options:0 range:NSMakeRange(0, originUrlString.length)];
            if (results1.count > 0)//是白名單,允許訪問
            {
                return request;
            }
            
        }
        
#pragma mark - 黑名單匹配
        if (![blackList isEqualToString:@""])
        {
            //1.1將正則表達(dá)式設(shè)置為OC規(guī)則
            NSRegularExpression *regular2 = [[NSRegularExpression alloc] initWithPattern:blackList options:NSRegularExpressionCaseInsensitive error:nil];
            //2.利用規(guī)則匹配字符串獲取匹配結(jié)果
            NSArray *results2 = [regular2 matchesInString:originUrlString options:0 range:NSMakeRange(0, originUrlString.length)];
            
            if (results2.count > 0 ) //黑名單,返回nil;
            {
                return request;
            }
            
        }
        
        if (![whiteList isEqualToString:@""]&&![blackList isEqualToString:@""])
        {
#pragma mark - 發(fā)送到服務(wù)端打印日志
            
            //do something
            
        }
        
        
    }
    
    
    
    
    return request;
}

實(shí)現(xiàn)NSURLConnectionDelegate和NSURLConnectionDataDelegate



#warning  筆者聲明:這里是有大坑的,最好是全部的代理方法都實(shí)現(xiàn)一遍,不然有可能會(huì)出現(xiàn)各種問題
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
    [self.client URLProtocol:self
            didFailWithError:error];
}
- (NSURLRequest *)connection:(NSURLConnection *)connection willSendRequest:(NSURLRequest *)request redirectResponse:(NSURLResponse *)response
{
//    if (response != nil)
//    {
//        [[self client] URLProtocol:self wasRedirectedToRequest:request redirectResponse:response];
//    }
#warning  lanjie wen ti 這里需要回傳[self client] 消息,那么需要重定向的網(wǎng)頁就會(huì)出現(xiàn)問題:host不對(duì)或者造成跨域調(diào)用導(dǎo)致資源無法加載
     [[self client] URLProtocol:self wasRedirectedToRequest:request redirectResponse:response];
//    return request; // 這里如果返回 request 會(huì)重新請(qǐng)求一次
    return nil;
}
- (BOOL)connectionShouldUseCredentialStorage:(NSURLConnection *)connection
{
    return YES;
}
- (void)connection:(NSURLConnection *)connection didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
{
    [self.client URLProtocol:self didReceiveAuthenticationChallenge:challenge];
}
- (void)connection:(NSURLConnection *)connection didCancelAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
{
    [self.client URLProtocol:self didCancelAuthenticationChallenge:challenge];
}
- (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];
}
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse
{
    return cachedResponse;
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
    [self.client URLProtocolDidFinishLoading:self];
}

繼承的 CCURLProtocol 類的代碼

CCURLProtocol.h 文件

#import <Foundation/Foundation.h>

@interface CCURLProtocol : NSURLProtocol

@end

CCURLProtocol.m 文件



#import "CCURLProtocol.h"
#import "AFNetWork.h"

static NSString * const URLProtocolHandledKey = @"URLProtocolHandledKey";
static NSDictionary *_holdUpDic;
@interface CCURLProtocol ()<NSURLConnectionDelegate>

@property (nonatomic, strong) NSURLConnection *connection;
@end


@implementation CCURLProtocol
+(NSDictionary *)getHoldUpDic
{
    if (!_holdUpDic)
    {
#pragma mark - 這里是獲取黑白名單的數(shù)據(jù)
         /*
                 [AFNetWork postWithURL:@"" Params:@"" Success:^(NSURLSessionDataTask *task, id responseObject) {
            //獲取廣告攔截資料
            _holdUpDic = responseObject;
            
            //寫入本地plist文件
            BOOL success = [_holdUpDic writeToFile:path atomically:YES];
            if (success )
            {
                NSLog(@"寫入成功");
                
            }else
            {
                NSLog(@"寫入失敗");
            }

        }];
        
        _holdUpDic = [NSDictionary dictionaryWithContentsOfFile:path];
         */
    }
    return _holdUpDic;
}
+ (BOOL)canInitWithRequest:(NSURLRequest *)request
{
    
    //只處理http和https請(qǐng)求
    NSString *scheme = [[request URL] scheme];
    if ( ([scheme caseInsensitiveCompare:@"http"] == NSOrderedSame )||([scheme caseInsensitiveCompare:@"https"] == NSOrderedSame ))
    {
        //看看是否已經(jīng)處理過了,防止無限循環(huán)
        if ([NSURLProtocol propertyForKey:URLProtocolHandledKey inRequest:request]) {
            return NO;
        }
        
        return YES;//處理
        
    }
    return NO;
}

+ (NSURLRequest *) canonicalRequestForRequest:(NSURLRequest *)request
{
    //網(wǎng)頁發(fā)生變動(dòng)
   // [[NSNotificationCenter defaultCenter] postNotificationName:PageChangeNotification object:self userInfo:nil];
   // NSLog(@"canonicalRequestForRequest:%@",request.URL.absoluteString);
    NSMutableURLRequest *mutableReqeust = [request mutableCopy];
    mutableReqeust = [self redirectHostInRequset:mutableReqeust];
    return mutableReqeust;
}

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

- (void)startLoading
{
    NSMutableURLRequest *mutableReqeust = [[self request] mutableCopy];
    
    //打標(biāo)簽,防止無限循環(huán)
    [NSURLProtocol setProperty:@YES forKey:URLProtocolHandledKey inRequest:mutableReqeust];
//    
    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];
}
- (NSURLRequest *)connection:(NSURLConnection *)connection willSendRequest:(NSURLRequest *)request redirectResponse:(NSURLResponse *)response
{
//    if (response != nil)
//    {
//        [[self client] URLProtocol:self wasRedirectedToRequest:request redirectResponse:response];
//    }
#warning  lanjie wen ti 這里需要回傳[self client] 消息,那么需要重定向的網(wǎng)頁就會(huì)出現(xiàn)問題:host不對(duì)或者造成跨域調(diào)用導(dǎo)致資源無法加載
     [[self client] URLProtocol:self wasRedirectedToRequest:request redirectResponse:response];
//    return request; // 這里如果返回 request 會(huì)重新請(qǐng)求一次
    return nil;
}
- (BOOL)connectionShouldUseCredentialStorage:(NSURLConnection *)connection
{
    return YES;
}
- (void)connection:(NSURLConnection *)connection didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
{
    [self.client URLProtocol:self didReceiveAuthenticationChallenge:challenge];
}
- (void)connection:(NSURLConnection *)connection didCancelAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
{
    [self.client URLProtocol:self didCancelAuthenticationChallenge:challenge];
}
- (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];
}
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse
{
    return cachedResponse;
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
    [self.client URLProtocolDidFinishLoading:self];
}

#pragma mark -- private

+(NSMutableURLRequest*)redirectHostInRequset:(NSMutableURLRequest*)request
{
  //沒有域名的URL請(qǐng)求就原路返回,不能返回nil ,不然在跳轉(zhuǎn)APP的時(shí)候會(huì)被攔截返回空出錯(cuò)(或者其他情況).
  //eg:  mqq://im/chat?chat_type=wpa&uin=1299101858&version=1&src_type=web  跳轉(zhuǎn)到指定QQ用戶的聊天窗口
    if ([request.URL host].length == 0) {
        return request;
    }
    NSString *originUrlString = request.URL.absoluteString;
    
    //獲取主機(jī)名字,在這里執(zhí)行正則匹配
    NSString *originHostString = [request.URL host];
    NSRange hostRange = [originUrlString rangeOfString:originHostString];
    //找不到主機(jī)名,返回
    if (hostRange.location == NSNotFound) {
        return request;
    }
    
    if (originUrlString != nil) {
        //獲取攔截的黑白名單數(shù)據(jù)(過濾名單)
  //這個(gè)是自定義方法,你們自己隨意發(fā)揮,哈哈哈.
#warning --- 思路實(shí)現(xiàn)
/*

這里的匹配黑白名單一般只是**匹配域名** 
思路 1:匹配白名單->匹配黑名單-> 如果兩個(gè)都沒有,就向服務(wù)器打印日志. (拉外網(wǎng))
思路 2:匹配白名單 

以下代碼運(yùn)用思路1 實(shí)現(xiàn)

eg: 這個(gè)是過濾的規(guī)則的例子格式
.*(.qq.com|api.weibo.com|.weibo.com|.baidu.com|.weixin.qq.com|.sina.com|.sina.cn).*

*/
        NSDictionary *dic = [self getHoldUpDic];
        if (!dic)//如果為空不處理黑白名單
        {
            return request;
        }
        
        //白名單
        NSString *whiteList = dic[@"whiteList"];
        //黑名單
        NSString * blackList = dic[@"blackList"];
        
#pragma mark - 白名單匹配
        
        //1.1將正則表達(dá)式設(shè)置為OC規(guī)則
        if (![whiteList isEqualToString:@""])
        {
            NSRegularExpression *regular1 = [[NSRegularExpression alloc] initWithPattern:whiteList options:NSRegularExpressionCaseInsensitive error:nil];
            //2.利用規(guī)則測(cè)試字符串獲取匹配結(jié)果
            NSArray *results1 = [regular1 matchesInString:originUrlString options:0 range:NSMakeRange(0, originUrlString.length)];
            if (results1.count > 0)//是白名單,允許訪問
            {
                return request;
            }
            
        }
        
#pragma mark - 黑名單匹配
        if (![blackList isEqualToString:@""])
        {
            //1.1將正則表達(dá)式設(shè)置為OC規(guī)則
            NSRegularExpression *regular2 = [[NSRegularExpression alloc] initWithPattern:blackList options:NSRegularExpressionCaseInsensitive error:nil];
            //2.利用規(guī)則匹配字符串獲取匹配結(jié)果
            NSArray *results2 = [regular2 matchesInString:originUrlString options:0 range:NSMakeRange(0, originUrlString.length)];
            
            if (results2.count > 0 ) //黑名單,返回nil;
            {
                return request;
            }
            
        }
        
        if (![whiteList isEqualToString:@""]&&![blackList isEqualToString:@""])
        {
#pragma mark - 發(fā)送到服務(wù)端打印日志
            
            //do something
            
        }
       
    }
    return request;
}


@end

demo 現(xiàn)在正在整理,之后再奉上.如有疑問,歡迎留言.

以上來源參考自網(wǎng)絡(luò),如有侵權(quán)請(qǐng)私信筆者.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,094評(píng)論 25 709
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,578評(píng)論 19 139
  • 本文是逐行翻譯,便于參照原文,如有歧義或者疑問請(qǐng)閱讀原文比較。于 2017.1.25===============...
    Auditore閱讀 1,621評(píng)論 4 5
  • 姓名:王建勛 公司:思沃技術(shù)171期利他2組王建勛 211/230期志工 【知-學(xué)習(xí)】 誦《六項(xiàng)精進(jìn)》大綱2遍,共...
    常修閱讀 564評(píng)論 0 1
  • 彩虹有7種顏色 簡(jiǎn)譜有7個(gè)音階 7年是一次細(xì)胞更替 7天是一周美好生活 而7也是我的有緣人 從小學(xué)到高中的12年我...
    晨陽Indra閱讀 770評(píng)論 0 0

友情鏈接更多精彩內(nèi)容