這次分析網(wǎng)絡(luò)監(jiān)測這塊,因為這功能平時用于接口調(diào)試非常多。
核心類

網(wǎng)絡(luò)監(jiān)測涉及到的類如上圖。
- 最為主要的兩個類FLEXNetworkObserver、FLEXNetworkRecorder。
- UI相關(guān)的類有FLEXNetworkSettingsTableViewController、FLEXNetworkTransactionDetailTableViewController、FLEXNetworkTransactionTableViewCell。
- 數(shù)據(jù)模型相關(guān)的類FLEXNetworkTransaction
根據(jù)類名大致能猜到FLEXNetworkObserver用于網(wǎng)絡(luò)監(jiān)測,而FLEXNetworkRecorder用于網(wǎng)絡(luò)記錄。
FLEXNetworkObserver
首先為了監(jiān)測系統(tǒng)類的行為,iOS中常用的方式就是swizzles。業(yè)界有個比較牛逼的名稱,面向切片編程,說的就是這中方式。
| 類 | 介紹 |
|---|---|
| FLEXNetworkObserver | 通過swizzleNSURLConnection和NSURLSession兩個類的代理方法來達(dá)到監(jiān)測整個URL加載系統(tǒng)。 |
| FLEXNetworkRecorder | 用于維護(hù)請求歷史記錄和緩存響應(yīng)結(jié)果 |
注入NSURLConnection和NSURLSession代理
一般情況下都是在+ (void)load方法中進(jìn)行swizzle。對于加入運(yùn)行期系統(tǒng)中的每個類及分類來說,必定會調(diào)用此方法,而且僅僅調(diào)用一次。當(dāng)包含類或分類的程序庫載入系統(tǒng)時,就會執(zhí)行此方法,而這通常就是指應(yīng)用程序啟動的時候。
通過觀察堆棧我們可以看到更為詳細(xì)的調(diào)用信息:

大致調(diào)用順序如下:
- dyld 開始將程序二進(jìn)制文件初始化
- 交由 ImageLoader 讀取 image,其中包含了我們的類、方法等各種符號
- 由于 runtime 向 dyld 綁定了回調(diào),當(dāng) image 加載到內(nèi)存后,dyld 會通知 runtime 進(jìn)行處理
- runtime 接手后調(diào)用 map_images 做解析和處理,接下來 load_images 中調(diào)用 call_load_methods 方法,遍歷所有加載進(jìn)來的 Class,按繼承層級依次調(diào)用 Class 的 +load 方法和其 Category 的 +load 方法
如果想了解整個類加載詳細(xì)過程可以看看這里iOS 程序 main 函數(shù)之前發(fā)生了什么
在注入的時候通常只注入一次。這里就通過單例的寫法如下:
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// your code ...
}
swizzle所有代理
swizzle的入口是injectIntoAllNSURLConnectionDelegateClasses。swizzle所有實現(xiàn)了NSURLConnection和NSURLSession代理類,而且代理方法多,這里用了一個數(shù)組保持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.
*/
根據(jù)上面文檔的意思,獲取加載類的總共數(shù)量:int numClasses = objc_getClassList(NULL, 0);
接下來的邏輯就比較簡單了
- 遍歷所有加載的類
- 遍歷每個類的方法列表
- 遍歷需要swizzle的方法數(shù)組,匹配方法是否需要swizzle
一共三層循環(huán),簡化代碼如下。
for (NSInteger classIndex = 0; classIndex < numClasses; ++classIndex) {
Class class = classes[classIndex];
if (class == [FLEXNetworkObserver class]) {
continue;
}
// 使用runtime而不用NSObject的方法是為了避免消息發(fā)送,這樣效率更高
// 有一些類沒有在這里swizzle,F(xiàn)LEX同樣在+initialize 方法中swizzle了所有類
// 注意了: 調(diào)用 class_getInstanceMethod() 會 像類發(fā)送 +initialize消息. 這也是為什么FLEX遍歷方法列表的原因。
unsigned int methodCount = 0;
Method *methods = class_copyMethodList(class, &methodCount);// 獲得方法總數(shù)
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]) {
// 如果實現(xiàn)了NSURLConnection和NSURLSession代理則swizzle
[self injectIntoDelegateClass:class];
matchingSelectorFound = YES;
break;
}
}
if (matchingSelectorFound) {
break;
}
}
free(methods);
}
free(classes);
}
具體swizzle過程
由于swizzle代理方法過程是一樣的所以這里選取NSURLConnectionDelegate中的connection:willSendRequest:redirectResponse:說明。
基本思路其實就是如下兩張圖:


