使用 NSURLProtocol 拦截app内部的网络请求

就在昨天被一个大佬问了一个问题,如何拦截app内部的所有网络请求,并且在请求的头部动态添加一些内容,之前有看过这方面的资料,但是自己没有去实现过,产品也没这个需求,现在被人问到了,也就来仔细研究一下。APP内的网络请求的监控,相信很多APP内都有这个模块,通过监控APP内的网络请求,观察各个API的稳定性。这些数据,一般我们都会先收集起来,在一段时间内,上传到服务器。在iOS中,出了WK的网络请求,其他的所有网络请求都可以通过NSURLProtocol来拦截监控。
在iOS中苹果提供了NSURLConnection、NSURLSession等优秀的网路接口供我们来调用,开源社区也有很多的开源库,如之前的ASIHttpRequest 现在的AFNetworking和Alamofire,我们接下来介绍的NSURLProtocol,都可以监控到这些开源库的网络请求

NSURLProtocol是iOS网络加载系统中很强的一部分,它其实是一个抽象类,我们可以通过继承子类化来拦截APP中的网络请求。

NSURLProtocol 是真的很强,可以拦截应用内几乎所有的网络请求(除了WKWebView),并可以修改请求头,返回client任意自定义的数据等等,据说很多做网络缓存都是利用这个类的。
我们的APP内的所有请求都需要增加公共的头,像这种我们就可以直接通过NSURLProtocol来实现,当然实现的方式有很多种 ;再比如我们需要将APP某个API进行一些访问的统计,我们需要统计APP内的网络请求失败率,都可以用到。

具体使用步骤

  1. 子类化 NSURLProtocol 为 CustomURLProtocol(子类名可以自己随便起)
static NSString* const URLProtocolHandledKey = @"URLProtocolHandledKey";
@interface CustomURLProtocol ()<NSURLConnectionDelegate>
@property (nonatomic, strong) NSURLSession *session;
@property (nonatomic, strong) NSURLRequest *myRequest;
@end
  1. 在NSURLProtocol中,我们需要告诉它哪些网络请求是需要我们拦截的,这个是通过方法can​Init​With​Request:​来实现的,比如我们现在需要拦截全部的HTTP和HTTPS请求,那么这个逻辑我们就可以在can​Init​With​Request:​中来定义
+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
    //避免死循环
    if ([NSURLProtocol propertyForKey:URLProtocolHandledKey inRequest:request]) {
        return NO;
    }
    if ([request.URL.scheme isEqualToString:@"http"]
        || [request.URL.scheme isEqualToString:@"https"]) {
        return YES;
    }
    return NO;
}
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request {
    NSMutableURLRequest *mutableReqeust = [request mutableCopy];
    //标记一下,避免死循环
    [NSURLProtocol setProperty:@YES
                        forKey:URLProtocolHandledKey
                     inRequest:mutableReqeust];
    return [mutableReqeust copy];
}
+ (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b {
    return [super requestIsCacheEquivalent:a toRequest:b];
}
  1. 开始加载和结束加载
//开始加载
- (void)startLoading {
    NSURLRequest *request = [[self class] canonicalRequestForRequest:self.request];
    self.connection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:YES];
    self.myRequest =  request;
}
//结束加载
- (void)stopLoading {
    [self.connection cancel];
}
  1. 执行connection代理方法,实现数据监听

- (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];
}

在我们上层业务调用网络请求的时候,首先会调用我们的can​Init​With​Request:方法,询问是否对该请求进行处理,接着会调用我们的canonicalRequestForRequest:来自定义一个request,接着又会去调用can​Init​With​Request:询问自定义的request是否需要处理,我们又返回YES,然后又去调用了canonicalRequestForRequest:,这样,就形成了一个死循环了,这肯定是我们不希望看到的

  1. 在应用启动的时候注册进去
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    [NSURLProtocol registerClass:CustomURLProtocol.class];
    return YES;
}

到这里基本差不多了,到这里NSURLProtocol的使用方法大家应该有所了解了。下面主要讲一下NSURLProtocol在
使用过程中可能会遇到的坑,给自己以及需要的朋友留个提醒。

  1. 如果[NSURLSession sharedSession]创建的session对象是可以拦截的,如果是NSURLSession是使用这两个方法创建的就拦截不到了
+ (NSURLSession *)sessionWithConfiguration:(NSURLSessionConfiguration *)configuration;
+ (NSURLSession *)sessionWithConfiguration:(NSURLSessionConfiguration *)configuration 
                                  delegate:(nullable id <NSURLSessionDelegate>)delegate 
                             delegateQueue:(nullable NSOperationQueue *)queue;

也不是说一定拦击不到,点进 NSURLSessionConfiguration 类的文件中可以看到如下说明

/* An optional array of Class objects which subclass NSURLProtocol.
   The Class will be sent +canInitWithRequest: when determining if
   an instance of the class can be used for a given URL scheme.
   You should not use +[NSURLProtocol registerClass:], as that
   method will register your class with the default session rather
   than with an instance of NSURLSession. 
   Custom NSURLProtocol subclasses are not available to background
   sessions.
 */
@property (nullable, copy) NSArray<Class> *protocolClasses;

一个可选的类对象数组,它是NSURLProtocol的子类。类将被发送+canInitWithRequest:当确定是否该类的实例可以用于给定的URL模式。因此,您不应该使用+[NSURLProtocol registerClass:]方法将您的类注册为默认会话而不是NSURLSession的实例。自定义NSURLProtocol子类对后台不可用会话。
我有一个大胆想法,就是写一个 NSURLSessionConfiguration 的分类,然后也写个protocolClasses属性的get方法,由于在方法调用的时候会优先调用分类方法,所以NSURLSessionConfiguration类在调用protocolClasses时就会调用到分类中的方法,代码如下

@implementation NSURLSessionConfiguration (Custom)

- (NSArray<Class> *)protocolClasses {
    return @[CustomURLProtocol.class];
}
@end
  1. 上面一开始就已经说了,对于WebView的请求,目前NSURLProtocol还不能拦截WKWebView的请求,只能拦截UIWebview的,但后者好像AppStore已经不让审核通过了。

  2. NSURLProtocol在拦截NSURLSession的POST请求时不能获取到Request中的HTTPBody,这个貌似早就国外的论坛上传开了,但国内好像还鲜有人知,据苹果官方的解释是Body是NSData类型,即可能为二进制内容,而且还没有大小限制,所以可能会很大,为了性能考虑,索性就拦截时就不拷贝了(内流满面脸)。为了解决这个问题,我们可以通过把Body数据放到Header中,不过Header的大小好像是有限制的,我试过2M是没有问题,不过超过10M就直接Request timeout了。。。而且当Body数据为二进制数据时这招也没辙了,因为Header里都是文本数据,另一种方案就是用一个NSDictionary或NSCache保存没有请求的Body数据,用URL为key,最后方法就是别用NSURLSession,老老实实用古老的NSURLConnection算了。。。

  3. 使用NSURLProtocol时,在那两个类方法可以发送同步网络请求,而实例方法,如startLoading则进入死锁,直至超时,原因是执行实例方法所在的线程并没有启动runloop,而NSURLConnection这些网络请求需要依赖于runloop的,因此这些请求根本发不出去,所以必须使用异步请求,NSURLConnection/NSURLSession的异步请求的线程保证启动了runloop。

以上就是我目前发现的坑,欢迎大家补充,也希望对大家开发有所帮助哈~。
所幸的是NSURLProtocol对于大量并发的请求支持的还不错,不然就要弃用了~

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

推荐阅读更多精彩内容