監(jiān)測(cè)app的流量

前言

用戶在月初發(fā)來了一個(gè)反饋工單糕簿,說是我們app的流量在7號(hào)之前就用了6個(gè)多G的流量屈扎,并且附上了手機(jī)自帶流量消耗占比的圖片怀挠。這讓我們很納悶析蝴,我們app也不過是請(qǐng)求幾個(gè)接口,獲取一下定位的一些信息绿淋,怎么會(huì)消耗那么多流量呢闷畸?查看了埋點(diǎn)記錄,發(fā)現(xiàn)了用戶在請(qǐng)求線路信息的接口吞滞,最高的1秒中請(qǐng)求了4次佑菩。這個(gè)為了經(jīng)常更新線路公交的狀態(tài),我們只加了15秒的定時(shí)器去請(qǐng)求裁赠〉钅可是為什么會(huì)請(qǐng)求這么頻繁呢?

經(jīng)過一些列的代碼查找佩捞,后來發(fā)現(xiàn)在寫側(cè)邊欄凸舵,顯示線路上公交信息的時(shí)候,只有創(chuàng)建定時(shí)器的失尖,沒有去銷毀定時(shí)器啊奄,所以導(dǎo)致定時(shí)器在指數(shù)的去刷新線路上公交信息的接口。這也幸虧后臺(tái)做了一部分?jǐn)r截掀潮,不然菇夸,想想都覺得恐怖,指數(shù)性的去請(qǐng)求接口仪吧。

于是由這個(gè)問題的反思庄新,應(yīng)該往代碼去監(jiān)聽統(tǒng)計(jì)一下自己app的流量的消耗。

準(zhǔn)備

查看了一些博客以及github的文章,很多都是通過監(jiān)聽手機(jī)的流量消耗的择诈,自己app的流量的消耗卻沒有多少械蹋,于是借鑒了一些博客和github的文章。

以下為一些參考的文章和庫:

1羞芍、移動(dòng)端性能監(jiān)控方案Hertz

2哗戈、iOS 流量監(jiān)控分析

3、使用NSURLProtocol時(shí)要注意的一些問題

4荷科、iOS 開發(fā)中使用 NSURLProtocol 攔截 HTTP 請(qǐng)求

5唯咬、獲取NSURLResponse的HTTPVersion

但以上這些資料對(duì)我們的需求都有不足之處:

1. Request 和 Response 記在同一條記錄

在實(shí)際的網(wǎng)絡(luò)請(qǐng)求中 Request 和 Response 不一定是成對(duì)的,如果網(wǎng)絡(luò)斷開畏浆、或者突然關(guān)閉進(jìn)程胆胰,都會(huì)導(dǎo)致不成對(duì)現(xiàn)象,如果將 Request 和 Response 記錄在同一條數(shù)據(jù)刻获,將會(huì)對(duì)統(tǒng)計(jì)造成偏差

2. 上行流量記錄不精準(zhǔn)

主要的原因有三大類:

直接忽略了 Header 和 Line 部分還有Query部分

忽略了 Cookie 部分蜀涨,實(shí)際上,臃腫的 Cookie 也是消耗流量的一部分

body 部分的字節(jié)大小計(jì)算直接使用了HTTPBody.length不夠準(zhǔn)確

3. 下行流量記錄不精準(zhǔn)

主要原因有:

直接忽略了 Header 和 Status-Line 部分

body 部分的字節(jié)大小計(jì)算直接使用了expectedContentLength不夠準(zhǔn)確

忽略了 gzip 壓縮蝎毡,在實(shí)際網(wǎng)絡(luò)編程中勉盅,往往都使用了 gzip 來進(jìn)行數(shù)據(jù)壓縮,而系統(tǒng)提供的一些監(jiān)聽方法顶掉,返回的 NSData 實(shí)際是解壓過的草娜,如果直接統(tǒng)計(jì)字節(jié)數(shù)會(huì)造成大量偏差

后文將詳細(xì)講述。

開始自己上代碼

