这次分析网络监测这块,因为这功能平时用于接口调试非常多。
核心类
网络监测涉及到的类如上图。
- 最为主要的两个类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);
接下来的逻辑就比较简单了
- 遍历所有加载的类
- 遍历每个类的方法列表
- 遍历需要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
的原理。
捋一捋思路:
- 得到原代理方法的selector,得到新定义的swizzledSelector。准备将swizzledSelector指向selector的imp.
- 得到原代理方法的方法描述(如果不是swizzle代理方法,没有这步)
- 定义两个Block(Block参数与返回值要和代理方法一样),用于swizzle原有selector。这里为什么要定义两个呢。因为可能存在虽然有代理头文件,但是并没有真真的实现代理,而且因为已经实现了代理,需要防止重复嗅探,因为父类如果实现了代理,只要调用原来的imp,父类的imp就会执行。这样一共就嗅探了两次。
- 进行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