Android Hybrid App通信

在Android開發(fā)中,能夠?qū)崿F(xiàn)Web調(diào)用Native代碼的方法主要有以下方法:
1.Schema:WebView攔截頁面跳轉(zhuǎn)
2.JavascriptInterface
3.WebChromeClient.onJsPrompt()
而native代碼通信js只能使用WebView.loadUrl(“javascript:function()”)
若需要Native與js進行雙向通信,則可使用JSBridge,同時,還有一些開源框架如:safe-java-js-webview-bridge等
以下就詳細介紹這些通信方法:

1.Schema:WebView攔截頁面跳轉(zhuǎn)

這種方法實現(xiàn)相對簡單,例如,在HTML界面中添加如下代碼:
<a href="myapp://tonative/param?id=123">gotoActivity</a>
然后要在要跳轉(zhuǎn)到的Activity中進行聲明:
<activity android:name=".Activity"> <intent-filter> <action android:name="android.intent.action.VIEW"/> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.BROWSABLE" /> <data android:host="tonative" android:scheme="myapp" /> </intent-filter> </activity>
我們可以通過聲明不同的host實現(xiàn)打開不同的Activity,在打開的Activity里可以通過如下代碼獲取html頁面?zhèn)鬟^來的參數(shù):

    Intent intent = getIntent();
    String action = intent.getAction();
    if(Intent.ACTION_VIEW.equals(action)){
        Uri uri = intent.getData();
        if(uri != null){
            String id = uri.getQueryParameter("id");
            Toast.makeText(this,id,Toast.LENGTH_LONG).show();
        }
    }

但這樣其實有個問題,我們一般會重寫WebViewClient的shouldOverrideUrlLoading方法來實現(xiàn)在本頁內(nèi)的跳轉(zhuǎn)都是由本W(wǎng)ebview打開,而不是跳轉(zhuǎn)到系統(tǒng)瀏覽器處理。這樣設(shè)置后,‘href=”myapp://tonative/param?id=123”’這樣的請求也被攔截到了本W(wǎng)ebview里,從而失效,因此,我們需要做一個判斷

