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已经被替换,当然这里只是简单演示给出思路,具体思想还有很多细节要处理。
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,希望我的文章对你有帮助。
网友评论