WebView学习笔记

参考:
WebView全面解析
安卓中原生与H5(webview)之间交互时cookie的同步
详细的Webview使用攻略
WebView与JavaScript的交互总结
webview与HTTPS
速度提升框架:VasSonic

Android展示网页内容有两种方式,一种是通过手机系统浏览器,二是在布局中内嵌WebView
WebView是android中一个非常重要的控件,它的作用是用来展示一个web页面。它使用的内核是webkit引擎,4.4版本之后,直接使用Chrome作为内置网页浏览器

0.加载网页和内容

    //方式一:加载一个网页
    webView.loadUrl("http://www.baidu.com");

    //方式二:加载应用资源文件内的网页
    webView.loadUrl("file:///android_asset/test.html");

    //方式三:加载一段代码
    webView.loadData(String data,String mimeType, String encoding);
   //方式四:加载网页或内容(推荐)
    webView.loadDataWithBaseURL("http://www.baidu.com", body, "text/html", "utf-8",null);

1.WebSetting

方法名 方法描述
setJavaScriptEnabled(true) 是否支持JS加载,默认为false
setJavaScriptCanOpenWindowsAutomatically(true) 支持通过JS打开新窗口
setPluginsEnabled(true) 是否支持插件
setUseWideViewPort(true) 将图片调整到适合webview的大小
setLoadWithOverviewMode 缩放至屏幕的大小
setSupportZoom(true) 支持缩放,默认为true。是setBuiltInZoomControls(true)的前提
setBuiltInZoomControls(true) 设置内置的缩放控件。若为false,则该WebView不可缩放
setDisplayZoomControls(false) 隐藏原生的缩放控件
setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK) LOAD_CACHE_ONLY: 不使用网络,只读取本地缓存数据LOAD_DEFAULT: (默认)根据cache-control决定是否从网络上取数据。LOAD_NO_CACHE: 不使用缓存,只从网络获取数据.LOAD_CACHE_ELSE_NETWORK,只要本地有,无论是否过期,或者no-cache,都使用缓存中的数据。
setAllowFileAccess(true) 设置可以访问文件
setLoadsImagesAutomatically(true) 支持自动加载图片
setLayoutAlgorithm(WebSettings.LayoutAlgorithm.SINGLE_COLUMN) 支持内容重新布局
setDefaultTextEncodingName("utf-8") 设置编码格式
setDomStorageEnabled(true) 开启 DOM storage API 功能,有的网页如淘宝样式加载不出来就需要配置此项DOM storage
setDatabaseEnabled(true) 开启 database storage API 功能
setAppCacheEnabled(true) 开启 Application Caches 功能
setAppCachePath(cacheDirPath) 设置 Application Caches 缓存目录
setAppCacheMaxSize(1024*8) 缓存的最大空间
setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW) 5.1以上默认禁止了https和http混用,所以大于5.1的需要开启
setRenderPriority(WebSettings.RenderPriority.HIGH) 提高渲染的优先级
setStandardFontFamily("") 设置 WebView 的字体,默认字体为 "sans-serif"
webSettings.setDefaultFontSize(20) 设置WebView字体的大小,默认大小为 16
webSettings.setMinimumFontSize(12) 设置 WebView支持的最小字体大小,默认为 8
setGeolocationEnabled(true) 允许网页执行定位操作

2.WebViewClient

方法名 方法描述
onPageStarted(WebView view, String url, Bitmap favicon) 页面开始加载
onPageFinished(WebView view, String url) 页面加载结束
boolean shouldOverrideUrlLoading(WebView view, String url)或(新5.0以上)boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) 拦截URL请求,重定向
onReceivedError() 加载页面的服务器出现错误(比如404)时回调。
onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) https请求异常
shouldInterceptRequest(WebView view, String url) 每次发生资源加载
onLoadResource(WebView view, String url) 页面加载资源时调用
shouldOverrideKeyEvent() 重写此方法才能处理浏览器中的按键事件
doUpdateVisitedHistory() 更新历史记录
onFormResubmission() 应用程序重新请求网页数据
onReceivedHttpAuthRequest() 获取返回信息授权请求
onScaleChanged() WebView发生缩放改变时调用。
onUnhandledKeyEvent() Key事件未被加载时调用
  • shouldOverrideUrlLoading():拦截URL请求,重定向(有2个方法,一个是兼容5.0以下,一个是兼容5.0以上,保险起见两个都重写)。
    • 无论返回true还是false,只要为WebView设置了WebViewClient,系统就不会再将url交给第三方的浏览器去处理了。
    • 如果返回false,代表将url交给当前WebView加载,也就是正常的加载状态;shouldOverrideUrlLoading()返回true,代表开发者已经对url进行了处理,WebView就不会再对这个url进行加载了。
    • 另外,使用post的方式加载页面,此方法不会被调用。
