Round 1

Recently, the company’s file server has been reformed, so even the image loading request must carry token, otherwise it cannot be loaded. However, SDWebImage is used for image loading in our project. When I heard this requirement, I felt no fluctuation in my mind, and my mind was already…. You know, but you still have to do it. Three times five by two, you’ve written the following code:

[[SDWebImageDownloader sharedDownloader] setValue:Your token "@" forHTTPHeaderField:@"Authorization"];
Copy the code

The SDWebImage download is handled by the SDWebImage downloader singleton class, so add this code in the appropriate place in your project, and all the places in your project that use SDWebImage for image loading will carry the token

Round 2

After this modification, the places that could not be loaded became normal, until one day, it was spring… I need to add a requirement in the project, which needs to reference a private Pod library of the company. It is another operation, the integration is completed, the logic writing is completed, run, the picture is not loaded, I don’t know what the situation is, I confirm again, my token setting is completed

I went to ask my colleague who wrote this Pod whether I had not finished the configuration. After two seconds of meditation, he said, after adding the token, you need to modify the source code in SDWebImageDownloader. I:?? Then he found the source code, which looks like this:

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task willPerformHTTPRedirection:(NSHTTPURLResponse *)response newRequest:(NSURLRequest *)request completionHandler:(void(^) (NSURLRequest * _Nullable))completionHandler {

    // Identify the operation that runs this task and pass it the delegate method
    NSOperation<SDWebImageDownloaderOperation> *dataOperation = [self operationWithTask:task];
    if ([dataOperation respondsToSelector:@selector(URLSession:task:willPerformHTTPRedirection:newRequest:completionHandler:)]) {
        [dataOperation URLSession:session task:task willPerformHTTPRedirection:response newRequest:request completionHandler:completionHandler];
    } else {
        if(completionHandler) { completionHandler(request); }}}Copy the code

He explained that some image links in this function module carry parameters that do not directly point to the resource, so the request will be redirected, so it needs to be processed here. After processing, the code looks like this:

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task willPerformHTTPRedirection:(NSHTTPURLResponse *)response newRequest:(NSURLRequest *)request completionHandler:(void(^) (NSURLRequest * _Nullable))completionHandler {
    
    // Identify the operation that runs this task and pass it the delegate method
    NSOperation<SDWebImageDownloaderOperation> *dataOperation = [self operationWithTask:task];
    NSMutableURLRequest *customRequest = [[NSMutableURLRequest alloc] initWithURL:request.URL];
    customRequest.allHTTPHeaderFields = self.HTTPHeaders;
    if ([dataOperation respondsToSelector:@selector(URLSession:task:willPerformHTTPRedirection:newRequest:completionHandler:)]) {
        [dataOperation URLSession:session task:task willPerformHTTPRedirection:response newRequest:customRequest completionHandler:completionHandler];
    } else {
        if(completionHandler) { completionHandler(customRequest); }}}Copy the code

After I modify the code, run, indeed, the problem is solved, but no, our SDWebImage is integrated through Pod, so we can directly modify the source code of three parties under Pod folder, so the next update, won’t it be overwritten? This is a new problem, so I began to think about how to solve the redirection problem without modifying the source code of the tripartite library, and AOP immediately came to mind

A good AOP library for iOS development must have Aspects names. Then I started to think about the steps. First of all, according to the code my colleague provided to solve the problem, he changed the parameter request to a customRequest. The allHTTPHeaderFields are then reset

NSMutableURLRequest *customRequest = [[NSMutableURLRequest alloc] initWithURL:request.URL];
customRequest.allHTTPHeaderFields = self.HTTPHeaders;
Copy the code

After printing the allHTTPHeaderFields of Request and customRequest, I found that the former has less tokens than the latter. No wonder it cannot be loaded. Here I would like to mention the following method

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task willPerformHTTPRedirection:(NSHTTPURLResponse *)response newRequest:(NSURLRequest *)request completionHandler:(void(^) (NSURLRequest * _Nullable))completionHandler 
Copy the code

This is not the SDWebImageDownloader method, it is the NSURLSessionTaskDelegate protocol method, SDWebImageDownloader does its own redirection in the protocol method

To paraphrase my initial thoughts, since the problem is that the request does not carry the token we added manually when redirecting, and the redirection must be handled here, we will directly set the relevant parameters to request, there is no need to create a new customRequest instance

[request setValue:self.HTTPHeaders forKey:@"allHTTPHeaderFields"];
Copy the code

Since request is an NSURLRequest object, its allHTTPHeaderFields is a readonly property, which we can’t modify directly, so I took it for granted to use KVC, and then run

"[<NSURLRequest 0x2800efa50> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key allHTTPHeaderFields."
Copy the code

NSURLRequest does not have the corresponding allHTTPHeaderFields key. For a moment, I clicked on the NSURLRequest class to make sure that it did, but in the spirit of curiosity, I wondered if it was used internally by NSURLRequest if it wasn’t called allHTTPHeaderFields, but no, this property was named in NSURLRequest, and even if it was something else, it should be reassigned internally and I shouldn’t give an error here, but I printed it out via runtime, Reconfirming that he does have the allHTTPHeaderFields attribute, I searched for related questions and found a keyword

+ (BOOL)accessInstanceVariablesDirectly
Copy the code

This is for KVC. In general, when a class implements this method and returns YES, it can be assigned by KVC. If it returns NO, it cannot be assigned by KVC

I’m guessing that this method in the NSURLRequest returns NO, and that’s it, it doesn’t work, and I’m instantiating a new object

Round 3

Make along while want to save trouble looks not good, that pull down, directly start AOP modification:

    NSError * error ;
    [[SDWebImageDownloader sharedDownloader] aspect_hookSelector:@selector(URLSession: task: willPerformHTTPRedirection: newRequest: completionHandler:) withOptions:AspectPositionInstead usingBlock:^(id<AspectInfo> aspectInfo, NSString *num){
        
        NSArray * param = aspectInfo.arguments;
        NSURLRequest * request = param[3];
        NSURLSessionTask * task = param[1];
        NSMutableURLRequest *customRequest = [[NSMutableURLRequest alloc] initWithURL:request.URL];
        customRequest.allHTTPHeaderFields = task.currentRequest.allHTTPHeaderFields;
        request = customRequest;
        NSInvocation * invocation = aspectInfo.originalInvocation;
        [invocation setArgument:&request atIndex:5];
        [invocation invoke];
        
    } error:&error];
Copy the code

Here, aspectInfo is the information of the hook method. It can be used to fetch the parameters of the original method, call the object, etc. We need to add our token here, so we need to fetch the parameters for modification

  • Arguments is an array of input arguments to the original method
  • The Invocation is a message object that contains all the information for a method

Through URLSession: task: willPerformHTTPRedirection: newRequest: CompletionHandler: We know that the request index is 3, and the Task index is 1 (we’re fetching it because we need to get the original header, so we can’t discard it), and then reassign the request, complete the modification, and call the method again

 [invocation setArgument:&request atIndex:5];
 [invocation invoke];
Copy the code

Since we only need to modify the request object, we only need to reset this method entry. Why is the index 5 assigned, because the first two are occupied by the method self and _cmd respectively, so we set the parameter from the index 2

Run again, well, images load smoothly, problem solved.

In this way, we do not need to modify the source code of the three parties to solve the problem, otherwise the source code to modify the Pod update each time our changes will be overwritten, if the version is not noticed, the test is not regressive coverage, it is easy to bring the problem online