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

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

核心类

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

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

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

FLEXNetworkObserver

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

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

注入NSURLConnection和NSURLSession代理

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

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


大致调用顺序如下:

  • 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:说明。

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


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 <NSURLConnectionDelegate> slf, NSURLConnection *connection, NSURLRequest *request, NSURLResponse *response);
    // 没有实现代理block,在这里进行网络嗅探
    NSURLConnectionWillSendRequestBlock undefinedBlock = ^NSURLRequest *(id <NSURLConnectionDelegate> slf, NSURLConnection *connection, NSURLRequest *request, NSURLResponse *response) {
        // 网络嗅探,保存网络请求状态
        [[FLEXNetworkObserver sharedObserver] connection:connection willSendRequest:request redirectResponse:response delegate:slf];
        return request;
    };
    // 有实现代理的block
    NSURLConnectionWillSendRequestBlock implementationBlock = ^NSURLRequest *(id <NSURLConnectionDelegate> 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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335

推荐阅读更多精彩内容