NSURLPtotocol 网络hooker

先说下URL Loading System

如图所示,URL Loading System是iOS一系列网络请求类的集合,包括已经过期不用的NSConnection和现在流行的NSURLSession,还包括一些请求认证的类,一个sessionConfig的类,还有关于处理请求缓存的类等,当然还包括我们要说的这个NSURLProtocol类。

对,我没说错,NSURLPtotocol类并不是一个protocol,他其实就是一个类,而且是一个“虚基类”-虚拟的父类吧。

URL Loading System可以发出的请求种类有ftp://,http://,https://,file://,data:// 请求。

NSURLProtocol的作用

NSURLProtocol可以拦截监听每一个URL Loading System中发出request请求,记住是URL Loading System中那些类发出的请求,也支持AFNetwoking,UIWebView发出的request。如果不是这些类发出的请求,NSURLProtocol就没办法拦截和监听了。

  • 忽略网络请求使用本地缓存
  • 重定向网络请求
  • 改变request的请求头

NSURLProtocol的使用

因为NSURLProtocol是一个虚基类,所以不能直接使用它,要想使用它就必须自定义一个类成为他的子类,然后实现他里面的必须实现的一些方法,那么我们还要告诉系统:“喂,你发出的request,要让我的子类XXX类过一遍啊!”所以NSURLProtocol有一个register方法告诉系统那个子类要起作用。

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

    [NSURLProtocol registerClass:[TFURLProtocol class]];

    return YES;
}

相对应的也有unregistClass方法,不让某个子类起作用,这个起作用的时候并不是一定要在appDelegate中,你想要他在什么时候起作用,某个请求之前注册他就行,相应的不想他起作用就unregist他就行了。

子类必须实现的一些方法

+ (BOOL)canInitWithRequest:(NSURLRequest *)request

每次有一个请求的时候都会调用这个方法,在这个方法里面判断这个请求是否需要被处理拦截,如果返回YES就代表这个request需要被处理,反之就是不需要被处理。

+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
    if ([NSURLProtocol propertyForKey:protocolKey inRequest:request]) {
        return NO;
    }

    NSString *scheme = [[request URL] scheme];
    if ([scheme caseInsensitiveCompare:@"http"] == NSOrderedSame ||
        [scheme caseInsensitiveCompare:@"https"] == NSOrderedSame) {
        return YES;
    }

    return NO;
}

+ (NSURLRequest *) canonicalRequestForRequest:(NSURLRequest *)request

这个方法就是返回规范的request,一般使用就是直接返回request,不做任何处理的

+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request {
    return request;
}

- (void)startLoading

这个方法作用很大,把当前请求的request拦截下来以后,在这个方法里面对这个request做各种处理,比如添加请求头,重定向网络,使用自定义的缓存等。作用非常之大。下面就是一个重定向的例子。

/**
 * 开始请求
 */
- (void)startLoading {
    NSMutableURLRequest *request = [self.request mutableCopy];
    //把访问百度的request改为访问Google了
    request.URL = [NSURL URLWithString:@"http://www.google.com"];

    [NSURLProtocol setProperty:@(YES) forKey:protocolKey inRequest:request];

    //使用NSURLSession继续把重定向的request发送出去
    NSURLSessionConfiguration *config = [NSURLSessionConfiguration ephemeralSessionConfiguration];
    NSOperationQueue *mainQueue = [NSOperationQueue mainQueue];

    NSURLSession *session = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:mainQueue];

    NSURLSessionDataTask *task = [session dataTaskWithRequest:request];

    [task resume];
}

- (void)stopLoading

相应的还有一个停止请求的方法,也是要实现的。

死循环的坑

有没有看到这两句代码?

if ([NSURLProtocol propertyForKey:protocolKey inRequest:request]) {
    return NO;
}

[NSURLProtocol setProperty:@(YES) forKey:protocolKey inRequest:request];

这两句是为了防止死循环的,也是NSURLProtocol里必须写的方法。试想一下当我在startLoading的时候还会继续发出这个request,那么这个时候还是会拦截到这个request,然后进行处理,然后再次在startLoading中发送出去,然后继续拦截。。。。。。。。

所以在我们startLoading里面,我们对这个request进行标记,标记他已经被处理过了,然后在canInitWithRequest方法中根据这个标记拿到这个request,如果被标记了,就不再次进行处理了,如果没有标记过就要进行处理,这就很好的解决了死循环问题。

