作者:敖志敏
本文為原創(chuàng)文章,轉(zhuǎn)載請(qǐng)注明作者及出處
前文地址:iOS 性能監(jiān)控方案 Wedjat(上篇)
國(guó)內(nèi)移動(dòng)網(wǎng)絡(luò)環(huán)境非常復(fù)雜匙姜,WIFI、4G冯痢、3G氮昧、2.5G(Edge)、2G 等多種移動(dòng)網(wǎng)絡(luò)并存浦楣,用戶的網(wǎng)絡(luò)可能會(huì)在 WIFI/4G/3G/2.5G/2G 類型之間切換袖肥,這是移動(dòng)網(wǎng)絡(luò)和傳統(tǒng)網(wǎng)絡(luò)一個(gè)很大的區(qū)別,被稱作是 Connection Migration 問(wèn)題振劳。此外椎组,還存在國(guó)內(nèi)運(yùn)營(yíng)商網(wǎng)絡(luò)的 DNS 解析慢、失敗率高历恐、DNS 被劫持的問(wèn)題寸癌;還有國(guó)內(nèi)運(yùn)營(yíng)商互聯(lián)和海外訪問(wèn)國(guó)內(nèi)帶寬低傳輸慢等問(wèn)題专筷。這些網(wǎng)絡(luò)問(wèn)題令人非常頭疼。移動(dòng)網(wǎng)絡(luò)的現(xiàn)狀造成了用戶在使用過(guò)程中經(jīng)常會(huì)遇到各種網(wǎng)絡(luò)問(wèn)題蒸苇,網(wǎng)絡(luò)問(wèn)題將直接導(dǎo)致用戶無(wú)法在 App 進(jìn)行操作磷蛹,當(dāng)一些關(guān)鍵的業(yè)務(wù)接口出現(xiàn)錯(cuò)誤時(shí),甚至?xí)苯訉?dǎo)致用戶的大量流失填渠。網(wǎng)絡(luò)問(wèn)題不僅給移動(dòng)開(kāi)發(fā)帶來(lái)了巨大的挑戰(zhàn)弦聂,同時(shí)也給網(wǎng)絡(luò)監(jiān)控帶來(lái)了全新的機(jī)遇鸟辅。以往要解決這些問(wèn)題氛什,只能靠經(jīng)驗(yàn)和猜想,而如果能站在 App 的視角對(duì)網(wǎng)絡(luò)進(jìn)行監(jiān)控匪凉,就能更有針對(duì)性地了解產(chǎn)生問(wèn)題的根源枪眉。
網(wǎng)絡(luò)監(jiān)控一般通過(guò) NSURLProtocol
和代碼注入(Hook)這兩種方式來(lái)實(shí)現(xiàn),由于 NSURLProtocol
作為上層接口再层,使用起來(lái)更為方便贸铜,因此很自然選擇它作為網(wǎng)絡(luò)監(jiān)控的方案,但是 NSURLProtocol
屬于 URL Loading System 體系中聂受,應(yīng)用層的協(xié)議支持有限蒿秦,只支持 FTP,HTTP蛋济,HTTPS 等幾個(gè)應(yīng)用層協(xié)議棍鳖,對(duì)于使用其他協(xié)議的流量則束手無(wú)策,所以存在一定的局限性碗旅。監(jiān)控底層網(wǎng)絡(luò)庫(kù) CFNetwork
則沒(méi)有這個(gè)限制渡处。
下面是網(wǎng)絡(luò)采集的關(guān)鍵性能指標(biāo):
- TCP 建立連接時(shí)間
- DNS 時(shí)間
- SSL 時(shí)間
- 首包時(shí)間
- 響應(yīng)時(shí)間
- HTTP 錯(cuò)誤率
- 網(wǎng)絡(luò)錯(cuò)誤率
NSURLProtocol
//為了避免 canInitWithRequest 和 canonicalRequestForRequest 出現(xiàn)死循環(huán)
static NSString * const HJHTTPHandledIdentifier = @"hujiang_http_handled";
@interface HJURLProtocol () <NSURLSessionTaskDelegate, NSURLSessionDataDelegate>
@property (nonatomic, strong) NSURLSessionDataTask *dataTask;
@property (nonatomic, strong) NSOperationQueue *sessionDelegateQueue;
@property (nonatomic, strong) NSURLResponse *response;
@property (nonatomic, strong) NSMutableData *data;
@property (nonatomic, strong) NSDate *startDate;
@property (nonatomic, strong) HJHTTPModel *httpModel;
@end
+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
if (![request.URL.scheme isEqualToString:@"http"] &&
![request.URL.scheme isEqualToString:@"https"]) {
return NO;
}
if ([NSURLProtocol propertyForKey:HJHTTPHandledIdentifier inRequest:request] ) {
return NO;
}
return YES;
}
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request {
NSMutableURLRequest *mutableReqeust = [request mutableCopy];
[NSURLProtocol setProperty:@YES
forKey:HJHTTPHandledIdentifier
inRequest:mutableReqeust];
return [mutableReqeust copy];
}
- (void)startLoading {
self.startDate = [NSDate date];
self.data = [NSMutableData data];
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
self.sessionDelegateQueue = [[NSOperationQueue alloc] init];
self.sessionDelegateQueue.maxConcurrentOperationCount = 1;
self.sessionDelegateQueue.name = @"com.hujiang.wedjat.session.queue";
NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:self.sessionDelegateQueue];
self.dataTask = [session dataTaskWithRequest:self.request];
[self.dataTask resume];
httpModel = [[NEHTTPModel alloc] init];
httpModel.request = self.request;
httpModel.startDateString = [self stringWithDate:[NSDate date]];
NSTimeInterval myID = [[NSDate date] timeIntervalSince1970];
double randomNum = ((double)(arc4random() % 100))/10000;
httpModel.myID = myID+randomNum;
}
- (void)stopLoading {
[self.dataTask cancel];
self.dataTask = nil;
httpModel.response = (NSHTTPURLResponse *)self.response;
httpModel.endDateString = [self stringWithDate:[NSDate date]];
NSString *mimeType = self.response.MIMEType;
// 解析 response,流量統(tǒng)計(jì)等
}
#pragma mark - NSURLSessionTaskDelegate
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
if (!error) {
[self.client URLProtocolDidFinishLoading:self];
} else if ([error.domain isEqualToString:NSURLErrorDomain] && error.code == NSURLErrorCancelled) {
} else {
[self.client URLProtocol:self didFailWithError:error];
}
self.dataTask = nil;
}
#pragma mark - NSURLSessionDataDelegate
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
didReceiveData:(NSData *)data {
[self.client URLProtocol:self didLoadData:data];
}
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler {
[[self client] URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageAllowed];
completionHandler(NSURLSessionResponseAllow);
self.response = response;
}
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task willPerformHTTPRedirection:(NSHTTPURLResponse *)response newRequest:(NSURLRequest *)request completionHandler:(void (^)(NSURLRequest * _Nullable))completionHandler {
if (response != nil){
self.response = response;
[[self client] URLProtocol:self wasRedirectedToRequest:request redirectResponse:response];
}
}
Hertz 使用的是
NSURLProtocol
這種方式祟辟,通過(guò)繼承NSURLProtocol
医瘫,實(shí)現(xiàn)NSURLConnectionDelegate
來(lái)實(shí)現(xiàn)截取行為渣叛。
Hook
如果我們使用手工埋點(diǎn)的方式來(lái)監(jiān)控網(wǎng)絡(luò)或听,會(huì)侵入到業(yè)務(wù)代碼角钩,維護(hù)成本會(huì)非常高利凑。通過(guò) Hook 將網(wǎng)絡(luò)性能監(jiān)控的代碼自動(dòng)注入就可以避免上面的問(wèn)題缀雳,做到真實(shí)用戶體驗(yàn)監(jiān)控(RUM: Real User Monitoring)已旧,監(jiān)控應(yīng)用在真實(shí)網(wǎng)絡(luò)環(huán)境中的性能襟企。
AOP(Aspect Oriented Programming樱拴,面向切面編程)馍悟,是通過(guò)預(yù)編譯方式和運(yùn)行期動(dòng)態(tài)代理實(shí)現(xiàn)在不修改源代碼的情況下給程序動(dòng)態(tài)添加功能的一種技術(shù)畔濒。其核心思想是將業(yè)務(wù)邏輯(核心關(guān)注點(diǎn),系統(tǒng)的主要功能)與公共功能(橫切關(guān)注點(diǎn)锣咒,如日志侵状、事物等)進(jìn)行分離赞弥,降低復(fù)雜性,提高軟件系統(tǒng)模塊化趣兄、可維護(hù)性和可重用性绽左。其中核心關(guān)注點(diǎn)采用 OOP 方式進(jìn)行代碼的編寫,橫切關(guān)注點(diǎn)采用 AOP 方式進(jìn)行編碼艇潭,最后將這兩種代碼進(jìn)行組合形成系統(tǒng)拼窥。AOP 被廣泛應(yīng)用在日志記錄,性能統(tǒng)計(jì)蹋凝,安全控制鲁纠,事務(wù)處理,異常處理等領(lǐng)域鳍寂。
在 iOS 中 AOP 的實(shí)現(xiàn)是基于 Objective-C 的 Runtime 機(jī)制改含,實(shí)現(xiàn) Hook 的三種方式分別為:Method Swizzling、NSProxy 和 Fishhook迄汛。前兩者適用于 Objective-C 實(shí)現(xiàn)的庫(kù)捍壤,如 NSURLConnection
和 NSURLSession
,Fishhook 則適用于 C 語(yǔ)言實(shí)現(xiàn)的庫(kù)鞍爱,如 CFNetwork
鹃觉。
下圖是阿里百川碼力監(jiān)控給出的三類網(wǎng)絡(luò)接口需要 hook 的方法
接下來(lái)分別來(lái)討論這三種實(shí)現(xiàn)方式:
Method Swizzling
Method swizzling 是利用 Objective-C Runtime 特性把一個(gè)方法的實(shí)現(xiàn)與另一個(gè)方法的實(shí)現(xiàn)進(jìn)行替換的技術(shù)。每個(gè) Class 結(jié)構(gòu)體中都有一個(gè) Dispatch Table
的成員變量睹逃,Dispatch Table
中建立了每個(gè) SEL
(方法名)和對(duì)應(yīng)的 IMP
(方法實(shí)現(xiàn)盗扇,指向 C 函數(shù)的指針)的映射關(guān)系,Method Swizzling 就是將原有的 SEL
和 IMP
映射關(guān)系打破唯卖,并建立新的關(guān)聯(lián)來(lái)達(dá)到方法替換的目的粱玲。
因此利用 Method swizzling 可以替換原始實(shí)現(xiàn),在替換的實(shí)現(xiàn)中加入網(wǎng)絡(luò)性能埋點(diǎn)行為拜轨,然后調(diào)用原始實(shí)現(xiàn)抽减。
NSProxy
NSProxy is an abstract superclass defining an API for objects that act as stand-ins for other objects or for objects that don’t exist yet. Typically, a message to a proxy is forwarded to the real object or causes the proxy to load (or transform itself into) the real object. Subclasses of NSProxy can be used to implement transparent distributed messaging (for example, NSDistantObject) or for lazy instantiation of objects that are expensive to create.
這是 Apple 官方文檔給 NSProxy
的定義,NSProxy
和 NSObject
一樣都是根類橄碾,它是一個(gè)抽象類卵沉,你可以通過(guò)繼承它,并重寫 -forwardInvocation:
和 -methodSignatureForSelector:
方法以實(shí)現(xiàn)消息轉(zhuǎn)發(fā)到另一個(gè)實(shí)例法牲。綜上史汗,NSProxy
的目的就是負(fù)責(zé)將消息轉(zhuǎn)發(fā)到真正的 target 的代理類。
Method swizzling 替換方法需要指定類名拒垃,但是 NSURLConnectionDelegate
和 NSURLSessionDelegate
是由業(yè)務(wù)方指定停撞,通常來(lái)說(shuō)是不確定,所以這種場(chǎng)景不適合使用 Method swizzling。使用 NSProxy
可以解決上面的問(wèn)題戈毒,具體實(shí)現(xiàn):proxy delegate 替換 NSURLConnection
和 NSURLSession
原來(lái)的 delegate艰猬,當(dāng) proxy delegate 收到回調(diào)時(shí),如果是要 hook 的方法埋市,則調(diào)用 proxy 的實(shí)現(xiàn)冠桃,proxy 的實(shí)現(xiàn)最后會(huì)調(diào)用原來(lái)的 delegate;如果不是要 hook 的方法道宅,則通過(guò)消息轉(zhuǎn)發(fā)機(jī)制將消息轉(zhuǎn)發(fā)給原來(lái)的 delegate食听。下圖示意了整個(gè)操作流程。
Fishhook
fishhook 是一個(gè)由 Facebook 開(kāi)源的第三方框架污茵,其主要作用就是動(dòng)態(tài)修改 C 語(yǔ)言的函數(shù)實(shí)現(xiàn)樱报,我們可以使用 fishhook 來(lái)替換動(dòng)態(tài)鏈接庫(kù)中的 C 函數(shù)實(shí)現(xiàn),具體來(lái)說(shuō)就是去替換 CFNetwork
和 CoreFoundation
中的相關(guān)函數(shù)省咨。后面會(huì)在講監(jiān)控 CFNetwork
詳細(xì)說(shuō)明肃弟,這里不再贅述。
講解完 iOS 上 hook 的實(shí)現(xiàn)技術(shù)零蓉,接下來(lái)討論在 NSURLConnection
、NSURLSession
和 CFNetwork
中穷缤,如何將上面的三種技術(shù)應(yīng)用到實(shí)踐中敌蜂。
NSURLConnection
NSURLSession
CFNetwork
概述
以 NeteaseAPM 作為案例來(lái)講解如何通過(guò) CFNetwork
實(shí)現(xiàn)網(wǎng)絡(luò)監(jiān)控,它是通過(guò)使用代理模式來(lái)實(shí)現(xiàn)的津肛,具體來(lái)說(shuō)章喉,是在 CoreFoundation
Framework 的 CFStream
實(shí)現(xiàn)一個(gè) Proxy Stream 從而達(dá)到攔截的目的,記錄通過(guò) CFStream
讀取的網(wǎng)絡(luò)數(shù)據(jù)長(zhǎng)度身坐,然后再轉(zhuǎn)發(fā)給 Original Stream秸脱,流程圖如下:
詳細(xì)描述
由于 CFNetwork
都是 C 函數(shù)實(shí)現(xiàn),想要對(duì) C 函數(shù) 進(jìn)行 Hook 需要使用 Dynamic Loader Hook 庫(kù)函數(shù) - fishhook部蛇,
Dynamic Loader(dyld)通過(guò)更新 Mach-O 文件中保存的指針的方法來(lái)綁定符號(hào)摊唇。借用它可以在 Runtime 修改 C 函數(shù)調(diào)用的函數(shù)指針。fishhook 的實(shí)現(xiàn)原理:遍歷
__DATA segment
里面__nl_symbol_ptr
涯鲁、__la_symbol_ptr
兩個(gè) section 里面的符號(hào)巷查,通過(guò) Indirect Symbol Table、Symbol Table 和 String Table 的配合抹腿,找到自己要替換的函數(shù)岛请,達(dá)到 hook 的目的。
CFNetwork
使用 CFReadStreamRef
做數(shù)據(jù)傳遞警绩,使用回調(diào)函數(shù)來(lái)接收服務(wù)器響應(yīng)崇败。當(dāng)回調(diào)函數(shù)收到流中有數(shù)據(jù)的通知后,將數(shù)據(jù)保存到客戶端的內(nèi)存中肩祥。顯然對(duì)流的讀取不適合使用修改字符串表的方式后室,如果這樣做的話也會(huì) hook 系統(tǒng)也在使用的 read
函數(shù)微渠,而系統(tǒng)的 read
函數(shù)不僅僅被網(wǎng)絡(luò)請(qǐng)求的 stream 調(diào)用,還有所有的文件處理咧擂,而且 hook 頻繁調(diào)用的函數(shù)也是不可取的逞盆。
使用上述方式的缺點(diǎn)就是無(wú)法做到選擇性的監(jiān)控和 HTTP 相關(guān)的 CFReadStream
,而不涉及來(lái)自文件和內(nèi)存的 CFReadStream
松申,NeteaseAPM 的解決方案是在系統(tǒng)構(gòu)造 HTTP Stream 時(shí)云芦,將一個(gè) NSInputStream
的子類 ProxyStream
橋接為 CFReadStream
返回給用戶,來(lái)達(dá)到單獨(dú)監(jiān)控 HTTP Stream 的目的贸桶。
具體的實(shí)現(xiàn)思路就是:首先設(shè)計(jì)一個(gè)繼承自 NSObject
并持有 NSInputStream
對(duì)象的 Proxy 類舅逸,持有的 NSInputStream
記為 OriginalStream。將所有發(fā)向 Proxy 的消息轉(zhuǎn)發(fā)給 OriginalStream 處理皇筛,然后再重寫 NSInputStream
的 read:maxLength:
方法琉历,如此一來(lái),我們就可以獲取到 stream 的大小了水醋。
XXInputStreamProxy
類的代碼如下:
- (instancetype)initWithStream:(id)stream {
if (self = [super init]) {
_stream = stream;
}
return self;
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
return [_stream methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
[anInvocation invokeWithTarget:_stream];
}
繼承 NSInputStream
并重寫 read:maxLength:
方法:
- (NSInteger)read:(uint8_t *)buffer maxLength:(NSUInteger)len {
NSInteger readSize = [_stream read:buffer maxLength:len];
// 記錄 readSize
return readSize;
}
XX_CFReadStreamCreateForHTTPRequest
會(huì)被用來(lái)替換系統(tǒng)的 CFReadStreamCreateForHTTPRequest
方法
static CFReadStreamRef (*original_CFReadStreamCreateForHTTPRequest)(CFAllocatorRef __nullable alloc,
CFHTTPMessageRef request);
/**
XXInputStreamProxy 持有 original CFReadStreamRef旗笔,轉(zhuǎn)發(fā)消息到 original CFReadStreamRef,
在 read 方法中記錄獲取數(shù)據(jù)的大小
*/
static CFReadStreamRef XX_CFReadStreamCreateForHTTPRequest(CFAllocatorRef alloc,
CFHTTPMessageRef request) {
// 使用系統(tǒng)方法的函數(shù)指針完成系統(tǒng)的實(shí)現(xiàn)
CFReadStreamRef originalCFStream = original_CFReadStreamCreateForHTTPRequest(alloc, request);
// 將 CFReadStreamRef 轉(zhuǎn)換成 NSInputStream拄踪,并保存在 XXInputStreamProxy蝇恶,最后返回的時(shí)候再轉(zhuǎn)回 CFReadStreamRef
NSInputStream *stream = (__bridge NSInputStream *)originalCFStream;
XXInputStreamProxy *outStream = [[XXInputStreamProxy alloc] initWithClient:stream];
CFRelease(originalCFStream);
CFReadStreamRef result = (__bridge_retained CFReadStreamRef)outStream;
return result;
}
使用 fishhook 替換函數(shù)地址
void save_original_symbols() {
original_CFReadStreamCreateForHTTPRequest = dlsym(RTLD_DEFAULT, "CFReadStreamCreateForHTTPRequest");
}
rebind_symbols((struct rebinding[1]){{"CFReadStreamCreateForHTTPRequest", XX_CFReadStreamCreateForHTTPRequest, (void *)& original_CFReadStreamCreateForHTTPRequest}}, 1);
根據(jù) CFNetwork
API 的調(diào)用方式,使用 fishhook 和 Proxy Stream 獲取 C 函數(shù)的設(shè)計(jì)模型如下:
NSURLSessionTaskMetrics/NSURLSessionTaskTransactionMetrics
Apple 在 iOS 10 的 NSURLSessionTaskDelegate
代理中新增了 -URLSession: task:didFinishCollectingMetrics:
方法惶桐,如果實(shí)現(xiàn)這個(gè)代理方法撮弧,就可以通過(guò)該回調(diào)的 NSURLSessionTaskMetrics
類型參數(shù)獲取到采集的網(wǎng)絡(luò)指標(biāo),實(shí)現(xiàn)對(duì)網(wǎng)絡(luò)請(qǐng)求中 DNS 查詢/TCP 建立連接/TLS 握手/請(qǐng)求響應(yīng)等各環(huán)節(jié)時(shí)間的統(tǒng)計(jì)姚糊。
/*
* Sent when complete statistics information has been collected for the task.
*/
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didFinishCollectingMetrics:(NSURLSessionTaskMetrics *)metrics API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
NSURLSessionTaskMetrics
NSURLSessionTaskMetrics
對(duì)象封裝了 session task 的指標(biāo)贿衍,每個(gè) NSURLSessionTaskMetrics
對(duì)象有 taskInterval
和 redirectCount
屬性,還有在執(zhí)行任務(wù)時(shí)產(chǎn)生的每個(gè)請(qǐng)求/響應(yīng)事務(wù)中收集的指標(biāo)救恨。
-
transactionMetrics
:transactionMetrics
數(shù)組包含了在執(zhí)行任務(wù)時(shí)產(chǎn)生的每個(gè)請(qǐng)求/響應(yīng)事務(wù)中收集的指標(biāo)贸辈。/* * transactionMetrics array contains the metrics collected for every request/response transaction created during the task execution. */ @property (copy, readonly) NSArray<NSURLSessionTaskTransactionMetrics *> *transactionMetrics;
-
taskInterval
:任務(wù)從創(chuàng)建到完成花費(fèi)的總時(shí)間,任務(wù)的創(chuàng)建時(shí)間是任務(wù)被實(shí)例化時(shí)的時(shí)間忿薇;任務(wù)完成時(shí)間是任務(wù)的內(nèi)部狀態(tài)將要變?yōu)橥瓿傻臅r(shí)間裙椭。/* * Interval from the task creation time to the task completion time. * Task creation time is the time when the task was instantiated. * Task completion time is the time when the task is about to change its internal state to completed. */ @property (copy, readonly) NSDateInterval *taskInterval;
-
redirectCount
:記錄了被重定向的次數(shù)。/* * redirectCount is the number of redirects that were recorded. */ @property (assign, readonly) NSUInteger redirectCount;
NSURLSessionTaskTransactionMetrics
NSURLSessionTaskTransactionMetrics
對(duì)象封裝了任務(wù)執(zhí)行時(shí)收集的性能指標(biāo)署浩,包括了 request
和 response
屬性揉燃,對(duì)應(yīng) HTTP 的請(qǐng)求和響應(yīng),還包括了從 fetchStartDate
開(kāi)始筋栋,到 responseEndDate
結(jié)束之間的指標(biāo)炊汤,當(dāng)然還有 networkProtocolName
和 resourceFetchType
屬性。
-
request
:表示了網(wǎng)絡(luò)請(qǐng)求對(duì)象。/* * Represents the transaction request. */ @property (copy, readonly) NSURLRequest *request;
-
response
:表示了網(wǎng)絡(luò)響應(yīng)對(duì)象抢腐,如果網(wǎng)絡(luò)出錯(cuò)或沒(méi)有響應(yīng)時(shí)姑曙,response
為nil
。/* * Represents the transaction response. Can be nil if error occurred and no response was generated. */ @property (nullable, copy, readonly) NSURLResponse *response;
-
networkProtocolName
:獲取資源時(shí)使用的網(wǎng)絡(luò)協(xié)議迈倍,由 ALPN 協(xié)商后標(biāo)識(shí)的協(xié)議伤靠,比如 h2, http/1.1, spdy/3.1。@property (nullable, copy, readonly) NSString *networkProtocolName;
-
isProxyConnection
:是否使用代理進(jìn)行網(wǎng)絡(luò)連接啼染。/* * This property is set to YES if a proxy connection was used to fetch the resource. */ @property (assign, readonly, getter=isProxyConnection) BOOL proxyConnection;
-
isReusedConnection
:是否復(fù)用已有連接宴合。/* * This property is set to YES if a persistent connection was used to fetch the resource. */ @property (assign, readonly, getter=isReusedConnection) BOOL reusedConnection;
-
resourceFetchType
:NSURLSessionTaskMetricsResourceFetchType
枚舉類型,標(biāo)識(shí)資源是通過(guò)網(wǎng)絡(luò)加載迹鹅,服務(wù)器推送還是本地緩存獲取的卦洽。/* * Indicates whether the resource was loaded, pushed or retrieved from the local cache. */ @property (assign, readonly) NSURLSessionTaskMetricsResourceFetchType resourceFetchType;
對(duì)于下面所有 NSDate
類型指標(biāo),如果任務(wù)沒(méi)有完成斜棚,所有相應(yīng)的 EndDate
指標(biāo)都將為 nil
阀蒂。例如,如果 DNS 解析超時(shí)弟蚀、失敗或者客戶端在解析成功之前取消蚤霞,domainLookupStartDate
會(huì)有對(duì)應(yīng)的數(shù)據(jù),然而 domainLookupEndDate
以及在它之后的所有指標(biāo)都為 nil
粗梭。
這幅圖示意了一次 HTTP 請(qǐng)求在各環(huán)節(jié)分別做了哪些工作
如果是復(fù)用已有的連接或者從本地緩存中獲取資源争便,下面的指標(biāo)都會(huì)被賦值為 nil
:
domainLookupStartDate
domainLookupEndDate
connectStartDate
connectEndDate
secureConnectionStartDate
secureConnectionEndDate
-
fetchStartDate
:客戶端開(kāi)始請(qǐng)求的時(shí)間,無(wú)論資源是從服務(wù)器還是本地緩存中獲取断医。@property (nullable, copy, readonly) NSDate *fetchStartDate;
-
domainLookupStartDate
:DNS 解析開(kāi)始時(shí)間,Domain -> IP 地址奏纪。/* * domainLookupStartDate returns the time immediately before the user agent started the name lookup for the resource. */ @property (nullable, copy, readonly) NSDate *domainLookupStartDate;
-
domainLookupEndDate
:DNS 解析完成時(shí)間鉴嗤,客戶端已經(jīng)獲取到域名對(duì)應(yīng)的 IP 地址。/* * domainLookupEndDate returns the time after the name lookup was completed. */ @property (nullable, copy, readonly) NSDate *domainLookupEndDate;
-
connectStartDate
:客戶端與服務(wù)器開(kāi)始建立 TCP 連接的時(shí)間序调。/* * connectStartDate is the time immediately before the user agent started establishing the connection to the server. * * For example, this would correspond to the time immediately before the user agent started trying to establish the TCP connection. */ @property (nullable, copy, readonly) NSDate *connectStartDate;
-
secureConnectionStartDate
:HTTPS 的 TLS 握手開(kāi)始時(shí)間醉锅。/* * If an encrypted connection was used, secureConnectionStartDate is the time immediately before the user agent started the security handshake to secure the current connection. * * For example, this would correspond to the time immediately before the user agent started the TLS handshake. * * If an encrypted connection was not used, this attribute is set to nil. */ @property (nullable, copy, readonly) NSDate *secureConnectionStartDate;
-
secureConnectionEndDate
:HTTPS 的 TLS 握手結(jié)束時(shí)間。/* * If an encrypted connection was used, secureConnectionEndDate is the time immediately after the security handshake completed. * * If an encrypted connection was not used, this attribute is set to nil. */ @property (nullable, copy, readonly) NSDate *secureConnectionEndDate;
-
-
connectEndDate
:客戶端與服務(wù)器建立 TCP 連接完成時(shí)間发绢,包括 TLS 握手時(shí)間硬耍。/* * connectEndDate is the time immediately after the user agent finished establishing the connection to the server, including completion of security-related and other handshakes. */ @property (nullable, copy, readonly) NSDate *connectEndDate;
-
requestStartDate
:開(kāi)始傳輸 HTTP 請(qǐng)求的 header 第一個(gè)字節(jié)的時(shí)間。/* * requestStartDate is the time immediately before the user agent started requesting the source, regardless of whether the resource was retrieved from the server or local resources. * * For example, this would correspond to the time immediately before the user agent sent an HTTP GET request. */ @property (nullable, copy, readonly) NSDate *requestStartDate;
-
requestEndDate
:HTTP 請(qǐng)求最后一個(gè)字節(jié)傳輸完成的時(shí)間边酒。/* * requestEndDate is the time immediately after the user agent finished requesting the source, regardless of whether the resource was retrieved from the server or local resources. * * For example, this would correspond to the time immediately after the user agent finished sending the last byte of the request. */ @property (nullable, copy, readonly) NSDate *requestEndDate;
-
responseStartDate
:客戶端從服務(wù)器接收到響應(yīng)的第一個(gè)字節(jié)的時(shí)間经柴。/* * responseStartDate is the time immediately after the user agent received the first byte of the response from the server or from local resources. * * For example, this would correspond to the time immediately after the user agent received the first byte of an HTTP response. */ @property (nullable, copy, readonly) NSDate *responseStartDate;
-
responseEndDate
:客戶端從服務(wù)器接收到最后一個(gè)字節(jié)的時(shí)間。/* * responseEndDate is the time immediately after the user agent received the last byte of the resource. */ @property (nullable, copy, readonly) NSDate *responseEndDate;
Wrap up
iOS 的網(wǎng)絡(luò)監(jiān)控有兩種實(shí)現(xiàn)方式:NSURLProtocol
和代碼注入(Hook)墩朦,文中給出了通過(guò) NSURLProtocol
實(shí)現(xiàn)監(jiān)控的具體實(shí)現(xiàn)坯认,然后分別介紹了在 iOS 中如何使用 Method Swizzling 、NSProxy 和 Fishhook 進(jìn)行 AOP Hook,文章也給出了三種 AOP Hook 技術(shù)在 NSURLConnection
牛哺、NSURLSession
和 CFNetwork
的案例陋气。最后詳細(xì)介紹在 iOS 10 中新引入的 NSURLSessionTaskMetrics
和 NSURLSessionTaskTransactionMetrics
類,它們可以被用于獲取網(wǎng)絡(luò)相關(guān)的元數(shù)據(jù)引润,比如 DNS 查詢巩趁、TLS 握手、請(qǐng)求響應(yīng)等環(huán)節(jié)的耗時(shí)淳附,這些數(shù)據(jù)可以幫助開(kāi)發(fā)人員更好地分析網(wǎng)絡(luò)性能议慰。
參考資料
- 移動(dòng)端性能監(jiān)控方案 Hertz
- NetworkEye
- netfox
- 網(wǎng)易 NeteaseAPM iOS SDK 技術(shù)實(shí)現(xiàn)分享
- Mobile Application Monitor IOS組件設(shè)計(jì)技術(shù)分享
- 性能可視化實(shí)踐之路
更多有關(guān) iOS 技術(shù)文章,請(qǐng)關(guān)注「滬江技術(shù)學(xué)院」微信公眾號(hào)燃观。