NSURLProtocol之网络拦截

NSURLProtocol之网络拦截_第1张图片
Qinz
NSURLProtocol是一个抽象类,需要子类去实例化,在使用的时候注册该子类,则可在自定义的 NSURLProtocol 中拦截所有的请求,进行广告过滤或重定向等操作,下面将以拦截百度为例分析该过程及使用方法。
1. 首先我们需要创建一个NSURLProtocol的子类,在使用的时候进行注册:
   [NSURLProtocol registerClass:[QURLProtocol class]];
  • 1.1 这里注意要释放
- (void)dealloc{
    [NSURLProtocol unregisterClass:[KCURLProtocol class]];
}
2. 接下来在子类中重写必须实现的5个方法:
+ (BOOL)canInitWithRequest:(NSURLRequest *)request;
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request;
+ (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b;
- (void)startLoading;
- (void)stopLoading;
3. 我们在控制器中加载一个百度的网页
  NSURLRequest *request = [NSURLRequest requestWithURL: 
  [NSURL URLWithString:@"https://www.baidu.com/"]];
  [self.webView loadRequest:request];
4. 在canInitWithRequest方法中拦截百度网址,代码如下:
+(BOOL)canInitWithRequest:(NSURLRequest *)request{
    //已经拦截过的就不再k拦截,避免死循环
    if ([NSURLProtocol propertyForKey:QZProtocolKey inRequest:request]) {
        return NO;
    }
   //拦截百度,这里可以使用isEqualToString进行精准拦截
    if ([[request.URL absoluteString] containsString:@"www.baidu.com"]) {

        return YES;
    }
    return NO;
}
5. 接下来在startLoading对拦截的地址进行重定向,代码如下:
- (void)startLoading{
    
    //标记,下次不拦截自己设置的
    [NSURLProtocol setProperty:@(YES) forKey:QZProtocolKey inRequest:[self.request mutableCopy]];
 
    //重定向
    if ([[self.request.URL absoluteString] isEqualToString:@"https://www.baidu.com/"]) {
      
        NSString*url = @"https://www.jianshu.com/";
        NSURLRequest*myRequest = [NSURLRequest requestWithURL:[NSURL URLWithString:url]];
        
        NSURLSessionConfiguration *configuration =
        [NSURLSessionConfiguration defaultSessionConfiguration];
        
        self.queue = [[NSOperationQueue alloc] init];
        self.queue.maxConcurrentOperationCount = 1;
        self.queue.name = @"com.Qinz.cn";
        
        NSURLSession *session =
        [NSURLSession sessionWithConfiguration:configuration
                                      delegate:self
                                 delegateQueue:self.queue];
        //偷梁换柱
        self.task = [session dataTaskWithRequest:myRequest];
        [self.task resume];
        
    }
}
6. 记得在stopLoading方法中对任务进行取消:
- (void)stopLoading{
    
     [self.task cancel];
}
7. 在NSURLSessionDataDelegate中对重定向的数据进行处理:
#pragma mark - NSURLSessionDataDelegate
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
    didReceiveData:(NSData *)data {
    
    if ([self.request.URL.absoluteString isEqualToString:@"https://www.baidu.com/"]) {
        // 将接收到的数据返回给系统处理
        [self.client URLProtocol:self didLoadData:data];
    }
}

- (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 task:(NSURLSessionTask *)task willPerformHTTPRedirection:(NSHTTPURLResponse *)response newRequest:(NSURLRequest *)request completionHandler:(void (^)(NSURLRequest * _Nullable))completionHandler {
    
    if (response != nil){
        [[self client] URLProtocol:self wasRedirectedToRequest:request redirectResponse:response];
    }
}
8. 这里简单演示下广告过滤,因为广告一般为图片,所以我们拦截相关类型的图片,然后替换为自己的数据,代码如下:
+(BOOL)canInitWithRequest:(NSURLRequest *)request{
    
    //Hook图片,用于广告过滤等
    NSArray *array = @[@"png", @"jpeg", @"gif", @"jpg"];
    if([array containsObject:request.URL.pathExtension]){
        return YES;
    }
    return NO;
}

- (void)startLoading{
    //过滤广告
    NSArray *array = @[@"png",@"jpg",@"jpeg"];
    if ([array containsObject:[self.request.URL pathExtension]]) {
        NSData *data = [self getImageData];
        [self.client URLProtocol:self didLoadData:data];
    }
    
}
  • 8.1 百度的logo已经被替换,当然这里只是简单演示给出思路,具体思想还有很多细节要处理。


    NSURLProtocol之网络拦截_第2张图片
    image.png
9. 上面还有两个方法,没特殊需求重写父类即可:
//返回规范的request
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request{   
    return request;
}
/**
这个方法主要用来判断两个请求是否是同一个请求,如果是,则可以使用缓存数据,通常只需要调用父类的实现即可,默认为YES
 */
+ (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b {
    return [super requestIsCacheEquivalent:a toRequest:b];
}
10. 当我们对Session进行拦截时会发现不成功(如AF),这里需要进行特殊处理:
    AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
    NSString *url  = @"http://www.baidu.com";
    [manager GET:url parameters:nil progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
        
        NSLog(@"AFN---%@",responseObject);
    } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
        NSLog(@"AFN---%@",error);
    }];
11. 交换系统Session配置方法,返回我们自己的子类即可:
#pragma mark - hook

+ (void)hookNSURLSessionConfiguration{
    
    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:@"没有这个方法 无法交换"];
    }
    method_exchangeImplementations(originalMethod, stubMethod);
}

- (NSArray *)protocolClasses {
    return @[[QURLProtocol class]];
    //如果还有其他的监控protocol,也可以在这里加进去
}
注意:当我们在startLoading进行拦截处理时,要做好对应的逻辑判断,否则会引发崩溃!

以上就是对NSURLProtocol拦截网络详细分析,当然NSURLProtocol还可以做很多事情,如增加公共请求头,对API进行一些访问的统计等。最后附上Demo下载,如果帮助到你请给一个Star!

我是Qinz,希望我的文章对你有帮助。

你可能感兴趣的:(NSURLProtocol之网络拦截)