首先我們得了解網(wǎng)絡(luò)請(qǐng)求具體都有哪些內(nèi)容痒筒,從而方便的去監(jiān)聽這些數(shù)據(jù)來統(tǒng)計(jì)宰闰。


http報(bào)文

從上圖可以看出,主要是兩塊簿透,請(qǐng)求報(bào)文和響應(yīng)報(bào)文移袍。(也是就大家熟知的NSURLRequest和NSURLResponse)

既然如此,那就來兩張抓包的數(shù)據(jù)圖老充,來看一下:


request
respose

接下來咱們就具體分析以下request和response吧


這塊我采用的大家耳熟能詳?shù)腘SURLProtocol葡盗,NSURLProtocol方式除了通過 CFNetwork 發(fā)出的網(wǎng)絡(luò)請(qǐng)求,全部都可以攔截到啡浊。

Apple 文檔中對(duì)NSURLProtocol有非常詳細(xì)的描述和使用介紹

An abstract class that handles the loading of protocol-specific URL data.

如果想更詳細(xì)的了解NSURLProtocol觅够,也可以看大佐的這篇文章

在每一個(gè) HTTP 請(qǐng)求開始時(shí),URL 加載系統(tǒng)創(chuàng)建一個(gè)合適的NSURLProtocol對(duì)象處理對(duì)應(yīng)的 URL 請(qǐng)求巷嚣,而我們需要做的就是寫一個(gè)繼承自NSURLProtocol的類喘先,并通過- registerClass:方法注冊(cè)我們的協(xié)議類,然后 URL 加載系統(tǒng)就會(huì)在請(qǐng)求發(fā)出時(shí)使用我們創(chuàng)建的協(xié)議對(duì)象對(duì)該請(qǐng)求進(jìn)行處理廷粒。

NSURLProtocol是一個(gè)抽象類窘拯,需要做的第一步就是集成它红且,完成我們的自定義設(shè)置。

創(chuàng)建自己的CLLURLProtocol涤姊,為它添加幾個(gè)屬性并實(shí)現(xiàn)相關(guān)接口:

@interface CLLURLProtocol() <NSURLConnectionDelegate, NSURLConnectionDataDelegate>

@property (nonatomic, strong) NSURLConnection *connection;

@property (nonatomic, strong) NSURLRequest *cll_request;

@property (nonatomic, strong) NSURLResponse *cll_response;

@property (nonatomic, strong) NSMutableData *cll_data;

@end

canInitWithRequest?&?canonicalRequestForRequest:

static NSString *const CLLHTTP = @"CLLHTTP";

+ (BOOL)canInitWithRequest:(NSURLRequest*)request {

? ? if (![request.URL.scheme isEqualToString:@"http"]) {

? ? ? ? returnNO;

? ? }

? ? // 攔截過的不再攔截

? ? if([NSURLProtocolpropertyForKey:CLLHTTPinRequest:request] ) {

? ? ? ? returnNO;

? ? }

? ? return YES;

}


+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request {

? ? NSMutableURLRequest*mutableReqeust = [requestmutableCopy];

? ? [NSURLProtocol setProperty:@YES

? ? ? ? ? ? ? ? ? ? ? ? forKey:CLLHTTP

?? ? ? ? ? ? ? ? ? ? inRequest:mutableReqeust];

? ? return[mutableReqeustcopy];

}

startLoading:

- (void)startLoading {

? ? NSURLRequest *request = [[self class] canonicalRequestForRequest:self.request];

? ? self.connection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:YES];

? ? self.cll_request = self.request;

}

didReceiveResponse:

- (void)connection:(NSURLConnection*)connectiondidReceiveResponse:(NSURLResponse*)response {

? ? [[self client] URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageAllowed];

? ? self.cll_response= response;

}

didReceiveData:

- (void)connection:(NSURLConnection*)connectiondidReceiveData:(NSData*)data {

? ? [self.client URLProtocol:self didLoadData:data];

? ? [self.cll_dataappendData:data];

}

以上部分是為了在單次 HTTP 請(qǐng)求中記錄各個(gè)所需要屬性暇番。

記錄 Resquest 信息

