JsBridge工作原理分析

項(xiàng)目地址

https://github.com/lzyzsd/JsBridge

輔助資料

WebView的Js對象注入漏洞解決方案
WebView開車指南

因果關(guān)系

這是第一次真正意義上,閱讀源碼橙凳,起因是公司的一款A(yù)pp有大量需求是原生要與網(wǎng)頁之間進(jìn)行交互,而大家都知道Android低版本下與網(wǎng)頁交互之間存在漏洞普办,還不了解這個漏洞是怎么回事的可參照WebView的Js對象注入漏洞解決方案,再此不在贅述徘钥。

總體概括

  • java 調(diào)用js的方式webview.loadUrl("javascript:方法名");
  • js調(diào)用java的方式通過iframe.src的方式觸發(fā)shouldOverrideUrlLoading方法
  • 從哪來就回哪去(從哪個端來就回到哪個端去)
  • 你call我衔蹲,我就response給你(responseId和callbackId的轉(zhuǎn)換)

核心類

JsBridge核心類

尋找入口

拿到這個工程后,首先要找到程序的入口呈础,不難發(fā)現(xiàn)舆驶,在示例代碼中:

webView.loadUrl("file:///android_asset/demo.html");

而該WebView是JsBridge里的BridgeWebView橱健,代碼有:

<com.github.lzyzsd.jsbridge.BridgeWebView
  android:id="@+id/webView"
  android:layout_width="match_parent"
  android:layout_height="match_parent" >
</com.github.lzyzsd.jsbridge.BridgeWebView>

//代碼中初始化
webView = (BridgeWebView) findViewById(R.id.webView);

我們將目光轉(zhuǎn)移到這個BridgeWebView上,該類繼承系統(tǒng)WebView贞远,順帶也看看當(dāng)這個類初始化都做了哪些事情代碼有:

public class BridgeWebView extends WebView {
    // ......
   public BridgeWebView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }
    public BridgeWebView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init();
    }
    public BridgeWebView(Context context) {
        super(context);
        init();
    }

    private void init() {
        this.setVerticalScrollBarEnabled(false);
        this.setHorizontalScrollBarEnabled(false);
        this.getSettings().setJavaScriptEnabled(true);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            WebView.setWebContentsDebuggingEnabled(true);
        }
        // 設(shè)置WebViewClient
        this.setWebViewClient(generateBridgeWebViewClient());
    }

    protected BridgeWebViewClient generateBridgeWebViewClient() {
        return new BridgeWebViewClient(this);
    }
    //......
}

至此程序已經(jīng)將我們的目光鎖定在了BridgeWebViewClient中畴博,該類同樣是繼承系統(tǒng)WebViewClient,其關(guān)鍵點(diǎn)是在onPageFinished方法中蓝仲,程序是在此處給目標(biāo)網(wǎng)頁注入了一段javascript代碼,這段javascript代碼應(yīng)該就是工程名中的Bridge官疲,至于是如何起到Bridge效果的袱结,敬請看下文介紹,代碼有:

public class BridgeWebViewClient extends WebViewClient {
    //......
    @Override
    public void onPageFinished(WebView view, String url) {
        super.onPageFinished(view, url);

        //頁面加載結(jié)束, 注入本地js代碼
        if (BridgeWebView.toLoadJs != null) {
            BridgeUtil.webViewLoadLocalJs(view, BridgeWebView.toLoadJs);
        }

        if (webView.getStartupMessage() != null) {
            for (Message m : webView.getStartupMessage()) {
                webView.dispatchMessage(m);
            }
            webView.setStartupMessage(null);
        }
    }
    //......
}

接著程序的路徑往下走途凫,我們看看
BridgeUtil.webViewLoadLocalJs(view, BridgeWebView.toLoadJs);都做了哪些操作垢夹,代碼有:

public static void webViewLoadLocalJs(WebView view, String path){
    //讀取本地js代碼
    String jsContent = assetFile2Str(view.getContext(), path);
    
    //通過loadUrl的方式調(diào)用網(wǎng)頁方法
    view.loadUrl("javascript:" + jsContent);
}

至此程序已經(jīng)初始化完畢,接下來我們將工程的功能分為兩個方面來展開分析维费,一個是 java call js ,另一個則是 js call java

java call js

言外之意也便是客戶端調(diào)用網(wǎng)頁方法果元,要完成這個操作客戶端和網(wǎng)頁都需要初始化一些工作,代碼有:
客戶端:

