iOS 流量監控分析

餓了麼物流技術團隊發表於2018-06-07

個人部落格連結

由於騎手不能隨時處在有 WIFI 的狀態,流量變成了很敏感的問題,為了精確到每個 API 的流量,進行鍼對性的優化,開始在我們的 APM 中新增流量監控功能。

本文將記錄自己做流量監控方面的總結。其中包括了非常多的踩坑經驗,和現有一些方案的缺陷分析,對我來說是一個非常有意義的過程。

乾貨預警?請做好讀大量程式碼的準備???

一、資料收集

就目前來說,各家大廠基本都有自己的 APM(包括我們公司其實之前也有一套 APM,但是由於各個事業部的需求不同,尚不能完全滿足物流平臺的需要)但各家大廠目前開源的 APM 專案卻不多,當然也可能是由於各家的業務場景差異比較大且對資料的後續處理不同。

所以本次在查閱資料階段,沒有太多的原始碼可選參考,但有不少文章。

以下是一些本次開發過程中參考的文章和開源庫:

  1. iOS-Monitor-Platform
  2. GodEye
  3. NetworkEye
  4. 移動端效能監控方案Hertz
  5. 使用NSURLProtocol注意的一些問題
  6. iOS 開發中使用 NSURLProtocol 攔截 HTTP 請求
  7. 獲取NSURLResponse的HTTPVersion

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

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

在實際的網路請求中 Request 和 Response 不一定是成對的,如果網路斷開、或者突然關閉程式,都會導致不成對現象,如果將 Request 和 Response 記錄在同一條資料,將會對統計造成偏差

2. 上行流量記錄不精準

主要的原因有三大類:

  1. 直接忽略了 Header 和 Line 部分
  2. 忽略了 Cookie 部分,實際上,臃腫的 Cookie 也是消耗流量的一部分
  3. body 部分的位元組大小計算直接使用了 HTTPBody.length 不夠準確

3. 下行流量記錄不精準

主要原因有:

  1. 直接忽略了 Header 和 Status-Line 部分
  2. body 部分的位元組大小計算直接使用了 expectedContentLength 不夠準確
  3. 忽略了 gzip 壓縮,在實際網路程式設計中,往往都使用了 gzip 來進行資料壓縮,而系統提供的一些監聽方法,返回的 NSData 實際是解壓過的,如果直接統計位元組數會造成大量偏差

後文將詳細講述。

二、需求

先簡單羅列我們的需求:

  1. Request 基本資訊記錄
  2. 上行流量
  3. Reponse 基本資訊記錄
  4. 下行流量
  5. 資料歸類:按照 host 和 path 歸類,一條記錄記載改 host/path 的 Request 記錄數Response 記錄數Reqeust 總流量(上行流量)Reponse 總流量(下行流量)

我們的側重點是流量統計,為了方便分析 APP 使用中哪些 API 消耗流量多。所以對上行、下行流量都需要儘量準確記錄。

最終的資料庫表展示:

iOS 流量監控分析

type 欄位表示的是『該條記錄是 Request 還是 Response』,幾個 length 分別記錄了流量的各個細節,包括:總位元組數、Line 位元組數、Header 位元組數、Body 位元組數。

最後的介面展示類似於:

iOS 流量監控分析

三、分析現有資料

現在分析一下上面收集到的資料有哪些不足之處。

GodEye | NetworkEye

NetworkEye 是 GodEye 的一部分,可以單獨拆出來使用的網路監控庫。

查閱兩者的原始碼後發現,NetworkEye

  1. 僅僅記錄了 Reponse 的流量
  2. 通過 expectedContentLength 記錄是不準確的(後面將會說到)
  3. 僅僅記錄了總和,這對我們來說是無意義的,不能分析出哪條 API 流量使用多

iOS 流量監控分析

移動端效能監控方案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 中記錄了整個網路請求結束的時間、 response 資料大小、request 資料大小以及一些其他資料。

總體來說是比較詳細的,但是這裡並沒有給出 self.responseDataLength 的具體邏輯,另外 connection.originalRequest.HTTPBody.length 僅僅是 Request body 的大小。

iOS-Monitor-Platform

這篇文章比較詳細的介紹了整個 APM 製作的過程,貼出了很多程式碼段,應該說非常詳細也極具參考價值。

在流量部分,也分別針對了上行流量、下行流量進行了區分,但其中:

  1. 沒有處理 gzip 壓縮情況
  2. 對 Header 計算大小的方式是 Dictionary 轉 NSData,然而實際上頭部並不是 Json 格式(這塊我覺得很迷,因為作者特意展示了 HTTP 報文組成)