wv.setWebViewClient(new WebViewClient(){
        @Override
        public boolean shouldOverrideUrlLoading(WebView view, String url) {
            String scheme = Uri.parse(url).getScheme();//還需要判斷host
            if (TextUtils.equals("myapp", scheme)) {
                Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
                startActivity(intent);
                return true;
            }
            return false;
        }

return true,表明這次請求交給系統(tǒng)來處理。

2.JavascriptInterface

首先Java代碼要實現(xiàn)一個類,它的作用是提供給Javascript調(diào)用。

public class JavascriptInterface {
  @JavascriptInterface
  public void showToast(String toast) {
      Toast.makeText(MainActivity.this, toast, Toast.LENGTH_SHORT).show();
  }
}

然后把這個類添加到WebView的JavascriptInterface中。
webView.addJavascriptInterface(new JavascriptInterface(), "javascriptInterface");
在Javascript代碼中就能直接通過“javascriptInterface”直接調(diào)用了該Native的類的方法。
function showToast(toast) { javascript:javascriptInterface.showToast(toast); }

3.WebChromeClient.onJsPrompt()

其實除了WebChromeClient.onJsPrompt(),還有WebChromeClient.onJsAlert()和WebChromeClient.onJsConfirm()。顧名思義,這三個Js給Native代碼的回調(diào)接口的作用分別是展示提示信息,展示警告信息和展示確認信息。鑒于,alert和confirm在Js的使用率很高,所以JSBridge的解決方案中都傾向于選用onJsPrompt()。
Js中調(diào)用window.prompt(message, value)
WebChromeClient.onJsPrompt()就會受到回調(diào)。onJsPrompt()方法的message參數(shù)的值正是Js的方法window.prompt()的message的值。

public class CustomWebChromeClient extends WebChromeClient {
  @Override
  public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult 
result) {
    // 處理JS 的調(diào)用邏輯
    result.confirm();
    return true;
  }
}

4.JSBridge

JSBridge可以實現(xiàn)JavaScript與Native層間的通信,native層調(diào)用js方法可使用WebView.loadUrl(“javascript:function()”),js調(diào)用native層可使用prompt來實現(xiàn)。JSBridge實現(xiàn)的原理和過程如下圖所示:

JSBridge.PNG

4.1.定義js和native層的通信協(xié)議

首先需要定義js和native層的通信協(xié)議:
jsbridge://className:callbackAddress/methodName?jsonObj
className為js調(diào)用native的相應(yīng)的類名,methodName為調(diào)用的該類的一個方法,jsonObj為該方法所傳入的參數(shù)。
我們在js中調(diào)用native方法的時候,在js中注冊一個callback,然后將該callback在指定的位置上緩存起來,然后native層執(zhí)行完畢對應(yīng)方法后通過WebView.loadUrl調(diào)用js中的方法,回調(diào)對應(yīng)的callback。協(xié)議中的callbackAddress即為即為js中對應(yīng)的回調(diào)函數(shù)。

4.2.JS調(diào)用native層

js中要完成通信功能,要有兩個方法call、onFinish,其中方法call首先要能夠生成native需要的協(xié)議uri,還要將callback對象存儲在callbacks數(shù)組中,存儲的位置即為port,并調(diào)用window.prompt(uri, “”)將uri傳遞到native層。另一個方法onFinish用于接受native回傳的port值和執(zhí)行結(jié)果,根據(jù)port值從callbacks中得到原始的callback函數(shù),執(zhí)行callback函數(shù),之后從callbacks中刪除。最后將這兩個函數(shù)暴露給外部的JSBrige對象,通過一個for循環(huán)一一賦值即可。實現(xiàn)代碼如下:

(function (win) {
var hasOwnProperty = Object.prototype.hasOwnProperty;
var JSBridge = win.JSBridge || (win.JSBridge = {});
var JSBRIDGE_PROTOCOL = 'JSBridge';
var Inner = {
    callbacks: {},
    call: function (obj, method, params, callback) {
        console.log(obj+" "+method+" "+params+" "+callback);
        var port = Util.getPort();
        console.log(port);
        this.callbacks[port] = callback;
        var uri=Util.getUri(obj,method,params,port);
        console.log(uri);
        window.prompt(uri, "");
    },
    onFinish: function (port, jsonObj){
        var callback = this.callbacks[port];
        callback && callback(jsonObj);
        delete this.callbacks[port];
    },
};
var Util = {
    getPort: function () {      //隨機生成port
        return Math.floor(Math.random() * (1 << 30));
    },
    getUri:function(obj, method, params, port){    //生成native需要的協(xié)議uri
        params = this.getParam(params);
        var uri = JSBRIDGE_PROTOCOL + '://' + obj + ':' + port + '/' + method + '?' + params;
        return uri;
    },
    getParam:function(obj){    //生成json字符串
        if (obj && typeof obj === 'object') {
            return JSON.stringify(obj);
        } else {
            return obj || '';
        }
    }
};
for (var key in Inner) {
    if (!hasOwnProperty.call(JSBridge, key)) {
        JSBridge[key] = Inner[key];
    }
}
})(window); 

4.3.native層的實現(xiàn)

首先要將js傳來的uri獲取到,可編寫一個WebChromeClient子類:

public class JSBridgeWebChromeClient extends WebChromeClient {
  @Override
  public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
      result.confirm(JSBridge.callJava(view, message));
      return true;
  }
}

然后要將該對象設(shè)置給WebView:
mWebView.setWebChromeClient(new JSBridgeWebChromeClient());