@Override
public void onClick(View v) {
    if (button.equals(v)) {
        webView.callHandler("functionInJs", "data from Java", new CallBackFunction() {
            @Override
            public void onCallBack(String data) {
                Log.i(TAG, "reponse data from js :" + data);
            }
        });
    }
}

網(wǎng)頁端:

bridge.registerHandler("functionInJs", function(data, responseCallback) {
    document.getElementById("show").innerHTML = ("data from Java: = " + data);
    var responseData = "Javascript Says Right back aka!";
    responseCallback(responseData);
});

//而調(diào)用了bridge.registerHandler方法后,js庫中做的操作有
function registerHandler(handlerName, handler) {
    //保存該handler犀盟,即上文中的function(data, responseCallback)
    messageHandlers[handlerName] = handler;
}

既然是java call js那么主動權(quán)就應(yīng)在java端而晒,那么我們來看看客戶端的初始化工作具體做了哪些事情以及當(dāng)觸發(fā)了事件后都做了什么,代碼有:

public void callHandler(String handlerName, String data, CallBackFunction callBack) {
    doSend(handlerName, data, callBack);
}

private void doSend(String handlerName, String data, CallBackFunction responseCallback) {
    //準(zhǔn)備Message
    Message m = new Message();
    if (!TextUtils.isEmpty(data)) {
        //為message添加data元素
        m.setData(data);
    }
    
    //保存responseCallback
    if (responseCallback != null) {
        String callbackStr = String.format(BridgeUtil.CALLBACK_ID_FORMAT, ++uniqueId + (BridgeUtil.UNDERLINE_STR + SystemClock.currentThreadTimeMillis()));
        responseCallbacks.put(callbackStr, responseCallback);
        
        //為message添加callbackId元素
        m.setCallbackId(callbackStr);
    }
    
    if (!TextUtils.isEmpty(handlerName)) {

        //為message添加handlerName元素
        m.setHandlerName(handlerName);
    }
    queueMessage(m);
}

private void queueMessage(Message m) {
    if (startupMessage != null) {
        startupMessage.add(m);
    } else {
        dispatchMessage(m);
    }
}

void dispatchMessage(Message m) {
    String messageJson = m.toJson();
    //escape special characters for json string
    messageJson = messageJson.replaceAll("(\\\\)([^utrn])", "\\\\\\\\$1$2");
    messageJson = messageJson.replaceAll("(?<=[^\\\\])(\")", "\\\\\"");
    String javascriptCommand = String.format(BridgeUtil.JS_HANDLE_MESSAGE_FROM_JAVA, messageJson);
    if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
        //程序走到這里阅畴,已經(jīng)將程序拉扯到了網(wǎng)頁倡怎,別忘了看概括中客戶端是如何和網(wǎng)頁之間拉扯的
        this.loadUrl(javascriptCommand);
    }
}

上述代碼主要的作用是構(gòu)建Message,以及保存responseCallback,為的是接收網(wǎng)頁響應(yīng)數(shù)據(jù)的一個回調(diào)。執(zhí)行完this.loadUrl(javascriptCommand);后贱枣,讓我們來看看是要去執(zhí)行js庫中的哪個方法监署,根據(jù)斷點(diǎn)或者打印日志的方式最后拿到javascriptCommand的值是:

javascriptCommand="javascript:WebViewJavascriptBridge._handleMessageFromNative('{\"callbackId\":\"JAVA_CB_1_3799\",\"data\":\"data from Java\",\"handlerName\":\"functionInJs\"}');"

所以程序來到j(luò)s中,客戶端將傳遞的data,作為_handleMessageFromNative()函數(shù)的參數(shù)纽哥,對應(yīng)的_handleMessageFromNative()方法有:

function _handleMessageFromNative(messageJSON) {
    console.log(messageJSON);
    if (receiveMessageQueue && receiveMessageQueue.length > 0) {
        receiveMessageQueue.push(messageJSON);
    } else {
        //程序走到這里
        _dispatchMessageFromNative(messageJSON);
    }
}

