JSBridge 實(shí)現(xiàn)機(jī)制

概述

在android 中 4.2之前addJavaScriptInterface()迄靠,提供給js調(diào)用native的方法,存在安全隱患,具體怎么發(fā)生的請(qǐng)看這里addJavaScriptInterface 隱患
android 現(xiàn)在基本借助webViewClient中的 shouldOverrideUrlLoading(url)和WebChromeClient 中的onJsPrompt(url)函數(shù)來解決問題.而由onJsPrompt(url)出現(xiàn)的解決方案有一個(gè)比較成熟的框架WebViewJsBridge. 今天我要講的就是這個(gè)框架,因?yàn)楸救艘哺鉕C開發(fā)锦聊,也做了一段時(shí)間了,正好看到這一塊了箩绍,就做一個(gè)OC版本的分析孔庭,原理基本一致.

原理

分析一個(gè)東西的一種方式可以先從結(jié)果入手,知道怎么用材蛛,再?gòu)氖褂玫娜肟谠驳剑鹨簧钊胪诰颍瑥狞c(diǎn)到面卑吭,這是我一貫的做事風(fēng)格芽淡,好了,不說了先看使用. 假如H5端想打開native的相冊(cè)界面豆赏,以下是OC端調(diào)用代碼.

self.bridge = [WebViewJavascriptBridge bridgeForWebView:self.webView];
    [self.bridge setWebViewDelegate:self];
    
    
    /* JS調(diào)用OC的API:訪問相冊(cè),這是核心代碼 registerHandler方法 */
    [self.bridge registerHandler:@"openCamera" handler:^(id data, WVJBResponseCallback responseCallback) {
        NSLog(@"需要%@圖片", data[@"count"]);//data 是js端返回回來的數(shù)據(jù). responseCallback是OC端希望返回給js的數(shù)據(jù).
        responseCallback(@"我收到消息了");
        UIImagePickerController *imageVC = [[UIImagePickerController alloc] init];
        imageVC.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;
        [self presentViewController:imageVC animated:YES completion:nil];
    }];

我們進(jìn)入registerHanlder函數(shù)

typedef void (^WVJBResponseCallback)(id responseData);
typedef void (^WVJBHandler)(id data, WVJBResponseCallback responseCallback);

@property (strong, nonatomic) NSMutableDictionary* messageHandlers;
- (void)registerHandler:(NSString *)handlerName handler:(WVJBHandler)handler {
    _base.messageHandlers[handlerName] = [handler copy];
}

不難看出我們所有在OC中調(diào)用registerHandler函數(shù)后的數(shù)據(jù)都存放在messaeHandlers字典中.我們知道key是上面提到的@"openCamera" 字符串.

接下來我們出發(fā)一個(gè)H5中的動(dòng)作挣菲,打開native相冊(cè)界面
轉(zhuǎn)到H5中,提供主要代碼.

