前言
用戶在月初發(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ì)宰闰。
從上圖可以看出,主要是兩塊簿透,請(qǐng)求報(bào)文和響應(yīng)報(bào)文移袍。(也是就大家熟知的NSURLRequest和NSURLResponse)
既然如此,那就來兩張抓包的數(shù)據(jù)圖老充,來看一下:
接下來咱們就具體分析以下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)確的:
其次笆制,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í)際需求就可以。有啥不懂的可以私信我呦