function _dispatchMessageFromNative(messageJSON) {
    setTimeout(function() {
        var message = JSON.parse(messageJSON);
        var responseCallback;
        if (message.responseId) {
            responseCallback = responseCallbacks[message.responseId];
            if (!responseCallback) {
                return;
            }
            responseCallback(message.responseData);
            delete responseCallbacks[message.responseId];
        } else {
            //還記得我們message中元素嗎钠乏? callbackId,handlerName, data,所以程序會走到這里
            if (message.callbackId) {
                var callbackResponseId = message.callbackId;
                //聲明 responseCallback春塌,注意里面會調(diào)用 _doSend方法晓避,稍后會用到
                responseCallback = function(responseData) {
                    _doSend({
                        responseId: callbackResponseId,
                        responseData: responseData
                    });
                };
            }
            
            var handler = WebViewJavascriptBridge._messageHandler;
            if (message.handlerName) {
                //還記得網(wǎng)頁在初始化的時候,保存的handler嗎摔笤?
                handler = messageHandlers[message.handlerName];
            }
            //查找指定handler
            try {
                //通過handlerName能到Handler够滑,也就是調(diào)用demo.html中registerHandler時的那個callback函數(shù),data是客戶端給網(wǎng)頁的數(shù)據(jù)吕世,而responseCallback就是網(wǎng)頁在收到消息后回調(diào)給客戶端的Callback
                handler(message.data, responseCallback);
            } catch (exception) {
                if (typeof console != 'undefined') {
                    console.log("WebViewJavascriptBridge: WARNING: javascript handler threw.", message, exception);
                }
            }
        }
    });
}

通過上面的js代碼handler(message.data, responseCallback);data我們不再關(guān)心彰触,網(wǎng)頁拿到它后想看什么就干什么主要還是看心情吧!我們現(xiàn)在關(guān)心的是這個responseCallback是干嘛的命辖?因?yàn)閺哪睦飦砘啬睦锶タ鲆悖覀兪菑目蛻舳硕鴣矸直停罱K也要葉落歸根回到客戶端。言歸正傳還記得剛才那個responseCallback是怎樣聲明的嗎尔许?這里我們需要注意的是js代碼將客戶端的callbackId轉(zhuǎn)換成了responseId,至于為什么轉(zhuǎn)換么鹤,看看概括中說的,你call我味廊,我就response給你蒸甜,如下:

responseCallback = function(responseData) {
    _doSend({
        responseId: callbackResponseId,
        responseData: responseData
    });
};

那么讓我們來看看_doSend方法都做了哪些工作,注意關(guān)鍵轉(zhuǎn)折點(diǎn)就在此處了余佛,看看是如何將網(wǎng)頁代碼又再次拉扯到客戶端的:

function _doSend(message, responseCallback) {
    if (responseCallback) {
        var callbackId = 'cb_' + (uniqueId++) + '_' + new Date().getTime();
        responseCallbacks[callbackId] = responseCallback;
        message.callbackId = callbackId;
    }
    //保存網(wǎng)頁返回給客戶端的數(shù)據(jù)
    sendMessageQueue.push(message);

    //通過Iframe.src來觸發(fā)客戶端的shouldOverrideUrlLoading方法柠新,進(jìn)而將網(wǎng)頁拉回到客戶端
    messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
}

注意上面有提到保存網(wǎng)頁返回給客戶端的數(shù)據(jù),既然push了數(shù)據(jù)辉巡,那么肯定會回來取該數(shù)據(jù)(至于什么時候來取請接著往下看)恨憎。還有很重要的一點(diǎn)是通過Iframe.src來觸發(fā)客戶端的shouldOverrideUrlLoading方法,進(jìn)而將網(wǎng)頁拉回到客戶端,所以程序又回到了客戶端的shouldOverrideUrlLoading方法郊楣,代碼有:

@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
    try {
        url = URLDecoder.decode(url, "UTF-8");
    } catch (UnsupportedEncodingException e) {
        e.printStackTrace();
    }

    // 如果是返回數(shù)據(jù)
    if (url.startsWith(BridgeUtil.YY_RETURN_DATA)) { 
        webView.handlerReturnData(url);
        return true;
    //自定義schema
    } else if (url.startsWith(BridgeUtil.YY_OVERRIDE_SCHEMA)) { 
        //根據(jù)js的代碼可得知url是yy://__QUEUE_MESSAGE__/
        webView.flushMessageQueue();
        return true;
    } else {
        return super.shouldOverrideUrlLoading(view, url);
    }
}

走到這個地方后憔恳,通俗理解是客戶端已經(jīng)得知了js有給我響應(yīng)數(shù)據(jù)了,至于是什么數(shù)據(jù)净蚤,以及該怎么去取钥组,派誰去取目前還是個問題,閑話少活塞栅,繼續(xù)追蹤代碼:

void flushMessageQueue() {
    if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
        loadUrl(BridgeUtil.JS_FETCH_QUEUE_FROM_JAVA, new CallBackFunction() {

            @Override
            public void onCallBack(String data) {
                List<Message> list = null;
                try {
                    list = Message.toArrayList(data);
                } catch (Exception e) {
                    e.printStackTrace();
                    return;
                }
                if (list == null || list.size() == 0) {
                    return;
                }
                for (int i = 0; i < list.size(); i++) {
                    Message m = list.get(i);
                    String responseId = m.getResponseId();
                    if (!TextUtils.isEmpty(responseId)) {
                        CallBackFunction function = responseCallbacks.get(responseId);
                        String responseData = m.getResponseData();
                        function.onCallBack(responseData);
                        responseCallbacks.remove(responseId);
                    } else {
                        CallBackFunction responseFunction = null;
                        final String callbackId = m.getCallbackId();
                        if (!TextUtils.isEmpty(callbackId)) {
                            responseFunction = new CallBackFunction() {
                                @Override
                                public void onCallBack(String data) {
                                    Message responseMsg = new Message();
                                    responseMsg.setResponseId(callbackId);
                                    responseMsg.setResponseData(data);
                                    queueMessage(responseMsg);
                                }
                            };
                        } else {
                            responseFunction = new CallBackFunction() {
                                @Override
                                public void onCallBack(String data) {
                                    // do nothing
                                }
                            };
                        }
                        BridgeHandler handler;
                        if (!TextUtils.isEmpty(m.getHandlerName())) {
                            handler = messageHandlers.get(m.getHandlerName());
                        } else {
                            handler = defaultHandler;
                        }
                        if (handler != null){
                            handler.handler(m.getData(), responseFunction);
                        }
                    }
                }
            }
        });
    }
}

可以看看上面的flushMessageQueue()方法一言不合就先調(diào)用了loadUrl方法者铜,我們不得不好奇這個方法做了哪些操作:

public void loadUrl(String jsUrl, CallBackFunction returnCallback) {
    //jsUrl="javascript:WebViewJavascriptBridge._fetchQueue();"
    this.loadUrl(jsUrl);
    responseCallbacks.put(BridgeUtil.parseFunctionName(jsUrl), returnCallback);
}

loadUrl的主要功能是客戶端要派人去取網(wǎng)頁返回給客戶端的數(shù)據(jù)了,在此時此刻放椰,先給這個取數(shù)據(jù)的人起一個名字作烟,即_fetchQueue,然后記錄在小本本上,方便取回來后匯報工作給領(lǐng)導(dǎo)砾医。既然已經(jīng)派人去取東西拿撩,那我們悄悄跟著它:

function _fetchQueue() {
    //取事先在_doSend方法中保存的數(shù)據(jù)
    var messageQueueString = JSON.stringify(sendMessageQueue);

    //取完之后清空數(shù)據(jù)隊(duì)列
    sendMessageQueue = [];

    //Iframe.src=yy://return/_fetchQueue/[{"responseId":"JAVA_CB_1_2522","responseData":"Javascript Says Right back aka!"}]
    //元素有responseId  responseData
    messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://return/_fetchQueue/' + encodeURIComponent(messageQueueString);
}

取完數(shù)據(jù)之后,高高興興趕緊給領(lǐng)導(dǎo)報告如蚜,同樣是通過Iframe.src的方式來觸發(fā)shouldOverrideUrlLoading方法

@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
    try {
        url = URLDecoder.decode(url, "UTF-8");
    } catch (UnsupportedEncodingException e) {
        e.printStackTrace();
    }

    // 如果是返回數(shù)據(jù)
    if (url.startsWith(BridgeUtil.YY_RETURN_DATA)) { 
        //根據(jù)js的代碼可得知url是yy://return/_fetchQueue/[{"responseId":"JAVA_CB_1_2522","responseData":"Javascript Says Right back aka!"}]
        webView.handlerReturnData(url);
        return true;
    //自定義schema
    } else if (url.startsWith(BridgeUtil.YY_OVERRIDE_SCHEMA)) { 
        webView.flushMessageQueue();
        return true;
    } else {
        return super.shouldOverrideUrlLoading(view, url);
    }
}

客戶端拿到這個Url之后就要解析該Url,因?yàn)閁rl里面有網(wǎng)頁返回的數(shù)據(jù)