webView.setWebViewClient(new WebViewClient(){

      //重定向URL请求,返回true表示拦截此url,返回false表示不拦截此url。
      @Override
      public boolean shouldOverrideUrlLoading(WebView view, String url) {
          //作用1:重定向url
          if(url.startsWith("weixin://")){
              url = url.replace("weixin://","http://");
              webView.loadUrl(url);
          }

          //作用2:在本页面的WebView打开,防止外部浏览器打开此链接
          view.loadUrl(url);
          return true;
      }
  });
  • 情况一:loadUrl()无重定向时
onPageStarted->onPageFinished
  • 情况二:loadUrl()网页A重定向到B时
onPageStarted->onPageFinished->shouldOverrideUrlLoading->onPageStarted->onPageFinished

也有可能重定向多次

shouldOverrideUrlLoading->shouldOverrideUrlLoading->onPageStarted->onPageFinished
  • 情况三:在已加载的页面中点击链接,加载页面A(无重定向)
shouldOverrideUrlLoading->onPageStarted->onPageFinished
  • 情况四:在已加载的页面中点击链接,加载页面A(页面A重定向至页面B)
shouldOverrideUrlLoading->shouldOverrideUrlLoading->onPageStarted->onPageFinished
  • 情况五:执行goBack/goForward/reload方法
onPageStarted->onPageFinished
  • 情况六:发生资源加载
shouldInterceptRequest->onLoadResource
  • onReceivedSslError():webView默认是不处理https请求的,页面显示空白,需要进行如下设置:
webView.setWebViewClient(new WebViewClient() {    
        @Override    
        public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {    
            handler.proceed();    //表示等待证书响应
        // handler.cancel();      //表示挂起连接,为默认方式
        // handler.handleMessage(null);    //可做其他处理
        }    
    });
异常:未知URL空间异常
            public boolean shouldOverrideUrlLoading(WebView view, String url) {
                if (!url.startsWith("http")) return true;
                return super.shouldOverrideUrlLoading(view, url);
            }

3.WebChromeClient

辅助 WebView 处理 Javascript 的对话框,网站图标,网站标题等等

方法名 方法描述
onProgressChanged() 获得网页的加载进度并显示。
onReceivedTitle() 获得网页的标题时回调。
onReceivedIcon() 获得网页的图标时回调。
onCreateWindow() 打开新窗口时回调。
onCloseWindow() 关闭窗口时回调。
onJsAlert() 网页弹出提示框时触发此方法
onJsConfirm() 支持javascript的确认框
onJsPrompt() 支持javascript输入框,点击确认返回输入框中的值,点击取消返回 null。
@Override
  public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
      Toast.makeText(MainActivity.this,"Im alert",Toast.LENGTH_SHORT).show();

      //部分机型只会弹出一次提示框,调用此方法即可解决此问题。
      result.cancel();
      //返回true表示不弹出系统的提示框,返回false表示弹出
      return true;
  }
webview.setWebChromeClient(new WebChromeClient() {
        
            @Override
public boolean onJsConfirm(WebView view, String url, String message, final JsResult result) {
    new AlertDialog.Builder(MainActivity.this)
            .setTitle("JsConfirm")
            .setMessage(message)
            .setPositiveButton("OK", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    result.confirm();
                }
            })
            .setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    result.cancel();
                }
            })
            .setCancelable(false)
            .show();
// 返回布尔值:判断点击时确认还是取消
// true表示点击了确认;false表示点击了取消;
    return true;
}
webview.setWebChromeClient(new WebChromeClient() {
            @Override
            public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, final JsPromptResult result) {
    final EditText et = new EditText(MainActivity.this);
    et.setText(defaultValue);
    new AlertDialog.Builder(MainActivity.this)
            .setTitle(message)
            .setView(et)
            .setPositiveButton("OK", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    result.confirm(et.getText().toString());
                }
            })
            .setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    result.cancel();
                }
            })
            .setCancelable(false)
            .show();

    return true;
}

4.Cookie