為?NSURLReques?添加一個(gè)擴(kuò)展:NSURLRequest+DoggerMonitor

Line

對(duì)于NSURLRequest?我沒有 NSURLReponse 私有接口將其轉(zhuǎn)換成 CFNetwork 相關(guān)數(shù)據(jù),但是我們很清楚 HTTP 請(qǐng)求報(bào)文 Line 部分的組成思喊,所以我們可以添加一個(gè)方法壁酬,獲取一個(gè)經(jīng)驗(yàn) Line。

- (NSUInteger)cll_getLineLength {

? ? NSString *lineStr = [NSString stringWithFormat:@"%@ %@ %@ %@ %@\n", self.URL.host,self.HTTPMethod, self.URL.path, @"HTTP/1.1",self.URL.query];

? ? NSData *lineData = [lineStr dataUsingEncoding:NSUTF8StringEncoding];

? ? returnlineData.length;

}

Header

Header 這里有一個(gè)非常大的坑搔涝。

request.allHTTPHeaderFields拿到的頭部數(shù)據(jù)是有很多缺失的,這塊跟業(yè)內(nèi)朋友交流的時(shí)候和措,發(fā)現(xiàn)很多人都沒有留意到這個(gè)問題庄呈。

缺失的部分不僅僅是上面一篇文章中說到的 Cookie。

如果通過 Charles 抓包派阱,可以看到诬留,會(huì)缺失包括但不僅限于以下字段:

Accept

Connection

Host

這個(gè)問題非常的迷,同時(shí)由于無法轉(zhuǎn)換到 CFNetwork 層贫母,所以一直拿不到準(zhǔn)確的 Header 數(shù)據(jù)文兑。

最后,我在 so 上也找到了兩個(gè)相關(guān)問題腺劣,供大家參考

NSUrlRequest: where an app can find the default headers for HTTP request?

NSMutableURLRequest, cant access all request headers sent out from within my iPhone program

兩個(gè)問題的回答基本表明了绿贞,如果你是通過 CFNetwork 來發(fā)起請(qǐng)求的,才可以拿到完整的 Header 數(shù)據(jù)橘原。

所以這塊只能拿到大部分的 Header籍铁,但是基本上缺失的都固定是那幾個(gè)字段,對(duì)我們流量統(tǒng)計(jì)的精確度影響不是很大趾断。

那么主要就針對(duì) cookie 部分進(jìn)行補(bǔ)全:

- (NSUInteger)cll_getHeadersLengthWithCookie {

? ? NSUIntegerheadersLength =0;

? ? NSDictionary *headerFields =self.allHTTPHeaderFields;

? ? NSDictionary *cookiesHeader = [selfcll_getCookies];

? ? // 添加 cookie 信息

? ? if(cookiesHeader.count) {

? ? ? ? NSMutableDictionary*headerFieldsWithCookies = [NSMutableDictionarydictionaryWithDictionary:headerFields];

? ? ? ? [headerFieldsWithCookiesaddEntriesFromDictionary:cookiesHeader];

? ? ? ? headerFields = [headerFieldsWithCookiescopy];

? ? }

? ? NSLog(@"%@", headerFields);

? ? NSString*headerStr =@"";

? ? for(NSString*keyinheaderFields.allKeys) {

? ? ? ? headerStr = [headerStrstringByAppendingString:key];

? ? ? ? headerStr = [headerStrstringByAppendingString:@": "];

? ? ? ? if([headerFieldsobjectForKey:key]) {

? ? ? ? ? ? headerStr = [headerStrstringByAppendingString:headerFields[key]];

? ? ? ? }

? ? ? ? headerStr = [headerStrstringByAppendingString:@"\r\n"];

? ? }

? ? headerStr = [headerStrstringByAppendingString:@"\r\n"];

? ? NSData *headerData = [headerStr dataUsingEncoding:NSUTF8StringEncoding];

? ? headersLength = headerData.length;

? ? returnheadersLength;

}

