iOS开发之--- NSURLProtocol

作者: 树下老男孩 | 来源:发表于2015-06-16 11:38 被阅读30827次

    最近在项目里由于电信那边发生dns发生域名劫持,因此需要手动将URL请求的域名重定向到指定的IP地址,但是由于请求可能是通过NSURLConnection,NSURLSession或者AFNetworking等方式,因此要想统一进行处理,一开始是想通过Method Swizzling去hook cfnetworking底层方法,后来发现其实有个更好的方法--NSURLProtocol。

    NSURLProtocol

    NSURLProtocol能够让你去重新定义苹果的URL加载系统 (URL Loading System)的行为,URL Loading System里有许多类用于处理URL请求,比如NSURL,NSURLRequest,NSURLConnection和NSURLSession等,当URL Loading System使用NSURLRequest去获取资源的时候,它会创建一个NSURLProtocol子类的实例,你不应该直接实例化一个NSURLProtocol,NSURLProtocol看起来像是一个协议,但其实这是一个类,而且必须使用该类的子类,并且需要被注册。

    使用场景

    不管你是通过UIWebView, NSURLConnection 或者第三方库 (AFNetworking, MKNetworkKit等),他们都是基于NSURLConnection或者 NSURLSession实现的,因此你可以通过NSURLProtocol做自定义的操作。

    • 重定向网络请求
    • 忽略网络请求,使用本地缓存
    • 自定义网络请求的返回结果
    • 一些全局的网络请求设置

    拦截网络请求

    子类化NSURLProtocol并注册

    @interface CustomURLProtocol : NSURLProtocol
    @end
    

    然后在application:didFinishLaunchingWithOptions:方法中注册该CustomURLProtocol,一旦注册完毕后,它就有机会来处理所有交付给URL Loading system的网络请求。

    - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
        //注册protocol
        [NSURLProtocol registerClass:[CustomURLProtocol class]];
        return YES;
    }
    

    实现CustomURLProtocol

    注册好了之后,现在可以开始实现NSURLProtocol的一些方法:

    • +canInitWithRequest:
      这个方法主要是说明你是否打算处理对应的request,如果不打算处理,返回NO,URL Loading System会使用系统默认的行为去处理;如果打算处理,返回YES,然后你就需要处理该请求的所有东西,包括获取请求数据并返回给 URL Loading System。网络数据可以简单的通过NSURLConnection去获取,而且每个NSURLProtocol对象都有一个NSURLProtocolClient实例,可以通过该client将获取到的数据返回给URL Loading System。
      这里有个需要注意的地方,想象一下,当你去加载一个URL资源的时候,URL Loading System会询问CustomURLProtocol是否能处理该请求,你返回YES,然后URL Loading System会创建一个CustomURLProtocol实例然后调用NSURLConnection去获取数据,然而这也会调用URL Loading System,而你在+canInitWithRequest:中又总是返回YES,这样URL Loading System又会创建一个CustomURLProtocol实例导致无限循环。我们应该保证每个request只被处理一次,可以通过+setProperty:forKey:inRequest:标示那些已经处理过的request,然后在+canInitWithRequest:中查询该request是否已经处理过了,如果是则返回NO。
    + (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;
    }
    
    • +canonicalRequestForRequest:
      通常该方法你可以简单的直接返回request,但也可以在这里修改request,比如添加header,修改host等,并返回一个新的request,这是一个抽象方法,子类必须实现。
    + (NSURLRequest *) canonicalRequestForRequest:(NSURLRequest *)request {
        NSMutableURLRequest *mutableReqeust = [request mutableCopy];
        mutableReqeust = [self redirectHostInRequset:mutableReqeust];
        return mutableReqeust;
    }
    
    +(NSMutableURLRequest*)redirectHostInRequset:(NSMutableURLRequest*)request
    {
        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;
        }
        //定向到bing搜索主页
        NSString *ip = @"cn.bing.com";
        
        // 替换域名
        NSString *urlString = [originUrlString stringByReplacingCharactersInRange:hostRange withString:ip];
        NSURL *url = [NSURL URLWithString:urlString];
        request.URL = url;
    
        return request;
    }
    
    • +requestIsCacheEquivalent:toRequest:
      主要判断两个request是否相同,如果相同的话可以使用缓存数据,通常只需要调用父类的实现。
    + (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b
    {
        return [super requestIsCacheEquivalent:a toRequest:b];
    }
    
    • -startLoading -stopLoading
      这两个方法主要是开始和取消相应的request,而且需要标示那些已经处理过的request。
    - (void)startLoading
    {
        NSMutableURLRequest *mutableReqeust = [[self request] mutableCopy];
        //标示改request已经处理过了,防止无限循环
        [NSURLProtocol setProperty:@YES forKey:URLProtocolHandledKey inRequest:mutableReqeust];
        self.connection = [NSURLConnection connectionWithRequest:mutableReqeust delegate:self];
    }
    
    - (void)stopLoading
    {
        [self.connection cancel];
    }
    
    
    • NSURLConnectionDataDelegate方法
      在处理网络请求的时候会调用到该代理方法,我们需要将收到的消息通过client返回给URL Loading System。
    - (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];
    }
    
    - (void) connectionDidFinishLoading:(NSURLConnection *)connection {
        [self.client URLProtocolDidFinishLoading:self];
    }
    
    - (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
        [self.client URLProtocol:self didFailWithError:error];
    }
    
    

    现在你已经可以截取request并做你想做的事了,这里有个demo可以参考一下,截取request并重新定向到新的地址,具体dns解析方法可以参看DNS解析) ,如有不对,欢迎指正,哈~(有遇到iOS8 hook sdwebimage会发起多次请求,可以看下底下的评论)

    相关文章

      网友评论

      • jooooker:楼主。请问下。AF3.0用你这个网络请求的话会无限循环。怎么解决呢?
      • Ilovecoding822:afn3.0真的不起作用,楼主给点意见啊
        jorgon:afn3.0 sessionconfig又protocolClasses,需替换他的get方法:
        Class cls = NSClassFromString(@"__NSCFURLSessionConfiguration")?: NSClassFromString(@"NSURLSessionConfiguration");
        Method originalMethod = class_getInstanceMethod(cls, @selector(protocolClasses));
        Method stubMethod = class_getInstanceMethod([self class], @selector(protocolClasses));
        if (!originalMethod || !stubMethod) {
        [NSException raise:NSInternalInconsistencyException format:@"Couldn't load NEURLSessionConfiguration."];
        }
        method_exchangeImplementations(originalMethod, stubMethod);
        92b6d8cfbe48:我用afn3.0也没作用,你现在找到解决方案了吗?
      • Gavin_peng:楼主你好,我在做这一块的过程中遇到了一点问题,在iOS8.3系统上,拦截SDWebImage发出的请求的时候,当图片比较大,1M左右的时候,有时会加载失败,我打印出程序的流程,发现NSURLProtocol的startLoading方法,会被不同的线程差不多同时执行2-3遍左右,在iOS9以上不会出现这个问题,楼主遇到过吗?
        树下老男孩:会发起多次请求的情况可以看下
        Gavin_peng:在iOS8.x系统上出现的这个问题可以通过添加一个标志位来实现,不过此处涉及到多线程,需要用到锁机制,通过控制标志位确保一个url只被加载一次即可。:sob:
        树下老男孩:@Gavin_peng 这个倒没遇到过:joy:
      • 656d9360b5b4:好文章,必须mark一下
      • Liberalism:楼主,有个问题想向你请教一下。在请求地址重定向时,我希望我输入的任何Url 都转到 www.baidu.com,于是我写了@“www.baidu.com”,一直都不行。但是用您写的@"cn.bing.com"就没有问题,这块儿有什么注意点吗?
      • wokenshin:LZ你好 遇到过 AFN 报 1003错误的吗?我 在公寓的网络环境下 时不时的就会报这个错。问题是其他App都能正常使用。 在公司的网络环境下就一点问题都没有。我现在没搞清楚问题的源头 和解决办法。
      • mark666:这个应该是ios9以后才能用吧
      • cb3fc6332154:你好,我想问下仅仅为了做webView缓存用这个方案会不会杀鸡用牛刀了?直接基于NSURLCache操作是否更合适一些?
      • BB区块链开发: //定向到bing搜索主页
        // NSString *ip = @"cn.bing.com";
        //
        // // 替换host
        // NSString *urlString = [originUrlString stringByReplacingCharactersInRange:hostRange withString:ip];
        // NSURL *url = [NSURL URLWithString:urlString];
        // request.URL = url;
        [request addValue:@"yang" forHTTPHeaderField:@"name"];

        return request;
        // 修改头信息 没有成功 .. 这是为什么呢
        谢谢 .
      • Misaki_yuyi:您好 楼主 我用了本地文件 替换 拦截的请求 为什么会报错?
        + (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request {

        // 修改了请求的头部信息
        NSMutableURLRequest * mutableReq = [request mutableCopy];

        NSLog(@"%@",mutableReq.URL);
        if([mutableReq.URL.absoluteString hasSuffix:@"bootstrap.min.css"])
        {
        NSString * filePath = [[NSBundle mainBundle] pathForResource:@"bootstrap" ofType:@"css"];
        NSURL * url = [NSURL fileURLWithPath:filePath];
        mutableReq.URL = url;
        }

        return [mutableReq copy];
        }
        Misaki_yuyi:@树下的老男孩 报webthread的错误
        树下老男孩:@Misaki_yuyi 啥错误
      • zuolingfeng:赞,解释的很清楚。。
      • lonelyprince:你好,我想问下,UIWebview的请求是可以跳转到NSURLprotocol做处理的,这个是没问题的。如果我想使用AVplayer去播放视频的时候,能不能用NSURLprotocol做处理? 我这边测试的结果是不能进入NSURLprotocol方法,这样正常吗?
        lonelyprince:@树下的老男孩 好的,多谢提醒 :grin:
        树下老男孩:@lonelyprince 应该avplayer用的是更底层的网络接口,所以没发hook,查一下
      • 五月飞:你好,请问iOS有类似安卓网络拦截器的功能吗,NSURLProtocol可以实现吗
        五月飞:@树下的老男孩 在拦截方法里,写一个发起刷新token的请求,等这个刷新方法走完以后,再回到之前的请求上去,能做到嘛
        树下老男孩:@五月飞 NSURLProtocol可以做拦截
      • fba08aef555c:请问,我用NSURLProtocol缓存webview数据的时候,出现卡死现象,直到网页全部加载出来,界面才不卡死,是怎么回事啊
      • 谈Xx:最后一段的 代理方法 相当于是固定要实现的吗
      • 巴图鲁:膜拜
      • GDCoder:ios已经上线项目 偶尔会出现 Code=-1003 "未能找到使用指定主机名的服务器。" 报错。。 安卓pc端 均没有出现类似的问题,这个可以解决吗?大神
        树下老男孩:@最后一课 dns的问题?
      • 641f42fe675c:但https怎么应对劫持呢,你不能把域名替换为自己解析出来的ip地址
      • 操哥:hook cfnetworking底层方法,hook的是哪些函数
      • 空转风:if ([request.URL.absoluteString isEqual: @"http://s.wapadv.com/slot/100996/"]) {
        return NO;
        }
        是不是就是说不会加载这个网址?
      • iVikings:我用NSURLProtol劫持SDWebImage的下载图片请求,在iOS8系统上面会重复下载图片,请问下这个是什么问题呢?
      • leftwater:先点个赞
      • 清河湾:楼主需要去注意一下有些坑要填啊,关于重定向的URL请求的时候会出错,另附上别人的解决文章,希望互相学习,哈哈哈
        http://www.cocoachina.com/ios/20141225/10765.html
        zyg:@树下的老男孩 canonicalRequestForRequest 这个会递归调用 好奇怪
        树下老男孩:@漂泊不定 有没有具体点的,啥错误,怎么解决简单说说
        清河湾:@漂泊不定 这个坑我帮你填一点
      • Bob林:afn3.0 不起作用
        清河湾:@Python的日常 不是哟,还有,我不是楼主啦
        Bob林:@漂泊不定 楼主你是做 apm 的吗,这一块难度怎么样
        清河湾:@Python的日常 不会啊,我用的是可以的,Ajax可以拦截得到,包括 css ,js,图片,请求都行的
      • angelab:如果拦截的是nsurlsession那self.connection = [NSURLConnection connectionWithRequest:mutableReqeust delegate:这个还适用吗。我自己测试的是程序会卡。然后我改成了 NSURLSessionDataTask * task = [self.session dataTaskWithRequest:request];就好了,但是我不知道该怎么判断拦截的是nsurlconnection还是nsurlsession,请问该怎么判断
        Joy___:@angelab 适用吧?
      • 柠檬丶Lemon:那我可以拦截网页中所有的img标签的src吗?不让图片加载出来吗
        ba78406fa40a:@柠檬丶Lemon 可以的
      • e5b67824629b:请问CFHostCancelInfoResolution 这个函数为什么使用起来没有起作用?我正在测试从webview打开一次网页的所有网络请求(地址请求、js、css文件的拉取以及ajax请求)都经过网络dns获取ip,并记录这个过程的总用时。但是在文中的这个类里面调用CFHostCancelInfoResolution系统方法不起作用。请问以上想法能否实现?或者能否提供一下CFHostCancelInfoResolution的正确使用方法?
        e5b67824629b:@树下的老男孩 我试了在webviewloadurl之前进行cancel,但是在nsurlprotocol截取请求的时候依然能从host里面查到ip。或者说换个思路,cfhost能设置缓存时间么?把缓存时间设置为0能否让cfhost不进行缓存
        树下老男孩:@卟幺要 你单独cancel能cancel掉么,不使用nsurlprotocol
      • 奔跑的码农:写的不错 去年做webView缓存的时候也用的这个 比较的好用!! 推荐篇英文的教程吧 写的不错http://www.raywenderlich.com/59982/nsurlprotocol-tutorial
      • 大芋:更正,NSURLConnectionDelegate似乎应该是 NSURLConnectionDataDelegate
        树下老男孩:@大芋 thanks~更正
      • dcc7e0b17c02:感谢您的文章,让我受益匪浅.
        但是我在实际使用中发现一个问题,+canonicalRequestForRequest:
        在这个方法里面如果为request添加新的http header的话,在- startLoading方法里面再查看request的allHTTPHeaderFields会很奇怪,在模拟器上面,新添加的header是正常的,但是在真机上header根本没有添加上去,不知是何原因,请指教.
        树下老男孩:@cescxu 我这iPod iOS8 跟5s iOS9 真机都是可以的,你是什么机器
        dcc7e0b17c02:@树下的老男孩 感谢您的回复,我也在真机试了您的demo,也发现了类似的问题,就是无法定向到bing的主页.模拟器可以正常定向到bing主页.这个问题和我那个应该是一样的原因..
        树下老男孩:@cescxu 真机我倒没测试过,我抽空试看看, :smile:
      • 暗夜血狐:怎么统计app的流量
      • 不是谢志伟:好文章, 正想用NSURLProtocol 来做APM的网络性能检测呢
        csxfno21:用NSURLProtocol能重新定义AFNetwork的Session吗?我们公司也在做APM这块,之前是通过hook来替换NSURLConnection的方法。但是hook NSURLSession就不行。
        不是谢志伟:@蔡董1990 可以呀, 加我扣扣吧? Five三One368454 ^ ^
        52429f9c6107:@不是谢志伟 你好,我最近也在做这个apm 的东西,现在一部分功能已经实现,我想我们可以交流下。
      • 南栀倾寒:已解决
      • WELCommand:好厉害
      • 树下老男孩:@南栀倾寒 可以看看这个 http://stackoverflow.com/questions/5572258/ios-webview-remote-html-with-local-image-files
        a92b53eda0ed:@傅hc 你好,我现在也正在做简单的webview的离线缓存,有什么好的建议吗
        傅hc:@树下的老男孩 这个好像没用,我想简单的实现UIWebView离线缓存,有好的建议吗?
      • 南栀倾寒:我就想问问 怎么做http缓存 的 比如wenVIew
        高高叔叔:http://blog.csdn.net/horisea/article/details/53815596 可以看看这个缓存webview的
        MaxWellPro:如果要缓存网页所有数据就用NSURLCache,如果只是image就用NSURLProtocol拦截后用SDWebImageDownloader去下载图片

      本文标题:iOS开发之--- NSURLProtocol

      本文链接:https://www.haomeiwen.com/subject/zufsqttx.html