void handlerReturnData(String url) {
    //解析url拿到函數(shù)名稱
    String functionName = BridgeUtil.getFunctionFromReturnUrl(url);
    
    //通過函數(shù)名稱獲取CallBackFunction
    CallBackFunction f = responseCallbacks.get(functionName);
    
    //解析Url獲取數(shù)據(jù)
    String data = BridgeUtil.getDataFromReturnUrl(url);
    if (f != null) {
        //獲取數(shù)據(jù)響應(yīng)給客戶端
        f.onCallBack(data);
        //清除該消息
        responseCallbacks.remove(functionName);
        return;
    }
}

還記得那個小本本上記錄的東西嗎压恒?_fetchQueue有沒有想到點(diǎn)什么?所以我們需要在小本本上查找到這個_fetchQueuecallback
這個callback其實(shí)就是在flushMessageQueue()方法中調(diào)用loadUrl時候保存的callback,所以程序就要回到那個callback中:

void flushMessageQueue() {
    if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
        loadUrl(BridgeUtil.JS_FETCH_QUEUE_FROM_JAVA, new CallBackFunction() {

            @Override
            public void onCallBack(String data) {
                //程序最終來到這里
                List<Message> list = null;
                try {
                    list = Message.toArrayList(data);
                } catch (Exception e) {
                    e.printStackTrace();
                    return;
                }
                if (list == null || list.size() == 0) {
                    return;
                }
                for (int i = 0; i < list.size(); i++) {
                    Message m = list.get(i);
                    String responseId = m.getResponseId();
                    if (!TextUtils.isEmpty(responseId)) {
                        // 進(jìn)而走到這個分支內(nèi)错邦,還記得網(wǎng)頁返回給我們的元素嗎探赫?responseId 、responseData,
                        CallBackFunction function = responseCallbacks.get(responseId);
                        String responseData = m.getResponseData();
                        function.onCallBack(responseData);
                        responseCallbacks.remove(responseId);
                    } else {
                        CallBackFunction responseFunction = null;
                        final String callbackId = m.getCallbackId();
                        if (!TextUtils.isEmpty(callbackId)) {
                            responseFunction = new CallBackFunction() {
                                @Override
                                public void onCallBack(String data) {
                                    Message responseMsg = new Message();
                                    responseMsg.setResponseId(callbackId);
                                    responseMsg.setResponseData(data);
                                    queueMessage(responseMsg);
                                }
                            };
                        } else {
                            responseFunction = new CallBackFunction() {
                                @Override
                                public void onCallBack(String data) {
                                    // do nothing
                                }
                            };
                        }
                        BridgeHandler handler;
                        if (!TextUtils.isEmpty(m.getHandlerName())) {
                            handler = messageHandlers.get(m.getHandlerName());
                        } else {
                            handler = defaultHandler;
                        }
                        if (handler != null){
                            handler.handler(m.getData(), responseFunction);
                        }
                    }
                }
            }
        });
    }
}

終于客戶端拿到了數(shù)據(jù)撬呢,終于要響應(yīng)給我們了伦吠,當(dāng)程序進(jìn)入這個分支后

if (!TextUtils.isEmpty(responseId)) {
    CallBackFunction function = responseCallbacks.get(responseId);
    String responseData = m.getResponseData();
    function.onCallBack(responseData);
    responseCallbacks.remove(responseId);
}

這里的responseId就是客戶端最起初始化生成的callbackId,只是在js_doSend方法的時候?qū)⒖蛻舳说?code>callbackId轉(zhuǎn)換為了responseId,而這個callbackId里面可存有客戶端的handler,是客戶端接收網(wǎng)頁數(shù)據(jù)的回調(diào)毛仪,程序最終回到下面代碼的onCallBack方法中:

@Override
public void onClick(View v) {
    if (button.equals(v)) {
        webView.callHandler("functionInJs", "data from Java", new CallBackFunction() {
            @Override
            public void onCallBack(String data) {
                Log.i(TAG, "reponse data from js :" + data);
            }
        });
    }
}

可以舒口氣了搁嗓,至此打完收工,特奉上整個流程的示意圖:

java_call_js

js call java

上面費(fèi)了九牛二虎之力箱靴,終于將java call js說清楚腺逛,好的那我們接下來繼續(xù)談?wù)?strong>js call java那些事,想多了衡怀,想多了棍矛。。狈癞。由于有了上面的基礎(chǔ)茄靠,理解js call java并沒有那么難了,還是交給聰明的你去完成蝶桶,在此只奉上流程示意圖。