- (NSDictionary<NSString *, NSString *> *)cll_getCookies {

? ? NSDictionary *cookiesHeader;

? ? NSHTTPCookieStorage *cookieStorage = [NSHTTPCookieStorage sharedHTTPCookieStorage];

? ? NSArray *cookies = [cookieStoragecookiesForURL:self.URL];

? ? if(cookies.count) {

? ? ? ? cookiesHeader = [NSHTTPCookierequestHeaderFieldsWithCookies:cookies];

? ? }

? ? returncookiesHeader;

}

body?

最后是 body 部分拒名,這里也有個(gè)坑。通過 NSURLConnection 發(fā)出的網(wǎng)絡(luò)請(qǐng)求 resquest.HTTPBody 拿到的是 nil芋酌。需要轉(zhuǎn)而通過 HTTPBodyStream 讀取 stream 來獲取 request 的 Body 大小增显。

- (NSUInteger)cll_getBodyLength {

? ? NSDictionary *headerFields =self.allHTTPHeaderFields;

? ? NSUIntegerbodyLength = [self.HTTPBodylength];

? ? if([headerFieldsobjectForKey:@"Content-Encoding"]) {

? ? ? ? NSData*bodyData;

? ? ? ? if(self.HTTPBody==nil) {

? ? ? ? ? ? uint8_td[1024] = {0};

? ? ? ? ? ? NSInputStream*stream =self.HTTPBodyStream;

? ? ? ? ? ? NSMutableData*data = [[NSMutableDataalloc]init];

? ? ? ? ? ? [streamopen];

? ? ? ? ? ? while([streamhasBytesAvailable]) {

? ? ? ? ? ? ? ? NSIntegerlen = [streamread:dmaxLength:1024];

? ? ? ? ? ? ? ? if(len >0&& stream.streamError==nil) {

? ? ? ? ? ? ? ? ? ? [dataappendBytes:(void*)dlength:len];

? ? ? ? ? ? ? ? }

? ? ? ? ? ? }

? ? ? ? ? ? bodyData = [datacopy];

? ? ? ? ? ? [streamclose];

? ? ? ? }else{

? ? ? ? ? ? bodyData =self.HTTPBody;

? ? ? ? }

? ? ? ? bodyLength = [[bodyDatagzippedData]length];

? ? }

? ? returnbodyLength;

}

記錄 Response 信息

前面的代碼實(shí)現(xiàn)了在網(wǎng)絡(luò)請(qǐng)求過程中為cll_response和cll_data賦值,那么在stopLoading方法中脐帝,就可以分析cll_response和cll_data對(duì)象同云,獲取下行流量等相關(guān)信息。

需要說明的是堵腹,如果需要獲得非常精準(zhǔn)的流量梢杭,一般來說只有通過 Socket 層獲取是最準(zhǔn)確的,因?yàn)榭梢垣@取包括握手秸滴、揮手的數(shù)據(jù)大小武契。當(dāng)然,我們的目的是為了分析 App 的耗流量 API,所以僅從應(yīng)用層去分析也基本滿足了我們的需要咒唆。

上文中說到了報(bào)文的組成届垫,那么按照?qǐng)?bào)文所需要的內(nèi)容獲取。

Status Line

非常遺憾的是NSURLResponse沒有接口能直接獲取報(bào)文中的 Status Line全释,甚至連 HTTP Version 等組成 Status Line 內(nèi)容的接口也沒有装处。

最后,我通過轉(zhuǎn)換到 CFNetwork 相關(guān)類浸船,才拿到了 Status Line 的數(shù)據(jù)妄迁,這其中可能涉及到了讀取私有 API

這里我為NSURLResponse添加了一個(gè)擴(kuò)展:NSURLResponse+DoggerMonitor,并為其添加statusLineFromCF方法

typedef CFHTTPMessageRef (*cllURLResponseGetHTTPResponse)(CFURLRef response);

