FLEX源码分析二(网络监测swizzle)

这次分析网络监测这块,因为这功能平时用于接口调试非常多。

核心类

FLEX源码分析二(网络监测swizzle)_第1张图片

网络监测涉及到的类如上图。

  • 最为主要的两个类FLEXNetworkObserver、FLEXNetworkRecorder。
  • UI相关的类有FLEXNetworkSettingsTableViewController、FLEXNetworkTransactionDetailTableViewController、FLEXNetworkTransactionTableViewCell。
  • 数据模型相关的类FLEXNetworkTransaction

根据类名大致能猜到FLEXNetworkObserver用于网络监测,而FLEXNetworkRecorder用于网络记录。

FLEXNetworkObserver

首先为了监测系统类的行为,iOS中常用的方式就是swizzles。业界有个比较牛逼的名称,面向切片编程,说的就是这中方式。

介绍
FLEXNetworkObserver 通过swizzleNSURLConnection和NSURLSession两个类的代理方法来达到监测整个URL加载系统。
FLEXNetworkRecorder 用于维护请求历史记录和缓存响应结果

注入NSURLConnection和NSURLSession代理

一般情况下都是在+ (void)load方法中进行swizzle。对于加入运行期系统中的每个类及分类来说,必定会调用此方法,而且仅仅调用一次。当包含类或分类的程序库载入系统时,就会执行此方法,而这通常就是指应用程序启动的时候。

通过观察堆栈我们可以看到更为详细的调用信息:


FLEX源码分析二(网络监测swizzle)_第2张图片

大致调用顺序如下:

  • dyld 开始将程序二进制文件初始化
  • 交由 ImageLoader 读取 image,其中包含了我们的类、方法等各种符号
  • 由于 runtime 向 dyld 绑定了回调,当 image 加载到内存后,dyld 会通知 runtime 进行处理
  • runtime 接手后调用 map_images 做解析和处理,接下来 load_images 中调用 call_load_methods 方法,遍历所有加载进来的 Class,按继承层级依次调用 Class 的 +load 方法和其 Category 的 +load 方法

如果想了解整个类加载详细过程可以看看这里iOS 程序 main 函数之前发生了什么

在注入的时候通常只注入一次。这里就通过单例的写法如下:

static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
    // your code ...
}

swizzle所有代理

swizzle的入口是injectIntoAllNSURLConnectionDelegateClasses。swizzle所有实现了NSURLConnection和NSURLSession代理类,而且代理方法多,这里用了一个数组保持swizzle的方法。

const SEL selectors[] = {
            @selector(connectionDidFinishLoading:),
            @selector(connection:willSendRequest:redirectResponse:),
            @selector(connection:didReceiveResponse:),
            @selector(connection:didReceiveData:),
            @selector(connection:didFailWithError:),
            @selector(URLSession:task:willPerformHTTPRedirection:newRequest:completionHandler:),
            @selector(URLSession:dataTask:didReceiveData:),
            @selector(URLSession:dataTask:didReceiveResponse:completionHandler:),
            @selector(URLSession:task:didCompleteWithError:),
            @selector(URLSession:dataTask:didBecomeDownloadTask:delegate:),
            @selector(URLSession:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:),
            @selector(URLSession:downloadTask:didFinishDownloadingToURL:)
        };

怎样才能获得所有的类呢?runtime有一个方法可以直接获取到objc_getClassList。文档注释如下:

/**

  • Obtains the list of registered class definitions.
  • @param buffer An array of \c Class values. On output, each \c Class value points to
  • one class definition, up to either \e bufferCount or the total number of registered classes,
  • whichever is less. You can pass \c NULL to obtain the total number of registered class
  • definitions without actually retrieving any class definitions.
  • @param bufferCount An integer value. Pass the number of pointers for which you have allocated space
  • in \e buffer. On return, this function fills in only this number of elements. If this number is less
  • than the number of registered classes, this function returns an arbitrary subset of the registered classes.
  • @return An integer value indicating the total number of registered classes.
  • @note The Objective-C runtime library automatically registers all the classes defined in your source code.
  • You can create class definitions at runtime and register them with the \c objc_addClass function.
  • @warning You cannot assume that class objects you get from this function are classes that inherit from \c NSObject,
  • so you cannot safely call any methods on such classes without detecting that the method is implemented first.
    */

根据上面文档的意思,获取加载类的总共数量:int numClasses = objc_getClassList(NULL, 0);
接下来的逻辑就比较简单了

  1. 遍历所有加载的类
  2. 遍历每个类的方法列表
  3. 遍历需要swizzle的方法数组,匹配方法是否需要swizzle

一共三层循环,简化代码如下。

for (NSInteger classIndex = 0; classIndex < numClasses; ++classIndex) {
                Class class = classes[classIndex];

                if (class == [FLEXNetworkObserver class]) {
                    continue;
                }

                // 使用runtime而不用NSObject的方法是为了避免消息发送,这样效率更高
                // 有一些类没有在这里swizzle,FLEX同样在+initialize 方法中swizzle了所有类
                // 注意了: 调用 class_getInstanceMethod() 会 像类发送 +initialize消息. 这也是为什么FLEX遍历方法列表的原因。
                unsigned int methodCount = 0;
                Method *methods = class_copyMethodList(class, &methodCount);// 获得方法总数
                BOOL matchingSelectorFound = NO;
                for (unsigned int methodIndex = 0; methodIndex < methodCount; methodIndex++) {
                    for (int selectorIndex = 0; selectorIndex < numSelectors; ++selectorIndex) {
                        if (method_getName(methods[methodIndex]) == selectors[selectorIndex]) {
                            // 如果实现了NSURLConnection和NSURLSession代理则swizzle
                            [self injectIntoDelegateClass:class];
                            matchingSelectorFound = YES;
                            break;
                        }
                    }
                    if (matchingSelectorFound) {
                        break;
                    }
                }
                free(methods);
            }
            
            free(classes);
        }

