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

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

當我們做混合APP 或者是 純網頁的APP(就是只加一個UIWebView的套殼APP)時,經常會遇到討厭的廣告劫持!!!
特別是那些 營運商 的 DNS劫持,植入廣告,真的是太惡心了.

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

眾所周知,我們原生iOS的UIWebView 要于html網頁進行交互 有兩個途徑

1: 通過UIWebView 的 stringByEvaluatingJavaScriptFromString: 方法實現與HTML網頁的交互.

注意: 這個方法必須在網頁加載完成之后才會有效,也就是再 delegate 中的 webViewDidFinishLoad: 方法執行過之后

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

使用這個框架進行交互的網上有這相當多的教程,我這就放幾個傳送門就好了.

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

這個是使用 stringByEvaluatingJavaScriptFromString 方法的

NSURLProtocol

概念
NSURLProtocol :它可以輕松地重定義整個URL Loading System。當你注冊自定義NSURLProtocol后,就有機會對所有的請求進行統一的處理,基于這一點它可以讓你實現以下的功能

·自定義請求和響應
·提供自定義的全局緩存支持
·重定向網絡請求
·提供HTTP Mocking (方便前期測試)
·其他一些全局的網絡請求修改需求

使用方法

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

[NSURLProtocol registerClass:[CCURLProtocol class]];

實現NSURLProtocol的相關方法
當遍歷到我們自定義的NSURLProtocol時,系統先會調用canInitWithRequest:這個方法。顧名思義,這是整個流程的入口,只有這個方法返回YES我們才能夠繼續后續的處理。我們可以在這個方法的實現里面進行請求的過濾,篩選出需要進行處理的請求。

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

}

當篩選出需要處理的請求后,就可以進行后續的處理,需要至少實現如下4個方法

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

    NSLog(@"canonicalRequestForRequest:%@",request.URL.absoluteString);
    NSMutableURLRequest *mutableReqeust = [request mutableCopy];
#warning --- 攔截URL 進行規則過濾 
   //在這個方法  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];
    
    //打標簽,防止無限循環
    [NSURLProtocol setProperty:@YES forKey:URLProtocolHandledKey inRequest:mutableReqeust];
//    
    self.connection = [NSURLConnection connectionWithRequest:mutableReqeust delegate:self];
}
//
- (void)stopLoading
{
    [self.connection cancel];
}

canonicalRequestForRequest: 進行過濾 ,返回規范化后的request

**requestIsCacheEquivalent:toRequest: **用于判斷你的自定義reqeust是否相同,這里返回默認實現即可。它的主要應用場景是某些直接使用緩存而非再次請求網絡的地方。

startLoadingstopLoading 實現請求和取消流程。

redirectHostInRequset: 方法的實現

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

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

以下代碼運用思路1 實現

eg: 這個是過濾的規則的例子格式
.*(.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將正則表達式設置為OC規則
        if (![whiteList isEqualToString:@""])
        {
            NSRegularExpression *regular1 = [[NSRegularExpression alloc] initWithPattern:whiteList options:NSRegularExpressionCaseInsensitive error:nil];
            //2.利用規則測試字符串獲取匹配結果
            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將正則表達式設置為OC規則
            NSRegularExpression *regular2 = [[NSRegularExpression alloc] initWithPattern:blackList options:NSRegularExpressionCaseInsensitive error:nil];
            //2.利用規則匹配字符串獲取匹配結果
            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 - 發送到服務端打印日志
            
            //do something
            
        }
        
        
    }
    
    
    
    
    return request;
}

實現NSURLConnectionDelegate和NSURLConnectionDataDelegate



#warning  筆者聲明:這里是有大坑的,最好是全部的代理方法都實現一遍,不然有可能會出現各種問題
- (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] 消息,那么需要重定向的網頁就會出現問題:host不對或者造成跨域調用導致資源無法加載
     [[self client] URLProtocol:self wasRedirectedToRequest:request redirectResponse:response];
//    return request; // 這里如果返回 request 會重新請求一次
    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 - 這里是獲取黑白名單的數據
         /*
                 [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請求
    NSString *scheme = [[request URL] scheme];
    if ( ([scheme caseInsensitiveCompare:@"http"] == NSOrderedSame )||([scheme caseInsensitiveCompare:@"https"] == NSOrderedSame ))
    {
        //看看是否已經處理過了,防止無限循環
        if ([NSURLProtocol propertyForKey:URLProtocolHandledKey inRequest:request]) {
            return NO;
        }
        
        return YES;//處理
        
    }
    return NO;
}

+ (NSURLRequest *) canonicalRequestForRequest:(NSURLRequest *)request
{
    //網頁發生變動
   // [[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];
    
    //打標簽,防止無限循環
    [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] 消息,那么需要重定向的網頁就會出現問題:host不對或者造成跨域調用導致資源無法加載
     [[self client] URLProtocol:self wasRedirectedToRequest:request redirectResponse:response];
//    return request; // 這里如果返回 request 會重新請求一次
    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請求就原路返回,不能返回nil ,不然在跳轉APP的時候會被攔截返回空出錯(或者其他情況).
  //eg:  mqq://im/chat?chat_type=wpa&uin=1299101858&version=1&src_type=web  跳轉到指定QQ用戶的聊天窗口
    if ([request.URL host].length == 0) {
        return request;
    }
    NSString *originUrlString = request.URL.absoluteString;
    
    //獲取主機名字,在這里執行正則匹配
    NSString *originHostString = [request.URL host];
    NSRange hostRange = [originUrlString rangeOfString:originHostString];
    //找不到主機名,返回
    if (hostRange.location == NSNotFound) {
        return request;
    }
    
    if (originUrlString != nil) {
        //獲取攔截的黑白名單數據(過濾名單)
  //這個是自定義方法,你們自己隨意發揮,哈哈哈.
#warning --- 思路實現
/*

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

以下代碼運用思路1 實現

eg: 這個是過濾的規則的例子格式
.*(.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將正則表達式設置為OC規則
        if (![whiteList isEqualToString:@""])
        {
            NSRegularExpression *regular1 = [[NSRegularExpression alloc] initWithPattern:whiteList options:NSRegularExpressionCaseInsensitive error:nil];
            //2.利用規則測試字符串獲取匹配結果
            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將正則表達式設置為OC規則
            NSRegularExpression *regular2 = [[NSRegularExpression alloc] initWithPattern:blackList options:NSRegularExpressionCaseInsensitive error:nil];
            //2.利用規則匹配字符串獲取匹配結果
            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 - 發送到服務端打印日志
            
            //do something
            
        }
       
    }
    return request;
}


@end

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

以上來源參考自網絡,如有侵權請私信筆者.

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 173,242評論 25 708
  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,915評論 18 139
  • 本文是逐行翻譯,便于參照原文,如有歧義或者疑問請閱讀原文比較。于 2017.1.25===============...
    Auditore閱讀 1,544評論 4 5
  • 姓名:王建勛 公司:思沃技術171期利他2組王建勛 211/230期志工 【知-學習】 誦《六項精進》大綱2遍,共...
    常修閱讀 531評論 0 1
  • 彩虹有7種顏色 簡譜有7個音階 7年是一次細胞更替 7天是一周美好生活 而7也是我的有緣人 從小學到高中的12年我...
    晨陽Indra閱讀 616評論 0 0