前言
用戶在月初發(fā)來了一個(gè)反饋工單,說是我們app的流量在7號之前就用了6個(gè)多G的流量,并且附上了手機(jī)自帶流量消耗占比的圖片。這讓我們很納悶,我們app也不過是請求幾個(gè)接口,獲取一下定位的一些信息,怎么會消耗那么多流量呢?查看了埋點(diǎn)記錄,發(fā)現(xiàn)了用戶在請求線路信息的接口,最高的1秒中請求了4次。這個(gè)為了經(jīng)常更新線路公交的狀態(tài),我們只加了15秒的定時(shí)器去請求。可是為什么會請求這么頻繁呢?
經(jīng)過一些列的代碼查找,后來發(fā)現(xiàn)在寫側(cè)邊欄,顯示線路上公交信息的時(shí)候,只有創(chuàng)建定時(shí)器的,沒有去銷毀定時(shí)器,所以導(dǎo)致定時(shí)器在指數(shù)的去刷新線路上公交信息的接口。這也幸虧后臺做了一部分?jǐn)r截,不然,想想都覺得恐怖,指數(shù)性的去請求接口。
于是由這個(gè)問題的反思,應(yīng)該往代碼去監(jiān)聽統(tǒng)計(jì)一下自己app的流量的消耗。
準(zhǔn)備
查看了一些博客以及github的文章,很多都是通過監(jiān)聽手機(jī)的流量消耗的,自己app的流量的消耗卻沒有多少,于是借鑒了一些博客和github的文章。
以下為一些參考的文章和庫:
3、使用NSURLProtocol時(shí)要注意的一些問題
4、iOS 開發(fā)中使用 NSURLProtocol 攔截 HTTP 請求
但以上這些資料對我們的需求都有不足之處:
1. Request 和 Response 記在同一條記錄
在實(shí)際的網(wǎng)絡(luò)請求中 Request 和 Response 不一定是成對的,如果網(wǎng)絡(luò)斷開、或者突然關(guān)閉進(jìn)程,都會導(dǎo)致不成對現(xiàn)象,如果將 Request 和 Response 記錄在同一條數(shù)據(jù),將會對統(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ù)會造成大量偏差
后文將詳細(xì)講述。
開始自己上代碼
首先我們得了解網(wǎng)絡(luò)請求具體都有哪些內(nèi)容,從而方便的去監(jiān)聽這些數(shù)據(jù)來統(tǒng)計(jì)。

從上圖可以看出,主要是兩塊,請求報(bào)文和響應(yīng)報(bào)文。(也是就大家熟知的NSURLRequest和NSURLResponse)
既然如此,那就來兩張抓包的數(shù)據(jù)圖,來看一下:


接下來咱們就具體分析以下request和response吧
這塊我采用的大家耳熟能詳?shù)腘SURLProtocol,NSURLProtocol方式除了通過 CFNetwork 發(fā)出的網(wǎng)絡(luò)請求,全部都可以攔截到。
Apple 文檔中對NSURLProtocol有非常詳細(xì)的描述和使用介紹
An abstract class that handles the loading of protocol-specific URL data.
如果想更詳細(xì)的了解NSURLProtocol,也可以看大佐的這篇文章
在每一個(gè) HTTP 請求開始時(shí),URL 加載系統(tǒng)創(chuàng)建一個(gè)合適的NSURLProtocol對象處理對應(yīng)的 URL 請求,而我們需要做的就是寫一個(gè)繼承自NSURLProtocol的類,并通過- registerClass:方法注冊我們的協(xié)議類,然后 URL 加載系統(tǒng)就會在請求發(fā)出時(shí)使用我們創(chuàng)建的協(xié)議對象對該請求進(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 請求中記錄各個(gè)所需要屬性。
記錄 Resquest 信息
為?NSURLReques?添加一個(gè)擴(kuò)展:NSURLRequest+DoggerMonitor
Line
對于NSURLRequest?我沒有 NSURLReponse 私有接口將其轉(zhuǎn)換成 CFNetwork 相關(guān)數(shù)據(jù),但是我們很清楚 HTTP 請求報(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 抓包,可以看到,會缺失包括但不僅限于以下字段:
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ā)起請求的,才可以拿到完整的 Header 數(shù)據(jù)。
所以這塊只能拿到大部分的 Header,但是基本上缺失的都固定是那幾個(gè)字段,對我們流量統(tǒng)計(jì)的精確度影響不是很大。
那么主要就針對 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ò)請求 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ò)請求過程中為cll_response和cll_data賦值,那么在stopLoading方法中,就可以分析cll_response和cll_data對象,獲取下行流量等相關(guān)信息。
需要說明的是,如果需要獲得非常精準(zhǔn)的流量,一般來說只有通過 Socket 層獲取是最準(zhǔn)確的,因?yàn)榭梢垣@取包括握手、揮手的數(shù)據(jù)大小。當(dāng)然,我們的目的是為了分析 App 的耗流量 API,所以僅從應(yīng)用層去分析也基本滿足了我們的需要。
上文中說到了報(bào)文的組成,那么按照報(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ì)算大?。?/p>
- (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
對于 Body 的計(jì)算,上文看到有些文章里采用的expectedContentLength或者去NSURLResponse對象的allHeaderFields中獲取Content-Length值,其實(shí)都不夠準(zhǔn)確。
首先 API 文檔中對expectedContentLength也有介紹是不準(zhǔn)確的:

其次,HTTP 1.1 標(biāo)準(zhǔn)里也有介紹Content-Length字段不一定是每個(gè) Response 都帶有的,最重要的是,Content-Length只是表示 Body 部分的大小。
我的方式是,在前面代碼中有寫到,在didReceiveData中對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ò)請求接收到的數(shù)據(jù)。
但需要注意對 gzip 情況進(jìn)行區(qū)別分析。我們知道 HTTP 請求中,客戶端在發(fā)送請求的時(shí)候會帶上Accept-Encoding,這個(gè)字段的值將會告知服務(wù)器客戶端能夠理解的內(nèi)容壓縮算法。而服務(wù)器進(jìn)行相應(yīng)時(shí),會在 Response 中添加Content-Encoding告知客戶端選中的壓縮算法。
所以,我們在stopLoading中獲取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;方法中對 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;
}
針對 NSURLSession 的處理
直接使用CLLURLProtocol并registerClass并不能完整的攔截所有網(wǎng)絡(luò)請求,因?yàn)橥ㄟ^NSURLSession的sharedSession發(fā)出的請求是無法被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拿到的將會是我們設(shè)置好的protocolClasses。
如此,我們再為CLLURLProtocol添加start和stop方法,用于啟動網(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 啟動流量監(jiān)控模塊 */
+ (void)startWithProtocolClasses:(NSArray*)protocolClasses;
/** 僅以 CLLURLProtocol 啟動流量監(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ù)器,存儲統(tǒng)計(jì),這種根據(jù)實(shí)際需求就可以。有啥不懂的可以私信我呦