NSURLProtocol的使用
- 注冊protocol。
Class cls = NSClassFromString(@"__NSCFURLSessionConfiguration") ?: NSClassFromString(@"NSURLSessionConfiguration");
[self swizzleSelector:@selector(protocolClasses) fromClass:cls toClass:[self class]];
以上方法適合于NSURLSession的網(wǎng)絡(luò)請求,替換掉protocolClasses。如果使用的是NSURLConnection,則應(yīng)使用下面的方法:
[NSURLProtocol registerClass:[GYMockURLProtocol class]];
- 實現(xiàn)protocol中必須實現(xiàn)的方法。
+ (BOOL)canInitWithRequest:(NSURLRequest *)request;
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request;
+ (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b;
- (id)initWithRequest:(NSURLRequest *)request cachedResponse:(NSCachedURLResponse *)cachedResponse client:(id<NSURLProtocolClient>)client;
- (void)startLoading;
- (void)stopLoading;
canInitWithRequest是自己注冊的URLProtocol類(比如此處的GYMockURLProtocol)的方法入口。只有這個方法返回YES時,才會執(zhí)行下面的方法,返回NO時就直接運行系統(tǒng)的方法。
canonicalRequestForRequest返回規(guī)范化的request。This method returns a canonical version of the given request。一般直接返回傳入的request,而且不要在這里做規(guī)范化的操作,否則可能出現(xiàn)反復(fù)調(diào)用等奇怪現(xiàn)象。
requestIsCacheEquivalent請求是使用緩存還是發(fā)起新請求。直接返回NO,設(shè)置每次都發(fā)起新請求。
- (id)initWithRequest:(NSURLRequest *)request cachedResponse:(NSCachedURLResponse *)cachedResponse client:(id<NSURLProtocolClient>)client;初始化方法,非必須實現(xiàn)。
startLoading核心方法,真正的網(wǎng)絡(luò)請求和mock方法這這個方法里面定義并執(zhí)行。
stopLoading網(wǎng)絡(luò)請求完成后需要調(diào)用的方法。
GYHttpMock使用示例:
- 添加需要mock的地址
mockRequest(@"GET", @"https://httpbin.org/get").isUpdatePartResponseBody(YES); - 發(fā)起網(wǎng)絡(luò)請求
NSURLSession * session = [NSURLSession sharedSession];
[[session dataTaskWithURL:[NSURL URLWithString:@"https://httpbin.org/get"] completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if (error) {
NSLog(@"%@",error);
}else{
NSLog(@"%@",data);
}
}] resume];
第二步中發(fā)起的網(wǎng)絡(luò)請求就會被GYHttpMock mock住。
注意:如果第一步中不添加isUpdatePartResponseBody(YES),則第二步中發(fā)起的網(wǎng)絡(luò)請求就會被GYHttpMock mock住,不會真的發(fā)起網(wǎng)絡(luò)請求,只是返回code碼是200的請求頭。當(dāng)添加了isUpdatePartResponseBody(YES)時才會發(fā)起網(wǎng)絡(luò)請求。
關(guān)于GYHttpMock的具體API見微信團隊的博客。
代碼詳解:
mockRequest方法會初始化GYHttpMock,添加兩個hook。依據(jù)傳入的URL和方法名創(chuàng)建GYMockRequest實例并保存。添加的兩個hook是:GYNSURLConnectionHook,GYNSURLSessionHook。命名看起來是添加兩個hook,其實是在完成上面說的方法注冊和方法交換的功能。
當(dāng)發(fā)起網(wǎng)絡(luò)請求時,會先調(diào)用GYHttpMock的canInitWithRequest方法。此時會檢查第一步中是否有該url對應(yīng)的GYMockRequest實例,如果有則返回YES,表明我們想要自己攔截請求或者修改response,GYHttpMock的startLoading等方法會被調(diào)用。如果返回NO就會把控制權(quán)交還給系統(tǒng)。
GYHttpMock使用的是NSURLConnection發(fā)起真正的網(wǎng)絡(luò)請求。
無論是攔截請求,還是修改response,都應(yīng)該主動調(diào)用
- (void)URLProtocol:(NSURLProtocol *)protocol didReceiveResponse:(NSURLResponse *)response cacheStoragePolicy:(NSURLCacheStoragePolicy)policy;
- (void)URLProtocol:(NSURLProtocol *)protocol didLoadData:(NSData *)data;
- (void)URLProtocolDidFinishLoading:(NSURLProtocol *)protocol;
等URLProtocol的方法,將最終數(shù)據(jù)返回給系統(tǒng),系統(tǒng)會將數(shù)據(jù)傳遞給外部的網(wǎng)絡(luò)請求者,比如AFNetworking或者NSURLSession的delegate。
GYHttpMock還有一個值得一提的點是他使用了函數(shù)式編程的思想。GYMockRequestDSL和GYMockResponseDSL里面通過block實現(xiàn)了函數(shù)式編程。實現(xiàn)方法是定義blcok屬性,在block中完成操作,并最終在block中將self返回給調(diào)用者。
舉例如下:
typedef GYMockResponseDSL *(^ResponseWithHeadersMethod)(NSDictionary *);
@property (nonatomic, strong, readonly) ResponseWithHeaderMethod withHeader;
- (ResponseWithHeadersMethod)withHeaders; {
return ^(NSDictionary *headers) {
for (NSString *header in headers) {
NSString *value = [headers objectForKey:header];
[self.response setHeader:header value:value];
}
return self;
};
}
GYHttpMock修改網(wǎng)絡(luò)返回數(shù)據(jù)需要調(diào)用者清楚返回數(shù)據(jù)的結(jié)構(gòu),并預(yù)先定義好和該結(jié)構(gòu)相符的字典才能完成修改。具體的修改過程是通過 遞歸,層級檢查返回的json結(jié)構(gòu),在合適的地方添加新內(nèi)容。調(diào)用的方法如下:
- (void)addEntriesFromDictionary:(NSDictionary *)dict to:(NSMutableDictionary *)targetDict