Target
監(jiān)聽 app 內(nèi)所有的網(wǎng)絡(luò)請(qǐng)求,并將請(qǐng)求的參數(shù)和返回值顯示在手機(jī)端。相當(dāng)于自己抓自己的包,這樣不在電腦前也能夠精確的觀察接口動(dòng)態(tài),或者在接手一個(gè)新的項(xiàng)目時(shí),可以清楚的看到某個(gè)界面的接口請(qǐng)求情況,幫助理清楚界面邏輯。Demo里面監(jiān)測(cè)的是 UIWebView,實(shí)際在項(xiàng)目里面監(jiān)測(cè)api接口效果更佳。


原理
原理是使用 NSURLProtocol 攔截所有 URL Loading System 中發(fā)出 request 請(qǐng)求。 攔截到之后,以我們的方式發(fā)出這個(gè)請(qǐng)求,這樣這個(gè)請(qǐng)求的返回?cái)?shù)據(jù)就能被我們統(tǒng)一捕獲。同時(shí),我們將返回的數(shù)據(jù)回調(diào)給原始發(fā)出者,以保證app正常運(yùn)行。在捕獲返回?cái)?shù)據(jù)和請(qǐng)求本身的參數(shù)后就能完整的將一次api調(diào)用顯示出來了。
NSURLProtocol介紹
NSURLProtocol 是屬于 Foundation 框架里的 URL Loading System 的一部分。它是一個(gè)抽象類, 需要繼承它后, 重寫一系列父類的方法, 且在向系統(tǒng)注冊(cè)后, 就可以攔截到所有來自 URL Loading System 中發(fā)出 request 請(qǐng)求, 包括使用 NSURLConnection 和 NSURLSession 發(fā)出去的請(qǐng)求, 使用這兩者的第三方框架就也能監(jiān)聽到, 比如 AFNetWorking。而視圖方面, 通過UIWebView、WKWebView 發(fā)出去的請(qǐng)求也能被監(jiān)聽到(WKWebView 的攔截會(huì)有些問題)。

