由于騎手不能隨時處在有 WIFI 的狀態(tài),流量變成了很敏感的問題,為了精確到每個 API 的流量,進行針對性的優(yōu)化,開始在我們的 APM 中添加流量監(jiān)控功能。
本文將記錄自己做流量監(jiān)控方面的總結。其中包括了非常多的踩坑經(jīng)驗,和現(xiàn)有一些方案的缺陷分析,對我來說是一個非常有意義的過程。
干貨預警 請做好讀大量代碼的準備
一、資料收集
就目前來說,各家大廠基本都有自己的 APM(包括我們公司其實之前也有一套 APM,但是由于各個事業(yè)部的需求不同,尚不能完全滿足物流平臺的需要)但各家大廠目前開源的 APM 項目卻不多,當然也可能是由于各家的業(yè)務場景差異比較大且對數(shù)據(jù)的后續(xù)處理不同。
所以本次在查閱資料階段,沒有太多的源碼可選參考,但有不少文章。 以下是一些本次開發(fā)過程中參考的文章和開源庫:
iOS-Monitor-Platform GodEye NetworkEye 移動端性能監(jiān)控方案Hertz 使用NSURLProtocol注意的一些問題 iOS 開發(fā)中使用 NSURLProtocol 攔截 HTTP 請求 獲取NSURLResponse的HTTPVersion
但以上這些資料對我們的需求都有不足之處:
1. Request 和 Response 記在同一條記錄
在實際的網(wǎng)絡請求中 Request 和 Response 不一定是成對的,如果網(wǎng)絡斷開、或者突然關閉進程,都會導致不成對現(xiàn)象,如果將 Request 和 Response 記錄在同一條數(shù)據(jù),將會對統(tǒng)計造成偏差
2. 上行流量記錄不精準
主要的原因有三大類:
直接忽略了 Header 和 Line 部分 忽略了 Cookie 部分,實際上,臃腫的 Cookie 也是消耗流量的一部分 body 部分的字節(jié)大小計算直接使用了 HTTPBody.length 不夠準確
3. 下行流量記錄不精準
主要原因有:
直接忽略了 Header 和 Status-Line 部分 body 部分的字節(jié)大小計算直接使用了 expectedContentLength 不夠準確 忽略了 gzip 壓縮,在實際網(wǎng)絡編程中,往往都使用了 gzip 來進行數(shù)據(jù)壓縮,而系統(tǒng)提供的一些監(jiān)聽方法,返回的 NSData 實際是解壓過的,如果直接統(tǒng)計字節(jié)數(shù)會造成大量偏差
后文將詳細講述。
二、需求
先簡單羅列我們的需求:
Request 基本信息記錄 上行流量 Reponse 基本信息記錄 下行流量 數(shù)據(jù)歸類:按照 host 和 path 歸類,一條記錄記載改 host/path 的 Request 記錄數(shù),Response 記錄數(shù),Reqeust 總流量(上行流量),Reponse 總流量(下行流量)
我們的側重點是流量統(tǒng)計,為了方便分析 APP 使用中哪些 API 消耗流量多。所以對上行、下行流量都需要盡量準確記錄。
最終的數(shù)據(jù)庫表展示:
type 字段表示的是『該條記錄是 Request 還是 Response』,幾個 length 分別記錄了流量的各個細節(jié),包括:總字節(jié)數(shù)、Line 字節(jié)數(shù)、Header 字節(jié)數(shù)、Body 字節(jié)數(shù)。
最后的界面展示類似于:
三、分析現(xiàn)有資料
現(xiàn)在分析一下上面收集到的資料有哪些不足之處。
GodEye | NetworkEye:
NetworkEye 是 GodEye 的一部分,可以單獨拆出來使用的網(wǎng)絡監(jiān)控庫。 查閱兩者的源碼后發(fā)現(xiàn),NetworkEye
僅僅記錄了 Reponse 的流量 通過 expectedContentLength 記錄是不準確的(后面將會說到) 僅僅記錄了總和,這對我們來說是無意義的,不能分析出哪條 API 流量使用多
移動端性能監(jiān)控方案Hertz:
美團的文章中展示幾個代碼片段:
- (void)connectionDidFinishLoading:(NSURLConnection *)connection { [self.client URLProtocolDidFinishLoading:self]; self.data = nil; if (connection.originalRequest) { WMNetworkUsageDataInfo *info = [[WMNetworkUsageDataInfo alloc] init]; self.connectionEndTime = [[NSDate date] timeIntervalSince1970]; info.responseSize = self.responseDataLength; info.requestSize = connection.originalRequest.HTTPBody.length; info.contentType = [WMNetworkUsageURLProtocol getContentTypeByURL:connection.originalRequest.URL andMIMEType:self.MIMEType]; [[WMNetworkMeter sharedInstance] setLastDataInfo:info]; [[WMNetworkUsageManager sharedManager] recordNetworkUsageDataInfo:info]; }
在 connectionDidFinishLoading 中記錄了整個網(wǎng)絡請求結束的時間、 response 數(shù)據(jù)大小、request 數(shù)據(jù)大小以及一些其他數(shù)據(jù)。
總體來說是比較詳細的,但是這里并沒有給出 self.responseDataLength 的具體邏輯,另外 connection.originalRequest.HTTPBody.length 僅僅是 Request body 的大小。
iOS-Monitor-Platform:
這篇文章比較詳細的介紹了整個 APM 制作的過程,貼出了很多代碼段,應該說非常詳細也極具參考價值。
在流量部分,也分別針對了上行流量、下行流量進行了區(qū)分,但其中:
沒有處理 gzip 壓縮情況 對 Header 計算大小的方式是 Dictionary 轉(zhuǎn) NSData,然而實際上頭部并不是 Json 格式(這塊我覺得很迷,因為作者特意展示了 HTTP 報文組成)
四、動手自己做
HTTP 報文
為了更好的讓大家了解 HTTP 流量計算的一些關鍵信息,首先要了解 HTTP 報文的組成。
再來隨便抓個包具體看看:
iOS 下的網(wǎng)絡監(jiān)控
這塊我采用的大家耳熟能詳?shù)?NSURLProtocol,NSURLProtocol 方式除了通過 CFNetwork 發(fā)出的網(wǎng)絡請求,全部都可以攔截到。
Apple 文檔中對 NSURLProtocol 有非常詳細的描述和使用介紹
An abstract class that handles the loading of protocol-specific URL data.
如果想更詳細的了解 NSURLProtocol,也可以看大佐的這篇文章
在每一個 HTTP 請求開始時,URL 加載系統(tǒng)創(chuàng)建一個合適的 NSURLProtocol 對象處理對應的 URL 請求,而我們需要做的就是寫一個繼承自 NSURLProtocol 的類,并通過 - registerClass: 方法注冊我們的協(xié)議類,然后 URL 加載系統(tǒng)就會在請求發(fā)出時使用我們創(chuàng)建的協(xié)議對象對該請求進行處理。
NSURLProtocol 是一個抽象類,需要做的第一步就是集成它,完成我們的自定義設置。
創(chuàng)建自己的 DMURLProtocol,為它添加幾個屬性并實現(xiàn)相關接口:
@interface DMURLProtocol() NSURLConnectionDelegate, NSURLConnectionDataDelegate> @property (nonatomic, strong) NSURLConnection *connection; @property (nonatomic, strong) NSURLRequest *dm_request; @property (nonatomic, strong) NSURLResponse *dm_response; @property (nonatomic, strong) NSMutableData *dm_data; @end
canInitWithRequest & canonicalRequestForRequest:
static NSString *const DMHTTP = @'LPDHTTP';
+ (BOOL)canInitWithRequest:(NSURLRequest *)request { if (![request.URL.scheme isEqualToString:@'http']) { return NO; } // 攔截過的不再攔截 if ([NSURLProtocol propertyForKey:LPDHTTP inRequest:request] ) { return NO; } return YES; }
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request { NSMutableURLRequest *mutableReqeust = [request mutableCopy]; [NSURLProtocol setProperty:@YES forKey:DMHTTP inRequest:mutableReqeust]; return [mutableReqeust copy]; }
startLoading:
- (void)startLoading { NSURLRequest *request = [[self class] canonicalRequestForRequest:self.request]; self.connection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:YES]; self.dm_request = self.request; }
didReceiveResponse:
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response { [[self client] URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageAllowed]; self.dm_response = response; }
didReceiveData:
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data { [self.client URLProtocol:self didLoadData:data]; [self.dm_data appendData:data]; }
以上部分是為了在單次 HTTP 請求中記錄各個所需要屬性。
記錄 Response 信息
前面的代碼實現(xiàn)了在網(wǎng)絡請求過程中為 dm_response 和 dm_data 賦值,那么在 stopLoading 方法中,就可以分析 dm_response 和 dm_data 對象,獲取下行流量等相關信息。
需要說明的是,如果需要獲得非常精準的流量,一般來說只有通過 Socket 層獲取是最準確的,因為可以獲取包括握手、揮手的數(shù)據(jù)大小。當然,我們的目的是為了分析 App 的耗流量 API,所以僅從應用層去分析也基本滿足了我們的需要。
上文中說到了報文的組成,那么按照報文所需要的內(nèi)容獲取。
Status Line
非常遺憾的是 NSURLResponse 沒有接口能直接獲取報文中的 Status Line,甚至連 HTTP Version 等組成 Status Line 內(nèi)容的接口也沒有。
最后,我通過轉(zhuǎn)換到 CFNetwork 相關類,才拿到了 Status Line 的數(shù)據(jù),這其中可能涉及到了讀取私有 API
這里我為 NSURLResponse 添加了一個擴展:NSURLResponse+DoggerMonitor,并為其添加 statusLineFromCF 方法
typedef CFHTTPMessageRef (*DMURLResponseGetHTTPResponse)(CFURLRef response); - (NSString *)statusLineFromCF { NSURLResponse *response = self; NSString *statusLine = @''; // 獲取CFURLResponseGetHTTPResponse的函數(shù)實現(xiàn) NSString *funName = @'CFURLResponseGetHTTPResponse'; DMURLResponseGetHTTPResponse originURLResponseGetHTTPResponse = dlsym(RTLD_DEFAULT, [funName UTF8String]); SEL theSelector = NSSelectorFromString(@'_CFURLResponse'); if ([response respondsToSelector:theSelector] && NULL != originURLResponseGetHTTPResponse) { // 獲取NSURLResponse的_CFURLResponse CFTypeRef cfResponse = CFBridgingRetain([response performSelector:theSelector]); if (NULL != cfResponse) { // 將CFURLResponseRef轉(zhuǎn)化為CFHTTPMessageRef CFHTTPMessageRef messageRef = originURLResponseGetHTTPResponse(cfResponse); statusLine = (__bridge_transfer NSString *)CFHTTPMessageCopyResponseStatusLine(messageRef); CFRelease(cfResponse); } } return statusLine; }
通過調(diào)用私有 API _CFURLResponse 獲得 CFTypeRef 再轉(zhuǎn)換成 CFHTTPMessageRef,獲取 Status Line。
再將其轉(zhuǎn)換成 NSData 計算字節(jié)大?。?/span>
- (NSUInteger)dm_getLineLength { NSString *lineStr = @''; if ([self isKindOfClass:[NSHTTPURLResponse class]]) { NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)self; lineStr = [self statusLineFromCF]; } NSData *lineData = [lineStr dataUsingEncoding:NSUTF8StringEncoding]; return lineData.length; }
Header
通過 httpResponse.allHeaderFields 拿到 Header 字典,再拼接成報文的 key: value 格式,轉(zhuǎn)換成 NSData 計算大?。?/span>
- (NSUInteger)dm_getHeadersLength { NSUInteger headersLength = 0; if ([self isKindOfClass:[NSHTTPURLResponse class]]) { NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)self; NSDictionaryNSString *, NSString *> *headerFields = httpResponse.allHeaderFields; NSString *headerStr = @''; for (NSString *key in headerFields.allKeys) { headerStr = [headerStr stringByAppendingString:key]; headerStr = [headerStr stringByAppendingString:@': ']; if ([headerFields objectForKey:key]) { headerStr = [headerStr stringByAppendingString:headerFields[key]]; } headerStr = [headerStr stringByAppendingString:@'\n']; } NSData *headerData = [headerStr dataUsingEncoding:NSUTF8StringEncoding]; headersLength = headerData.length; } return headersLength; }
Body
對于 Body 的計算,上文看到有些文章里采用的 expectedContentLength 或者去 NSURLResponse 對象的 allHeaderFields 中獲取 Content-Length 值,其實都不夠準確。
首先 API 文檔中對 expectedContentLength 也有介紹是不準確的:
其次,HTTP 1.1 標準里也有介紹 Content-Length 字段不一定是每個 Response 都帶有的,最重要的是,Content-Length 只是表示 Body 部分的大小。
我的方式是,在前面代碼中有寫到,在 didReceiveData 中對 dm_data 進行了賦值 didReceiveData:
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data { [self.client URLProtocol:self didLoadData:data]; [self.dm_data appendData:data]; }
那么在 stopLoading 方法中,就可以拿到本次網(wǎng)絡請求接收到的數(shù)據(jù)。
但需要注意對 gzip 情況進行區(qū)別分析。我們知道 HTTP 請求中,客戶端在發(fā)送請求的時候會帶上 Accept-Encoding,這個字段的值將會告知服務器客戶端能夠理解的內(nèi)容壓縮算法。而服務器進行相應時,會在 Response 中添加 Content-Encoding 告知客戶端選中的壓縮算法。
所以,我們在 stopLoading 中獲取 Content-Encoding,如果使用了 gzip,則模擬一次 gzip 壓縮,再計算字節(jié)大?。?/span>
- (void)stopLoading { [self.connection cancel]; DMNetworkTrafficLog *model = [[DMNetworkTrafficLog alloc] init]; model.path = self.request.URL.path; model.host = self.request.URL.host; model.type = DMNetworkTrafficDataTypeResponse; model.lineLength = [self.dm_response dm_getLineLength]; model.headerLength = [self.dm_response dm_getHeadersLength]; if ([self.dm_response isKindOfClass:[NSHTTPURLResponse class]]) { NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)self.dm_response; NSData *data = self.dm_data; if ([[httpResponse.allHeaderFields objectForKey:@'Content-Encoding'] isEqualToString:@'gzip']) { // 模擬壓縮 data = [self.dm_data gzippedData]; } model.bodyLength = data.length; } model.length = model.lineLength + model.headerLength + model.bodyLength; [model settingOccurTime]; [[DMDataManager defaultDB] addNetworkTrafficLog:model]; }
這里 gzippedData 參考這個庫的內(nèi)容
[[DMDataManager defaultDB] addNetworkTrafficLog:model]; 是調(diào)用持久化層的代碼將數(shù)據(jù)落庫。
記錄 Resquest 信息
Line
很遺憾,對于NSURLRequest 我沒有像 NSURLReponse 一樣幸運的找到私有接口將其轉(zhuǎn)換成 CFNetwork 相關數(shù)據(jù),但是我們很清楚 HTTP 請求報文 Line 部分的組成,所以我們可以添加一個方法,獲取一個經(jīng)驗 Line。
同樣為 NSURLReques 添加一個擴展:NSURLRequest+DoggerMonitor
- (NSUInteger)dgm_getLineLength { NSString *lineStr = [NSString stringWithFormat:@'%@ %@ %@\n', self.HTTPMethod, self.URL.path, @'HTTP/1.1']; NSData *lineData = [lineStr dataUsingEncoding:NSUTF8StringEncoding]; return lineData.length; }
Header
Header 這里有一個非常大的坑。
request.allHTTPHeaderFields 拿到的頭部數(shù)據(jù)是有很多缺失的,這塊跟業(yè)內(nèi)朋友交流的時候,發(fā)現(xiàn)很多人都沒有留意到這個問題。
缺失的部分不僅僅是上面一篇文章中說到的 Cookie。
如果通過 Charles 抓包,可以看到,會缺失包括但不僅限于以下字段:
Accept Connection Host
這個問題非常的迷,同時由于無法轉(zhuǎn)換到 CFNetwork 層,所以一直拿不到準確的 Header 數(shù)據(jù)。
最后,我在 so 上也找到了兩個相關問題,供大家參考
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 兩個問題的回答基本表明了,如果你是通過 CFNetwork 來發(fā)起請求的,才可以拿到完整的 Header 數(shù)據(jù)。
所以這塊只能拿到大部分的 Header,但是基本上缺失的都固定是那幾個字段,對我們流量統(tǒng)計的精確度影響不是很大。
那么主要就針對 cookie 部分進行補全:
- (NSUInteger)dgm_getHeadersLengthWithCookie { NSUInteger headersLength = 0; NSDictionaryNSString *, NSString *> *headerFields = self.allHTTPHeaderFields; NSDictionaryNSString *, NSString *> *cookiesHeader = [self dgm_getCookies]; // 添加 cookie 信息 if (cookiesHeader.count) { NSMutableDictionary *headerFieldsWithCookies = [NSMutableDictionary dictionaryWithDictionary:headerFields]; [headerFieldsWithCookies addEntriesFromDictionary:cookiesHeader]; headerFields = [headerFieldsWithCookies copy]; } NSLog(@'%@', headerFields); NSString *headerStr = @''; for (NSString *key in headerFields.allKeys) { headerStr = [headerStr stringByAppendingString:key]; headerStr = [headerStr stringByAppendingString:@': ']; if ([headerFields objectForKey:key]) { headerStr = [headerStr stringByAppendingString:headerFields[key]]; } headerStr = [headerStr stringByAppendingString:@'\n']; } NSData *headerData = [headerStr dataUsingEncoding:NSUTF8StringEncoding]; headersLength = headerData.length; return headersLength; }
- (NSDictionaryNSString *, NSString *> *)dgm_getCookies { NSDictionaryNSString *, NSString *> *cookiesHeader; NSHTTPCookieStorage *cookieStorage = [NSHTTPCookieStorage sharedHTTPCookieStorage]; NSArrayNSHTTPCookie *> *cookies = [cookieStorage cookiesForURL:self.URL]; if (cookies.count) { cookiesHeader = [NSHTTPCookie requestHeaderFieldsWithCookies:cookies]; } return cookiesHeader; }
body
最后是 body 部分,這里也有個坑。通過 NSURLConnection 發(fā)出的網(wǎng)絡請求 resquest.HTTPBody 拿到的是 nil。
需要轉(zhuǎn)而通過 HTTPBodyStream 讀取 stream 來獲取 request 的 Body 大小。
- (NSUInteger)dgm_getBodyLength { NSDictionaryNSString *, NSString *> *headerFields = self.allHTTPHeaderFields; NSUInteger bodyLength = [self.HTTPBody length]; if ([headerFields objectForKey:@'Content-Encoding']) { NSData *bodyData; if (self.HTTPBody == nil) { uint8_t d[1024] = {0}; NSInputStream *stream = self.HTTPBodyStream; NSMutableData *data = [[NSMutableData alloc] init]; [stream open]; while ([stream hasBytesAvailable]) { NSInteger len = [stream read:d maxLength:1024]; if (len > 0 && stream.streamError == nil) { [data appendBytes:(void *)d length:len]; } } bodyData = [data copy]; [stream close]; } else { bodyData = self.HTTPBody; } bodyLength = [[bodyData gzippedData] length]; } return bodyLength; }
落庫
最后在 DMURLProtocol 的 - (nullable NSURLRequest *)connection:(NSURLConnection *)connection willSendRequest:(NSURLRequest *)request redirectResponse:(nullable NSURLResponse *)response; 方法中對 resquest 調(diào)用報文各個部分大小方法后落庫:
-(NSURLRequest *)connection:(NSURLConnection *)connection willSendRequest:(NSURLRequest *)request redirectResponse:(NSURLResponse *)response { if (response != nil) { self.dm_response = response; [self.client URLProtocol:self wasRedirectedToRequest:request redirectResponse:response]; } DMNetworkTrafficLog *model = [[DMNetworkTrafficLog alloc] init]; model.path = request.URL.path; model.host = request.URL.host; model.type = DMNetworkTrafficDataTypeRequest; model.lineLength = [connection.currentRequest dgm_getLineLength]; model.headerLength = [connection.currentRequest dgm_getHeadersLengthWithCookie]; model.bodyLength = [connection.currentRequest dgm_getBodyLength]; model.length = model.lineLength + model.headerLength + model.bodyLength; [model settingOccurTime]; [[DMDataManager defaultDB] addNetworkTrafficLog:model]; return request; }
針對 NSURLSession 的處理
直接使用 DMURLProtocol 并 registerClass 并不能完整的攔截所有網(wǎng)絡請求,因為通過 NSURLSession 的 sharedSession 發(fā)出的請求是無法被 NSURLProtocol 代理的。
我們需要讓 [NSURLSessionConfiguration defaultSessionConfiguration].protocolClasses 的屬性中也設置我們的 DMURLProtocol,這里通過 swizzle,置換 protocalClasses 的 get 方法:
編寫一個 DMURLSessionConfiguration
#import @interface DMURLSessionConfiguration : NSObject @property (nonatomic,assign) BOOL isSwizzle; + (DMURLSessionConfiguration *)defaultConfiguration; - (void)load; - (void)unload; @end
#import 'DMURLSessionConfiguration.h' #import #import 'DMURLProtocol.h' #import 'DMNetworkTrafficManager.h' @implementation DMURLSessionConfiguration + (DMURLSessionConfiguration *)defaultConfiguration { static DMURLSessionConfiguration *staticConfiguration; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ staticConfiguration=[[DMURLSessionConfiguration alloc] init]; }); return staticConfiguration; } - (instancetype)init { self = [super init]; if (self) { self.isSwizzle = NO; } return self; } - (void)load { self.isSwizzle = YES; Class cls = NSClassFromString(@'__NSCFURLSessionConfiguration') ?: NSClassFromString(@'NSURLSessionConfiguration'); [self swizzleSelector:@selector(protocolClasses) fromClass:cls toClass:[self class]]; } - (void)unload { self.isSwizzle=NO; Class cls = NSClassFromString(@'__NSCFURLSessionConfiguration') ?: NSClassFromString(@'NSURLSessionConfiguration'); [self swizzleSelector:@selector(protocolClasses) fromClass:cls toClass:[self class]]; } - (void)swizzleSelector:(SEL)selector fromClass:(Class)original toClass:(Class)stub { Method originalMethod = class_getInstanceMethod(original, selector); Method stubMethod = class_getInstanceMethod(stub, selector); if (!originalMethod || !stubMethod) { [NSException raise:NSInternalInconsistencyException format:@'Couldn't load NEURLSessionConfiguration.']; } method_exchangeImplementations(originalMethod, stubMethod); } - (NSArray *)protocolClasses { // DMNetworkTrafficManager 中的 protocolClasses 可以給使用者設置自定義的 protocolClasses return [DMNetworkTrafficManager manager].protocolClasses; } @end
這樣,我們寫好了方法置換,在執(zhí)行過該類單例的 load 方法后,[NSURLSessionConfiguration defaultSessionConfiguration].protocolClasses 拿到的將會是我們設置好的 protocolClasses。
如此,我們再為 DMURLProtocol 添加 start 和 stop 方法,用于啟動網(wǎng)絡監(jiān)控和停止網(wǎng)絡監(jiān)控:
+ (void)start { DMURLSessionConfiguration *sessionConfiguration = [DMURLSessionConfiguration defaultConfiguration]; for (id protocolClass in [DMNetworkTrafficManager manager].protocolClasses) { [NSURLProtocol registerClass:protocolClass]; } if (![sessionConfiguration isSwizzle]) { // 設置交換 [sessionConfiguration load]; } } + (void)end { DMURLSessionConfiguration *sessionConfiguration = [DMURLSessionConfiguration defaultConfiguration]; [NSURLProtocol unregisterClass:[DMURLProtocol class]]; if ([sessionConfiguration isSwizzle]) { // 取消交換 [sessionConfiguration unload]; } }
到此,基本完成了整個網(wǎng)絡流量監(jiān)控。 再提供一個 Manger 方便使用者調(diào)用:
#import @class DMNetworkLog; @interface DMNetworkTrafficManager : NSObject /** 所有 NSURLProtocol 對外設置接口,可以防止其他外來監(jiān)控 NSURLProtocol */ @property (nonatomic, strong) NSArray *protocolClasses; /** 單例 */ + (DMNetworkTrafficManager *)manager; /** 通過 protocolClasses 啟動流量監(jiān)控模塊 */ + (void)startWithProtocolClasses:(NSArray *)protocolClasses; /** 僅以 DMURLProtocol 啟動流量監(jiān)控模塊 */ + (void)start; /** 停止流量監(jiān)控 */ + (void)end; @end
#import 'DMNetworkTrafficManager.h' #import 'DMURLProtocol.h' @interface DMNetworkTrafficManager () @end @implementation DMNetworkTrafficManager #pragma mark - Public + (DMNetworkTrafficManager *)manager { static DMNetworkTrafficManager *manager; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ manager=[[DMNetworkTrafficManager alloc] init]; }); return manager; } + (void)startWithProtocolClasses:(NSArray *)protocolClasses { [self manager].protocolClasses = protocolClasses; [DMURLProtocol start]; } + (void)start { [self manager].protocolClasses = @[[DMURLProtocol class]]; [DMURLProtocol start]; } + (void)end { [DMURLProtocol end]; } @end
五、代碼
本文中貼出了比較多的代碼,為了便于大家整體觀看,可以到 這里 來閱讀。 由于其中包含了一些數(shù)據(jù)操作的內(nèi)容不需要關心,所以我直接省略了,雖然沒有 Demo,但我相信大家都是能理解整個監(jiān)控結構的。
六、Other
如果你的 APP 從 iOS 9 支持,可以使用 NetworkExtension,通過 NetworkExtension 可以通過 VPN 的形式接管整個網(wǎng)絡請求,省掉了上面所有的煩惱。
|