iOS 性能監(jiān)控方案 Wedjat(下篇)

作者:敖志敏
本文為原創(chuàng)文章,轉(zhuǎn)載請(qǐng)注明作者及出處

image.png

前文地址: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é)議支持有限蒿秦,只支持 FTPHTTP蛋济,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-CRuntime 機(jī)制改含,實(shí)現(xiàn) Hook 的三種方式分別為:Method SwizzlingNSProxyFishhook迄汛。前兩者適用于 Objective-C 實(shí)現(xiàn)的庫(kù)捍壤,如 NSURLConnectionNSURLSessionFishhook 則適用于 C 語(yǔ)言實(shí)現(xiàn)的庫(kù)鞍爱,如 CFNetwork鹃觉。

下圖是阿里百川碼力監(jiān)控給出的三類網(wǎng)絡(luò)接口需要 hook 的方法

image.png

接下來(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 就是將原有的 SELIMP映射關(guān)系打破唯卖,并建立新的關(guān)聯(lián)來(lái)達(dá)到方法替換的目的粱玲。

image.png

因此利用 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 的定義,NSProxyNSObject 一樣都是根類橄碾,它是一個(gè)抽象類卵沉,你可以通過(guò)繼承它,并重寫 -forwardInvocation:-methodSignatureForSelector: 方法以實(shí)現(xiàn)消息轉(zhuǎn)發(fā)到另一個(gè)實(shí)例法牲。綜上史汗,NSProxy 的目的就是負(fù)責(zé)將消息轉(zhuǎn)發(fā)到真正的 target 的代理類。

Method swizzling 替換方法需要指定類名拒垃,但是 NSURLConnectionDelegateNSURLSessionDelegate 是由業(yè)務(wù)方指定停撞,通常來(lái)說(shuō)是不確定,所以這種場(chǎng)景不適合使用 Method swizzling。使用 NSProxy 可以解決上面的問(wèn)題戈毒,具體實(shí)現(xiàn):proxy delegate 替換 NSURLConnectionNSURLSession 原來(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è)操作流程。

image.png

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ō)就是去替換 CFNetworkCoreFoundation 中的相關(guān)函數(shù)省咨。后面會(huì)在講監(jiān)控 CFNetwork 詳細(xì)說(shuō)明肃弟,這里不再贅述。

講解完 iOS 上 hook 的實(shí)現(xiàn)技術(shù)零蓉,接下來(lái)討論在 NSURLConnectionNSURLSessionCFNetwork 中穷缤,如何將上面的三種技術(shù)應(yīng)用到實(shí)踐中敌蜂。

NSURLConnection

image.png

NSURLSession

image.png

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秸脱,流程圖如下:

image.png

詳細(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 的目的贸桶。

image.png

具體的實(shí)現(xiàn)思路就是:首先設(shè)計(jì)一個(gè)繼承自 NSObject 并持有 NSInputStream 對(duì)象的 Proxy 類舅逸,持有的 NSInputStream 記為 OriginalStream。將所有發(fā)向 Proxy 的消息轉(zhuǎn)發(fā)給 OriginalStream 處理皇筛,然后再重寫 NSInputStreamread: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ì)模型如下:

image.png

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ì)象有 taskIntervalredirectCount 屬性,還有在執(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)署浩,包括了 requestresponse 屬性揉燃,對(duì)應(yīng) HTTP 的請(qǐng)求和響應(yīng),還包括了從 fetchStartDate 開(kāi)始筋栋,到 responseEndDate 結(jié)束之間的指標(biāo)炊汤,當(dāng)然還有 networkProtocolNameresourceFetchType 屬性。

  • 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í)姑曙,responsenil

    /*
     * 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é)分別做了哪些工作

image.png

如果是復(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 SwizzlingNSProxyFishhook 進(jìn)行 AOP Hook,文章也給出了三種 AOP Hook 技術(shù)在 NSURLConnection牛哺、NSURLSessionCFNetwork 的案例陋气。最后詳細(xì)介紹在 iOS 10 中新引入的 NSURLSessionTaskMetricsNSURLSessionTaskTransactionMetrics 類,它們可以被用于獲取網(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ò)性能议慰。

參考資料

![]](http://upload-images.jianshu.io/upload_images/6560691-ec9194547993f9d1.gif?imageMogr2/auto-orient/strip)

更多有關(guān) iOS 技術(shù)文章,請(qǐng)關(guān)注「滬江技術(shù)學(xué)院」微信公眾號(hào)燃观。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末褒脯,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子缆毁,更是在濱河造成了極大的恐慌番川,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,273評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件脊框,死亡現(xiàn)場(chǎng)離奇詭異颁督,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)浇雹,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,349評(píng)論 3 398
  • 文/潘曉璐 我一進(jìn)店門沉御,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人昭灵,你說(shuō)我怎么就攤上這事吠裆。” “怎么了烂完?”我有些...
    開(kāi)封第一講書人閱讀 167,709評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵试疙,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我抠蚣,道長(zhǎng)祝旷,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書人閱讀 59,520評(píng)論 1 296
  • 正文 為了忘掉前任嘶窄,我火速辦了婚禮怀跛,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘柄冲。我一直安慰自己吻谋,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,515評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布羊初。 她就那樣靜靜地躺著滨溉,像睡著了一般什湘。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上晦攒,一...
    開(kāi)封第一講書人閱讀 52,158評(píng)論 1 308
  • 那天闽撤,我揣著相機(jī)與錄音,去河邊找鬼脯颜。 笑死哟旗,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的栋操。 我是一名探鬼主播闸餐,決...
    沈念sama閱讀 40,755評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼矾芙!你這毒婦竟也來(lái)了舍沙?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書人閱讀 39,660評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤剔宪,失蹤者是張志新(化名)和其女友劉穎拂铡,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體葱绒,經(jīng)...
    沈念sama閱讀 46,203評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡感帅,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,287評(píng)論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了地淀。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片失球。...
    茶點(diǎn)故事閱讀 40,427評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖帮毁,靈堂內(nèi)的尸體忽然破棺而出实苞,到底是詐尸還是另有隱情,我是刑警寧澤烈疚,帶...
    沈念sama閱讀 36,122評(píng)論 5 349
  • 正文 年R本政府宣布硬梁,位于F島的核電站,受9級(jí)特大地震影響胞得,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜屹电,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,801評(píng)論 3 333
  • 文/蒙蒙 一阶剑、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧危号,春花似錦牧愁、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 32,272評(píng)論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)兔朦。三九已至,卻和暖如春磨确,著一層夾襖步出監(jiān)牢的瞬間沽甥,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 33,393評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工乏奥, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留摆舟,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,808評(píng)論 3 376
  • 正文 我出身青樓邓了,卻偏偏與公主長(zhǎng)得像恨诱,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子骗炉,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,440評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容