攔截到后我們可以修改原來 requeset,我們可以什么都不做,那么這個(gè)請(qǐng)求的行為就會(huì)跟之前的一模一樣。 有趣的是我們也可以對(duì)它進(jìn)行修改,比如給它添加參數(shù),讓這個(gè)請(qǐng)求行為發(fā)生變化?;蛘邔?duì)返回的 response 進(jìn)行修改,亦或干脆重定向到新的資源,你想要A,我返回給你你B。總之, 是否返回?cái)?shù)據(jù), 返回什么數(shù)據(jù), 已經(jīng)由我們決定了。
這里我們讓這個(gè)請(qǐng)求以我們寫的方式發(fā)送出去,以便拿到服務(wù)端返回的數(shù)據(jù)。
攔截請(qǐng)求的方式
-
對(duì)于
UIWebView和NSURLConnection只需要構(gòu)建NSURLProtocol的子類,在子類中重載必要的方法, 并向系統(tǒng)注冊(cè)[NSURLProtocol registerClass:[SGQURLProtocol class]];即可攔截.#import <Foundation/Foundation.h> @interface SGQURLProtocol : NSURLProtocol @end -
對(duì)于
NSURLSession,需要通過配置NSURLSessionConfiguration對(duì)象的protocolClasses屬性NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration]; sessionConfiguration.protocolClasses = @[[SGQURLProtocol class]];這是原理,但是我們不能侵入別人寫好的代碼,在里面加上這句代碼。于是我們使用
method swizzing- (void)load { 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 NSURLSession hook."]; } method_exchangeImplementations(originalMethod, stubMethod); } - (NSArray *)protocolClasses { return @[[SGQURLProtocol class]]; } -
對(duì)于
WKWebView,除了上述操作外, 由于其基于wekkit內(nèi)核, 使用到了WKBrowsingContextController和registerSchemeForCustomProtocol。 我們需要通過反射的方式拿到了私有的class&selector。通過kvc取到browsingContextController,通過把注冊(cè)把http和https請(qǐng)求交給NSURLProtocol處理.+ (void)registerForWKWebView { Class class = [[[WKWebView new] valueForKey:@"browsingContextController"] class]; SEL selector = NSSelectorFromString(@"registerSchemeForCustomProtocol:");; if ([(id)class respondsToSelector:selector]) { [(id)class performSelector:selector withObject:@"http"]; [(id)class performSelector:selector withObject:@"https"]; } }
這里需要聲明的是,對(duì)于 WKWebView里面的請(qǐng)求,這樣注冊(cè)后,雖然可以攔截到,但是由于系統(tǒng)原因,會(huì)導(dǎo)致POST請(qǐng)求的請(qǐng)求體會(huì)丟失,導(dǎo)致請(qǐng)求本身會(huì)失敗WKWebView NSURLProtocol問題,所以我們主要還是監(jiān)聽 app 內(nèi)本身的請(qǐng)求。
NSURLProtocol子類中需要重寫的方法
#import "SGQURLProtocol.h"
#import "SGQRequestListener.h"
#import "SGQMockObject.h"
#import "NSURLRequest+ResponseTime.h"
static NSString * const kHandedRequestKey = @"kHandedRequestKey";
@implementation SGQURLProtocol
/*
是否對(duì)這個(gè)請(qǐng)求進(jìn)行攔截
返回YES,則這個(gè)request還會(huì)進(jìn)入后續(xù)方法調(diào)用
返回NO,則不會(huì)對(duì)這個(gè)request有任何影響了。
*/
+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
if ([[SGQRequestListener sharedInstance] isRequestURLInBlackList:request]) {
return NO;
}
// 這個(gè)標(biāo)記在 startLoading 方法中打上,是為了防止死循環(huán)。因?yàn)槲覀冊(cè)?startLoading方法發(fā)出去的請(qǐng)求也會(huì)被攔截到進(jìn)到這里
if ([NSURLProtocol propertyForKey:kHandedRequestKey inRequest:request]) {
return NO;
}
return YES;
}
/// cache啥的這里不管
+ (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b {
return NO;
}
/// 一般可以在這里copy出一個(gè)可變的request,進(jìn)行屬性的修改,然后返回。后續(xù)則會(huì)這個(gè)返回的request發(fā)出請(qǐng)求
+ (NSURLRequest*)canonicalRequestForRequest:(NSURLRequest*)request {
return request;
}
/*
最終那個(gè)被攔截的request或者會(huì)進(jìn)到這里,我們?cè)谶@里發(fā)出請(qǐng)求,獲取返回?cái)?shù)據(jù)。
同時(shí)也將數(shù)據(jù)回調(diào)給原始的client
*/
- (void)startLoading {
NSMutableURLRequest *request = [self.request mutableCopy];
request.startDate = [NSDate date];
[NSURLProtocol setProperty:@(YES) forKey:kHandedRequestKey inRequest:request];
id<NSURLProtocolClient> client = [self client];
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[NSURLConnection sendAsynchronousRequest:request
queue:queue
completionHandler:^(NSURLResponse *response, NSData *data, NSError *error){
request.endDate = [NSDate date];
if (error) {
[client URLProtocol:self didFailWithError:[NSError errorWithDomain:NSCocoaErrorDomain code:NSUserCancelledError userInfo:nil]];
} else {
[client URLProtocol:self didReceiveResponse:response
cacheStoragePolicy:NSURLCacheStorageNotAllowed];
[client URLProtocol:self didLoadData:data];
[client URLProtocolDidFinishLoading:self];
}
dispatch_async(dispatch_get_main_queue(), ^{
SGQMockObject *loadingObject = [SGQMockObject objectWithRequest:request response:response responseData:data error:error responseTime:request.responseTime];
[[SGQRequestListener sharedInstance] addAnObject:loadingObject];
});
}];
#pragma clang diagnostic pop
}
- (void)stopLoading { }
@end
可以看到,在這里我們成功地拿到了一次請(qǐng)求的參數(shù)部分和返回值部分,解析后顯示出來就行了。在公司項(xiàng)目中使用,可以在后臺(tái)界面設(shè)置開關(guān)打開,打開后就能監(jiān)測(cè)接口返回?cái)?shù)據(jù)了。
SGQMockObject *loadingObject = [SGQMockObject objectWithRequest:request response:response responseData:data error:error responseTime:request.responseTime];
[[SGQRequestListener sharedInstance] addAnObject:loadingObject];
)
附一張我在項(xiàng)目中使用的圖
pod 'RequestListener'
// 注意,需要在設(shè)置根window后才能調(diào)用
[[SGQRequestListener sharedInstance] startMock];