- (NSString*)statusLineFromCF{

? ? NSURLResponse*response =self;

? ? NSString*statusLine =@"";

? ? // 獲取CFURLResponseGetHTTPResponse的函數(shù)實(shí)現(xiàn)

? ? NSString *funName = @"CFURLResponseGetHTTPResponse";

? ? cllURLResponseGetHTTPResponseoriginURLResponseGetHTTPResponse =

? ? dlsym(RTLD_DEFAULT, [funNameUTF8String]);

? ? SELtheSelector =NSSelectorFromString(@"_CFURLResponse");

? ? if([responserespondsToSelector:theSelector] &&

? ? ? ? NULL!= originURLResponseGetHTTPResponse) {

? ? ? ? // 獲取NSURLResponse的_CFURLResponse

? ? ? ? CFTypeRefcfResponse =CFBridgingRetain([responseperformSelector:theSelector]);

? ? ? ? if(NULL!= cfResponse) {

? ? ? ? ? ? // 將CFURLResponseRef轉(zhuǎn)化為CFHTTPMessageRef

? ? ? ? ? ? CFHTTPMessageRefmessageRef = originURLResponseGetHTTPResponse(cfResponse);

? ? ? ? ? ? statusLine = (__bridge_transferNSString*)CFHTTPMessageCopyResponseStatusLine(messageRef);

? ? ? ? ? ? CFRelease(cfResponse);

? ? ? ? }

? ? }

? ? returnstatusLine;

}

通過調(diào)用私有 API?_CFURLResponse?獲得?CFTypeRef?再轉(zhuǎn)換成?CFHTTPMessageRef李命,獲取 Status Line登淘。

再將其轉(zhuǎn)換成 NSData 計(jì)算字節(jié)大小:

- (NSUInteger)cll_getLineLength{

? ? NSString*lineStr =@"";

? ? if ([self isKindOfClass:[NSHTTPURLResponse class]]) {

? ? ? ? NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)self;

? ? ? ? lineStr = [selfstatusLineFromCF];

? ? }

? ? NSData *lineData = [lineStr dataUsingEncoding:NSUTF8StringEncoding];

? ? returnlineData.length;

}

Header

通過?httpResponse.allHeaderFields?拿到 Header 字典封字,再拼接成報(bào)文的 key: value 格式黔州,轉(zhuǎn)換成 NSData 計(jì)算大小:

- (NSUInteger)cll_getHeadersLength {

? ? NSUIntegerheadersLength =0;

? ? if ([self isKindOfClass:[NSHTTPURLResponse class]]) {

? ? ? ? NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)self;

? ? ? ? NSDictionary *headerFields = httpResponse.allHeaderFields;

? ? ? ? NSString*headerStr =@"";

? ? ? ? for(NSString*keyinheaderFields.allKeys) {

? ? ? ? ? ? headerStr = [headerStrstringByAppendingString:key];

? ? ? ? ? ? headerStr = [headerStrstringByAppendingString:@": "];

? ? ? ? ? ? if([headerFieldsobjectForKey:key]) {

? ? ? ? ? ? ? ? headerStr = [headerStrstringByAppendingString:headerFields[key]];

? ? ? ? ? ? }

? ? ? ? ? ? headerStr = [headerStrstringByAppendingString:@"\r\n"];

? ? ? ? }

? ? ? ? headerStr = [headerStrstringByAppendingString:@"\r\n"];

? ? ? ? NSData*headerData = [headerStrdataUsingEncoding:NSUTF8StringEncoding];

? ? ? ? headersLength = headerData.length;

? ? }

? ? returnheadersLength;

}

Body

對(duì)于 Body 的計(jì)算阔籽,上文看到有些文章里采用的expectedContentLength或者去NSURLResponse對(duì)象的allHeaderFields中獲取Content-Length值流妻,其實(shí)都不夠準(zhǔn)確。

首先 API 文檔中對(duì)expectedContentLength也有介紹是不準(zhǔn)確的:


body

其次笆制,HTTP 1.1 標(biāo)準(zhǔn)里也有介紹Content-Length字段不一定是每個(gè) Response 都帶有的绅这,最重要的是,Content-Length只是表示 Body 部分的大小在辆。

我的方式是君躺,在前面代碼中有寫到,在didReceiveData中對(duì)cll_data進(jìn)行了賦值