<body>
        <h2>JS調(diào)用OC中的方法</h2>
        <button id="btn">訪問OC相冊(cè)</button>
    </body>
    <script>
       // 這段代碼是固定的掷邦,必須要放到j(luò)s中
       function setupWebViewJavascriptBridge(callback) {
            if (window.WebViewJavascriptBridge) { return callback(WebViewJavascriptBridge); }
            if (window.WVJBCallbacks) { return window.WVJBCallbacks.push(callback); }
            window.WVJBCallbacks = [callback];
            var WVJBIframe = document.createElement('iframe');
            WVJBIframe.style.display = 'none';
            WVJBIframe.src = 'wvjbscheme://__BRIDGE_LOADED__';
            document.documentElement.appendChild(WVJBIframe);
            setTimeout(function() { document.documentElement.removeChild(WVJBIframe) }, 0)
        }
    
        // 與OC交互的所有JS方法都要在這里注冊(cè),才能讓OC和JS之間相互調(diào)用
       setupWebViewJavascriptBridge(function(bridge) {
           /* OC給JS提供公開的API, 在JS中可以手動(dòng)調(diào)用此API, 并且可以接收OC中傳過來的參數(shù),同時(shí)可回調(diào)OC */
                                    
           // 調(diào)用OC中的打開相冊(cè)方法
           document.getElementById('btn').onclick = function () {
               bridge.callHandler('openCamera', {'count':'10張'}, function responseCallback(responseData) {
                   console.log("OC中返回的參數(shù):", responseData)
               });
           };    
    </script>

頁(yè)面加載完時(shí)會(huì)觸發(fā)setUpWebViewJavascripBridge(callabck)方法白胀,此時(shí)webview中的shouldStartLoadWithRequest方法會(huì)被觸發(fā),我們看看實(shí)現(xiàn).

#define kOldProtocolScheme @"wvjbscheme"
#define kNewProtocolScheme @"https"
#define kQueueHasMessage   @"__wvjb_queue_message__"
#define kBridgeLoaded      @"__bridge_loaded__"

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
    if (webView != _webView) { return YES; }
    
    NSURL *url = [request URL];
    __strong WVJB_WEBVIEW_DELEGATE_TYPE* strongDelegate = _webViewDelegate;
    if ([_base isWebViewJavascriptBridgeURL:url]) {
        //頁(yè)面初次加載完畢后會(huì)執(zhí)行該代碼.
        if ([_base isBridgeLoadedURL:url]) {
          //將一對(duì)象注入到H5頁(yè)面中
            [_base injectJavascriptFile];
        } else if ([_base isQueueMessageURL:url]) {
            NSString *messageQueueString = [self _evaluateJavascript:[_base webViewJavascriptFetchQueyCommand]];
            [_base flushMessageQueue:messageQueueString];
        } else {
            [_base logUnkownMessage:url];
        }
        return NO;
    } else if (strongDelegate && [strongDelegate respondsToSelector:@selector(webView:shouldStartLoadWithRequest:navigationType:)]) {
        return [strongDelegate webView:webView shouldStartLoadWithRequest:request navigationType:navigationType];
    } else {
        return YES;
    }
}

//當(dāng)傳過來的是以wvjbscheme和https開頭的url時(shí)返回YES.
- (BOOL)isWebViewJavascriptBridgeURL:(NSURL*)url {
    if (![self isSchemeMatch:url]) {
        return NO;
    }
    return [self isBridgeLoadedURL:url] || [self isQueueMessageURL:url];
}

- (BOOL)isSchemeMatch:(NSURL*)url {
    NSString* scheme = url.scheme.lowercaseString;
    return [scheme isEqualToString:kNewProtocolScheme] || [scheme isEqualToString:kOldProtocolScheme];
}

- (BOOL)isQueueMessageURL:(NSURL*)url {
    NSString* host = url.host.lowercaseString;
    return [self isSchemeMatch:url] && [host isEqualToString:kQueueHasMessage];
}
//這個(gè)表示url 包含wvjbscheme字符串.
- (BOOL)isBridgeLoadedURL:(NSURL*)url {
    NSString* host = url.host.lowercaseString;
    return [self isSchemeMatch:url] && [host isEqualToString:kBridgeLoaded];
}

從上的[_base injectJavascriptFile];我們轉(zhuǎn)進(jìn)去看看發(fā)生了什么.

- (void)injectJavascriptFile {

    NSString *js = WebViewJavascriptBridge_js();
    [self _evaluateJavascript:js];
    if (self.startupMessageQueue) {
        NSArray* queue = self.startupMessageQueue;
        self.startupMessageQueue = nil;
        for (id queuedMessage in queue) {
            [self _dispatchMessage:queuedMessage];
        }
    }
}

//我們?cè)倏纯磜ebView JavaScriptBridge_js()
NSString * WebViewJavascriptBridge_js() {
    #define __wvjb_js_func__(x) #x
    
    // BEGIN preprocessorJSCode
    static NSString * preprocessorJSCode = @__wvjb_js_func__(
;(function() {
    if (window.WebViewJavascriptBridge) {
        return;
    }

    if (!window.onerror) {
        window.onerror = function(msg, url, line) {
            console.log("WebViewJavascriptBridge: ERROR:" + msg + "@" + url + ":" + line);
        }
    }
    window.WebViewJavascriptBridge = {
        registerHandler: registerHandler,
        callHandler: callHandler,
        disableJavscriptAlertBoxSafetyTimeout: disableJavscriptAlertBoxSafetyTimeout,
        _fetchQueue: _fetchQueue,
        _handleMessageFromObjC: _handleMessageFromObjC
    };

    var messagingIframe;
    var sendMessageQueue = [];
    var messageHandlers = {};
    
    var CUSTOM_PROTOCOL_SCHEME = 'https';
    var QUEUE_HAS_MESSAGE = '__wvjb_queue_message__';
    
    var responseCallbacks = {};
    var uniqueId = 1;
    var dispatchMessagesWithTimeoutSafety = true;

    function registerHandler(handlerName, handler) {
        messageHandlers[handlerName] = handler;
    }
    
    function callHandler(handlerName, data, responseCallback) {
        if (arguments.length == 2 && typeof data == 'function') {
            responseCallback = data;
            data = null;
        }
        _doSend({ handlerName:handlerName, data:data }, responseCallback);
    }
    function disableJavscriptAlertBoxSafetyTimeout() {
        dispatchMessagesWithTimeoutSafety = false;
    }
    
    function _doSend(message, responseCallback) {
        if (responseCallback) {
            var callbackId = 'cb_'+(uniqueId++)+'_'+new Date().getTime();
            responseCallbacks[callbackId] = responseCallback;
            message['callbackId'] = callbackId;
        }
        sendMessageQueue.push(message);
        messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
    }

    function _fetchQueue() {
        var messageQueueString = JSON.stringify(sendMessageQueue);
        sendMessageQueue = [];
        return messageQueueString;
    }

    function _dispatchMessageFromObjC(messageJSON) {
        if (dispatchMessagesWithTimeoutSafety) {
            setTimeout(_doDispatchMessageFromObjC);
        } else {
             _doDispatchMessageFromObjC();
        }
        
        function _doDispatchMessageFromObjC() {
            var message = JSON.parse(messageJSON);
            var messageHandler;
            var responseCallback;

            if (message.responseId) {
                responseCallback = responseCallbacks[message.responseId];
                if (!responseCallback) {
                    return;
                }
                responseCallback(message.responseData);
                delete responseCallbacks[message.responseId];
            } else {
                if (message.callbackId) {
                    var callbackResponseId = message.callbackId;
                    responseCallback = function(responseData) {
                        _doSend({ handlerName:message.handlerName, responseId:callbackResponseId, responseData:responseData });
                    };
                }
                
                var handler = messageHandlers[message.handlerName];
                if (!handler) {
                    console.log("WebViewJavascriptBridge: WARNING: no handler for message from ObjC:", message);
                } else {
                    handler(message.data, responseCallback);
                }
            }
        }
    }
    
    function _handleMessageFromObjC(messageJSON) {
        _dispatchMessageFromObjC(messageJSON);
    }

    messagingIframe = document.createElement('iframe');
    messagingIframe.style.display = 'none';
    messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
    document.documentElement.appendChild(messagingIframe);

    registerHandler("_disableJavascriptAlertBoxSafetyTimeout", disableJavscriptAlertBoxSafetyTimeout);
    
    setTimeout(_callWVJBCallbacks, 0);
    function _callWVJBCallbacks() {
        var callbacks = window.WVJBCallbacks;
        delete window.WVJBCallbacks;
        for (var i=0; i<callbacks.length; i++) {
            callbacks[i](WebViewJavascriptBridge);
        }
    }
})();
    ); // END preprocessorJSCode

    #undef __wvjb_js_func__
    return preprocessorJSCode;
};

從上面的 NSString *js = WebViewJavascriptBridge_js();
[self _evaluateJavascript:js]中可知給H5端注冊(cè)了一個(gè)webViewJavaScriptBridge對(duì)象.該對(duì)象有callHanlder和register方法,并且有sendMessageQueue 消息隊(duì)列.

點(diǎn)擊打開相冊(cè)按鈕

 bridge.callHandler('openCamera', {'count':'10張'}, function responseCallback(responseData) {
                   console.log("OC中返回的參數(shù):", responseData)
               });

 function callHandler(handlerName, data, responseCallback) {
        if (arguments.length == 2 && typeof data == 'function') {
            responseCallback = data;
            data = null;
        }
        _doSend({ handlerName:handlerName, data:data }, responseCallback);
    }
    function _doSend(message, responseCallback) {
        if (responseCallback) {
            var callbackId = 'cb_'+(uniqueId++)+'_'+new Date().getTime();
            responseCallbacks[callbackId] = responseCallback;
            message['callbackId'] = callbackId;
        }
        sendMessageQueue.push(message);
        messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
    }

我們驚訝的發(fā)現(xiàn)會(huì)執(zhí)行_doSend方法創(chuàng)建一條消息,加入到消息隊(duì)列耙饰,該消息包含{handlerName:'openCamera',data:{'count':'10張'},callbackId:'xxxx'};messageingIframe.src= wvjbscheme://wvjb_queue_message,到這里會(huì)觸發(fā)webView的shouldStartRequest方法最終執(zhí)行

- (NSString *)webViewJavascriptFetchQueyCommand {
    return @"WebViewJavascriptBridge._fetchQueue();";
}

NSString *messageQueueString = [self _evaluateJavascript:[_base webViewJavascriptFetchQueyCommand]];
//messageQueueString 是啥纹笼,我們繼續(xù)轉(zhuǎn)到j(luò)sbridge對(duì)象
function _fetchQueue() {
        var messageQueueString =     JSON.stringify(sendMessageQueue);
        sendMessageQueue = [];
        return messageQueueString;
    }
//你會(huì)發(fā)現(xiàn)這個(gè)原來就是消息隊(duì)列json字符串.每條消息是啥上面已經(jīng)說過了.
 [_base flushMessageQueue:messageQueueString];

分析 [_base flushMessageQueue:messageQueueString]

- (void)flushMessageQueue:(NSString *)messageQueueString{
    if (messageQueueString == nil || messageQueueString.length == 0) {
        NSLog(@"WebViewJavascriptBridge: WARNING: ObjC got nil while fetching the message queue JSON from webview. This can happen if the WebViewJavascriptBridge JS is not currently present in the webview, e.g if the webview just loaded a new page.");
        return;
    }

    id messages = [self _deserializeMessageJSON:messageQueueString];
    for (WVJBMessage* message in messages) {
        if (![message isKindOfClass:[WVJBMessage class]]) {
            NSLog(@"WebViewJavascriptBridge: WARNING: Invalid %@ received: %@", [message class], message);
            continue;
        }
        [self _log:@"RCVD" json:message];
        NSString* responseId = message[@"responseId"];
        if (responseId) {
            WVJBResponseCallback responseCallback = _responseCallbacks[responseId];
            responseCallback(message[@"responseData"]);
            [self.responseCallbacks removeObjectForKey:responseId];
        } else {
            //顯然從上面分析可知.message不包含responseId
            WVJBResponseCallback responseCallback = NULL;
            NSString* callbackId = message[@"callbackId"];
            //從js中的消息得知這個(gè)callbackId是存在的.
            if (callbackId) {
                responseCallback = ^(id responseData) {
                    if (responseData == nil) {
                        responseData = [NSNull null];
                    }
                    
                    WVJBMessage* msg = @{ @"responseId":callbackId, @"responseData":responseData };
                    //當(dāng)我們這樣調(diào)用responseData(...)時(shí)會(huì)觸發(fā)下面的方法.
                    [self _queueMessage:msg];
                };
            } else {
                responseCallback = ^(id ignoreResponseData) {
                    // Do nothing
                };
            }
            //還記得我們剛開始registerHandler的 "openCamera"么,
            WVJBHandler handler = self.messageHandlers[message[@"handlerName"]];
            
            if (!handler) {
                NSLog(@"WVJBNoHandlerException, No handler for message from JS: %@", message);
                continue;
            }
           //注冊(cè)的那個(gè)block最終在這里接收了傳值.
            handler(message[@"data"], responseCallback);
        }
    }
}

兜兜轉(zhuǎn)轉(zhuǎn)又回到了起點(diǎn),從上面我們可以得知其native端和h5端含有相同的handlerName為了找到對(duì)應(yīng)的回調(diào)方法.在native端registerHandler 就必須在H5端callHandler苟跪,callHandler觸發(fā)了向OC端發(fā)送獲取消息的協(xié)議方法廷痘,之后OC端從H5端拿到了messageQueue,OC端最終解析messageQueue找到messageHandlers中的block 回調(diào)模塊WVJhandler, 之后block(data,responseCallback)得到了響應(yīng)的結(jié)果件已,但是這個(gè)responseCallback 又是怎么回事呢笋额?別急,還記得registerhandler中的responseCallback("我收到消息了");

分析responseCallback做了什么?

 responseCallback = ^(id responseData) {
                    if (responseData == nil) {
                        responseData = [NSNull null];
                    }
                    //WVJBMessage 是一個(gè)宏定義的字典.
                    WVJBMessage* msg = @{ @"responseId":callbackId, @"responseData":responseData };
                    //當(dāng)我們這樣調(diào)用responseData(...)時(shí)會(huì)觸發(fā)下面的方法.
                    [self _queueMessage:msg];
                };


- (void)_queueMessage:(WVJBMessage*)message {
    if (self.startupMessageQueue) {
        [self.startupMessageQueue addObject:message];
    } else {
        [self _dispatchMessage:message];
    }
}

- (void)_dispatchMessage:(WVJBMessage*)message {
    NSString *messageJSON = [self _serializeMessage:message pretty:NO];
    [self _log:@"SEND" json:messageJSON];
    messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\\" withString:@"\\\\"];
    messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\"" withString:@"\\\""];
    messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\'" withString:@"\\\'"];
    messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\n" withString:@"\\n"];
    messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\r" withString:@"\\r"];
    messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\f" withString:@"\\f"];
    messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\u2028" withString:@"\\u2028"];
    messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\u2029" withString:@"\\u2029"];
    
    NSString* javascriptCommand = [NSString stringWithFormat:@"WebViewJavascriptBridge._handleMessageFromObjC('%@');", messageJSON];
    if ([[NSThread currentThread] isMainThread]) {
        [self _evaluateJavascript:javascriptCommand];

    } else {
        dispatch_sync(dispatch_get_main_queue(), ^{
            [self _evaluateJavascript:javascriptCommand];
        });
    }
}