安卓中原生与H5(webview)之间交互时cookie的同步
在执行webview的loadurl之前,先执行cookie同步

  • 设置cookie
 public static void synchronousWebCookies(Context context,String url,String cookies){
    if ( !TextUtils.isEmpty(url) )
        if (!TextUtils.isEmpty(cookies) ) {
            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP){
                CookieSyncManager.createInstance( context);
            }
            CookieManager cookieManager = CookieManager.getInstance();
            cookieManager.setAcceptCookie( true );
            cookieManager.removeSessionCookie();// 移除
            cookieManager.removeAllCookie();
            StringBuilder sbCookie = new StringBuilder();//创建一个拼接cookie的容器,为什么这么拼接,大家查阅一下http头Cookie的结构  
            sbCookie.append(cookies);//拼接sessionId  
//          sbCookie.append(String.format(";domain=%s", ""));  
//          sbCookie.append(String.format(";path=%s", ""));  
            String cookieValue = sbCookie.toString();  
            cookieManager.setCookie(url, cookieValue);//为url设置cookie  
            CookieSyncManager.getInstance().sync();//同步cookie
                        String newCookie = cookieManager.getCookie(url);
            LogManager.i("同步后cookie", newCookie);
        }
    }
  • 获取cookie
public static String syncCookie(String url) {
    CookieManager cookieManager = CookieManager.getInstance();
    return cookieManager.getCookie(url);
}
  • 清除cookie
CookieManager.getInstance().removeSessionCookies();// 移除所有过期 cookie
CookieManager.getInstance().removeAllCookies(); // 移除所有的 cookie
//设置清除cookie后的回调方法
private void removeCookie(Context context) {
    CookieManager.getInstance().removeAllCookies(new ValueCallback<Boolean>() {
        @Override
        public void onReceiveValue(Boolean value) {
            // 清除结果
        }
    });
}

5. JS 交互

WebView与JavaScript的交互总结
最全面总结 Android WebView与 JS 的交互方式

Android调用JS 方法描述
loadUrl() loadUrl("javascript:callJS()");只会执行一次
evaluateJavascript() 通过WebViewevaluateJavascript(),可多次调用,需要Android 4.4以上

首先开启js

 //允许WebView使用JS
    settings.setJavaScriptEnabled(true);
    //支持通过JS打开新窗口(允许JS弹窗)
    settings.setJavaScriptCanOpenWindowsAutomatically(true);
  • a.Android通过loadUrl调用js
 btnCallJs.setOnClickListener(view1 -> webView.loadUrl("javascript:callJS()"));

只有第一次会调用,且调用需要在onPageFinished方法之后

  • b Android通过evaluateJavascript()调用js
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            mBinding.btnCallJs.setOnClickListener(view1 ->
                    mBinding.webView.evaluateJavascript("javascript:callJS()", s -> {
                        //s是JS方法的返回值
                        L.e(TAG,s);
                    }));
        }

这种方式比第一种方式效率高(执行时不会刷新页面),同时可以获取返回值。缺点是只兼容到Android4.4(19)版本以后,如果重写了WebChromeClientonJsAlert,返回值为true的情况下,JsResult需要调用cancel,如下:

@Override
  public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
      Toast.makeText(MainActivity.this,"Im alert",Toast.LENGTH_SHORT).show();
      //部分机型只会弹出一次提示框,调用此方法即可解决此问题。
      result.cancel();
      //返回true表示不弹出系统的提示框,返回false表示弹出
      return true;
  }
JS调用Android 方法描述
addJavascriptInterface() 对象映射webView.addJavascriptInterface(new AndroidJs(),"android");
shouldOverrideUrlLoading () WebViewClientshouldOverrideUrlLoading ()方法回调拦截url
onJsAlert() 通过 WebChromeClientonJsAlert()拦截JS对话框alert()
onJsConfirm() 通过 WebChromeClientonJsConfirm()拦截JS对话框confirm()
onJsPrompt() 通过 WebChromeClientonJsAlert()拦截JS对话框prompt()
  • a. addJavascriptInterface()的使用

创建一个测试的网页test.htmlassets文件夹中

<head>
</head>
<body>
<h1 onClick='callAndroid()'>这是JS调用原生1</h1>
<Script>
        function callAndroid(){
            android.hello("js调用了");
        }
    </Script>
</body>

创建一个对象,关联js调用方法

public class AndroidJs{
    // 定义JS需要调用的方法
    // 被JS调用的方法必须加入@JavascriptInterface注解
    @JavascriptInterface
    public void hello(String msg) {
        ToastUtil.showShort(msg);
    }
}

