Network layer data collection

Network layer data, generally to collect API request frequency, API request time, success rate and other information. If there is an unburied way to collect network information, it must be through the AOP approach, hook corresponding methods and corresponding delegate methods to implement this requirement.

Network data fetching for NSURLSession

NSURLSession initiates the network request according to the [Task Resume] generated by the response.

NSURLSession then provides two ways to handle callback requests: delegate and block.

Delegate callback mode

Network request callback is processed through the delegate callback. AFNetWorking requests initiated by NSURLSession are processed through the delegate callback. However, block callback is usually used.

Look at how the NSURLSession is initialized and how the delegate is set

+ (NSURLSession *)sessionWithConfiguration:(NSURLSessionConfiguration *)configuration;
+ (NSURLSession *)sessionWithConfiguration:(NSURLSessionConfiguration *)configuration delegate:(nullable id <NSURLSessionDelegate>)delegate delegateQueue:(nullable NSOperationQueue *)queue;
Copy the code

Is provided by the two class constructor, from the above two structure parameters, we can guess, actually sessionWithConfiguration: in the end is also called sessionWithConfiguration: delegate: delegateQueu: method, to initialize. We usually put sessionWithConfiguration: delegate: delegateQueu: called the factory class methods.

Another method that we often use to get session instances is sharedSession. What’s the difference between this session and the two class constructors? Actually we when initializing the session, regardless of which a class constructor calls when initializing the session, sharedSession will be called sessionWithConfiguration: method to initialize a singleton session, But the singleton session there are a lot of restrictions, such as cookies, cache, etc., specific instructions, see the developer.apple.com/documentati…

What does that mean? It’s a long sentence. It means that, if we initialize a session, through method sessionWithConfiguration:, actually inside NSURLSession can call this method twice, the first is that we take the initiative to call generates a session, return to us, The other call is sharedSession, which generates a default singleton session. Note: Since sharedSession is a singleton session, sharedSession is called only when the session is first generated. Of course, by method sessionWithConfiguration: delegate: delegateQueu: initialize the session is the same.

NSURLSession’s delegate is a read-only property, so we can only hook the session at initialization

@property (nullable.readonly.retain) id <NSURLSessionDelegate> delegate;
Copy the code

hook delegate

First of all, we have three methods that can get an instance of a session. There’s only one constructor that actually has a delegate. The other two methods don’t have a delegate.

A session without a delegate gets the result of a request through a block callback, so we can hook the session method that contains the block callback, and then pass in our own block to get the result of the network callback.

* * note: ** If a session has both a delegate and a block callback, the delegate will not be triggered and will be called directly to the block, because if there is no request made through a block callback, the session will actually call the block-containing method. More on this later

Let’s take a look at the code

We’ll start with the Hook class constructor, which acts as a hook delegate. Because of the need to delegate to get through the network to the callback class constructor only sessionWithConfiguration: delegate: delegateQueue: method, so you just need to put this constructor hook off, and then get a delegate, Then hook the delegate method

In a classification of NSURLSession, in the load method, we will sessionWithConfiguration: delegate: delegateQueue: hook

Hook_Method(cls, @selector(sessionWithConfiguration:delegate:delegateQueue:), cls, @selector(hook_sessionWithConfiguration:delegate:delegateQueue:),YES);
Copy the code

Specific hook implementation method, this method hook class method and hook instance method are put in the inside, because we will need hook session instance method later

