809eb27006eb27d68313fb77ce1ebff2
拦截 App 网络请求的那些事儿

一个App会产生很多网络请求, 或是自定义NSURLSession、NSURLConnection的;或是第三方库如AFNetworking、Alamofire、SDWebImage的;或是内嵌WebView如UIWebView、WKWebView的。所有这些请求我们都有办法拦截到中间的网络请求,然后做点黑魔法的事情。

目录

  1. 使用场景
  2. 拦截原理
  3. 拦截自定义的网络请求
  4. 拦截第三方库的网络请求
  5. 拦截WKWebview的网络请求

1. 使用场景

我们可以利用网络拦截做下面这些很酷的事情:

  • 重定向网络请求
  • 加载本地缓存js、css、png等资源
  • 修改Request的Header信息
  • 修改Response信息
  • 统计网络各环节时间(如DNS解析、TLS握手等)

2. 拦截原理

App中所有基于URL加载的网络请求都会经过URL Loading System, 它允许我们的App访问URL指定的内容的。
url loading system

url loading system

其中,有一个非常重要的部分就是NSURLProtocol,正如大神Mattt所说:

NSURLProtocol is both the most obscure and the most powerful part of the URL Loading System. It’s an abstract class that allows subclasses to define the URL loading behavior of new or existing schemes.

URLProtocol并不是一个协议,而是一个抽象类,需要子类化来实现具体的方法。它在系统中位置大致如下:
NSURLProtocol

NSURLProtocol

结合上述图片可以看出,NSURLProtocol可以拦截包括NSURLSessionNSURLConnection以及UIWebVIew的网络请求。

3. 拦截自定义的网络请求

根据上述原理,我们只要子类化一个CustomURLProtocol,然后registerClass就可以拦截自定义的网络请求了。

CustomURLProtocol的代码与注释:

#import "CustomURLProtocol.h"

//标记一个request是否被处理过,防止死循环,因为下面startLoading
//还会继续发这个request,此时还会拦截到这个request, 这个很重要
static NSString *kProtocolKey = @"CustomProtocolKey";

@interface CustomURLProtocol ()

@property (nonatomic,strong) NSURLSessionDataTask *task;

@end

@implementation CustomURLProtocol

//每个请求都会先走这个方法,判断是否要拦截这个请求
//返回YES,则URL Loading System 会创建一个对应NSURLProtocol子类的实例,即拦截请求
//返回NO,则直接跳到下一个Protocol
//此方法可以简单有效地控制拦截哪些请求,比如schme为https或者后缀是png等等
+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
    //防止死循环
    if ([NSURLProtocol propertyForKey:kProtocolKey inRequest:request]) {
        return  NO;
    }
    return YES;
}
//此方法可以重新改造request,比如重定向或修改UA
//也可以直接返回原始request
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request {
    NSMutableURLRequest *mutableReqeust = [request mutableCopy];

    [NSURLProtocol setProperty:@(YES)
                        forKey:kProtocolKey
                     inRequest:mutableReqeust];
    return [mutableReqeust copy];

}
//开始加载一个请求,要重新构造一个NSURLSession或者NSURLConnection的请求
//这里也可以使用本地缓存的文件直接返回,这样就减少了网络请求量
//这里面的self.request是在创建protocol实例时Url Loading System传入的
- (void)startLoading {
    NSURLSession *session = [NSURLSession
                             sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]
                             delegate:self delegateQueue:nil];
    self.task = [session dataTaskWithRequest:self.request];
    [self.task resume];

}

//取消一个请求
- (void)stopLoading {
    [self.task cancel];
    self.task = nil;
}

//请求回调
//每个NSURLProtocol子类都有一个client对象来处理response,这个client可以理解为就是Url Loading System, 交给他全权负责
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler {
    [[self client] URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageAllowed];

    completionHandler(NSURLSessionResponseAllow);
}

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

// 要根据error状态调用不同的client方法,否则会出现错误
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(nullable NSError *)error {
    if (error) {
        [self.client URLProtocol:self didFailWithError:error];
    } else {
        [self.client URLProtocolDidFinishLoading:self];
    }
}

然后我们在AppDelegate的didFinishLaunchingWithOptions:方法中注册一下这个protocol:

[NSURLProtocol registerClass:[CustomURLProtocol class]];

接下来,我们构造一个普通的URLSession网络请求:

NSURLRequest *req = [NSURLRequest requestWithURL:[NSURL URLWithString:@"https://xiaozhuanlan.com"]];
NSURLSession *session = [NSURLSession sharedSession];//注意这里使用的是sharedSession
self.dataTask = [session dataTaskWithRequest:req];
[self.dataTask  resume];

测试发现我们的请求会调用CustomURLProtocol`里的方法,也就是被我们拦截到了。

4. 拦截第三方库的网络请求

那我们如果直接像上面这样写是不是就能监听项目中所有的网络请求了呢?答案是否定的!

你可以在你的真实项目中实验一下,因为真实项目中一定会有第三方库用到网络请求,比如SDWebImage、AFNetworking、Alamofire等等,你会发现这些网络请求并没有被成功拦截到。为什么呢?

对比发现这些第三方库的网络请求的NSURLSession实例都是通过
sessionWithConfiguration:delegate:delegateQueue:方法获得的,
然而我们通过[NSURLSession sharedSession]生成session就可以拦截到请求,可能问题就出在这。

为了验证猜想,我们可以把前面例子中的[NSURLSession sharedSession]换成另外一种写法,就会发现拦截不到请求了:

NSURLRequest *req = [NSURLRequest requestWithURL:[NSURL URLWithString:@"https://xiaozhuanlan.com"]];
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *session = [NSURLSession sessionWithConfiguration:config
                                                          delegate:self
                                                     delegateQueue:nil];
self.dataTask = [session dataTaskWithRequest:req];
[self.dataTask  resume];

我们发现,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;

根据它的注释可以知道,这个数组里的protocol会收到+canInitWithRequest:消息从而拦截请求,而sessionWithConfiguration:delegate:delegateQueue:得到的session,他的configuration中已经有一个NSURLProtocol,我们再看registerClass:方法的定义:

/*! 
    @method registerClass:
    @abstract This method registers a protocol class, making it visible
    to several other NSURLProtocol class methods.
    @discussion When the URL loading system begins to load a request,
    each protocol class that has been registered is consulted in turn to
    see if it can be initialized with a given request. The first
    protocol handler class to provide a YES answer to
    <tt>+canInitWithRequest:</tt> "wins" and that protocol
    implementation is used to perform the URL load. There is no
    guarantee that all registered protocol classes will be consulted.
    Hence, it should be noted that registering a class places it first
    on the list of classes that will be consulted in calls to
    <tt>+canInitWithRequest:</tt>, moving it in front of all classes
    that had been registered previously.
    <p>A similar design governs the process to create the canonical form
    of a request with the <tt>+canonicalRequestForRequest:</tt> class
    method.
    @param protocolClass the class to register.
    @result YES if the protocol was registered successfully, NO if not.
    The only way that failure can occur is if the given class is not a
    subclass of NSURLProtocol.
*/
+ (BOOL)registerClass:(Class)protocolClass;

意思就是Url loading system开始加载一个请求时,会检测所有注册的protocol class,一旦一个protocol的+canInitWithRequest:方法返回YES, 就走那个protocol的方法。所以我们要确保CustomURLProtocol实例插入到数组的第一个。

top Created with Sketch.