didReceiveData:

- (void)connection:(NSURLConnection*)connectiondidReceiveData:(NSData*)data {

? ? [self.client URLProtocol:self didLoadData:data];

? ? [self.cll_dataappendData:data];

}

那么在stopLoading方法中开缎,就可以拿到本次網(wǎng)絡(luò)請(qǐng)求接收到的數(shù)據(jù)棕叫。

但需要注意對(duì) gzip 情況進(jìn)行區(qū)別分析。我們知道 HTTP 請(qǐng)求中奕删,客戶端在發(fā)送請(qǐng)求的時(shí)候會(huì)帶上Accept-Encoding俺泣,這個(gè)字段的值將會(huì)告知服務(wù)器客戶端能夠理解的內(nèi)容壓縮算法。而服務(wù)器進(jìn)行相應(yīng)時(shí)完残,會(huì)在 Response 中添加Content-Encoding告知客戶端選中的壓縮算法伏钠。

所以,我們?cè)趕topLoading中獲取Content-Encoding谨设,如果使用了 gzip熟掂,則模擬一次 gzip 壓縮,再計(jì)算字節(jié)大性稹:

- (void)stopLoading {

? ? [self.connection cancel];

? ? NSUInteger lineLen = [self.cll_response cll_getLineLength];

? ? NSUInteger headerLen = [self.cll_response cll_getHeadersLength];

? ? NSUIntegerbodyLen? =0;

? ? if ([self.cll_response isKindOfClass:[NSHTTPURLResponse class]]) {

? ? ? ? NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)self.cll_response;

? ? ? ? NSData*data =self.cll_data;

? ? ? ? if ([[httpResponse.allHeaderFields objectForKey:@"Content-Encoding"] isEqualToString:@"gzip"]) {

? ? ? ? ? ? data = [self.cll_datagzippedData];

? ? ? ? }

? ? ? ? bodyLen = data.length;

? ? }

? ? NSUIntegertotleLen = lineLen + headerLen + bodyLen;

? ? NSString*host =self.request.URL.host;

? ? NSString*path =self.request.URL.path;

}

在CLLURLProtocol的- (nullable NSURLRequest *)connection:(NSURLConnection *)connection willSendRequest:(NSURLRequest *)request redirectResponse:(nullable NSURLResponse *)response;方法中對(duì) resquest 調(diào)用報(bào)文各個(gè)部分大小方法:

-(NSURLRequest *)connection:(NSURLConnection *)connection willSendRequest:(NSURLRequest *)request redirectResponse:(NSURLResponse *)response {

? ? if(response !=nil) {

? ? ? ? self.cll_response= response;

? ? ? ? [self.client URLProtocol:self wasRedirectedToRequest:request redirectResponse:response];

? ? }

? ? NSUIntegerlineLen =[connection.currentRequestcll_getLineLength];

? ? NSUIntegerheaderCookieLen = [connection.currentRequestcll_getHeadersLengthWithCookie];

? ? NSUIntegerbodylen = [connection.currentRequestcll_getBodyLength];

? ? NSUIntegertotleLen = lineLen + headerCookieLen + bodylen;

? ? NSString*host = request.URL.host;

? ? NSString*path = request.URL.path;

? ? returnrequest;

}

針對(duì) NSURLSession 的處理

直接使用CLLURLProtocol并registerClass并不能完整的攔截所有網(wǎng)絡(luò)請(qǐng)求赴肚,因?yàn)橥ㄟ^NSURLSession的sharedSession發(fā)出的請(qǐng)求是無法被NSURLProtocol代理的素跺。

我們需要讓[NSURLSessionConfiguration defaultSessionConfiguration].protocolClasses的屬性中也設(shè)置我們的DMURLProtocol,這里通過 swizzle誉券,置換protocalClasses的 get 方法:

#import?<Foundation/Foundation.h>

@interface CLLURLSessionConfiguration : NSObject

@property (nonatomic,assign) BOOL isSwizzle;

+ (CLLURLSessionConfiguration *)defaultConfiguration;

- (void)load;

- (void)unload;