上面的代碼挺簡(jiǎn)單的篷扩,我不想多解釋太多 兄猩,最終會(huì)執(zhí)行下面的代碼.

 NSString* javascriptCommand = [NSString stringWithFormat:@"WebViewJavascriptBridge._handleMessageFromObjC('%@');", messageJSON];
//接下來轉(zhuǎn)入H5
function _dispatchMessageFromObjC(messageJSON) {
        if (dispatchMessagesWithTimeoutSafety) {
            setTimeout(_doDispatchMessageFromObjC);
        } else {
             _doDispatchMessageFromObjC();
        }
        
        function _doDispatchMessageFromObjC() {
            var message = JSON.parse(messageJSON);
            var messageHandler;
            var responseCallback;

            if (message.responseId) {
//WVJBMessage* msg = @{ @"responseId":callbackId, @"responseData":responseData };
                responseCallback = responseCallbacks[message.responseId];
                if (!responseCallback) {
                    return;
                }
                responseCallback(message.responseData);
                delete responseCallbacks[message.responseId];
            } else {
                if (message.callbackId) {
                    var callbackResponseId = message.callbackId;
                    responseCallback = function(responseData) {
                        _doSend({ handlerName:message.handlerName, responseId:callbackResponseId, responseData:responseData });
                    };
                }
                
                var handler = messageHandlers[message.handlerName];
                if (!handler) {
                    console.log("WebViewJavascriptBridge: WARNING: no handler for message from ObjC:", message);
                } else {
                    handler(message.data, responseCallback);
                }
            }
        }
    }
    
    function _handleMessageFromObjC(messageJSON) {
        _dispatchMessageFromObjC(messageJSON);
    }

