原創(chuàng):知識(shí)進(jìn)階型文章
無(wú)私奉獻(xiàn)悲没,為國(guó)為民,創(chuàng)作不易,請(qǐng)珍惜示姿,之后會(huì)持續(xù)更新甜橱,不斷完善
個(gè)人比較喜歡做筆記和寫(xiě)總結(jié),畢竟好記性不如爛筆頭哈哈栈戳,這些文章記錄了我的IOS成長(zhǎng)歷程岂傲,希望能與大家一起進(jìn)步
溫馨提示:由于簡(jiǎn)書(shū)不支持目錄跳轉(zhuǎn),大家可通過(guò)command + F 輸入目錄標(biāo)題后迅速尋找到你所需要的內(nèi)容
目錄
- 1子檀、NSURLProtocol介紹
- 2镊掖、CustomURLProtocol
- 3、WKWebView的加載
- 3褂痰、NSURLSession的加載
- 4亩进、加載AFNetworking
- 5、使用runtime來(lái)實(shí)現(xiàn)加載AFNetworking
- Demo
- 參考文獻(xiàn)
1缩歪、NSURLProtocol介紹
含義
NSURLProtocol
是蘋果為我們提供的 URL Loading System
的一部分归薛,能夠讓你去重新定義蘋果的URL Loading System
的行為。用一句話解釋NSURLProtocol
:就是一個(gè)蘋果允許的中間人攻擊匪蝙。NSURLProtocol
可以劫持系統(tǒng)所有基于CFsocket
的網(wǎng)絡(luò)請(qǐng)求主籍。不管你是通過(guò)NSURLSession
或者第三方庫(kù) (AFNetworking
, Alamofire
等)逛球,他們都是基于NSURLSession
實(shí)現(xiàn)的千元,因此你可以通過(guò)NSURLProtocol
做自定義的操作。WKWebView
基于Webkit
颤绕,并不走底層的CFsocket
幸海,所以NSURLProtocol
攔截不了WKWebView
中的請(qǐng)求。
具體流程
URL Loading System
里有許多類用于處理URL
請(qǐng)求屋厘,比如NSURL
涕烧,NSURLRequest
和NSURLSession
等,當(dāng)URL Loading System
使用NSURLRequest
去獲取資源的時(shí)候汗洒,它會(huì)創(chuàng)建一個(gè)NSURLProtocol
子類的實(shí)例,NSURLProtocol
看起來(lái)像是一個(gè)協(xié)議父款,但其實(shí)這是一個(gè)類溢谤,你不能直接實(shí)例化一個(gè)NSURLProtocol
,而是需要寫(xiě)一個(gè)繼承自 NSURLProtocol
的子類憨攒,并通過(guò)- registerClass:
方法注冊(cè)我們的協(xié)議類世杀,然后 URL
加載系統(tǒng)就會(huì)在請(qǐng)求發(fā)出時(shí)使用我們創(chuàng)建的協(xié)議對(duì)象對(duì)該請(qǐng)求進(jìn)行處理。
簡(jiǎn)單歸納下肝集,使用NSURLProtocol
的主要可以分為5個(gè)步驟:注冊(cè)—>攔截—>轉(zhuǎn)發(fā)—>回調(diào)—>結(jié)束瞻坝。即:注冊(cè)NSURLProtocol
子類 -> 使用NSURLProtocol
子類攔截請(qǐng)求 -> 使用NSURLSession
重新發(fā)起請(qǐng)求 -> 將NSURLSession
請(qǐng)求的響應(yīng)內(nèi)容返回 -> 結(jié)束
使用場(chǎng)景
舉個(gè)例子:因?yàn)?code>DNS發(fā)生域名劫持,所以需要手動(dòng)將URL
請(qǐng)求的域名重定向到指定的IP
地址杏瞻,但是由于請(qǐng)求可能是通過(guò)NSURLSession
或者AFNetworking
等方式所刀,因此要想統(tǒng)一進(jìn)行處理衙荐,可以采用NSURLProtocol
。
- 重定向網(wǎng)絡(luò)請(qǐng)求(可以解決
DNS
域名劫持問(wèn)題) - 忽略網(wǎng)絡(luò)請(qǐng)求浮创,使用本地緩存
- 自定義網(wǎng)絡(luò)請(qǐng)求的返回結(jié)果
Response
- 攔截圖片加載請(qǐng)求忧吟,轉(zhuǎn)為從本地文件加載
- 一些全局的網(wǎng)絡(luò)請(qǐng)求設(shè)置
- 快速進(jìn)行測(cè)試環(huán)境的切換
- 過(guò)濾掉一些非法請(qǐng)求
- 網(wǎng)絡(luò)的緩存處理(如網(wǎng)絡(luò)圖片緩存)
- 可以攔截基于系統(tǒng)的
NSURLSession
進(jìn)行封裝的網(wǎng)絡(luò)請(qǐng)求。目前WKWebView
無(wú)法被NSURLProtocol
攔截斩披。 - 當(dāng)有多個(gè)自定義
NSURLProtocol
注冊(cè)到系統(tǒng)中的話溜族,會(huì)按照他們注冊(cè)的反向順序依次調(diào)用URL
加載流程。當(dāng)其中有一個(gè)NSURLProtocol
攔截到請(qǐng)求的話垦沉,后續(xù)的NSURLProtocol
就無(wú)法攔截到該請(qǐng)求煌抒。
2、CustomURLProtocol
子類化
由于 NSURLProtocol
是一個(gè)抽象類厕倍,所以使用的時(shí)候必須先定義一個(gè)它的子類摧玫,這里我們新建CustomURLProtocol
繼承自NSURLProtocol
@interface CustomURLProtocol : NSURLProtocol
@end
注冊(cè)
對(duì)于基于NSURLSession
或者使用[NSURLSession sharedSession]
初始化對(duì)象創(chuàng)建的網(wǎng)絡(luò)請(qǐng)求绑青,調(diào)用registerClass
方法即可
- (void)viewDidLoad
{
[super viewDidLoad];
// 注冊(cè)NSURLProtocol的子類
[NSURLProtocol registerClass:[CustomURLProtocol class]];
}
一經(jīng)注冊(cè)之后闸婴,所有交給URL Loading system
的網(wǎng)絡(luò)請(qǐng)求都會(huì)被攔截坏挠,所以當(dāng)不需要攔截的時(shí)候降狠,要進(jìn)行注銷
- (void)dealloc
{
[NSURLProtocol unregisterClass:[CustomURLProtocol class]];
}
抽象對(duì)象必須實(shí)現(xiàn)的攔截方法
canInitWithRequest:所有注冊(cè)此Protocol
的請(qǐng)求都會(huì)經(jīng)過(guò)這個(gè)方法的判斷,該方法會(huì)拿到request
的對(duì)象庇楞,我們可以通過(guò)該方法的返回值來(lái)篩選request
是否需要被NSURLProtocol
做攔截處理。
此處嘗試攔截 http://www.baidu.com/ 即百度搜索首頁(yè)其中的標(biāo)題欄的Logo圖片,首先需要在打印出來(lái)的absoluteString
找到我們想要的Logo
圖片的URL
烙心,接著通過(guò)判斷是否相等進(jìn)行攔截乏沸,返回YES
即進(jìn)入攔截流程蹬跃。
// 通過(guò)該方法的返回值來(lái)篩選request是否需要被NSURLProtocol做攔截處理
+ (BOOL)canInitWithRequest:(NSURLRequest *)request
{
// 獲取所有的absoluteString
NSString *absoluteString = [[request URL] absoluteString];
NSLog(@"absoluteString--%@",absoluteString);
// 攔截百度標(biāo)題欄的logo圖片丹喻,返回YES進(jìn)行攔截驻啤,目的是替換為自己的海賊王圖片
if ([absoluteString isEqualToString:@"https://www.baidu.com/img/flexible/logo/plus_logo_web.png"])
{
return YES;
}
// 默認(rèn)返回NO骑冗,不進(jìn)行攔截
return NO;
}
canonicalRequestForRequest:可選方法,對(duì)需要攔截的請(qǐng)求進(jìn)行自定義的處理巧涧,這個(gè)方法用來(lái)統(tǒng)一處理請(qǐng)求request
對(duì)象的谤绳,可以修改頭信息,或者重定向堡称。沒(méi)有特殊需要却紧,則直接return request
断凶。通常我們的做法是直接return request
认烁,在后面的startLoading
方法中進(jìn)行攔截處理砚著。還有一點(diǎn)需要注意的是,如果要在這里做重定向以及添加頭信息的時(shí)候注意檢查是否已經(jīng)添加赶撰,因?yàn)檫@個(gè)方法可能被調(diào)用多次豪娜。
// 可選方法否灾,對(duì)需要攔截的請(qǐng)求進(jìn)行自定的處理墨技,沒(méi)有特殊需要扣汪,則直接return request
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request
{
return request;
}
requestIsCacheEquivalent:用來(lái)判斷兩個(gè)request
請(qǐng)求是否相同崭别,這個(gè)方法基本不常用茅主。如果相同,則可以使用緩存數(shù)據(jù)学搜。通常只需要調(diào)用父類的實(shí)現(xiàn)即可瑞佩,默認(rèn)為YES
炬丸。
// 用來(lái)判斷兩個(gè)request請(qǐng)求是否相同,這個(gè)方法基本不常用首启,通常只需要調(diào)用父類的實(shí)現(xiàn)即可
+ (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b
{
return [super requestIsCacheEquivalent:a toRequest:b];
}
initWithRequest:在攔截到網(wǎng)絡(luò)請(qǐng)求毅桃,并且對(duì)網(wǎng)絡(luò)請(qǐng)求進(jìn)行定制處理以后钥飞。我們需要將網(wǎng)絡(luò)請(qǐng)求重新發(fā)送出去读宙,就可以初始化一個(gè)NSURLProtocol
對(duì)象了唇兑。該方法會(huì)創(chuàng)建一個(gè)NSURLProtocol
實(shí)例幔亥,在這里直接調(diào)用super
的指定構(gòu)造器方法帕棉,實(shí)例化一個(gè)對(duì)象。
// 該方法會(huì)創(chuàng)建一個(gè)NSURLProtocol實(shí)例即纲,在這里直接調(diào)用super的指定構(gòu)造器方法低斋,將網(wǎng)絡(luò)請(qǐng)求重新發(fā)送出去
- (instancetype)initWithRequest:(NSURLRequest *)request cachedResponse:(NSCachedURLResponse *)cachedResponse client:(id<NSURLProtocolClient>)client
{
return [super initWithRequest:request cachedResponse:cachedResponse client:client];
}
轉(zhuǎn)發(fā)的核心方法startLoading
開(kāi)始請(qǐng)求的方法。在該方法中唇跨,把當(dāng)前請(qǐng)求的request
攔截下來(lái)以后买猖,可以在這里修改請(qǐng)求信息玉控,重定向網(wǎng)絡(luò)既棺,DNS
解析,使用自定義的緩存等薛窥。
在這里需要我們手動(dòng)的把請(qǐng)求發(fā)出去诅迷,可以使用原生NSURLSession
,也可以使用第三方網(wǎng)絡(luò)庫(kù)AFNetworking
滩租。對(duì)于NSURLSession
律想,就是發(fā)起一個(gè)NSURLSessionDataTask
技即,同時(shí)設(shè)置NSURLSessionDataDelegate
協(xié)議,接收Server
端的響應(yīng)葵陵。
一般下載前需要設(shè)置該請(qǐng)求正在進(jìn)行下載埃难,防止多次下載的情況發(fā)生。這個(gè)方法之后考抄,會(huì)回調(diào)<NSURLProtocolClient>
協(xié)議中的方法川梅。
此處我們想要攔截百度標(biāo)題欄的logo
圖片吧彪,再將其替換為自己本地的海賊王圖片姨裸,所以首先我們需要一個(gè)獲取本地圖片的方法。
// 取出本地圖片
- (NSData *)getImageData
{
NSString *fileName = [[NSBundle mainBundle] pathForResource:@"haizeiwang.jpg" ofType:@""];
return [NSData dataWithContentsOfFile:fileName];
}
接著調(diào)用client
的didLoadData
加載數(shù)據(jù)方法赡艰。
- (void)startLoading
{
// 獲取所有的absoluteString
NSString *absoluteString = [[self.request URL] absoluteString];
// 攔截百度標(biāo)題欄的logo圖片,替換為自己本地的海賊王圖片
if ([absoluteString isEqualToString:@"https://www.baidu.com/img/flexible/logo/plus_logo_web.png"])
{
// 取出本地圖片
NSData *data = [self getImageData];
// 接著調(diào)用client的didLoadData加載數(shù)據(jù)方法
[self.client URLProtocol:self didLoadData:data];
}
}
stopLoading:請(qǐng)求被停止换帜,結(jié)束網(wǎng)絡(luò)請(qǐng)求的操作惯驼。當(dāng)NSURLProtocolClient
的協(xié)議方法都回調(diào)完畢后,就會(huì)開(kāi)始執(zhí)行這個(gè)方法了说贝。
3乡恕、WKWebView的加載
引入#import <WebKit/WebKit.h>
框架,再聲明wk
變量
@interface NSURLProtocolViewController ()
@property (nonatomic, strong) WKWebView *wk;
@end
實(shí)現(xiàn)webViewButton
的回調(diào)事件loadWebView
函卒,首先需要移除之前的WKWebView
虱咧,并進(jìn)行網(wǎng)絡(luò)請(qǐng)求腕巡,這里為百度首頁(yè)逸雹。
// 加載WKWebView
- (void)loadWKWebView
{
// 移除舊的
[self.wk removeFromSuperview];
self.wk = nil;
// 創(chuàng)建新的WKWebView
self.wk = [[WKWebView alloc] initWithFrame:CGRectMake(0, 300, self.view.bounds.size.width, 600)];
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://www.baidu.com"]];
[self.wk loadRequest:request];
[self.view addSubview:self.wk];
}
WKWebView
打印出absoluteString
转质,無(wú)法找到百度Logo
圖片沸枯,所以不能進(jìn)行攔截替換圖片绑榴。原因是WKWebView
在獨(dú)立于app
進(jìn)程之外的進(jìn)程(webkit
)中執(zhí)行網(wǎng)絡(luò)請(qǐng)求,請(qǐng)求數(shù)據(jù)不經(jīng)過(guò)主進(jìn)程(URL Loading System
)赤套,因此,在WKWebView
上直接使用 NSURLProtocol
無(wú)法攔截請(qǐng)求剔氏。
其實(shí)WKWebview
在一開(kāi)始時(shí)候是會(huì)調(diào)用到NSURLProtocol
中的入口方法canInitWithRequest
的,但是就沒(méi)有然后了币旧,也就是說(shuō)WKWebview
是和NSURLProtocol
有一定關(guān)聯(lián)巍虫,只是在NSURLProtocol
的入口處返回NO
所以導(dǎo)致NSURLProtocol
不接管WKWebview
的請(qǐng)求占遥。
返回YES
的規(guī)則便是你所請(qǐng)求的URL
的Scheme
要和它內(nèi)部配置的CustomScheme
相同。不過(guò)這里有一個(gè)疑問(wèn)搔啊,蘋果在使用webkit
時(shí)候?yàn)槭裁磿?huì)把http/https
這樣大眾化的scheme
過(guò)濾掉,看來(lái)他是不建議開(kāi)發(fā)者來(lái)使用NSURLProtocol
旧蛾。
關(guān)于私有API,因?yàn)?code>WKBrowsingContextController和registerSchemeForCustomProtocol
應(yīng)該是私有的所以使用時(shí)候需要對(duì)字符串做下處理病袄,用加密的方式或者其他就可以了,實(shí)測(cè)可以過(guò)審核的左刽。
- (void)loadWKWebView
{
// 移除舊的
[self.wk removeFromSuperview];
self.wk = nil;
// 創(chuàng)建新的WKWebView
self.wk = [[WKWebView alloc] initWithFrame:CGRectMake(0, 300, self.view.bounds.size.width, 600)];
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://www.baidu.com"]];
[self.wk loadRequest:request];
[self.view addSubview:self.wk];
//注冊(cè)scheme
Class cls = NSClassFromString(@"WKBrowsingContextController");
SEL sel = NSSelectorFromString(@"registerSchemeForCustomProtocol:");
// cls 是否包含 sel方法
if ([cls respondsToSelector:sel]) {
// 通過(guò)http和https的請(qǐng)求,同理可通過(guò)其他的Scheme 但是要滿足ULR Loading System
[cls performSelector:sel withObject:@"http"];
[cls performSelector:sel withObject:@"https"];
}
}
大家會(huì)發(fā)現(xiàn)攔截不了post
請(qǐng)求(攔截到的post
請(qǐng)求body
體為空),這個(gè)其實(shí)和WKWebview
沒(méi)有關(guān)系菩咨,這個(gè)是蘋果為了提高效率加快流暢度所以在NSURLProtocol
攔截之后索性就不復(fù)制body
體內(nèi)的東西抽米,因?yàn)?code>body的大小沒(méi)有限制是目,開(kāi)發(fā)者可能會(huì)把很大的數(shù)據(jù)放進(jìn)去那就不好辦了。我們可以采取httpbodystream
的方式拿到body
嗤疯。
攔截成功后替換為了我們自己的海賊王圖片
剛才只是替換了一張圖片列敲,如果我想一次性替換所有圖片為我的海賊王呢戴而?只需要修改下
+ (BOOL)canInitWithRequest:(NSURLRequest *)request
{
// 獲取所有的absoluteString
NSString *absoluteString = [[request URL] absoluteString];
NSLog(@"absoluteString--%@",absoluteString);
/* 攔截百度標(biāo)題欄的logo圖片,返回YES進(jìn)行攔截扶踊,目的是替換為自己的海賊王圖片
if ([absoluteString isEqualToString:@"https://www.baidu.com/img/flexible/logo/plus_logo_web.png"])
{
return YES;
}
*/
// 直接hook所有圖片:比較URL的后綴是否屬于圖片,是則自定義忽略掉
NSString* extension = request.URL.pathExtension;
NSArray *array = @[@"png", @"jpeg", @"gif", @"jpg"];
if([array containsObject:extension]){
return YES;
}
// 默認(rèn)返回NO,不進(jìn)行攔截
return NO;
}
圖片加載的一般都是廣告尺锚,實(shí)體數(shù)據(jù)有一層model
包裝,所以只會(huì)去除掉廣告而不會(huì)打擾到實(shí)體數(shù)據(jù)伐厌。
- (void)startLoading
{
// 獲取所有的absoluteString
NSString *absoluteString = [[self.request URL] absoluteString];
/* 攔截百度標(biāo)題欄的logo圖片八酒,替換為自己本地的海賊王圖片
if ([absoluteString isEqualToString:@"https://www.baidu.com/img/flexible/logo/plus_logo_web.png"])
{
// 取出本地圖片
NSData *data = [self getImageData];
// 接著調(diào)用client的didLoadData加載數(shù)據(jù)方法
[self.client URLProtocol:self didLoadData:data];
}
*/
// 只要是圖片,全部替換為海賊王
NSString* extension = self.request.URL.pathExtension;
NSArray *array = @[@"png", @"jpeg", @"gif", @"jpg"];
if([array containsObject:extension])
{
// 取出本地圖片
NSData *data = [self getImageData];
// 接著調(diào)用client的didLoadData加載數(shù)據(jù)方法
[self.client URLProtocol:self didLoadData:data];
}
}
3、NSURLSession的加載
聲明要實(shí)現(xiàn)的委托<NSURLSessionDataDelegate>
@interface NSURLProtocolViewController ()<NSURLSessionDataDelegate>
實(shí)現(xiàn)URLSessionButton
的點(diǎn)擊方法loadNSURLSession
热鞍。創(chuàng)建NSURLSeesionConfiguration
,注意到一點(diǎn)澄港,此處在config
中注冊(cè)我們的自定義協(xié)議,之前[NSURLProtocol registerClass:[CustomURLProtocol class]];
已不再起作用狱意,可以直接注釋掉。
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
config.protocolClasses = @[[CustomURLProtocol class]];
創(chuàng)建會(huì)話對(duì)象:delegateQueue
網(wǎng)絡(luò)請(qǐng)求都是在后臺(tái)進(jìn)行,但是當(dāng)網(wǎng)絡(luò)請(qǐng)求完成后包各,可能會(huì)需要回到主線程進(jìn)行刷新界面操作,所以此時(shí)可以設(shè)置代理回調(diào)方法所執(zhí)行的隊(duì)列為主隊(duì)列靶庙。
NSURLSession *session = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:[NSOperationQueue mainQueue]];
創(chuàng)建并啟動(dòng)網(wǎng)絡(luò)任務(wù)
NSURLSessionDataTask *task = [session dataTaskWithURL:url];
[task resume];
總的來(lái)說(shuō)如下:
- (void)loadNSURLSession
{
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://www.baidu.com"]];
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
config.protocolClasses = @[[CustomURLProtocol class]];
NSOperationQueue *mainQueue = [NSOperationQueue mainQueue];
NSURLSession *session = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:mainQueue];
NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request];
NSLog(@"黑魔法視圖控制器: 加載NSURLSession");
[dataTask resume];
}
實(shí)現(xiàn)NSURLSessionDataDelegate
的已經(jīng)接收到響應(yīng)時(shí)調(diào)用的代理方法didReceiveData
方法问畅。
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler
{
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
if (httpResponse.statusCode == 200)
{
NSLog(@"請(qǐng)求成功");
NSLog(@"%@", httpResponse.allHeaderFields);// 響應(yīng)頭
// 初始化接收數(shù)據(jù)的NSData變量
_data = [[NSMutableData alloc] init];
//執(zhí)行Block回調(diào)來(lái)繼續(xù)接收響應(yīng)體數(shù)據(jù)
//執(zhí)行completionHandler 用于使網(wǎng)絡(luò)連接繼續(xù)接受數(shù)據(jù)
completionHandler(NSURLSessionResponseAllow);
}
else
{
NSLog(@"請(qǐng)求失敗");
}
}
didReceiveData:接收到數(shù)據(jù)包時(shí)調(diào)用的代理方法
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data
{
NSLog(@"收到了一個(gè)數(shù)據(jù)包 data == %@,接受到了%li字節(jié)的數(shù)據(jù)",data,data.length);
//拼接完整數(shù)據(jù)
[_data appendData:data];
NSLog(@"拼接完后為:%@", _data);
}
didCompleteWithError:數(shù)據(jù)接收完畢時(shí)調(diào)用的代理方法
// 數(shù)據(jù)接收完畢時(shí)調(diào)用的代理方法
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
{
NSLog(@"數(shù)據(jù)接收完成");
if (error)
{
NSLog(@"數(shù)據(jù)接收出錯(cuò)!");
_data = nil;// 清空出錯(cuò)的數(shù)據(jù)
}
else
{
//數(shù)據(jù)傳輸成功無(wú)誤,JSON解析數(shù)據(jù)
NSDictionary *dic = [NSJSONSerialization JSONObjectWithData:_data options:NSJSONReadingMutableLeaves error:nil];
NSLog(@"%@", dic);
}
}
配置CustomURLProtocol
中的startLoading
和canInitWithRequest
方法矾端。關(guān)于死循環(huán)了的問(wèn)題,因?yàn)?code>NSURLSessionDataTask發(fā)的請(qǐng)求還會(huì)被攔截到卵皂,攔截到后再發(fā)再攔秩铆,所以我們要對(duì)在startLoading
里的請(qǐng)求做一下標(biāo)識(shí)不讓它被攔截,原理就是我們?cè)?code>request對(duì)象里人為添加鍵值進(jìn)行標(biāo)識(shí)是否被處理了灯变,如果被處理了就在canInitWithRequest
方法里返回NO
不攔截殴玛。
定義一個(gè)字符串做key
static NSString *URLProtocolHandledKey = @"URLProtocolHandledKey";
標(biāo)示該request
已經(jīng)處理過(guò)了,防止無(wú)限循環(huán)
[NSURLProtocol setProperty:@(YES) forKey:URLProtocolHandledKey inRequest:mutableReqeust];
在canInitWithRequest
方法里返回NO
不攔截
+ (BOOL)canInitWithRequest:(NSURLRequest *)request
{
// 發(fā)現(xiàn)是處理過(guò)的請(qǐng)求直接返回NO不攔截此請(qǐng)求
if ([NSURLProtocol propertyForKey:URLProtocolHandledKey inRequest:request])
{
return NO;
}
return YES;
}
在創(chuàng)建request
時(shí)添祸,可以設(shè)置屬性cachePolicy
滚粟,決定從本地還是網(wǎng)絡(luò)上獲取內(nèi)容。那么如果是從本地取的話刃泌,是從哪取呢凡壤?NSURLCahe
實(shí)現(xiàn)了response
的緩存機(jī)制,將NSURLRequest
和NSCachedURLResponse
映射起來(lái)耙替。默認(rèn)情況下亚侠,Memory cache=4M
,Disk cache=20M
俗扇「悄危可以子類化NSURLCahe
實(shí)現(xiàn)自己的緩存邏輯。如果response
的httpHeader
里Cache-control/expires
設(shè)置為可以被緩存狐援,iOS會(huì)自動(dòng)的將其存到本地?cái)?shù)據(jù)庫(kù)中。路徑是沙盒路徑下Library/Caches/bundid/Cache.db
究孕。對(duì)于webview
的緩存也一樣啥酱,因?yàn)樗彩怯玫?code>NSURLCache。
判斷request
的cachePolicy
是否== NSURLRequestUseProtocolCachePolicy
厨诸。取response
的header
镶殷,是否有cache-control
和expire
字段。存在cache-control
則緩存微酬。存在expires
則緩存绘趋。cache-control
和expire
都沒(méi)有,認(rèn)為不緩存颗管。
這里在startLoading
中假定一個(gè)需求:攔截網(wǎng)絡(luò)數(shù)據(jù)陷遮,返回本地的模擬數(shù)據(jù),進(jìn)行測(cè)試垦江。不需要進(jìn)行調(diào)用本地測(cè)試數(shù)據(jù)則直接繼續(xù)進(jìn)行網(wǎng)絡(luò)請(qǐng)求帽馋,否則創(chuàng)建新的NSURLResponse
和NSData
,將其傳給client
。
// 二绽族、加載NSURLSession
- (void)startLoading
{
// 攔截的請(qǐng)求的request對(duì)象
NSMutableURLRequest *mutableReqeust = [self.request mutableCopy];
// 標(biāo)示該request已經(jīng)處理過(guò)了姨涡,防止無(wú)限循環(huán)
[NSURLProtocol setProperty:@(YES) forKey:URLProtocolHandledKey inRequest:mutableReqeust];
//這個(gè)enableDebug隨便根據(jù)自己的需求了,可以直接攔截到數(shù)據(jù)返回本地的模擬數(shù)據(jù)吧慢,進(jìn)行測(cè)試
BOOL enableDebug = NO;
if (enableDebug)
{
NSString *str = @"測(cè)試數(shù)據(jù)";
// 將NSString轉(zhuǎn)換為UTF-8數(shù)據(jù)
NSData *data = [str dataUsingEncoding:NSUTF8StringEncoding];
// 新的response
NSURLResponse *response = [[NSURLResponse alloc] initWithURL:mutableReqeust.URL
MIMEType:@"text/plain"
expectedContentLength:data.length
textEncodingName:nil];
// 將新的response作為request對(duì)應(yīng)的response涛漂,不緩存
[self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
// 設(shè)置request對(duì)應(yīng)的 響應(yīng)數(shù)據(jù) response data
[self.client URLProtocol:self didLoadData:data];
// 標(biāo)記請(qǐng)求結(jié)束
[self.client URLProtocolDidFinishLoading:self];
}
else
{
//使用NSURLSession繼續(xù)把request發(fā)送出去
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
NSOperationQueue *mainQueue = [NSOperationQueue mainQueue];
self.session = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:mainQueue];
NSURLSessionDataTask *task = [self.session dataTaskWithRequest:self.request];
[task resume];
}
}
上面采用NSURLSession
發(fā)送的網(wǎng)絡(luò)請(qǐng)求,所以實(shí)現(xiàn)NSURLSessionDelegate
代理方法進(jìn)行回調(diào)检诗。NSURLSessionDelegate
走的是繼續(xù)路線匈仗,所以需要和截取路線各自寫(xiě)一份client
的三個(gè)方法。
接收到返回信息時(shí)(還未開(kāi)始下載)執(zhí)行的代理方法:
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler
{
[self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
completionHandler(NSURLSessionResponseAllow);
}
接收到服務(wù)器返回的數(shù)據(jù)調(diào)用多次:
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data
{
// 打印返回?cái)?shù)據(jù)
NSString *dataStr = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
if (dataStr)
{
NSLog(@"***截取數(shù)據(jù)***: %@", dataStr);
}
[self.client URLProtocol:self didLoadData:data];
}
請(qǐng)求結(jié)束或者是失敗的時(shí)候調(diào)用:
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
{
if (error)
{
[self.client URLProtocol:self didFailWithError:error];
}
else
{
[self.client URLProtocolDidFinishLoading:self];
}
}
stopLoading
停止方法為:
- (void)stopLoading
{
// NSURLSession的停止方法
[self.session invalidateAndCancel];
self.session = nil;
}
現(xiàn)在基本完成了岁诉,需要注意下控制臺(tái)輸出的流程锚沸,我們很明顯看到,因?yàn)?code>NSURLProtocolViewController和CustomURLProtocol
均各自實(shí)現(xiàn)了一套NSURLSessionDelegate
協(xié)議以及創(chuàng)建NSURLSessionDataTask
涕癣,其存在明顯的調(diào)用先后的順序問(wèn)題哗蜈。
大致順序如下:NSURLProtocolViewController
創(chuàng)建NSURLSessionDataTask
--->跳到CustomURLProtocol
執(zhí)行其NSURLSessionDataTask
----->回到NSURLProtocolViewController
繼續(xù)自己之前的NSURLSessionDataTask
loadNSURLSession---------黑魔法視圖控制器: 加載NSURLSession
startLoading----------------自定義協(xié)議: 使用NSURLSession繼續(xù)把request發(fā)送出去
didReceiveResponse--------自定義協(xié)議: 接收到返回信息時(shí)(還未開(kāi)始下載)
didReceiveData-------------自定義協(xié)議: 截取數(shù)據(jù): <!DOCTYPE html>
didCompleteWithError------ 自定義協(xié)議: 請(qǐng)求結(jié)束
didReceiveResponse--------黑魔法視圖控制器: 請(qǐng)求成功
didReceiveResponse--------黑魔法視圖控制器: 響應(yīng)頭 {
didReceiveData-------------黑魔法視圖控制器: 收到了一個(gè)數(shù)據(jù)包 data
didReceiveData-------------黑魔法視圖控制器: 拼接完后為 {length =
didCompleteWithError------黑魔法視圖控制器: 數(shù)據(jù)接收完成
didCompleteWithError------黑魔法視圖控制器: 數(shù)據(jù)傳輸成功無(wú)誤,JSON解析數(shù)據(jù)后
注意一點(diǎn)坠韩,上面是當(dāng)enableDebug = NO
的時(shí)候距潘,使用NSURLSession
繼續(xù)把request
發(fā)送出去,并不是最初我們提到的需求直接攔截到數(shù)據(jù)返回本地的模擬數(shù)據(jù)只搁,進(jìn)行測(cè)試音比。當(dāng)設(shè)置enableDebug = YES
,便不會(huì)走CustomURLProtocol
的NSURLSessionDelegate
代理方法了氢惋,而是在client
拿到本地新創(chuàng)建的data
和response
后洞翩,直接進(jìn)入NSURLProtocolViewController
的NSURLSessionDelegate
運(yùn)行。
BOOL enableDebug = YES;
if (enableDebug)
{
NSString *str = @"測(cè)試數(shù)據(jù)";
// 將NSString轉(zhuǎn)換為UTF-8數(shù)據(jù)
NSData *data = [str dataUsingEncoding:NSUTF8StringEncoding];
// 新的response
NSURLResponse *response = [[NSURLResponse alloc] initWithURL:mutableReqeust.URL
MIMEType:@"text/plain"
expectedContentLength:data.length
textEncodingName:nil];
// 將新的response作為request對(duì)應(yīng)的response焰望,不緩存
[self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
// 設(shè)置request對(duì)應(yīng)的 響應(yīng)數(shù)據(jù) response data
[self.client URLProtocol:self didLoadData:data];
// 標(biāo)記請(qǐng)求結(jié)束
[self.client URLProtocolDidFinishLoading:self];
}
需要調(diào)整下NSURLSessionDelegate
中的方法骚亿,didReceiveResponse
刪除掉之前的httpResponse
判斷statusCode
狀態(tài)碼代碼段,因?yàn)榇藭r(shí)的response
是我們自定義的熊赖,不再是httpResponse
類型的了
// 已經(jīng)接收到響應(yīng)時(shí)調(diào)用的代理方法
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler
{
NSLog(@"黑魔法視圖控制器:URL---%@, expectedContentLength----%lld",response.URL, response.expectedContentLength);
_data = [[NSMutableData alloc] init];
completionHandler(NSURLSessionResponseAllow);
}
同樣的来屠,需要修改下didReceiveData
方法,將接收到的data
轉(zhuǎn)化為字符串輸出震鹉,可以看到控制圖順利輸出了我們的data
即測(cè)試數(shù)據(jù)
字符串俱笛。
// 接收到數(shù)據(jù)包時(shí)調(diào)用的代理方法
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data
{
NSLog(@"黑魔法視圖控制器: 收到了一個(gè)數(shù)據(jù)包 data == %@,接受到了%li字節(jié)的數(shù)據(jù)",data,data.length);
//拼接完整數(shù)據(jù)
[_data appendData:data];
NSString *dataStr = [[NSString alloc] initWithData:_data encoding:NSUTF8StringEncoding];
NSLog(@"黑魔法視圖控制器: 拼接完后為 %@", dataStr);
}
4传趾、加載AFNetworking
目前為止迎膜,我們上面的代碼已經(jīng)能夠監(jiān)控到絕大部分的網(wǎng)絡(luò)請(qǐng)求,但是呢墨缘,有一個(gè)卻是特殊的星虹,比如AFNetworking
請(qǐng)求零抬。因?yàn)?code>AFNetworking網(wǎng)絡(luò)請(qǐng)求的NSURLSession
實(shí)例方法都是通過(guò)sessionWithConfiguration:delegate:delegateQueue:
方法獲得的,我們是不能監(jiān)聽(tīng)到的宽涌,然而我們通過(guò)[NSURLSession sharedSession]
生成session
就可以攔截到請(qǐng)求平夜,原因就出在NSURLSessionConfiguration
上,我們進(jìn)到NSURLSessionConfiguration
里面看一下卸亮,他有一個(gè)屬性:
@property (nullable, copy) NSArray<Class> *protocolClasses;
我們能夠看出忽妒,這是一個(gè)NSURLProtocol
數(shù)組,上面我們提到了兼贸,我們監(jiān)控網(wǎng)絡(luò)是通過(guò)注冊(cè)NSURLProtocol
來(lái)進(jìn)行網(wǎng)絡(luò)監(jiān)控的段直,但是通過(guò)sessionWithConfiguration:delegate:delegateQueue:
得到的session
,他的configuration
中已經(jīng)有一個(gè)NSURLProtocol
溶诞,所以他不會(huì)走我們的protocol
來(lái)鸯檬,怎么解決這個(gè)問(wèn)題呢? 其實(shí)很簡(jiǎn)單螺垢,我們將NSURLSessionConfiguration
的屬性protocolClasses
的get
方法hook
掉喧务,通過(guò)返回我們自己的protocol
,這樣枉圃,我們就能夠監(jiān)控到通過(guò)sessionWithConfiguration:delegate:delegateQueue:
得到的session
的網(wǎng)絡(luò)請(qǐng)求功茴。所以對(duì)于AFNetworking
中網(wǎng)絡(luò)請(qǐng)求初始化方法可以修改為:
// 加載AFNetworking
- (void)loadAFNetworking
{
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
//指定其protocolClasses
configuration.protocolClasses = @[[CustomURLProtocol class]];
// 不采用manager初始化,改為以下方式
//AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
AFHTTPSessionManager *manager = [[AFHTTPSessionManager alloc] initWithBaseURL:nil sessionConfiguration:configuration];
[manager GET:@"http://www.baidu.com" parameters:nil progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
NSLog(@"responseObject:%@",responseObject);
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
NSLog(@"error:%@",error);
}];
}
很簡(jiǎn)單是吧孽亲,看看運(yùn)行結(jié)果是否真的實(shí)現(xiàn)了坎穿,如果是應(yīng)該也能和NSURLSession
一樣攔截成功,輸出自定義協(xié)議CustomURLProtocol
中NSURLSessionDelegate
的一大堆東西返劲,但是與加載NSURLSession
不同的是玲昧,因?yàn)樽兂闪?code>AFNetworking,所以不會(huì)打印出NSURLProtocolViewController
中NSURLSessionDelegate
的一大堆東西篮绿。
5、使用runtime來(lái)實(shí)現(xiàn)加載AFNetworking
新建一個(gè)FFSessionConfiguration
類搔耕,作為我們自定義的SessionConfiguration
,用來(lái)做方法交換痰娱。首先在該類中創(chuàng)建一個(gè)單例弃榨,用來(lái)在其他類中調(diào)用交換方法和還原方法。
// 單例
+ (FFSessionConfiguration *)defaultConfiguration
{
static FFSessionConfiguration *staticConfiguration;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
staticConfiguration = [[FFSessionConfiguration alloc] init];
});
return staticConfiguration;
}
創(chuàng)建一個(gè)isExchanged
屬性梨睁,用于判斷是否已經(jīng)交換過(guò)了鲸睛,現(xiàn)在對(duì)它進(jìn)行初始化為NO。
// 初始化
- (instancetype)init
{
self = [super init];
if (self) {
self.isExchanged = NO;
}
return self;
}
實(shí)現(xiàn)一個(gè)交換兩個(gè)類中同一個(gè)方法名的具體實(shí)現(xiàn)的方法坡贺,即swizzleSelector
來(lái)實(shí)現(xiàn)方法混淆官辈,此處需要引入#import <objc/runtime.h>
// 交換兩個(gè)方法箱舞,此處運(yùn)用到runtime
- (void)swizzleSelector:(SEL)selector fromClass:(Class)original toClass:(Class)stub
{
Method originalMethod = class_getInstanceMethod(original, selector);
Method stubMethod = class_getInstanceMethod(stub, selector);
// 有一個(gè)找不到就拋出異常
if (!originalMethod || !stubMethod)
{
[NSException raise:NSInternalInconsistencyException format:@"Couldn't load NEURLSessionConfiguration."];
}
// 交換二者的實(shí)現(xiàn)方法,即方法混淆
method_exchangeImplementations(originalMethod, stubMethod);
}
然后實(shí)現(xiàn)我們需要交換的那個(gè)同名方法拳亿,即protocolClasses
// 如果還有其他的監(jiān)控protocol晴股,也可以在這里加進(jìn)去
// 此處用到了CustomURLProtocol
- (NSArray *)protocolClasses
{
return @[[CustomURLProtocol class]];
}
我們最終的目的是要將NSURLSessionConfiguration
和 FFSessionConfiguration
中的protocolClasses
方法進(jìn)行交換,于是寫(xiě)出我們的核心方法load
// 交換掉 NSURLSessionConfiguration的protocolClasses方法
- (void)load
{
// 是否交換方法 YES
self.isExchanged = YES;
// NSURLSessionConfiguration
Class cls = NSClassFromString(@"__NSCFURLSessionConfiguration") ?: NSClassFromString(@"NSURLSessionConfiguration");
// 將NSURLSessionConfiguration 和 FFSessionConfiguration中的protocolClasses方法進(jìn)行交換
[self swizzleSelector:@selector(protocolClasses) fromClass:cls toClass:[self class]];
}
最后需要有還原為初始化狀態(tài)肺魁,不再攔截的方法unload
// 還原初始化
- (void)unload
{
// 是否交換方法 NO
self.isExchanged = NO;
Class cls = NSClassFromString(@"__NSCFURLSessionConfiguration") ?: NSClassFromString(@"NSURLSessionConfiguration");
// 再替換一次就回來(lái)了
[self swizzleSelector:@selector(protocolClasses) fromClass:cls toClass:[self class]];
}
還好电湘,這個(gè)方法混淆的實(shí)現(xiàn)并沒(méi)有我想象的困難。接下來(lái)在NSURLProtocolViewController
看下具體如何使用鹅经。我們需要實(shí)現(xiàn)一個(gè)方法來(lái)取得單例并在判斷沒(méi)有交換后進(jìn)行protocolClasses
的交換寂呛。
// 開(kāi)始監(jiān)聽(tīng)
+ (void)startMonitor
{
// 取得單例
FFSessionConfiguration *sessionConfiguration = [FFSessionConfiguration defaultConfiguration];
// 注冊(cè)
[NSURLProtocol registerClass:[CustomURLProtocol class]];
// 還沒(méi)有交換就交換
if (![sessionConfiguration isExchanged])
{
// 交換
[sessionConfiguration load];
}
}
同樣地,我們也需要實(shí)現(xiàn)一個(gè)類似方法來(lái)取消交換
// 停止監(jiān)聽(tīng)
+ (void)stopMonitor
{
// 取得單例
FFSessionConfiguration *sessionConfiguration = [FFSessionConfiguration defaultConfiguration];
// 當(dāng)不需要攔截的時(shí)候瘾晃,要進(jìn)行注銷
[NSURLProtocol unregisterClass:[CustomURLProtocol class]];
// 已經(jīng)交換過(guò)了就還原
if ([sessionConfiguration isExchanged])
{
// 還原
[sessionConfiguration unload];
}
}
然后進(jìn)入關(guān)鍵的NSURLProtocolViewController
中來(lái)實(shí)現(xiàn)調(diào)用贷痪。首先因?yàn)槲覀冊(cè)?code>startMonitor已經(jīng)注冊(cè)過(guò)了,所以需要注釋掉之前viewDidLoad
中的[NSURLProtocol registerClass:[CustomURLProtocol class]];
蹦误,直接改為[CustomURLProtocol startMonitor];
即可
- (void)viewDidLoad
{
[super viewDidLoad];
[self createSubviews];
// 注冊(cè)NSURLProtocol的子類
// 當(dāng)NSURLSeesionConfiguration使用protocolClasses注冊(cè)的時(shí)候劫拢,此處不再起作用,可以直接注釋掉
// 當(dāng)使用runtime攔截AFNetworking時(shí)胖缤,此處也需要注釋掉尚镰,因?yàn)樵谧远x協(xié)議里已經(jīng)配置過(guò)了
// [NSURLProtocol registerClass:[CustomURLProtocol class]];
// 使用runtime攔截AFNetworking時(shí),使用這句話
[CustomURLProtocol startMonitor];
}
同樣的原因哪廓,dealloc
也做相應(yīng)修改
- (void)dealloc
{
// 一經(jīng)注冊(cè)之后狗唉,所有交給URL Loading system的網(wǎng)絡(luò)請(qǐng)求都會(huì)被攔截,所以當(dāng)不需要攔截的時(shí)候涡真,要進(jìn)行注銷
// 當(dāng)使用runtime攔截AFNetworking時(shí)分俯,此處也需要注釋掉,因?yàn)樵谧远x協(xié)議里已經(jīng)配置過(guò)了
// [NSURLProtocol unregisterClass:[CustomURLProtocol class]];
// 使用runtime攔截AFNetworking時(shí)哆料,使用這句話
[CustomURLProtocol stopMonitor];
}
接下來(lái)最后一步啦缸剪,實(shí)現(xiàn)AFNetworkingRuntimeButton
的runtimeLoadAFNetworking
方法,使用默認(rèn)的manager
進(jìn)行初始化东亦,為什么需要這個(gè)方法呢杏节?因?yàn)橹搬槍?duì)AFNetworking
,我們的攔截方式是將NSURLSessionConfiguration
的屬性protocolClasses
的get
方法hook
掉典阵,通過(guò)返回我們自己的protocol
:
- (void)runtimeLoadAFNetworking
{
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
[manager GET:@"http://www.baidu.com" parameters:nil progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
NSLog(@"responseObject:%@",responseObject);
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
NSLog(@"error:%@",error);
}];
}
Demo
Demo在我的Github上奋渔,歡迎下載。
IOSAdvancedDemo