IOS網(wǎng)絡(luò):NSURLProtocol網(wǎng)絡(luò)攔截

原創(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介紹

URL Loading System
含義

NSURLProtocol是蘋果為我們提供的 URL Loading System 的一部分归薛,能夠讓你去重新定義蘋果的URL Loading System的行為。用一句話解釋NSURLProtocol:就是一個(gè)蘋果允許的中間人攻擊匪蝙。NSURLProtocol可以劫持系統(tǒng)所有基于CFsocket的網(wǎng)絡(luò)請(qǐng)求主籍。不管你是通過(guò)NSURLSession或者第三方庫(kù) (AFNetworkingAlamofire等)逛球,他們都是基于NSURLSession實(shí)現(xiàn)的千元,因此你可以通過(guò)NSURLProtocol做自定義的操作。WKWebView基于Webkit颤绕,并不走底層的CFsocket幸海,所以NSURLProtocol攔截不了WKWebView中的請(qǐng)求。

具體流程

URL Loading System里有許多類用于處理URL請(qǐng)求屋厘,比如NSURL涕烧,NSURLRequestNSURLSession等,當(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做攔截處理。

百度Logo.png

此處嘗試攔截 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)用clientdidLoadData加載數(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)求剔氏。

WKWebView無(wú)法實(shí)現(xiàn)攔截.png

其實(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)求的URLScheme要和它內(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嗤疯。

百度首頁(yè).png

攔截成功后替換為了我們自己的海賊王圖片

替換成功后的海賊王圖片.png

剛才只是替換了一張圖片列敲,如果我想一次性替換所有圖片為我的海賊王呢戴而?只需要修改下

+ (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];
    }
}
廣告等圖片全部替換為自定義圖片.png

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);
    }
}
運(yùn)行結(jié)果.png

配置CustomURLProtocol中的startLoadingcanInitWithRequest方法矾端。關(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ī)制,將NSURLRequestNSCachedURLResponse映射起來(lái)耙替。默認(rèn)情況下亚侠,Memory cache=4MDisk cache=20M俗扇「悄危可以子類化NSURLCahe實(shí)現(xiàn)自己的緩存邏輯。如果responsehttpHeaderCache-control/expires設(shè)置為可以被緩存狐援,iOS會(huì)自動(dòng)的將其存到本地?cái)?shù)據(jù)庫(kù)中。路徑是沙盒路徑下Library/Caches/bundid/Cache.db究孕。對(duì)于webview的緩存也一樣啥酱,因?yàn)樗彩怯玫?code>NSURLCache。

判斷requestcachePolicy是否== NSURLRequestUseProtocolCachePolicy厨诸。取responseheader镶殷,是否有cache-controlexpire字段。存在cache-control則緩存微酬。存在expires則緩存绘趋。cache-controlexpire都沒(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)建新的NSURLResponseNSData,將其傳給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ù)后

注意下控制臺(tái)輸出的流程.png

續(xù).png

注意一點(diǎn)坠韩,上面是當(dāng)enableDebug = NO的時(shí)候距潘,使用NSURLSession繼續(xù)把request發(fā)送出去,并不是最初我們提到的需求直接攔截到數(shù)據(jù)返回本地的模擬數(shù)據(jù)只搁,進(jìn)行測(cè)試音比。當(dāng)設(shè)置enableDebug = YES,便不會(huì)走CustomURLProtocolNSURLSessionDelegate代理方法了氢惋,而是在client拿到本地新創(chuàng)建的dataresponse后洞翩,直接進(jìn)入NSURLProtocolViewControllerNSURLSessionDelegate運(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);
}
控制圖輸出結(jié)果.png

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的屬性protocolClassesget方法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é)議CustomURLProtocolNSURLSessionDelegate的一大堆東西返劲,但是與加載NSURLSession不同的是玲昧,因?yàn)樽兂闪?code>AFNetworking,所以不會(huì)打印出NSURLProtocolViewControllerNSURLSessionDelegate的一大堆東西篮绿。

控制臺(tái)輸出酌呆,AF攔截成功了.png

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]];
}

我們最終的目的是要將NSURLSessionConfigurationFFSessionConfiguration中的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)AFNetworkingRuntimeButtonruntimeLoadAFNetworking方法,使用默認(rèn)的manager進(jìn)行初始化东亦,為什么需要這個(gè)方法呢杏节?因?yàn)橹搬槍?duì)AFNetworking,我們的攔截方式是將NSURLSessionConfiguration的屬性protocolClassesget方法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);
    }];
}
runtime加載AFNetworking成功了.png

Demo

Demo在我的Github上奋渔,歡迎下載。
IOSAdvancedDemo

參考文獻(xiàn)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
禁止轉(zhuǎn)載壮啊,如需轉(zhuǎn)載請(qǐng)通過(guò)簡(jiǎn)信或評(píng)論聯(lián)系作者嫉鲸。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市歹啼,隨后出現(xiàn)的幾起案子玄渗,更是在濱河造成了極大的恐慌座菠,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,277評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件藤树,死亡現(xiàn)場(chǎng)離奇詭異浴滴,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)也榄,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門巡莹,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人甜紫,你說(shuō)我怎么就攤上這事降宅。” “怎么了囚霸?”我有些...
    開(kāi)封第一講書(shū)人閱讀 163,624評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵腰根,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我拓型,道長(zhǎng)额嘿,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,356評(píng)論 1 293
  • 正文 為了忘掉前任劣挫,我火速辦了婚禮册养,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘压固。我一直安慰自己球拦,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,402評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布帐我。 她就那樣靜靜地躺著坎炼,像睡著了一般。 火紅的嫁衣襯著肌膚如雪拦键。 梳的紋絲不亂的頭發(fā)上谣光,一...
    開(kāi)封第一講書(shū)人閱讀 51,292評(píng)論 1 301
  • 那天,我揣著相機(jī)與錄音芬为,去河邊找鬼萄金。 笑死,一個(gè)胖子當(dāng)著我的面吹牛媚朦,可吹牛的內(nèi)容都是我干的捡絮。 我是一名探鬼主播,決...
    沈念sama閱讀 40,135評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼莲镣,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了涎拉?” 一聲冷哼從身側(cè)響起瑞侮,我...
    開(kāi)封第一講書(shū)人閱讀 38,992評(píng)論 0 275
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤的圆,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后半火,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體越妈,經(jīng)...
    沈念sama閱讀 45,429評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,636評(píng)論 3 334
  • 正文 我和宋清朗相戀三年钮糖,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了梅掠。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,785評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡店归,死狀恐怖阎抒,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情消痛,我是刑警寧澤且叁,帶...
    沈念sama閱讀 35,492評(píng)論 5 345
  • 正文 年R本政府宣布,位于F島的核電站秩伞,受9級(jí)特大地震影響逞带,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜纱新,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,092評(píng)論 3 328
  • 文/蒙蒙 一展氓、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧脸爱,春花似錦遇汞、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,723評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至捏鱼,卻和暖如春执庐,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背导梆。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,858評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工轨淌, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人看尼。 一個(gè)月前我還...
    沈念sama閱讀 47,891評(píng)論 2 370
  • 正文 我出身青樓递鹉,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親藏斩。 傳聞我的和親對(duì)象是個(gè)殘疾皇子躏结,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,713評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容