還記得當(dāng)初的callHandler('openCamera',{"count":10},function(data){
}); function(data)就是responseCallback回調(diào).用于接收OC端的回傳值.
在OC中responseCallback(@"我收到消息了")最終觸發(fā)了OC中的_dispatchMessage 方法,該方法進(jìn)而觸發(fā)H5中的_dispatchMessageFromObjC方法鉴未,最終通過callbackId找到對(duì)應(yīng)的callback,實(shí)現(xiàn)回傳值.

結(jié)語(yǔ)

我這里是單向從H5端觸發(fā)事件到回傳值得整個(gè)過程枢冤,還有從OC端觸發(fā)H5端的,這里就不分析了铜秆,原理一致.
最后 淹真,12點(diǎn)了還在碼字,打把王者连茧,睡了.

1核蘸、Android4.2以下,addJavascriptInterface方法有安全漏洞啸驯,js代碼可以獲取到Java層的運(yùn)行時(shí)對(duì)象客扎,來偽造當(dāng)前用戶執(zhí)行惡意代碼。
2罚斗、ios7以下徙鱼,JavaScript無法調(diào)用native代碼。
3针姿、通過js聲明的對(duì)象袱吆,是通過loadUrl注入到頁(yè)面中的,所以這個(gè)對(duì)象是js對(duì)象搓幌,而不是Java對(duì)象杆故,沒有g(shù)etClass等Object方法,因此也無法獲得Runtime對(duì)象溉愁,避免了惡意代碼的注入处铛。
4、JSBridge采用URL解析的交互方式拐揭,是一套成熟的解決方案撤蟆,便于拓展,無重大安全性問題堂污。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末家肯,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子盟猖,更是在濱河造成了極大的恐慌讨衣,老刑警劉巖换棚,帶你破解...
    沈念sama閱讀 211,290評(píng)論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異反镇,居然都是意外死亡固蚤,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,107評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門歹茶,熙熙樓的掌柜王于貴愁眉苦臉地迎上來夕玩,“玉大人,你說我怎么就攤上這事惊豺×敲希” “怎么了?”我有些...
    開封第一講書人閱讀 156,872評(píng)論 0 347
  • 文/不壞的土叔 我叫張陵尸昧,是天一觀的道長(zhǎng)揩页。 經(jīng)常有香客問我,道長(zhǎng)彻磁,這世上最難降的妖魔是什么碍沐? 我笑而不...
    開封第一講書人閱讀 56,415評(píng)論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮衷蜓,結(jié)果婚禮上累提,老公的妹妹穿的比我還像新娘。我一直安慰自己磁浇,他們只是感情好斋陪,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,453評(píng)論 6 385
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著置吓,像睡著了一般无虚。 火紅的嫁衣襯著肌膚如雪改执。 梳的紋絲不亂的頭發(fā)上沦疾,一...
    開封第一講書人閱讀 49,784評(píng)論 1 290
  • 那天李剖,我揣著相機(jī)與錄音变丧,去河邊找鬼。 笑死虑稼,一個(gè)胖子當(dāng)著我的面吹牛梦裂,可吹牛的內(nèi)容都是我干的童社。 我是一名探鬼主播告匠,決...
    沈念sama閱讀 38,927評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼戈抄,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了后专?” 一聲冷哼從身側(cè)響起划鸽,我...
    開封第一講書人閱讀 37,691評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后裸诽,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體嫂用,經(jīng)...
    沈念sama閱讀 44,137評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,472評(píng)論 2 326
  • 正文 我和宋清朗相戀三年崭捍,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了尸折。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片啰脚。...
    茶點(diǎn)故事閱讀 38,622評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡殷蛇,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出橄浓,到底是詐尸還是另有隱情粒梦,我是刑警寧澤,帶...
    沈念sama閱讀 34,289評(píng)論 4 329
  • 正文 年R本政府宣布荸实,位于F島的核電站匀们,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏准给。R本人自食惡果不足惜泄朴,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,887評(píng)論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望露氮。 院中可真熱鬧祖灰,春花似錦、人聲如沸畔规。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,741評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)叁扫。三九已至三妈,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間莫绣,已是汗流浹背畴蒲。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評(píng)論 1 265
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留对室,地道東北人模燥。 一個(gè)月前我還...
    沈念sama閱讀 46,316評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像软驰,于是被迫代替她去往敵國(guó)和親涧窒。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,490評(píng)論 2 348

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