目標(biāo)
- TCP建立連接時間
- DNS時間
- SSL/TLS時間
- 響應(yīng)總時間
- 請求頭、請求body、響應(yīng)頭、響應(yīng)body大小
- 支持統(tǒng)計原生網(wǎng)絡(luò)請求、React Native網(wǎng)絡(luò)請求
- 代碼無侵害
方案對比
方案一:通過 NSURLProtocol
來實現(xiàn)
通過向 NSURLProtocol
注冊自定義的 NSURLProtocol
子類愈涩,比如是 TESTURLProtocol
,然后每個由 NSURLConnection
或 NSURLSession
發(fā)起請求都會訪問 TESTURLProtocol
加矛。
注冊方式
[NSURLProtocol registerClass:[TESTURLProtocol class]]
問題一:如果 NSURLSession 使用下面兩個方法創(chuàng)建的履婉,只注冊是攔截不到的。
+ (NSURLSession *)sessionWithConfiguration:(NSURLSessionConfiguration *)configuration;
+ (NSURLSession *)sessionWithConfiguration:(NSURLSessionConfiguration *)configuration
delegate:(nullable id <NSURLSessionDelegate>)delegate
delegateQueue:(nullable NSOperationQueue *)queue;
需要把TESTURLProtocol
添加到 NSURLSessionConfiguration.protocolClasses
中斟览,可以通過 hook 來實現(xiàn)毁腿。
問題二:POST 請求 body 丟失問題。
在 TESTURLProtocol
中,使用 HTTPBodyStream
獲取 body已烤,并賦值到 body 中
- (void)startLoading {
//緩存下來
TestModel.request = [self.request cyl_getPostRequestIncludeBody];
}
@interface NSURLRequest (CYLNSURLProtocolExtension)
- (NSURLRequest *)cyl_getPostRequestIncludeBody;
@end
@implementation NSURLRequest (CYLNSURLProtocolExtension)
- (NSURLRequest *)cyl_getPostRequestIncludeBody {
return [[self cyl_getMutablePostRequestIncludeBody] copy];
}
- (NSMutableURLRequest *)cyl_getMutablePostRequestIncludeBody {
NSMutableURLRequest * req = [self mutableCopy];
if ([self.HTTPMethod isEqualToString:@"POST"]) {
if (!self.HTTPBody) {
NSInteger maxLength = 1024;
uint8_t d[maxLength];
NSInputStream *stream = self.HTTPBodyStream;
NSMutableData *data = [[NSMutableData alloc] init];
[stream open];
BOOL endOfStreamReached = NO;
//不能用 [stream hasBytesAvailable]) 判斷鸠窗,處理圖片文件的時候這里的[stream hasBytesAvailable]會始終返回YES,導(dǎo)致在while里面死循環(huán)胯究。
while (!endOfStreamReached) {
NSInteger bytesRead = [stream read:d maxLength:maxLength];
if (bytesRead == 0) { //文件讀取到最后
endOfStreamReached = YES;
} else if (bytesRead == -1) { //文件讀取錯誤
endOfStreamReached = YES;
} else if (stream.streamError == nil) {
[data appendBytes:(void *)d length:bytesRead];
}
}
req.HTTPBody = [data copy];
[stream close];
}
}
return req;
}
@end
參考方案:NSURLProtocol 攔截 NSURLSession 請求時body丟失問題解決方案探討
問題三:NSURLProtocol
可以攔截 UIWebView
的請求稍计,無法攔截 WKWebView
。
可以通過修改WKWebView的私有方法來實現(xiàn)裕循,注意如果提交 AppStore 需要加密處理
//注冊scheme
Class cls = NSClassFromString(@"WKBrowsingContextController");
SEL sel = NSSelectorFromString(@"registerSchemeForCustomProtocol:");
if ([cls respondsToSelector:sel]) {
// 通過http和https的請求臣嚣,同理可通過其他的Scheme 但是要滿足ULR Loading System
[cls performSelector:sel withObject:@"http"];
[cls performSelector:sel withObject:@"https"];
}
參考方案:NSURLProtocol對WKWebView的處理
NSURLProtocol 實現(xiàn)方案可參考 NetworkEye 可以監(jiān)聽 NSURLConnection
和 NSURLSession
請求,處理了問題意一和問題二费韭。
小結(jié)
優(yōu)點(diǎn):
- 可以比較輕松的獲取請求頭茧球、請求body庭瑰、響應(yīng)頭星持、響應(yīng)body、請求總時長(從開始請求到請求結(jié)束時長)弹灭;
- 沒有版本限制督暂。
缺點(diǎn):
- 時間方面沒法獲取各個階段的時長,比如穷吮,DNS時長逻翁、TCP時長、SSL時長等捡鱼;
- 流量方面需要自己計算大小八回,比如響應(yīng)body很多時候會有壓縮,所以在計算的時候需要模擬壓縮驾诈,得到的結(jié)果還是有誤差的缠诅;
- 需要創(chuàng)建新的請求來路由接口,對業(yè)務(wù)有一定的破壞性(最難接受的)乍迄。
方案二:通過監(jiān)聽 URLSession:task:didFinishCollectingMetrics:
中的 NSURLSessionTaskMetrics
來實現(xiàn)
通過 hook NSURLSession
的方法 sessionWithConfiguration:delegate:delegateQueue:
<NSURLSessionDelegate>)delegate
使用 NSProxy
來轉(zhuǎn)發(fā)管引,這樣就可以監(jiān)聽 URLSession:task:didFinishCollectingMetrics:
方法。
問題一:需要 iOS10 之后才能使用闯两,流量相關(guān)的統(tǒng)計需要 iOS13 之后才能使用
問題二:無法攔截通過 sharedSession
獲取 NSURLSession
的實例
問題三:同樣也有 POST 請求 body 丟失問題褥伴,以及 WKWebView
無法攔截問題, 實現(xiàn)方式跟上面類似
小結(jié)
優(yōu)點(diǎn):通過 NSURLSessionTaskMetrics
可以獲取很方便的獲取各個階段的請求耗時漾狼,以及流量的使用請求重慢。
缺點(diǎn):主要是問題一和問題二帶來的。
最終方案
在實際業(yè)務(wù)開發(fā)中逊躁,網(wǎng)絡(luò)請求的方式主要是 NSURLSession
似踱、NSURLConnection
和 AFNetworking
;AFNetworking
是基于 NSURLSession
和 NSURLConnection
實現(xiàn)的;業(yè)務(wù)如果是 React Native 來開發(fā)屯援,底層的網(wǎng)絡(luò)請求也是基于 NSURLSession
來實現(xiàn)的猛们。
另外,NSURLConnection
iOS9之后就被蘋果棄用了狞洋。
接下來弯淘,我們再看一下官方給的當(dāng)前系統(tǒng)的占有率。
只有 8% 的設(shè)備是低于 iOS13 的吉懊。
攔截目標(biāo):主要考慮 NSURLSession
的攔截庐橙;考慮到實際業(yè)務(wù)用很使用 AFNetworking
的 NSURLConnection
的請求,也還是要考慮 NSURLConnection
的攔截借嗽。
最終方案:以方案二為主态鳖,覆蓋不到的地方使用方案一來補(bǔ)充。
方案二攔截不到有兩種請求
- NSURLSession 通過 sharedSession 獲取的對象
- NSURLConnection 發(fā)起的請求恶导,雖然 iOS9 之后就已經(jīng)拋棄了浆竭,但是 AFNetworking 有部分是基于 NSURLConnection,而我們很多老代碼就是通過 NSURLConnection 發(fā)起的請求
所以針對這個兩種情況通過方案一的 NSURLProtocol 來實現(xiàn)惨寿,內(nèi)部生成 NSURLSession 實例邦泄,路由到方案二。
// 方案一裂垦,核心代碼邏輯顺囊,目的是路由接口至方案二上
@interface MTNMURLProtocol ()<NSURLSessionDataDelegate>
@property (nonatomic, strong) NSURLSession *session;
@end
@implementation MTNMURLProtocol
+ (void)install
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[NSURLProtocol registerClass:[MTNMURLProtocol class]];
});
}
+ (BOOL)canInitWithRequest:(NSURLRequest *)request
{
if ([MTNMDataManager.shareManager isWhitelistURL:request.URL])
{
return NO;
}
if (![request.URL.scheme isEqualToString:@"http"] &&
![request.URL.scheme isEqualToString:@"https"])
{
return NO;
}
if ([NSURLProtocol propertyForKey:@"MTNMURLProtocol" inRequest:request])
{
return NO;
}
return YES;
}
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request
{
NSMutableURLRequest *mutableReqeust = [request mutableCopy];
[NSURLProtocol setProperty:@YES forKey:@"MTNMURLProtocol" inRequest:mutableReqeust];
return [mutableReqeust copy];
}
- (NSURLSession *)session
{
if (!_session)
{
_session = [NSURLSession sessionWithConfiguration:NSURLSessionConfiguration.defaultSessionConfiguration delegate:self delegateQueue:nil];
}
return _session;
}
- (void)startLoading
{
[[self.session dataTaskWithRequest:self.request] resume];
}
- (void)stopLoading
{
[self.session getTasksWithCompletionHandler:^(NSArray<NSURLSessionDataTask *> * _Nonnull dataTasks, NSArray<NSURLSessionUploadTask *> * _Nonnull uploadTasks, NSArray<NSURLSessionDownloadTask *> * _Nonnull downloadTasks) {
for (NSURLSessionDataTask *task in dataTasks)
{
[task cancel];
}
}];
dispatch_async(dispatch_get_main_queue(), ^{
[self.session finishTasksAndInvalidate];
});
}
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data
{
[self.client URLProtocol:self didLoadData:data];
}
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler
{
[self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageAllowed];
if (completionHandler)
{
completionHandler(NSURLSessionResponseAllow);
}
}
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(nullable NSError *)error
{
if (error)
{
[self.client URLProtocol:self didFailWithError:error];
}
else
{
[self.client URLProtocolDidFinishLoading:self];
}
}
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task willPerformHTTPRedirection:(NSHTTPURLResponse *)response newRequest:(NSURLRequest *)request completionHandler:(void (^)(NSURLRequest * _Nullable))completionHandler
{
[self.client URLProtocol:self wasRedirectedToRequest:request redirectResponse:response];
if (completionHandler)
{
completionHandler(request);
}
}
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential * _Nullable))completionHandler
{
id<NSURLAuthenticationChallengeSender> sender = [MTNMURLAuthenticationChallengeSender senderWithCompletionHandler:completionHandler];
NSURLAuthenticationChallenge *wrappedChallenge = [[NSURLAuthenticationChallenge alloc] initWithAuthenticationChallenge:challenge sender:sender];
[self.client URLProtocol:self didReceiveAuthenticationChallenge:wrappedChallenge];
}
- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session
{
[self.client URLProtocolDidFinishLoading:self];
}
@end
// 方案二, 核心代碼邏輯
@interface MTNMURLSessionMetricsProxy : NSProxy<NSURLSessionDelegate, NSURLSessionTaskDelegate, NSURLSessionDataDelegate>
@property (nonatomic, strong) id target;
- (instancetype)initWithTarget:(id)target;
@end
@implementation MTNMURLSessionMetricsProxy
- (instancetype)initWithTarget:(id)target
{
self.target = target;
return self;
}
- (void)dealloc
{
if (_target)
{
_target = nil;
}
}
- (BOOL)respondsToSelector:(SEL)aSelector
{
if ([NSStringFromSelector(aSelector) isEqualToString:@"URLSession:task:didFinishCollectingMetrics:"] ||
[NSStringFromSelector(aSelector) isEqualToString:@"URLSession:dataTask:didReceiveResponse:completionHandler:"] ||
[NSStringFromSelector(aSelector) isEqualToString:@"URLSession:dataTask:didReceiveData:"] ||
[NSStringFromSelector(aSelector) isEqualToString:@"URLSession:task:didCompleteWithError:"])
{
return YES;
}
return [self.target respondsToSelector:aSelector];
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector
{
if (!self.target)
{
return [NSMethodSignature signatureWithObjCTypes:"v@"];
}
return [self.target methodSignatureForSelector:selector];
}
- (void)forwardInvocation:(NSInvocation *)invocation
{
if (!self.target)
{
return;
}
if ([self.target respondsToSelector:invocation.selector])
{
[invocation invokeWithTarget:self.target];
}
}
// dataTaskWithRequest: 和 dataTaskWithURL: 發(fā)起的請求通過 delegate 回調(diào)
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler
{
session.mt_responseBodyMutableData = [NSMutableData data];
if ([self.target respondsToSelector:@selector(URLSession:dataTask:didReceiveResponse:completionHandler:)])
{
[self.target URLSession:session dataTask:dataTask didReceiveResponse:response completionHandler:completionHandler];
}
else
{
if (completionHandler)
{
completionHandler(NSURLSessionResponseAllow);
}
}
}
// dataTaskWithRequest: 和 dataTaskWithURL: 發(fā)起的請求通過 delegate 回調(diào)
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data
{
if ([self.target respondsToSelector:@selector(URLSession:dataTask:didReceiveData:)])
{
[self.target URLSession:session dataTask:dataTask didReceiveData:data];
}
if (!session.mt_didAddData)
{
if (!session.mt_requestBodyData)
{
session.mt_requestBodyData = [self reqeustBobyForRequest:dataTask.originalRequest];
}
if (data)
{
[session.mt_responseBodyMutableData appendData:data];
}
}
}
// dataTaskWithRequest: 和 dataTaskWithURL: 發(fā)起的請求通過 delegate 回調(diào)
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(nullable NSError *)error
{
if ([self.target respondsToSelector:@selector(URLSession:task:didCompleteWithError:)])
{
[self.target URLSession:session task:task didCompleteWithError:error];
}
if (!session.mt_didAddData && error)
{
session.mt_error = error;
[session mt_checkAddRecordData];
}
}
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didFinishCollectingMetrics:(NSURLSessionTaskMetrics *)metrics
{
if ([self.target respondsToSelector:@selector(URLSession:task:didFinishCollectingMetrics:)])
{
[self.target URLSession:session task:task didFinishCollectingMetrics:metrics];
}
if (![MTNMDataManager.shareManager isWhitelistURL:task.originalRequest.URL])
{
for (NSURLSessionTaskTransactionMetrics *transMetric in metrics.transactionMetrics) {
if (transMetric.resourceFetchType == NSURLSessionTaskMetricsResourceFetchTypeNetworkLoad)
{
if (!session.mt_didAddData)
{
if (!session.mt_requestBodyData)
{
session.mt_requestBodyData = [self reqeustBobyForRequest:task.originalRequest];
}
session.mt_metrics = transMetric;
[session mt_checkAddRecordData];
}
}
}
}
}
- (NSData *)reqeustBobyForRequest:(NSURLRequest *)request
{
NSMutableData *data;
if ([request.HTTPMethod isEqualToString:@"POST"] && !request.HTTPBody)
{
NSInteger maxLength = 1024;
uint8_t d[maxLength];
NSInputStream *stream = request.HTTPBodyStream;
data = [[NSMutableData alloc] init];
[stream open];
BOOL endOfStreamReached = NO;
//不能用 [stream hasBytesAvailable]) 判斷,處理圖片文件的時候這里的[stream hasBytesAvailable]會始終返回YES蕉拢,導(dǎo)致在while里面死循環(huán)特碳。
while (!endOfStreamReached)
{
NSInteger bytesRead = [stream read:d maxLength:maxLength];
if (bytesRead == 0)
{ //文件讀取到最后
endOfStreamReached = YES;
} else if (bytesRead == -1)
{ //文件讀取錯誤
endOfStreamReached = YES;
} else if (stream.streamError == nil)
{
[data appendBytes:(void *)d length:bytesRead];
}
}
[stream close];
}
return request.HTTPBody ?: data;
}
@end
應(yīng)用時遇到時問題及解決
問題一:在 MTNMURLSessionMetricsProxy
攔截 NSURLSession
請求時,需要獲取相應(yīng)body晕换。
相應(yīng)body的獲取是有必要的午乓,不僅用于 iOS13 以下的接口統(tǒng)計,也可以用于本地網(wǎng)絡(luò)記錄查看届巩,方便排查問題硅瞧。
NSURLSession
的回調(diào)一般有兩種方式, delegate
和 block
的方式恕汇。
delegate
只需要在 NSProxy
子類中攔截即可
// dataTaskWithRequest: 和 dataTaskWithURL: 發(fā)起的請求通過 delegate 回調(diào)
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler
{
session.mt_responseBodyMutableData = [NSMutableData data];
if ([self.target respondsToSelector:@selector(URLSession:dataTask:didReceiveResponse:completionHandler:)])
{
[self.target URLSession:session dataTask:dataTask didReceiveResponse:response completionHandler:completionHandler];
}
else
{
if (completionHandler)
{
completionHandler(NSURLSessionResponseAllow);
}
}
}
// dataTaskWithRequest: 和 dataTaskWithURL: 發(fā)起的請求通過 delegate 回調(diào)
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data
{
if ([self.target respondsToSelector:@selector(URLSession:dataTask:didReceiveData:)])
{
[self.target URLSession:session dataTask:dataTask didReceiveData:data];
}
if (!session.mt_didAddData)
{
if (!session.mt_requestBodyData)
{
session.mt_requestBodyData = [self reqeustBobyForRequest:dataTask.originalRequest];
}
if (data)
{
[session.mt_responseBodyMutableData appendData:data];
}
}
}
blcok
的方式就復(fù)雜一點(diǎn)腕唧,需要 hook
掉 dataTaskWithRequest:completionHandler:
和 dataTaskWithURL:completionHandler:
來攔截獲取。
@implementation NSURLSession (MTNetworkMonitor)
- (NSURLSessionDataTask *)hook_dataTaskWithURL:(NSURL *)url completionHandler:(void (^)(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error))completionHandler
{
return [self hook_dataTaskWtihURL:url request:nil fromURL:YES completionHandler:completionHandler];
}
- (NSURLSessionDataTask *)hook_dataTaskWithRequest:(NSURLRequest *)request completionHandler:(void (^)(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error))completionHandler
{
return [self hook_dataTaskWtihURL:nil request:request fromURL:NO completionHandler:completionHandler];
}
- (NSURLSessionDataTask *)hook_dataTaskWtihURL:(NSURL *)url request:(NSURLRequest *)request fromURL:(BOOL)fromURL completionHandler:(void (^)(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error))completionHandler
{
[self addIfNotGuid];
void(^hookCompletionHandler)(NSData * _Nullable, NSURLResponse * _Nullable, NSError * _Nullable) = ^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error){
if (![MTNMDataManager.shareManager isWhitelistURL:url])
{
if (!self.mt_didAddData)
{
self.mt_error = error;
self.mt_responseBodyMutableData = data.mutableCopy;
[self mt_checkAddRecordData];
}
}
if (completionHandler) {
completionHandler(data, response, error);
}
};
void(^completion)(NSData * _Nullable, NSURLResponse * _Nullable, NSError * _Nullable) = completionHandler ? hookCompletionHandler : completionHandler;
if (fromURL)
{
return [self hook_dataTaskWithURL:url completionHandler:completion];
}
else
{
return [self hook_dataTaskWithRequest:request completionHandler:completion];
}
}
@end
問題二:NSURLSession
的 delegate
是強(qiáng)引用瘾英,會導(dǎo)致內(nèi)存泄漏枣接。
需要在合適的時機(jī)主動調(diào)用 finishTasksAndInvalidate
來釋放 delegate
對象。
問題三:在iOS14.0和iOS14.0.1系統(tǒng)上閃退缺谴,原因是獲取網(wǎng)絡(luò)類型導(dǎo)致的
網(wǎng)絡(luò)類型是通過 CTTelephonyNetworkInfo
獲取但惶,其中 CTRadioAccessTechnologyNRNSA
和 CTRadioAccessTechnologyNR
蘋果文檔是 iOS14.0 就可以用實際上,在 iOS14.0 和 iOS14.0.1 是沒有的。
問題三:在iOS14.0和iOS14.0.1系統(tǒng)上閃退膀曾,原因是獲取網(wǎng)絡(luò)類型導(dǎo)致的
網(wǎng)絡(luò)類型是通過 CTTelephonyNetworkInfo
獲取县爬,其中 CTRadioAccessTechnologyNRNSA
和 CTRadioAccessTechnologyNR
蘋果文檔是 iOS14.0 就可以用實際上,在 iOS14.0 和 iOS14.0.1 是沒有的添谊。