NSURLProtocolClient

如果我们使用UIWebView发送一个request,拦截以后当我们使用NSURLSession发出了request,那么这个request的response是无法回到这个UIWebView的,因为可以理解成不是同一个地方发出的request,这个response只能有session来处理,那我们怎么才能让这个response回到刚开始的UIWebView呢?

NSURLProtocolClient就可以看做是URL Loading System,我们把response告诉client,也就是URL Loading System,让他来继续处理这个response,因为一切都是基于URL Loading System发生的,所以把response交给他,他会自动处理这个response回到webView。

每一个NSURLProtocol的子类都有一个client对象来处理请求得到的response。其实下面这些写法都是差不多固定的。

-(void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
    if (error) {
        [self.client URLProtocol:self didFailWithError:error];
    } else {
        [self.client URLProtocolDidFinishLoading:self];
    }
}

-(void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler {
    [self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];

    completionHandler(NSURLSessionResponseAllow);
}

-(void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
    [self.client URLProtocol:self didLoadData:data];
}

- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask willCacheResponse:(NSCachedURLResponse *)proposedResponse completionHandler:(void (^)(NSCachedURLResponse *cachedResponse))completionHandler
{
    completionHandler(proposedResponse);
}

总结

NSURLProtocol的一些坑

  1. 死循环
  2. 调试恶心。因为打开一个页面,里面的每一个请求包括网页图片等都会去走一遍子类中请求处理的判断方法,导致很多想调试的request找不到。
  3. WKWebView不起作用,因为WKWebView走得是WebKit内核,不走苹果这一套逻辑,目前貌似还没有有效的解决方法。

注意点

可以注册多个NSURLProtocol的子类,注册多个NSURLProtocol子类会逆序去执行,也就是先注册的子类后执行。

常见用法总结

  1. 重定向网络请求(已经举过例子了)
  2. 改变request的请求头
- (void)startLoading {
    NSMutableURLRequest *request = [self.request mutableCopy];

    //给请求头添加一个请求体
    NSMutableDictionary *headers = [request.allHTTPHeaderFields mutableCopy];
    [headers setObject:@"ttf" forKey:@"i am ttf"];
    request.allHTTPHeaderFields = headers;

    [NSURLProtocol setProperty:@(YES) forKey:protocolKey inRequest:request];

    .....然后使用NSURLSession发送request
}

  1. 忽略网络请求使用本地缓存

首先自定一个URLResponse类,把资源转化为这个自定义类落地持久化,然后把这个类转换成URL Loading System可以接受的NSURLResponse类,发送给client,其实主要就是startLoading里面。

- (void) startLoading {

    //1\. 获取缓存的response
    CachedURLResponse *cachedResponse = [self cachedResponseForCurrentRequest];

    //2\. 判断缓存response是否存在
    if (cachedResponse) {

        NSData *data = cachedResponse.data;
        NSString *mimeType = cachedResponse.mimeType;
        NSString *encoding = cachedResponse.encoding;

        //构造一个新的response
        NSURLResponse *response = [[NSURLResponse alloc] initWithURL:self.request.URL
                                                            MIMEType:mimeType
                                               expectedContentLength:data.length
                                                    textEncodingName:encoding];

        //将新的response作为request对应的response
        [self.client URLProtocol:self
              didReceiveResponse:response
              cacheStoragePolicy:NSURLCacheStorageNotAllowed];

        //设置request对应的 响应数据 response data
        [self.client URLProtocol:self didLoadData:data];

        //标记请求结束
        [self.client URLProtocolDidFinishLoading:self];

    } else {
        NSMutableURLRequest *newRequest = [self.request mutableCopy];

        [NSURLProtocol setProperty:@YES
                            forKey:MyURLProtocolHandledKey
                         inRequest:newRequest];

        NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
        NSOperationQueue *mainQueue = [NSOperationQueue mainQueue];
        NSURLSession *session = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:mainQueue];
        NSURLSessionDataTask *task = [session dataTaskWithRequest:newRequest];

        [task resume];
    }

}

另外也可以参考一下“OHHTTPStubs的实现方式”,核心就是使用的NSURLProtocol。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,245评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,749评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,960评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,575评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,668评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,670评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,664评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,422评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,864评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,178评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,340评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,015评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,646评论 3 323
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,265评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,494评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,261评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,206评论 2 352

推荐阅读更多精彩内容