Activity使用时:加载网页test.html,让webView把对象,和网页函数关联起来

webView.loadUrl("file:///android_asset/test.html");
webView.addJavascriptInterface(new AndroidJs(),"android");
  • 注意:正常情况下这里的test.html应该是后台的网页地址,new AndroidJs()就是我们桥接的jsAndroid的对象,后面的的"android"就是我们在网页test.html中使用的android.hello("js调用了");
  • 它的调用顺序应该是,我们点击了网页test.html中的h1标签,触发onClick方法,执行callAndroid()函数,在callAndroid()函数中又会调用android.hello("js调用了");,它就会通过android去查询安卓对应的映射对象AndroidJs,找到后再调用里面AndroidJs里面的hello方法,并把参数传递进去。
  • 这种方法优点是使用简单,缺点是存在严重的漏洞问题,请看文章:你不知道的 Android WebView 使用漏洞
  • b.shouldOverrideUrlLoading ()的使用
  • 创建一个javascript.htmlsrc/main/assets文件夹里
<!DOCTYPE html>
<html>
<head>
    <script>
         function callAndroid(){
            /*约定的url协议为:js://webview?arg1=111&arg2=222*/
            document.location = "js://webview?arg1=111&arg2=222";
         }
      </script>
</head>
<!-- 点击按钮则调用callAndroid()方法  -->
<body>
<button type="button" id="button1" onclick="callAndroid()">点击调用Android代码</button>
</body>
</html>
  • 页面调用
        String url = "file:///android_asset/javascript.html";
        webView.loadUrl(url);
        webView.setWebViewClient(new WebViewClient(){
            @Override
            public boolean shouldOverrideUrlLoading(WebView view, String url) {
                //"js://webview?arg1=111&arg2=222"
                Uri uri = Uri.parse(url);
                String scheme = uri.getScheme();//空间:相当于冒号之前的部分
                String authority = uri.getAuthority();//授权:双斜杠之后,?之前的部分
                if (scheme.equals("js")){
                    if (authority.equals("webview")){
                        Set<String> keys = uri.getQueryParameterNames();//参数key:arg1,arg2
                        for (String key : keys) {
                            L.e("key=="+key);
                            L.e("value=="+uri.getQueryParameter(key));
                        }
                    }
                    return true;
                }
                return super.shouldOverrideUrlLoading(view, url);
            }

        });
image.png
  • 与后台定义好对应的空间和授权,拦截到对应的空间后,根据不同的参数来调用不同的Android原生方法,就是这样。
  • c.通过 拦截WebChromeClientonJsAlert(),onJsConfirm(),onJsPrompt()方法,来实现调用Android的原生方法。
@Override
public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
    L.e("url=="+url);
    L.e("message=="+message);
    return super.onJsAlert(view, url, message, result);
}
alert

test.html

<!DOCTYPE html>
<html>
<body>
<h1 onClick='cleanData()'>清空数据</h1>
<script>
    function cleanData(){
    var a=confirm("确定要清空数据吗?");
         if(a==true){
              document.write("恭喜你清空了数据!");
         }else{
              document.write("取消清空操作");
         }
    }
</script>
</body>
</html>
webView.setWebChromeClient(new WebChromeClient(){
            @Override
        public boolean onJsConfirm(WebView view, String url, String message, JsResult result) {
                L.e("url=="+url);
                L.e("message=="+message);
                new AlertDialog.Builder(_mActivity).
                        setTitle("警告").setMessage(message).setPositiveButton("确定",
                        new DialogInterface.OnClickListener() {
                            @Override
                            public void onClick(DialogInterface arg0, int arg1) {
                                result.confirm();
                            }
                        })
                        .setNegativeButton("取消", new DialogInterface.OnClickListener() {
                            @Override
                            public void onClick(DialogInterface dialogInterface, int i) {
                                result.cancel();
                            }
                        })
                        .create().show();
                return true;
            }
});
confirm
  • 我们可以拦截弹出框的提示语:"确定要清空数据吗?"AlertDialog,让后根据AlertDialog操作执行不同的JsResult,如确定result.confirm();取消result.cancel();

test.html

<!DOCTYPE html>
<html>
<body>
<h1 onClick='takePrompt()'>体能测试</h1>
<script>
    function takePrompt(){
        var result = prompt("请输入你的身高?","170");
        alert(result);
    }