@end

#import "CLLURLSessionConfiguration.h"

#import?<objc/runtime.h>

#import "CLLURLProtocol.h"

#import "CLLNetworkTrafficManager.h"

@implementation CLLURLSessionConfiguration

+ (CLLURLSessionConfiguration *)defaultConfiguration {

? ? staticCLLURLSessionConfiguration*staticConfiguration;

? ? staticdispatch_once_tonceToken;

? ? dispatch_once(&onceToken, ^{

? ? ? ? staticConfiguration=[[CLLURLSessionConfigurationalloc]init];

? ? });

? ? returnstaticConfiguration;

}

- (instancetype)init {

? ? self= [superinit];

? ? if(self) {

? ? ? ? self.isSwizzle=NO;

? ? }

? ? return self;

}

- (void)load{

? ? self.isSwizzle=YES;

? ? Class cls = NSClassFromString(@"__NSCFURLSessionConfiguration") ?: NSClassFromString(@"NSURLSessionConfiguration");

? ? [selfswizzleSelector:@selector(protocolClasses)fromClass:clstoClass:[selfclass]];

}

- (void)unload{

? ? self.isSwizzle=NO;

? ? Class cls = NSClassFromString(@"__NSCFURLSessionConfiguration") ?: NSClassFromString(@"NSURLSessionConfiguration");

? ? [selfswizzleSelector:@selector(protocolClasses)fromClass:clstoClass:[selfclass]];

}

- (void)swizzleSelector:(SEL)selectorfromClass:(Class)originaltoClass:(Class)stub {

? ? MethodoriginalMethod =class_getInstanceMethod(original, selector);

? ? MethodstubMethod =class_getInstanceMethod(stub, selector);

? ? if(!originalMethod || !stubMethod) {

? ? ? ? [NSException raise:NSInternalInconsistencyException format:@"Couldn't load NEURLSessionConfiguration."];

? ? }

? ? method_exchangeImplementations(originalMethod, stubMethod);

}

- (NSArray *)protocolClasses {

? ? return [CLLNetworkTrafficManager manager].protocolClasses;

}

@end

這樣指厌,我們寫好了方法置換,在執(zhí)行過該類單例的load方法后踊跟,[NSURLSessionConfiguration defaultSessionConfiguration].protocolClasses拿到的將會(huì)是我們?cè)O(shè)置好的protocolClasses踩验。

如此,我們?cè)贋镃LLURLProtocol添加start和stop方法商玫,用于啟動(dòng)網(wǎng)絡(luò)監(jiān)控和停止網(wǎng)絡(luò)監(jiān)控:

+ (void)start{

? ? CLLURLSessionConfiguration *sessionConfiguration = [CLLURLSessionConfiguration defaultConfiguration];

? ? for(idprotocolClassin[CLLNetworkTrafficManagermanager].protocolClasses) {

? ? ? ? [NSURLProtocolregisterClass:protocolClass];

? ? }

? ? if(![sessionConfigurationisSwizzle]) {

? ? ? ? [sessionConfigurationload];

? ? }

}

+ (void)end{

? ? CLLURLSessionConfiguration *sessionConfiguration = [CLLURLSessionConfiguration defaultConfiguration];

? ? [NSURLProtocol unregisterClass:[CLLURLProtocol class]];

? ? if([sessionConfigurationisSwizzle]) {

? ? ? ? [sessionConfigurationunload];

? ? }

}

到此,基本完成了整個(gè)網(wǎng)絡(luò)流量監(jiān)控地回。

再提供一個(gè) Manger 方便使用者調(diào)用:

#import??<Foundation/Foundation.h>

@class CLLNetworkLog;

@interface CLLNetworkTrafficManager : NSObject

@property (nonatomic, strong) NSArray *protocolClasses;

+ (CLLNetworkTrafficManager *)manager;

/** 通過 protocolClasses 啟動(dòng)流量監(jiān)控模塊 */

+ (void)startWithProtocolClasses:(NSArray*)protocolClasses;

/** 僅以 CLLURLProtocol 啟動(dòng)流量監(jiān)控模塊 */