之后就是JSBridgeWebChromeClient中調(diào)用的JSBridge類的實現(xiàn),首先JSBridge類要有一個統(tǒng)一管理暴露給js調(diào)用的類和方法,并且能實時添加:
JSBridge.register("jsName",javaClass.class)
這個javaClass就是滿足某種規(guī)范的類,我們規(guī)定這個類需要實現(xiàn)一個空接口,主要作用就混淆的時候不會發(fā)生錯誤,還有一個作用就是約束JSBridge.register方法第二個參數(shù)必須是該接口的實現(xiàn)類。那么我們定義這個接口
public interface IBridge{}
該類中有滿足規(guī)范的方法,該方法不具有返回值,因為返回值我們在回調(diào)中返回,因此參數(shù)列表應(yīng)含有一個callback,除了callback,還要有js傳來的方法調(diào)用所需的參數(shù),是一個json對象;方法的執(zhí)行結(jié)果需要通過callback傳遞回去,而java執(zhí)行js方法需要一個WebView對象,因此,暴露給js的方法應(yīng)滿足:
public static void methodName(WebView webview,JSONObject jsonObj,Callback callback){}

register方法的實現(xiàn)原理為,從一個Map中查找key是不是存在,不存在則反射拿到對應(yīng)的Class中的所有方法,將方法是public static void 類型的,并且參數(shù)是三個參數(shù),分別是Webview,JSONObject,Callback類型的,如果滿足條件,則將所有滿足條件的方法put進去。代碼如下:

public class JSBridge {
  private static Map<String, HashMap<String, Method>> exposedMethods = new HashMap<>();
  public static void register(String exposedName, Class<? extends IBridge> clazz) {
  if (!exposedMethods.containsKey(exposedName)) {
        try {
            exposedMethods.put(exposedName, getAllMethod(clazz));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
private static HashMap<String, Method> getAllMethod(Class injectedCls) throws Exception {
    HashMap<String, Method> mMethodsMap = new HashMap<>();
    Method[] methods = injectedCls.getDeclaredMethods();
    for (Method method : methods) {
        String name;
        if (method.getModifiers() != (Modifier.PUBLIC | Modifier.STATIC) || (name = method.getName()) == null) {
            continue;
        }
        Class[] parameters = method.getParameterTypes();
        if (null != parameters && parameters.length == 3) {
            if (parameters[0] == WebView.class && parameters[1] == JSONObject.class && parameters[2] == Callback.class) {
                mMethodsMap.put(name, method);
            }
        }
    }
    return mMethodsMap;
}
}

JSBridge類中的callJava方法,功能是將js傳來的uri進行解析,然后根據(jù)調(diào)用的類名別名從剛剛的map中查找是不是存在,存在的話拿到該類所有方法的methodMap,然后根據(jù)方法名從methodMap拿到方法,反射調(diào)用,并將參數(shù)傳進去,參數(shù)就是前文說的滿足條件的三個參數(shù),即WebView,JSONObject,Callback。

public static String callJava(WebView webView, String uriString) {
    String methodName = "";
    String className = "";
    String param = "{}";
    String port = "";
    if (!TextUtils.isEmpty(uriString) && uriString.startsWith("JSBridge")) {
        Uri uri = Uri.parse(uriString);
        className = uri.getHost();
        param = uri.getQuery();
        port = uri.getPort() + "";
        String path = uri.getPath();
        if (!TextUtils.isEmpty(path)) {
            methodName = path.replace("/", "");
        }
    }


    if (exposedMethods.containsKey(className)) {
        HashMap<String, Method> methodHashMap = exposedMethods.get(className);

        if (methodHashMap != null && methodHashMap.size() != 0 && methodHashMap.containsKey(methodName)) {
            Method method = methodHashMap.get(methodName);
            if (method != null) {
                try {
                    method.invoke(null, webView, new JSONObject(param), new Callback(webView, port));
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }
    return null;
}

可以看到該方法中使用了 new Callback(webView, port)進行新建對象,該對象就是用來回調(diào)js中回調(diào)方法的java對應(yīng)的類。這個類你需要將js傳來的port傳進來之外,還需要將WebView的引用傳進來,因為要使用到WebView的loadUrl方法,為了防止內(nèi)存泄露,這里使用弱引用。如果你需要回調(diào)js的callback,在對應(yīng)的方法里調(diào)用一下callback.apply()方法將返回數(shù)據(jù)傳入即可。

public class Callback {
  private static Handler mHandler = new Handler(Looper.getMainLooper());
  private static final String CALLBACK_JS_FORMAT = "javascript:JSBridge.onFinish('%s', %s);";
  private String mPort;
  private WeakReference<WebView> mWebViewRef;

  public Callback(WebView view, String port) {
    mWebViewRef = new WeakReference<>(view);
    mPort = port;
  }


  public void apply(JSONObject jsonObject) {
      final String execJs = String.format(CALLBACK_JS_FORMAT, mPort, String.valueOf(jsonObject));
      if (mWebViewRef != null && mWebViewRef.get() != null) {
        mHandler.post(new Runnable() {
            @Override
            public void run() {
                mWebViewRef.get().loadUrl(execJs);
            }
        });
     }
   }
}

通過Callback中的mWebViewRef.get().loadUrl("javascript:JSBridge.onFinish(port,jsonObj);"),就可將結(jié)果返回給js。

5.safe-java-js-webview-bridge

這個開源項目用到的原理也是JSBridge,都是js調(diào)用prompt函數(shù),傳輸一些參數(shù),onJsPrompt方法攔截到prompt動作,然后解析數(shù)據(jù),最后調(diào)用相應(yīng)的Native方法。

HostJsScope類中定義了所有可以被js調(diào)用的方法,這些方法都必須是靜態(tài)方法,并且所有的方法第一個參數(shù)必須是WebView。

這個庫中一個最關(guān)鍵的叫做JsCallJava,這個實現(xiàn)的就是js來調(diào)用Java方法的功能,這個類只用于InjectedWebChromeClient類

public class InjectedChromeClient extends WebChromeClient {

  private JsCallJava mJsCallJava;
  private boolean mIsInjectedJS;

  public InjectedChromeClient(String injectedName, Class injectedCls) {
    this(new JsCallJava(injectedName, injectedCls));
  }

  public InjectedChromeClient(JsCallJava jsCallJava) {
    mJsCallJava = jsCallJava;
  }

  // 處理Alert事件
  @Override
  public boolean onJsAlert(WebView view, String url, String message, final JsResult result) {
    result.confirm();
    return true;
  }

  @Override
  public void onProgressChanged(WebView view, int newProgress) {
    //為什么要在這里注入JS
    //1 OnPageStarted中注入有可能全局注入不成功,導(dǎo)致頁面腳本上所有接口任何時候都不可用
    //2 OnPageFinished中注入,雖然最后都會全局注入成功,但是完成時間有可能太晚,當頁面在初始化調(diào)用接口函數(shù)時會等待時間過長
    //3 在進度變化時注入,剛好可以在上面兩個問題中得到一個折中處理
    //為什么是進度大于25%才進行注入,因為從測試看來只有進度大于這個數(shù)字頁面才真正得到框架刷新加載,保證100%注入成功
    if (newProgress <= 25) {
        mIsInjectedJS = false;
    } else if (!mIsInjectedJS) {
        view.loadUrl(mJsCallJava.getPreloadInterfaceJS());
        mIsInjectedJS = true;
        StopWatch.log(" inject js interface completely on progress " + newProgress);
    }
    super.onProgressChanged(view, newProgress);
  }

  @Override
  public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
      result.confirm(mJsCallJava.call(view, message));
      StopWatch.log("onJsPrompt: " + view.toString() +", " + url +", " + message +", " + defaultValue + ", " + result) ;
      return true;
  }
} 

這個InjectedWebChromeClient是設(shè)給WebView的,在onProgressChange
方法中,向WebView注入了一段js代碼,這段js代碼如下:

javascript: (function(b) {
console.log("HostApp initialization begin");
var a = {
    queue: [],
    callback: function() {
        var d = Array.prototype.slice.call(arguments, 0);
        var c = d.shift();
        var e = d.shift();
        this.queue[c].apply(this, d);
        if (!e) {
            delete this.queue[c]
        }
    }
};
a.alert = a.alert = a.alert = a.delayJsCallBack = a.getIMSI = a.getOsSdk = a.goBack = a.overloadMethod = a.overloadMethod 
    = a.passJson2Java = a.passLongType = a.retBackPassJson = a.retJavaObject = a.testLossTime = a.toast = a.toast = function() {
    var f = Array.prototype.slice.call(arguments, 0);
    if (f.length < 1) {
        throw "HostApp call error, message:miss method name"
    }
    var e = [];
    for (var h = 1; h < f.length; h++) {
        var c = f[h];
        var j = typeof c;
        e[e.length] = j;
        if (j == "function") {
            var d = a.queue.length;
            a.queue[d] = c;
            f[h] = d
        }
    }
    var g = JSON.parse(prompt(JSON.stringify({
        method: f.shift(),
        types: e,
        args: f
    })));
    if (g.code != 200) {
        throw "HostApp call error, code:" + g.code + ", message:" + g.result
    }
    return g.result
};
//有時候,我們希望在該方法執(zhí)行前插入一些其他的行為用來檢查當前狀態(tài)或是監(jiān)測
//代碼行為,這就要用到攔截(Interception)或者叫注入(Injection)技術(shù)了
/**
 * Object.getOwnPropertyName 返回一個數(shù)組,內(nèi)容是指定對象的所有屬性
 *
 * 其后遍歷這個數(shù)組,分別做以下處理:
 * 1. 備份原始屬性;
 * 2. 檢查屬性是否為 function(即方法);
 * 3. 若是重新定義該方法,做你需要做的事情,之后 apply 原來的方法體。
 */
Object.getOwnPropertyNames(a).forEach(function(d) {
    var c = a[d];
    if (typeof c === "function" && d !== "callback") {
        a[d] = function() {
            return c.apply(a, [d].concat(Array.prototype.slice.call(arguments, 0)))
        }
    }
});
b.HostApp = a;
console.log("HostApp initialization end")
})(window);

這段代碼是在JsCallJava類的構(gòu)造函數(shù)方法中生成的,這個構(gòu)造方法做的事情就是解析HostJsScope類中的方法,把每一個方法的簽名都保持到private Map<String, Method> mMethodsMap中。

 public JsCallJava (String injectedName, Class injectedCls) {
    try {
        if (TextUtils.isEmpty(injectedName)) {
            throw new Exception("injected name can not be null");
        }
        mInjectedName = injectedName;
        mMethodsMap = new HashMap<String, Method>();
        //獲取自身聲明的所有方法(包括public private protected), getMethods會獲得所有繼承與非繼承的方法
        Method[] methods = injectedCls.getDeclaredMethods();
        StringBuilder sb = new StringBuilder("javascript:(function(b){console.log(\"");
        sb.append(mInjectedName);
        sb.append(" initialization begin\");var a={queue:[],callback:function(){var d=Array.prototype.slice.call(arguments,0);var c=d.shift();var e=d.shift();this.queue[c].apply(this,d);if(!e){delete this.queue[c]}}};");
        for (Method method : methods) {
            String sign;
            if (method.getModifiers() != (Modifier.PUBLIC | Modifier.STATIC) || (sign = genJavaMethodSign(method)) == null) {
                continue;
            }
            mMethodsMap.put(sign, method);
            sb.append(String.format("a.%s=", method.getName()));
        }

        sb.append("function(){var f=Array.prototype.slice.call(arguments,0);if(f.length<1){throw\"");
        sb.append(mInjectedName);
        sb.append(" call error, message:miss method name\"}var e=[];for(var h=1;h<f.length;h++){var c=f[h];var j=typeof c;e[e.length]=j;if(j==\"function\"){var d=a.queue.length;a.queue[d]=c;f[h]=d}}var g=JSON.parse(prompt(JSON.stringify({method:f.shift(),types:e,args:f})));if(g.code!=200){throw\"");
        sb.append(mInjectedName);
        sb.append(" call error, code:\"+g.code+\", message:\"+g.result}return g.result};Object.getOwnPropertyNames(a).forEach(function(d){var c=a[d];if(typeof c===\"function\"&&d!==\"callback\"){a[d]=function(){return c.apply(a,[d].concat(Array.prototype.slice.call(arguments,0)))}}});b.");
        sb.append(mInjectedName);
        sb.append("=a;console.log(\"");
        sb.append(mInjectedName);
        sb.append(" initialization end\")})(window);");
        mPreloadInterfaceJS = sb.toString();
    } catch(Exception e){
        Log.e(TAG, "init js error:" + e.getMessage());
    }
}

與前面介紹的JSBridge不同的是,js調(diào)用native代碼時并不是拼接成一個uri形式,而是將要調(diào)用的native方法的名字、參數(shù)類型、方法參數(shù)等封裝成一端JSON字符串,通過js的prompt方法傳到onJsPrompt方法中,JsCallJava調(diào)用call(WebView view, String msg)解析json字符串,其中還會驗證json中的方法參數(shù)類型和HostJsScope中同名方法參數(shù)類型是否一致等等。

public String call(WebView webView, String jsonStr) {
    if (!TextUtils.isEmpty(jsonStr)) {
        try {
            JSONObject callJson = new JSONObject(jsonStr);
            String methodName = callJson.getString("method");
            JSONArray argsTypes = callJson.getJSONArray("types");
            JSONArray argsVals = callJson.getJSONArray("args");
            String sign = methodName;
            int len = argsTypes.length();
            Object[] values = new Object[len + 1];
            int numIndex = 0;
            String currType;

            values[0] = webView;

            for (int k = 0; k < len; k++) {
                currType = argsTypes.optString(k);
                if ("string".equals(currType)) {
                    sign += "_S";
                    values[k + 1] = argsVals.isNull(k) ? null : argsVals.getString(k);
                } else if ("number".equals(currType)) {
                    sign += "_N";
                    numIndex = numIndex * 10 + k + 1;
                } else if ("boolean".equals(currType)) {
                    sign += "_B";
                    values[k + 1] = argsVals.getBoolean(k);
                } else if ("object".equals(currType)) {
                    sign += "_O";
                    values[k + 1] = argsVals.isNull(k) ? null : argsVals.getJSONObject(k);
                } else if ("function".equals(currType)) {
                    sign += "_F";
                    values[k + 1] = new JsCallback(webView, mInjectedName, argsVals.getInt(k));
                } else {
                    sign += "_P";
                }
            }

            Method currMethod = mMethodsMap.get(sign);

            // 方法匹配失敗
            if (currMethod == null) {
                return getReturn(jsonStr, 500, "not found method(" + sign + ") with valid parameters");
            }
            // 數(shù)字類型細分匹配
            if (numIndex > 0) {
                Class[] methodTypes = currMethod.getParameterTypes();
                int currIndex;
                Class currCls;
                while (numIndex > 0) {
                    currIndex = numIndex - numIndex / 10 * 10;
                    currCls = methodTypes[currIndex];
                    if (currCls == int.class) {
                        values[currIndex] = argsVals.getInt(currIndex - 1);
                    } else if (currCls == long.class) {
                        //WARN: argsJson.getLong(k + defValue) will return a bigger incorrect number
                        values[currIndex] = Long.parseLong(argsVals.getString(currIndex - 1));
                    } else {
                        values[currIndex] = argsVals.getDouble(currIndex - 1);
                    }
                    numIndex /= 10;
                }
            }

            return getReturn(jsonStr, 200, currMethod.invoke(null, values));
        } catch (Exception e) {
            //優(yōu)先返回詳細的錯誤信息
            if (e.getCause() != null) {
                return getReturn(jsonStr, 500, "method execute error:" + e.getCause().getMessage());
            }
            return getReturn(jsonStr, 500, "method execute error:" + e.getMessage());
        }
    } else {
        return getReturn(jsonStr, 500, "call data empty");
    }
}

如果方法正確執(zhí)行,call方法就返回一個json字符串code=200,否則就傳code=500,這個信息會通過prompt方法的返回值傳給js,這樣Html 5 代碼就能知道有沒有正確執(zhí)行了。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,316評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,481評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,241評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,939評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,697評論 6 409
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,182評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,247評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,406評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,933評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,772評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,973評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,516評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,209評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,638評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,866評論 1 285
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,644評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,953評論 2 373

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