Objective-C的動態(tài)特性,可以實現(xiàn)在運(yùn)行時偷換selector對應(yīng)的方法實現(xiàn),達(dá)到給方法掛鉤的目的。
每個類都有一個方法列表,存放著selector的名字和方法實現(xiàn)的映射關(guān)系。IMP有點類似函數(shù)指針,指向具體的Method實現(xiàn)。
歸根結(jié)底,都是偷換了selector的IMP。
因為可以把Block轉(zhuǎn)換為IMP,通過imp_implementationWithBlock實現(xiàn)。這也是一個非常知名開源庫BLockKit的原理。
捋一捋思路:
- 得到原代理方法的selector,得到新定義的swizzledSelector。準(zhǔn)備將swizzledSelector指向selector的imp.
- 得到原代理方法的方法描述(如果不是swizzle代理方法,沒有這步)
- 定義兩個Block(Block參數(shù)與返回值要和代理方法一樣),用于swizzle原有selector。這里為什么要定義兩個呢。因為可能存在雖然有代理頭文件,但是并沒有真真的實現(xiàn)代理,而且因為已經(jīng)實現(xiàn)了代理,需要防止重復(fù)嗅探,因為父類如果實現(xiàn)了代理,只要調(diào)用原來的imp,父類的imp就會執(zhí)行。這樣一共就嗅探了兩次。
- 進(jìn)行swizzle,判斷是否實現(xiàn)過代理,如果實現(xiàn)了,就把實現(xiàn)的block轉(zhuǎn)換為imp。如果沒有實現(xiàn)就用默認(rèn)的block。
來看點代碼:
// 參數(shù)和返回值和代理一樣的Block
typedef NSURLRequest *(^NSURLConnectionWillSendRequestBlock)(id <NSURLConnectionDelegate> slf, NSURLConnection *connection, NSURLRequest *request, NSURLResponse *response);
// 沒有實現(xiàn)代理block,在這里進(jìn)行網(wǎng)絡(luò)嗅探
NSURLConnectionWillSendRequestBlock undefinedBlock = ^NSURLRequest *(id <NSURLConnectionDelegate> slf, NSURLConnection *connection, NSURLRequest *request, NSURLResponse *response) {
// 網(wǎng)絡(luò)嗅探,保存網(wǎng)絡(luò)請求狀態(tài)
[[FLEXNetworkObserver sharedObserver] connection:connection willSendRequest:request redirectResponse:response delegate:slf];
return request;
};
// 有實現(xiàn)代理的block
NSURLConnectionWillSendRequestBlock implementationBlock = ^NSURLRequest *(id <NSURLConnectionDelegate> slf, NSURLConnection *connection, NSURLRequest *request, NSURLResponse *response) {
__block NSURLRequest *returnValue = nil;
// 防止重復(fù)嗅探
[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;
};
感覺有必要把防止重復(fù)嗅探這個部分好好說一下,一萬自己在理解這部分的時候花了不少的時間。
sniffWithoutDuplicationForObject
之前出現(xiàn)了一個bug。參數(shù)object可能為空,這種情況下直接調(diào)用原有imp即可,之前沒有做這樣對空的情況的處理。
究竟是如何來保證值嗅探最初的網(wǎng)絡(luò)請求呢(相比于父類也有實現(xiàn))。通過如下代碼實現(xiàn)
const void *key = selector;
// 是否已經(jīng)標(biāo)記過,標(biāo)記過則不再嗅探
if (!objc_getAssociatedObject(object, key)) {
sniffingBlock();
}
// 標(biāo)記已經(jīng)在最初的時候嗅探過
objc_setAssociatedObject(object, key, @YES, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
// 調(diào)用原來的(調(diào)用到了父類的執(zhí)行,因為父類同樣被swizzle這樣父類中的objc_getAssociatedObject(object, key)值就是為YES,不會再次被嗅探)
originalImplementationBlock();
objc_setAssociatedObject(object, key, nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
方法交換
到這里就相對簡單一點。將傳入的Block通過imp_implementationWithBlock轉(zhuǎn)為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
寫了這么多才僅僅介紹了網(wǎng)絡(luò)部分注入過程中的Swizzle的使用。??!關(guān)于網(wǎng)絡(luò)部分還有很多的要寫??磥淼梅趾脦灼榻B了。今天就這樣吧!
擴(kuò)展閱讀
iOS 程序 main 函數(shù)之前發(fā)生了什么
Crasher in FLEXNetworkObserver