+ (void)start;

/** 停止流量監(jiān)控 */

+ (void)end;

@end

#import "CLLNetworkTrafficManager.h"

#import "CLLURLProtocol.h"

@interface CLLNetworkTrafficManager ()

@end

@implementation CLLNetworkTrafficManager

#pragma mark- Public

+ (CLLNetworkTrafficManager *)manager {

? ? static CLLNetworkTrafficManager *manager;

? ? staticdispatch_once_tonceToken;

? ? dispatch_once(&onceToken, ^{

? ? ? ? manager=[[CLLNetworkTrafficManager alloc] init];

? ? });

? ? returnmanager;

}

+ (void)startWithProtocolClasses:(NSArray*)protocolClasses {

? ? [selfmanager].protocolClasses= protocolClasses;

? ? [CLLURLProtocol start];

}

+ (void)start{

? ? [self manager].protocolClasses = @[[CLLURLProtocol class]];

? ? [CLLURLProtocol start];

}

+ (void)end{

? ? [CLLURLProtocol end];

}

@end

至此網(wǎng)絡(luò)監(jiān)控就算完成了,當(dāng)然可以把統(tǒng)計(jì)到的流量保存到一個(gè)表中,然后上傳到服務(wù)器溜徙;也可以通過埋點(diǎn)直接傳給服務(wù)器九巡,存儲(chǔ)統(tǒng)計(jì)疏日,這種根據(jù)實(shí)際需求就可以。有啥不懂的可以私信我呦

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市爱榕,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖渣玲,帶你破解...
    沈念sama閱讀 221,430評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件枚钓,死亡現(xiàn)場(chǎng)離奇詭異多望,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,406評(píng)論 3 398
  • 文/潘曉璐 我一進(jìn)店門木西,熙熙樓的掌柜王于貴愁眉苦臉地迎上來燎猛,“玉大人昭卓,你說我怎么就攤上這事杂瘸。” “怎么了返干?”我有些...
    開封第一講書人閱讀 167,834評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)念恍。 經(jīng)常有香客問我,道長(zhǎng)瞳氓,這世上最難降的妖魔是什么庞瘸? 我笑而不...
    開封第一講書人閱讀 59,543評(píng)論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮瞬场,結(jié)果婚禮上眼五,老公的妹妹穿的比我還像新娘。我一直安慰自己彤灶,他們只是感情好弹砚,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,547評(píng)論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著枢希,像睡著了一般桌吃。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上苞轿,一...
    開封第一講書人閱讀 52,196評(píng)論 1 308
  • 那天茅诱,我揣著相機(jī)與錄音,去河邊找鬼搬卒。 笑死瑟俭,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的契邀。 我是一名探鬼主播摆寄,決...
    沈念sama閱讀 40,776評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼坯门!你這毒婦竟也來了微饥?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,671評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤古戴,失蹤者是張志新(化名)和其女友劉穎欠橘,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體现恼,經(jīng)...
    沈念sama閱讀 46,221評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡肃续,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,303評(píng)論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了叉袍。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片始锚。...
    茶點(diǎn)故事閱讀 40,444評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖喳逛,靈堂內(nèi)的尸體忽然破棺而出瞧捌,到底是詐尸還是另有隱情,我是刑警寧澤艺配,帶...
    沈念sama閱讀 36,134評(píng)論 5 350
  • 正文 年R本政府宣布察郁,位于F島的核電站衍慎,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏皮钠。R本人自食惡果不足惜稳捆,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,810評(píng)論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望麦轰。 院中可真熱鬧乔夯,春花似錦、人聲如沸款侵。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,285評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽新锈。三九已至甲脏,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間妹笆,已是汗流浹背块请。 一陣腳步聲響...
    開封第一講書人閱讀 33,399評(píng)論 1 272
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留拳缠,地道東北人墩新。 一個(gè)月前我還...
    沈念sama閱讀 48,837評(píng)論 3 376
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像窟坐,于是被迫代替她去往敵國(guó)和親海渊。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,455評(píng)論 2 359

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