static void Hook_Method(Class originalClass, SEL originalSel, Class replaceClass, SEL replaceSel, BOOL isHookClassMethod) {
    
    Method originalMethod = NULL;
    Method replaceMethod = NULL;
    
    if (isHookClassMethod) {
        originalMethod = class_getClassMethod(originalClass, originalSel);
        replaceMethod = class_getClassMethod(replaceClass, replaceSel);
    } else {
        originalMethod = class_getInstanceMethod(originalClass, originalSel);
        replaceMethod = class_getInstanceMethod(replaceClass, replaceSel);
    }
    if(! originalMethod || ! replaceMethod) {return;
    }
    IMP originalIMP = method_getImplementation(originalMethod);
    IMP replaceIMP = method_getImplementation(replaceMethod);
    
    const char *originalType = method_getTypeEncoding(originalMethod);
    const char *replaceType = method_getTypeEncoding(replaceMethod);
    
    // The class_replaceMethod method must be used to point the replacement method to the original implementation, and then the original implementation to the replacement method. Otherwise, if the original method is called at the end of this sentence, the implementation of the replacement method has not yet pointed to the original implementation. So now you have a loop
    if (isHookClassMethod) {
        Class originalMetaClass = objc_getMetaClass(class_getName(originalClass));
        Class replaceMetaClass = objc_getMetaClass(class_getName(replaceClass));
        class_replaceMethod(replaceMetaClass,replaceSel,originalIMP,originalType);
        class_replaceMethod(originalMetaClass,originalSel,replaceIMP,replaceType);
    } else {
        class_replaceMethod(replaceClass,replaceSel,originalIMP,originalType);
        class_replaceMethod(originalClass,originalSel,replaceIMP,replaceType);
    }
Copy the code

And then in our hook implementation method

+ (NSURLSession *)hook_sessionWithConfiguration: (NSURLSessionConfiguration *)configuration delegate: (id<NSURLSessionDelegate>)delegate delegateQueue: (NSOperationQueue *)queue {
    if (delegate) {
        Hook_Delegate_Method([delegate class].@selector(URLSession:dataTask:didReceiveData:), [self class].@selector(hook_URLSession:dataTask:didReceiveData:), @selector(none_URLSession:dataTask:didReceiveData:));
    }
    
    return [self hook_sessionWithConfiguration: configuration delegate: delegate delegateQueue: queue];
}
Copy the code

Again, hook delegate methods

/ / hook delegate method
static void Hook_Delegate_Method(Class originalClass, SEL originalSel, Class replaceClass, SEL replaceSel, SEL noneSel) {
    Method originalMethod = class_getInstanceMethod(originalClass, originalSel);
    Method replaceMethod = class_getInstanceMethod(replaceClass, replaceSel);
    if(! originalMethod) {// The delegate method is not implemented
        Method noneMethod = class_getInstanceMethod(replaceClass, noneSel);
        BOOL didAddNoneMethod = class_addMethod(originalClass, originalSel, method_getImplementation(noneMethod), method_getTypeEncoding(noneMethod));
        if (didAddNoneMethod) {
            NSLog(@" Unimplemented delegate method added successfully");
        }
        return;
    }
    BOOL didAddReplaceMethod = class_addMethod(originalClass, replaceSel, method_getImplementation(replaceMethod), method_getTypeEncoding(replaceMethod));
    if (didAddReplaceMethod) {
        NSLog(@" Hook method added successfully"); Method newMethod = class_getInstanceMethod(originalClass, replaceSel); method_exchangeImplementations(originalMethod, newMethod); }}Copy the code

One thing to note here is that if we want a hook’s delegate method to be unimplemented, and we want to hook away the method, we need to add the unimplemented delegate method to it first, and then replace the method

Then implement the corresponding replacement method in our replacement class

- (void)hook_URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
         didReceiveData:(NSData *)data {
    [self hook_URLSession:session dataTask:dataTask didReceiveData:data];
}

- (void)none_URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
         didReceiveData:(NSData *)data {
    NSLog(11 "@");
}
Copy the code

Replace the block callback

If the session doesn’t use the delegate to get the callback, what do we need to do?

Instead of a delegate, it’s a set of block request methods in the session, called asynchronous facilitation request methods, All definitions in a classification NSURLSession NSURLSession (NSURLSessionAsynchronousConvenience)

- (NSURLSessionDataTask *)dataTaskWithRequest:(NSURLRequest *)request completionHandler:(void(^) (NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error))completionHandler;
- (NSURLSessionDataTask *)dataTaskWithURL:(NSURL *)url completionHandler:(void(^) (NSData * _Nullable data, NSURLResponse * _Nullable response, NSError* _Nullable error))completionHandler; .Copy the code

Here is an example, to show how to hook with a block argument method, in fact, that is to construct a block with the same parameter, pass their own block in

Again, let’s just replace the method

Hook_Method(cls, @selector(dataTaskWithRequest:completionHandler:), cls, @selector(hook_dataTaskWithRequest:completionHandler:),NO);
Copy the code

And then, in our hook method

- (NSURLSessionDataTask *)hook_dataTaskWithRequest:(NSURLRequest *)request completionHandler:(void(^) (NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error))completionHandler {
    NSLog(@ "33");
    
    void (^customBlock)(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) = ^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        if (completionHandler) {
            completionHandler(data,response,error);
        }
        // do your own thing
    };
    if (completionHandler) {
        return [self hook_dataTaskWithRequest:request completionHandler:customBlock];
    } else {
        return [self hook_dataTaskWithRequest:request completionHandler:nil]; }}Copy the code

Notice that we need to check if the current block exists, because when we hook this method, if it’s the current session, the network callback needs to be done through the delegate, but the request will still go to our hook method, because it’s implemented inside the session, I guess it should be similar to the factory method of processing

So it says if the block callback is empty, just pass in nil, and then you can get the result of the callback through the delegate

Here is a simple example of a hook with a block argument and other methods are similar, not listed here

This article is mainly about the hook system’s default HTTP request method, because NSURLConnection has been abandoned, so there is no hook to do this, but the implementation is similar

In the next article, we will cover socket hooks, then view selection, etc. This series will share some of the main processing methods of unburied.

In addition: Before doing this hook, NSURLProtocol was also used to intercept some network processing, but this method was abandoned because it involved the problem of multiple protocols, because multiple protocols have been used in the current project. Moreover, based on previous processing, NSURLProtocol does as much work as that, so the AOP approach is adopted

Source code address: github.com/yangqian111…

Welcome to:

Contact me at [email protected]