具体swizzle过程

由于swizzle代理方法过程是一样的所以这里选取NSURLConnectionDelegate中的connection:willSendRequest:redirectResponse:说明。

基本思路其实就是如下两张图:

FLEX源码分析二(网络监测swizzle)_第3张图片

FLEX源码分析二(网络监测swizzle)_第4张图片

Objective-C的动态特性,可以实现在运行时偷换selector对应的方法实现,达到给方法挂钩的目的。
每个类都有一个方法列表,存放着selector的名字和方法实现的映射关系。IMP有点类似函数指针,指向具体的Method实现。

归根结底,都是偷换了selector的IMP。

因为可以把Block转换为IMP,通过imp_implementationWithBlock实现。这也是一个非常知名开源库BLockKit的原理。

捋一捋思路:

  1. 得到原代理方法的selector,得到新定义的swizzledSelector。准备将swizzledSelector指向selector的imp.
  2. 得到原代理方法的方法描述(如果不是swizzle代理方法,没有这步)
  3. 定义两个Block(Block参数与返回值要和代理方法一样),用于swizzle原有selector。这里为什么要定义两个呢。因为可能存在虽然有代理头文件,但是并没有真真的实现代理,而且因为已经实现了代理,需要防止重复嗅探,因为父类如果实现了代理,只要调用原来的imp,父类的imp就会执行。这样一共就嗅探了两次。
  4. 进行swizzle,判断是否实现过代理,如果实现了,就把实现的block转换为imp。如果没有实现就用默认的block。

来看点代码:

 // 参数和返回值和代理一样的Block
    typedef NSURLRequest *(^NSURLConnectionWillSendRequestBlock)(id  slf, NSURLConnection *connection, NSURLRequest *request, NSURLResponse *response);
    // 没有实现代理block,在这里进行网络嗅探
    NSURLConnectionWillSendRequestBlock undefinedBlock = ^NSURLRequest *(id  slf, NSURLConnection *connection, NSURLRequest *request, NSURLResponse *response) {
        // 网络嗅探,保存网络请求状态
        [[FLEXNetworkObserver sharedObserver] connection:connection willSendRequest:request redirectResponse:response delegate:slf];
        return request;
    };
    // 有实现代理的block
    NSURLConnectionWillSendRequestBlock implementationBlock = ^NSURLRequest *(id  slf, NSURLConnection *connection, NSURLRequest *request, NSURLResponse *response) {
        __block NSURLRequest *returnValue = nil;
        // 防止重复嗅探
        [self sniffWithoutDuplicationForObject:connection selector:selector sniffingBlock:^{
            undefinedBlock(slf, connection, request, response);
        } originalImplementationBlock:^{
            // 原始方法
            returnValue = ((id(*)(id, SEL, id, id, id))objc_msgSend)(slf, swizzledSelector, connection, request, response);
        }];
        return returnValue;
    };

感觉有必要把防止重复嗅探这个部分好好说一下,一万自己在理解这部分的时候花了不少的时间。

sniffWithoutDuplicationForObject

之前出现了一个bug。参数object可能为空,这种情况下直接调用原有imp即可,之前没有做这样对空的情况的处理。

究竟是如何来保证值嗅探最初的网络请求呢(相比于父类也有实现)。通过如下代码实现

    const void *key = selector;
    // 是否已经标记过,标记过则不再嗅探
    if (!objc_getAssociatedObject(object, key)) {
        sniffingBlock();
    }

    // 标记已经在最初的时候嗅探过
    objc_setAssociatedObject(object, key, @YES, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    // 调用原来的(调用到了父类的执行,因为父类同样被swizzle这样父类中的objc_getAssociatedObject(object, key)值就是为YES,不会再次被嗅探)
    originalImplementationBlock();
    objc_setAssociatedObject(object, key, nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

方法交换

到这里就相对简单一点。将传入的Block通过imp_implementationWithBlock转为IMP。

接下来就是最为常见的swizzle代码

    Method oldMethod = class_getInstanceMethod(cls, selector);
    if (oldMethod) {
    // 如果之前存在则先添加新方法,然后交换方法
        class_addMethod(cls, swizzledSelector, implementation, methodDescription.types);
        Method newMethod = class_getInstanceMethod(cls, swizzledSelector);
        method_exchangeImplementations(oldMethod, newMethod);
    } else {
    // 不存在则添加方法
        class_addMethod(cls, selector, implementation, methodDescription.types);
    }

THE END

写了这么多才仅仅介绍了网络部分注入过程中的Swizzle的使用。!关于网络部分还有很多的要写。看来得分好几篇介绍了。今天就这样吧!

扩展阅读

iOS 程序 main 函数之前发生了什么
Crasher in FLEXNetworkObserver

你可能感兴趣的:(FLEX源码分析二(网络监测swizzle))