四、動手自己做

HTTP 報文

為了更好的讓大家瞭解 HTTP 流量計算的一些關鍵資訊,首先要了解 HTTP 報文的組成。

iOS 流量監控分析

再來隨便抓個包具體看看:

iOS 流量監控分析

iOS 流量監控分析

iOS 下的網路監控

這塊我採用的大家耳熟能詳的 NSURLProtocolNSURLProtocol 方式除了通過 CFNetwork 發出的網路請求,全部都可以攔截到。

Apple 文件中對 NSURLProtocol 有非常詳細的描述和使用介紹

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

如果想更詳細的瞭解 NSURLProtocol,也可以看大佐的這篇文章

在每一個 HTTP 請求開始時,URL 載入系統建立一個合適的 NSURLProtocol 物件處理對應的 URL 請求,而我們需要做的就是寫一個繼承自 NSURLProtocol 的類,並通過 - registerClass: 方法註冊我們的協議類,然後 URL 載入系統就會在請求發出時使用我們建立的協議物件對該請求進行處理。

NSURLProtocol 是一個抽象類,需要做的第一步就是整合它,完成我們的自定義設定。

建立自己的 DMURLProtocol,為它新增幾個屬性並實現相關介面:

@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 資訊

前面的程式碼實現了在網路請求過程中為 dm_responsedm_data 賦值,那麼在 stopLoading 方法中,就可以分析 dm_responsedm_data 物件,獲取下行流量等相關資訊。

需要說明的是,如果需要獲得非常精準的流量,一般來說只有通過 Socket 層獲取是最準確的,因為可以獲取包括握手、揮手的資料大小。當然,我們的目的是為了分析 App 的耗流量 API,所以僅從應用層去分析也基本滿足了我們的需要。

上文中說到了報文的組成,那麼按照報文所需要的內容獲取。

Status Line

非常遺憾的是 NSURLResponse 沒有介面能直接獲取報文中的 Status Line,甚至連 HTTP Version 等組成 Status Line 內容的介面也沒有。

最後,我通過轉換到 CFNetwork 相關類,才拿到了 Status Line 的資料,這其中可能涉及到了讀取私有 API

這裡我為 NSURLResponse 新增了一個擴充套件:NSURLResponse+DoggerMonitor,併為其新增 statusLineFromCF 方法

typedef CFHTTPMessageRef (*DMURLResponseGetHTTPResponse)(CFURLRef response);

- (NSString *)statusLineFromCF {
    NSURLResponse *response = self;
    NSString *statusLine = @"";
    // 獲取CFURLResponseGetHTTPResponse的函式實現
    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轉化為CFHTTPMessageRef
            CFHTTPMessageRef messageRef = originURLResponseGetHTTPResponse(cfResponse);
            statusLine = (__bridge_transfer NSString *)CFHTTPMessageCopyResponseStatusLine(messageRef);
            CFRelease(cfResponse);
        }
    }
    return statusLine;
}
複製程式碼

通過呼叫私有 API _CFURLResponse 獲得 CFTypeRef 再轉換成 CFHTTPMessageRef,獲取 Status Line。

再將其轉換成 NSData 計算位元組大小:

- (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 格式,轉換成 NSData 計算大小:

- (NSUInteger)dm_getHeadersLength {
    NSUInteger headersLength = 0;
    if ([self isKindOfClass:[NSHTTPURLResponse class]]) {
        NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)self;
        NSDictionary<NSString *, 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 也有介紹是不準確的:

iOS 流量監控分析

其次,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 方法中,就可以拿到本次網路請求接收到的資料。

但需要注意對 gzip 情況進行區別分析。我們知道 HTTP 請求中,客戶端在傳送請求的時候會帶上 Accept-Encoding,這個欄位的值將會告知伺服器客戶端能夠理解的內容壓縮演算法。而伺服器進行相應時,會在 Response 中新增 Content-Encoding 告知客戶端選中的壓縮演算法

所以,我們在 stopLoading 中獲取 Content-Encoding,如果使用了 gzip,則模擬一次 gzip 壓縮,再計算位元組大小:

- (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 參考這個庫的內容

[[DMDataManager defaultDB] addNetworkTrafficLog:model]; 是呼叫持久化層的程式碼將資料落庫。

記錄 Resquest 資訊

Line

很遺憾,對於NSURLRequest 我沒有像 NSURLReponse 一樣幸運的找到私有介面將其轉換成 CFNetwork 相關資料,但是我們很清楚 HTTP 請求報文 Line 部分的組成,所以我們可以新增一個方法,獲取一個經驗 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 拿到的頭部資料是有很多缺失的,這塊跟業內朋友交流的時候,發現很多人都沒有留意到這個問題。

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

如果通過 Charles 抓包,可以看到,會缺失包括但不僅限於以下欄位:

  1. Accept
  2. Connection
  3. Host

這個問題非常的迷,同時由於無法轉換到 CFNetwork 層,所以一直拿不到準確的 Header 資料。

最後,我在 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 來發起請求的,才可以拿到完整的 Header 資料。

所以這塊只能拿到大部分的 Header,但是基本上缺失的都固定是那幾個欄位,對我們流量統計的精確度影響不是很大。

那麼主要就針對 cookie 部分進行補全:

- (NSUInteger)dgm_getHeadersLengthWithCookie {
    NSUInteger headersLength = 0;

    NSDictionary<NSString *, NSString *> *headerFields = self.allHTTPHeaderFields;
    NSDictionary<NSString *, 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;
}
複製程式碼
- (NSDictionary<NSString *, NSString *> *)dgm_getCookies {
    NSDictionary<NSString *, NSString *> *cookiesHeader;
    NSHTTPCookieStorage *cookieStorage = [NSHTTPCookieStorage sharedHTTPCookieStorage];
    NSArray<NSHTTPCookie *> *cookies = [cookieStorage cookiesForURL:self.URL];
    if (cookies.count) {
        cookiesHeader = [NSHTTPCookie requestHeaderFieldsWithCookies:cookies];
    }
    return cookiesHeader;
}
複製程式碼

body

最後是 body 部分,這裡也有個坑。通過 NSURLConnection 發出的網路請求 resquest.HTTPBody 拿到的是 nil。

需要轉而通過 HTTPBodyStream 讀取 stream 來獲取 request 的 Body 大小。

- (NSUInteger)dgm_getBodyLength {
    NSDictionary<NSString *, 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 呼叫報文各個部分大小方法後落庫:

-(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 的處理

直接使用 DMURLProtocolregisterClass 並不能完整的攔截所有網路請求,因為通過 NSURLSessionsharedSession 發出的請求是無法被 NSURLProtocol 代理的。

我們需要讓 [NSURLSessionConfiguration defaultSessionConfiguration].protocolClasses 的屬性中也設定我們的 DMURLProtocol,這裡通過 swizzle,置換 protocalClasses 的 get 方法:

編寫一個 DMURLSessionConfiguration

#import <Foundation/Foundation.h>

@interface DMURLSessionConfiguration : NSObject

@property (nonatomic,assign) BOOL isSwizzle;
+ (DMURLSessionConfiguration *)defaultConfiguration;
- (void)load;
- (void)unload;

@end
複製程式碼
#import "DMURLSessionConfiguration.h"
#import <objc/runtime.h>
#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
複製程式碼

這樣,我們寫好了方法置換,在執行過該類單例的 load 方法後,[NSURLSessionConfiguration defaultSessionConfiguration].protocolClasses 拿到的將會是我們設定好的 protocolClasses

如此,我們再為 DMURLProtocol 新增 startstop 方法,用於啟動網路監控和停止網路監控:

+ (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];
    }
}
複製程式碼

到此,基本完成了整個網路流量監控。

再提供一個 Manger 方便使用者呼叫:

#import <Foundation/Foundation.h>

@class DMNetworkLog;
@interface DMNetworkTrafficManager : NSObject

/** 所有 NSURLProtocol 對外設定介面,可以防止其他外來監控 NSURLProtocol */
@property (nonatomic, strong) NSArray *protocolClasses;


/** 單例 */
+ (DMNetworkTrafficManager *)manager;

/** 通過 protocolClasses 啟動流量監控模組 */
+ (void)startWithProtocolClasses:(NSArray *)protocolClasses;
/** 僅以 DMURLProtocol 啟動流量監控模組 */
+ (void)start;
/** 停止流量監控 */
+ (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
複製程式碼

五、程式碼

本文中貼出了比較多的程式碼,為了便於大家整體觀看,可以到 這裡 來閱讀。

由於其中包含了一些資料操作的內容不需要關心,所以我直接省略了,雖然沒有 Demo,但我相信大家都是能理解整個監控結構的。

六、Other

如果你的 APP 從 iOS 9 支援,可以使用 NetworkExtension,通過 NetworkExtension 可以通過 VPN 的形式接管整個網路請求,省掉了上面所有的煩惱。


有什麼問題都可以在博文後面留言,或者微博上私信我,或者郵件我 coderfish@163.com

博主是 iOS 妹子一枚。

希望大家一起進步。

我的微博:小魚周凌宇

相關文章