js_call_java

總結(jié)

通過一系列打斷點(diǎn)打印日志的方式掉冶,完美的熟悉了解了JsBridge的工作原理真竖,讀完之后,體會到了程序設(shè)計之巧妙以及簡潔厌小,雖不是很復(fù)雜恢共,但為以后閱讀其他源碼打下了堅(jiān)實(shí)的基礎(chǔ),由易至難璧亚,以此為起點(diǎn)再次出發(fā)讨韭!最后感謝您耐心的看完了整篇文章。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末癣蟋,一起剝皮案震驚了整個濱河市透硝,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌疯搅,老刑警劉巖濒生,帶你破解...
    沈念sama閱讀 222,183評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異幔欧,居然都是意外死亡罪治,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,850評論 3 399
  • 文/潘曉璐 我一進(jìn)店門礁蔗,熙熙樓的掌柜王于貴愁眉苦臉地迎上來觉义,“玉大人,你說我怎么就攤上這事浴井∩购В” “怎么了?”我有些...
    開封第一講書人閱讀 168,766評論 0 361
  • 文/不壞的土叔 我叫張陵,是天一觀的道長厉碟。 經(jīng)常有香客問我喊巍,道長,這世上最難降的妖魔是什么箍鼓? 我笑而不...
    開封第一講書人閱讀 59,854評論 1 299
  • 正文 為了忘掉前任崭参,我火速辦了婚禮,結(jié)果婚禮上款咖,老公的妹妹穿的比我還像新娘何暮。我一直安慰自己,他們只是感情好铐殃,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,871評論 6 398
  • 文/花漫 我一把揭開白布海洼。 她就那樣靜靜地躺著,像睡著了一般富腊。 火紅的嫁衣襯著肌膚如雪坏逢。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,457評論 1 311
  • 那天赘被,我揣著相機(jī)與錄音是整,去河邊找鬼。 笑死民假,一個胖子當(dāng)著我的面吹牛浮入,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播羊异,決...
    沈念sama閱讀 40,999評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼事秀,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了野舶?” 一聲冷哼從身側(cè)響起易迹,我...
    開封第一講書人閱讀 39,914評論 0 277
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎筒愚,沒想到半個月后赴蝇,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,465評論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡巢掺,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,543評論 3 342
  • 正文 我和宋清朗相戀三年句伶,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片陆淀。...
    茶點(diǎn)故事閱讀 40,675評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡考余,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出轧苫,到底是詐尸還是另有隱情楚堤,我是刑警寧澤疫蔓,帶...
    沈念sama閱讀 36,354評論 5 351
  • 正文 年R本政府宣布,位于F島的核電站身冬,受9級特大地震影響衅胀,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜酥筝,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,029評論 3 335
  • 文/蒙蒙 一滚躯、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧嘿歌,春花似錦掸掏、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,514評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至步脓,卻和暖如春愿待,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背靴患。 一陣腳步聲響...
    開封第一講書人閱讀 33,616評論 1 274
  • 我被黑心中介騙來泰國打工呼盆, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人蚁廓。 一個月前我還...
    沈念sama閱讀 49,091評論 3 378
  • 正文 我出身青樓,卻偏偏與公主長得像厨幻,于是被迫代替她去往敵國和親相嵌。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,685評論 2 360

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

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,304評論 25 707
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理况脆,服務(wù)發(fā)現(xiàn)饭宾,斷路器,智...
    卡卡羅2017閱讀 134,707評論 18 139
  • 這篇博客主要來介紹 WebView 的相關(guān)使用方法,常見的幾個漏洞盛末,開發(fā)中可能遇到的坑和最后解決相應(yīng)漏洞的源碼弹惦,以...
    Shawn_Dut閱讀 7,238評論 3 55
  • 今天公司搬遷,從原來的財富大酒店搬到西湖北路碧雅苑小區(qū)悄但,由于剛搬過來棠隐,還有很多東西沒有完善。 當(dāng)我看到新的辦公樓時...
    會香閱讀 3,178評論 0 0
  • 1.有清晰的定位: 清晰的定位影響到公眾號的推廣方式檐嚣、用戶的忠誠度助泽,一個好的公眾號,不僅要看粉絲數(shù)、粉絲增長率嗡贺,還...
    Echo_notes閱讀 575評論 0 1