</script>
</body>
</html>
        mBinding.webView.setWebChromeClient(new WebChromeClient(){
            @Override
            public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
                L.e(TAG,message + " , " + defaultValue);
                if(message.contains("身高")){
                    final EditText et = new EditText(_mActivity);
                    new AlertDialog.Builder(_mActivity)
                            .setMessage(message)
                            .setView(et)
                            .setPositiveButton("确定", new DialogInterface.OnClickListener() {
                                @Override
                                public void onClick(DialogInterface arg0, int arg1) {
                                    String s = et.getText().toString();
                                    result.confirm(s);
                                }
                            })
                            .create().show();
                }
                return true; //返回true表示不弹出系统提示框
            }
        });
prompt,默认值170

输入后

6. WebView相关方法

方法名 方法描述
canGoBack() 是否可以返回
goBack() 返回
canGoForward() 是否可以前进
goForward() 前进网页
goBackOrForward(intsteps) 以当前的index为起始点前进或者后退到历史记录中指定的steps,如果steps为负数则为后退,正数则为前进
onResume() 激活WebView为活跃状态,能正常执行网页的响应
onPause() 通过onPause动作通知内核暂停所有的动作,比如DOM的解析、plugin的执行、JavaScript执行
pauseTimers() 当应用程序(存在webview)被切换到后台时,这个方法不仅仅针对当前的webview而是全局的全应用程序的webview,它会暂停所有webviewlayout,parsing,javascripttimer。降低CPU功耗。
resumeTimers() 恢复pauseTimers状态
destroy() 销毁Webview
clearCache(true) 清除网页访问留下的缓存,由于内核缓存是全局的因此这个方法不仅仅针对webview而是针对整个应用程序.
clearHistory() 清除当前webview访问的历史记录,除了当前访问记录
clearFormData() 清除自动完成填充的表单数据
stopLoading () 停止当前加载
zoomBy(float zoomFactor) 在此Web视图中执行缩放操作,zoomFactor0.01100.0之间
zoomIn() 在此Web视图中执行放大操作
zoomOut() 在此Web视图中执行缩小

Fragment中返回网页的上一页,而不是直接关闭

    @Override
    public boolean onBackPressedSupport() {
        if (webView.canGoBack()) {
            webView.goBack();
            return true;
        }
        return super.onBackPressedSupport();
    }

Activity中返回网页的上一页,而不是直接关闭

   @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        if (keyCode == KeyEvent.KEYCODE_BACK && webView.canGoBack()) {
            webView.goBack();
            return true;
        }
        return super.onKeyDown(keyCode, event);
    }

7. Cache缓存

缓存机制

  • 当加载 html 页面时,WebView会在/data/data/包名目录下生成 database 与 cache 两个文件夹
  • 请求的 URL记录保存在 WebViewCache.db,而 URL的内容是保存在 WebViewCache 文件夹下
  • 优先使用缓存WebView.getSettings().setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK);
  • 不使用网络,只读取本地缓存数据LOAD_CACHE_ONLY
  • (默认)根据cache-control决定是否从网络上取数据LOAD_DEFAULT
  • 不使用网络,只读取本地缓存数据LOAD_NO_CACHE
  • 只要本地有,无论是否过期,或者no-cache,都使用缓存中的数据LOAD_CACHE_ELSE_NETWORK
离线加载: 每个 Application 只调用一次 WebSettings.setAppCachePath()WebSettings.setAppCacheMaxSize()
if (NetStatusUtil.isConnected(getApplicationContext())) {
    webSettings.setCacheMode(WebSettings.LOAD_DEFAULT);//根据cache-control决定是否从网络上取数据。
} else {
    webSettings.setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK);//没网,则从本地获取,即离线加载
}

webSettings.setDomStorageEnabled(true); // 开启 DOM storage API 功能
webSettings.setDatabaseEnabled(true);   //开启 database storage API 功能
webSettings.setAppCacheEnabled(true);//开启 Application Caches 功能

String cacheDirPath = getFilesDir().getAbsolutePath() + APP_CACAHE_DIRNAME;
webSettings.setAppCachePath(cacheDirPath); //设置  Application Caches 缓存目录

8. 安全

你不知道的 Android WebView 使用漏洞
WebView挂马漏洞
WebView远程执行代码漏洞浅析

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,569评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,499评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,271评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,087评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,474评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,670评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,911评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,636评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,397评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,607评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,093评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,418评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,074评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,092评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,865评论 0 196
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,726评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,627评论 2 270

推荐阅读更多精彩内容