Detailed explanation of the pits of nsurlprotocol in IOS development

Time:2021-2-18

NSURLProtocol

Nsurlprotocol allows you to redefine the behavior of Apple’s URL loading system. There are many classes in the URL loading system used to process URL requests, such as nsurl, nsurlrequest, nsurlconnection and nsurlsession When system uses nsurlrequest to obtain resources, it will create an instance of nsurlprotocol subclass. You should not directly instantiate a nsurlprotocol. Nsurlprotocol looks like a protocol, but it is actually a class, and it must use the subclass of the class, and it needs to be registered.

Usage scenarios

No matter you use uiwebview, nsurlconnection or third-party libraries (afnetworking, mknetworkkit, etc.), they are all based on nsurlconnection or nsurlsession, so you can do custom operations through nsurlprotocol.

  1. Redirecting network requests
  2. Ignore network requests and use local caching
  3. Customize the return result of network request
  4. Some global network request settings

After contacting the URL loading system in IOS system, we all know that nsurlprotocol is so powerful that it can intercept almost all network requests in the application (except wkwebview), modify the request header, return any customized data of the client, and so on. It is said that many network buffers use this class.

So, let’s first explain how to use nsurlprotocol.

1. Define a subclass of nsurlprotocol

In inheriting nsurlprotocol, we need to implement

+(bool) caninitwithrequest: (nsurlrequest *) request, which defines the URL rules to intercept requests

-(void) startloading. For intercepted requests, the system creates a nsurlprotocol object and executes the startloading method to start loading requests

-(void) stoploading. For intercepted requests, the nsurlprotocol object calls this method when it stops loading

+(nsurlrequest *) canonical request for request: (nsurlrequest *) request, optional method. For the request that needs to modify the request header, modify it in this method

The following code defines a subclass of nsurlprotocol that specifically intercepts the HTTPS request and re requests it through cfhttpmessage Ref

@interface CFHttpMessageURLProtocol () <NSStreamDelegate> { 
  NSMutableURLRequest *curRequest; 
  NSRunLoop *curRunLoop; 
  NSInputStream *inputStream; 
} 
 
@end 
 
@implementation CFHttpMessageURLProtocol 
 
/** 
 *Whether to intercept and process the specified request 
 * 
 *Request specified by @ param request 
 * 
 *@ return returns yes to intercept, and no to not intercept 
 */ 
+ (BOOL)canInitWithRequest:(NSURLRequest *)request { 
   
  /*Prevent infinite loop, because a request will also initiate a request in the process of being intercepted, which will come here again. If it is not processed, it will cause infinite loop*/ 
  if ([NSURLProtocol propertyForKey:protocolKey inRequest:request]) { 
    return NO; 
  } 
   
  NSString *url = request.URL.absoluteString; 
   
  //If the URL starts with HTTPS, it will be intercepted, otherwise it will not be processed 
  if ([url hasPrefix:@"https"]) { 
    return YES; 
  } 
  return NO; 
} 
 
/** 
 *If you need to redirect the request and add the specified header, you can do it in this method 
 */ 
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request { 
  return request; 
} 
 
/** 
 *Start loading. In this method, load a request 
 */ 
- (void)startLoading { 
  NSMutableURLRequest *request = [self.request mutableCopy]; 
  //Indicates that the request has been processed to prevent an infinite loop 
  [NSURLProtocol setProperty:@(YES) forKey:protocolKey inRequest:request]; 
  curRequest = request; 
  [self startRequest]; 
} 
 
/** 
 *Cancel request 
 */ 
- (void)stopLoading { 
  if (inputStream.streamStatus == NSStreamStatusOpen) { 
    [inputStream removeFromRunLoop:curRunLoop forMode:NSRunLoopCommonModes]; 
    [inputStream setDelegate:nil]; 
    [inputStream close]; 
  } 
  [self.client URLProtocol:self didFailWithError:[[NSError alloc] initWithDomain:@"stop loading" code:-1 userInfo:nil]]; 
}

The startrequest method in the above code is by copying the original request header and using cfhttpmessage ref to re initiate the request. As this part of the code has little to do with the content of this article, I will not put it here. If you are interested, please refer to my next blog.

2. Register nsurlprotocol before network request

//Register the nsurlprotocol to intercept requests 
[NSURLProtocol registerClass:[CFHttpMessageURLProtocol class]];

For nsurlsession requests, the way to register nsurlprotocol is slightly different. It is registered through nsurlsession configuration

//Nsurlsession example 
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration]; 
NSArray *protocolArray = @[ [CFHttpMessageURLProtocol class] ]; 
configuration.protocolClasses = protocolArray; 
NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:[NSOperationQueue mainQueue]]; 
NSURLSessionTask *task = [session dataTaskWithRequest:_request]; 
[task resume];

3. Log off nsurlprotocol after the request


[NSURLProtocol unregisterClass:[CFHttpMessageURLProtocol class]]; 

Well, you should know how to use nsurlprotocol. The following mainly talks about the pit that nsurlprotocol may encounter in the process of using. Leave a reminder for yourself and friends in need.

1. It has been said from the beginning that for WebView requests, nsurlprotocol can not intercept wkwebview requests at present, it can only intercept uiwebview requests, but the latter seems that the app store has not been approved (embarrassed face).

two When nsurlprotocol intercepts the post request of nsurlsession, it can’t get the HTTP body in the request. This seems to have been uploaded in foreign forums for a long time, but few people in China seem to know it. According to Apple’s official explanation, the body is nsdata type, which can be binary content, and there is no size limit, so it may be very large. For performance reasons, it is not necessary to simply intercept Copy it. In order to solve this problem, we can put the body data into the header, but the size of the header seems to be limited. I tried 2m, but it’s not a problem, but if it’s more than 10m, we can directly request timeout… Moreover, when the body data is binary data, this method is useless, because the header is full of text data. Another solution is to use an nsdictionary or nscache to save the body data that has not been requested, and use the URL as the key. Finally, don’t use nsurlsession, just use the old nsurlconnection…

three When using nsurlprotocol, the two class methods can send synchronous network requests, while the instance method, such as startloading, will enter deadlock until timeout. The reason is that the thread executing the instance method does not start runloop, and nsurlconnection these network requests need to depend on runloop, so these requests can’t be sent at all, so asynchronous requests must be used, n The thread of asynchronous request of surlconnection / nsurlsession ensures that runloop is started.

The above is the pit I found at present, welcome to add, and hope to help you develop. Ha ~ fortunately, nsurlprotocol supports a large number of concurrent requests well, otherwise it will be abandoned ~ hope to help you learn, and also hope you can support developer more.