Android WebView 基础学习

前言

由于业务需求更新迭代快,发布新版本的App需要时间,即使发布了也不能保证用户立即更新,因此越来越多的app使用Hybrid模式进行开发。
而Hybrid模式中,最重要的组件莫过于WebView了。

简介

Android Develops 上对于WebView的描述:

A View that displays web pages. This class is the basis upon which you can roll your own web browser or simply display some online content within your Activity. It uses the WebKit rendering engine to display web pages and includes methods to navigate forward and backward through a history, zoom in and out, perform text searches and more.

为了方便开发者实现在app内展示网页并与网页交互的需求,Android SDK提供了WebView组件。它继承自AbsoluteLayout,展示网页的同时,也可以在其中放入其他的子View。

从Android 4.4(KitKat)开始,原本基于WebKit的WebView开始基于Chromium内核。

作用

  • 显示和渲染Web页面
  • 直接使用html文件(网络上或本地assets中)作布局
  • 可和JavaScript交互调用

WebView控件功能强大,除了具有一般View的属性和设置外,还可以对url请求、页面加载、渲染、页面交互进行强大的处理

基本使用

1.WebView 初始化

添加WebView,最好使用在Java中动态加载,而不是放在layout里。因为在layout里的话,WebView会持有ActivityContext,可能会导致内存泄漏。

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_web_view);
        mLlWebView = findViewById(R.id.ll_web_view);
        initWebView();
    }

    private void initWebView() {
        // don't add WebView in layout, since it would cause memory leak
        LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
                ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);

        mWebView = new WebView(getApplicationContext());
//        mWebView = MyApplication.getInstance().getWebView();
        mWebView.setLayoutParams(params);
        mLlWebView.addView(mWebView);
    }

2.WebView 加载和获取Url

WebView支持加载web HTML,apk包内的assets中的HTML,手机本地存储的HTML,以及HTML代码块

    private void loadUrl() {
        // 加载web HTML
        mWebView.loadUrl("https://www.hupu.com");

        // 加载apk包内的assets中的HTML
        mWebView.loadUrl("file:///android_asset/index.html");

        // 加载手机本地存储的HTML
        mWebView.loadUrl("content://com.android.test/sdcard/test.html");

        // 加载HTML代码块
        // data:需要截取展示的内容,内容里不能出现 ’#’, ‘%’, ‘\’ , ‘?’ 这四个字符,若出现了需用 %23, %25, %27, %3f 对应来替代,否则会出现异常
        // mimeType:展示内容的类型,比如image/png,text/plain等
        // encoding:字节码
        mWebView.loadData(String data, String mimeType, String encoding)

        // 获取当前页面的Url
        String url = mWebView.getUrl();

        // 获取当前页面的原始Url,因为可能会经过多次重定向
        String originalUrl = mWebView.getOriginalUrl();

        // 重新reload当前的URL,即刷新
        mWebView.reload();
    }

记得申请网络权限

    <uses-permission android:name="android.permission.INTERNET" />

3.WebView 恢复、暂停

    @Override
    protected void onResume() {
        super.onResume();
        // 在先前调用onPause()后,
        // 我们可以调用该方法来恢复WebView的运行。
        // 激活WebView为活跃状态,能正常执行网页的响应
        mWebView.onResume();

        // 恢复pauseTimers时的所有操作。
        mWebView.resumeTimers();
    }

    @Override
    protected void onPause() {
        super.onPause();
        // 当页面被失去焦点被切换到后台不可见状态,需要执行onPause操作,
        // 通过onPause动作通知内核暂停所有的动作,比如DOM的解析、plugin的执行、JavaScript执行、动画的执行或定位的获取等。
        // 需要注意的是该方法并不会暂停JavaScript的执行。
        mWebView.onPause();

        // 该方法面向全局整个应用程序的WebView,
        // 它会暂停所有WebView的layout,parsing,JavaScript Timer。
        // 当程序进入后台时,该方法的调用可以降低CPU功耗。
        mWebView.pauseTimers();
    }

4.WebView 前进、后退

WebView支持前进、后退网页

    private void goForward() {
        // 用来确认WebView是否还有可向前的历史记录
        if (mWebView != null && mWebView.canGoForward()) {
            // 在WebView历史记录里前进到下一项
            mWebView.goForward();
        }
    }

    private void goBack() {
        // 用来确认WebView里是否还有可回退的历史记录
        if (mWebView != null && mWebView.canGoBack()) {
            // 在WebView历史记录后退到上一项
            mWebView.goBack();
        }
    }

    private void goBackorForward(int steps) {
        // 以当前的页面为起始点,用来确认WebView的历史记录是否足以后退或前进给定的步数,正数为前进,负数为后退
        if (mWebView != null && mWebView.canGoBackOrForward(steps)) {
            // 以当前页面为起始点,前进或后退历史记录中指定的步数,正数为前进,负数为后退
            mWebView.goBackOrForward(steps);
        }
    }

通常我们会在WebView里重写返回键的点击事件,通过该方法判断WebView里是否还有历史记录,若有则返回上一页。若没有,连按两下Back键,退出Activity

    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        if (keyCode == KeyEvent.KEYCODE_BACK) {
            if (mWebView != null && mWebView.canGoBack()) {
                mWebView.goBack();
                return true;
            } else if (event.getRepeatCount() == 0) {
                // back activity
                exit();
                return true;
            }
        }
        return super.onKeyDown(keyCode, event);
    }

    private void exit() {
        if ((System.currentTimeMillis() - mExitTime) > 2000) {
            Toast.makeText(WebViewActivity.this, "One More Press, then exit", Toast.LENGTH_SHORT).show();
            mExitTime = System.currentTimeMillis();
        } else {
            finish();
        }
    }

5.WebView 清除缓存

    private void clearCache() {
        // 清空网页访问留下的缓存数据。
        // 需要注意的时,由于缓存是全局的,所以只要是WebView用到的缓存都会被清空,即便其他地方也会使用到。
        // 该方法接受一个参数,从命名即可看出作用。若设为false,则只清空内存里的资源缓存,而不清空磁盘里的
        mWebView.clearCache(true);

        // 清除当前WebView访问的历史记录
        // 只会WebView访问历史记录里的所有记录除了当前访问记录
        mWebView.clearHistory();

        // 清除自动完成填充的表单数据。
        // 需要注意的是,该方法仅仅清除当前表单域自动完成填充的表单数据,并不会清除WebView存储到本地的数据。
        mWebView.clearFormData();
    }

6.WebView 滚动处理

    // 是否处于顶端
    private boolean isScrollTop() {
        if (mWebView == null) {
            return false;
        }

        // getScrollY(): 该方法返回的当前可见区域的顶端距整个页面顶端的距离,也就是当前内容滚动的距离
        boolean isTop = (mWebView.getScrollY() == 0);
        return isTop;
    }

    // 是否处于底端
    private boolean isScrollBottom() {
        if (mWebView == null) {
            return false;
        }

        // getHeight(): 方法都返回当前WebView这个容器的高度。其实以上两个方法都属于View
        // getContentHeight():该方法返回整个HTML页面的高度,但该高度值并不等同于当前整个页面的高度
        // 因为WebView有缩放功能, 所以当前整个页面的高度实际上应该是原始HTML的高度再乘上缩放比例
        // getScale()在sdk 17中已经被弃用,建议使用WebViewClient中的onScaleChanged获取缩放比例
        boolean isBottom = (mWebView.getContentHeight() * mWebView.getScale()
                == (mWebView.getHeight() + mWebView.getScrollY()));
        return isBottom;
    }

    // 向上滚动
    private void pageUp(boolean top) {
        if (mWebView != null) {
            // top为true时,将WebView展示的页面滑动至顶部
            // top为false时,将WebView展示的页面向上滚动一个页面高度
            mWebView.pageUp(top);
        }
    }

    // 向下滚动
    private void pageDown(boolean bottom) {
        if (mWebView != null) {
            // bottom为true时,将WebView展示的页面滑动至底部
            // top为false时,将WebView展示的页面向下滚动一个页面高度
            mWebView.pageDown(bottom);
        }
    }

7.WebView 销毁

避免内存泄漏,使用parent remove view,然后销毁webview

    @Override
    protected void onDestroy() {
        if (mWebView != null) {
            mWebView.loadDataWithBaseURL(null, "", "text/html", "utf-8", null);
            mWebView.clearHistory();

            ((ViewGroup) mWebView.getParent()).removeView(mWebView);
            // 销毁WebView。需要注意的是:
            // 这个方法的调用应在WebView从父容器中被remove掉之后。我们可以手动地调用
            mWebView.destroy();
            mWebView = null;
        }
        super.onDestroy();
    }

WebSettings

WebSettings是用来管理WebView配置的类。当WebView第一次创建时,内部会包含一个默认配置的集合。若我们想更改这些配置,便可以通过WebSettings里的方法来进行设置。

1.获取WebSettings

WebSettings对象可以通过WebView.getSettings()获得,它的生命周期是与它的WebView本身息息相关的,如果WebView被销毁了,那么任何由WebSettings调用的方法也同样不能使用。

        // 获取WebSettings
        WebSettings mWebSettings = mWebView.getSettings();

2.WebSettings里与JavaScript 相关设置

如果访问的页面中要与Javascript交互,则WebView必须设置支持Javascript
若加载的 html 里有JS 在执行动画等操作,会造成资源浪费(CPU、电量)
在 onStop 和 onResume 里分别把 setJavaScriptEnabled() 给设置成 false 和 true 即可

        // 设置WebView是否允许执行JavaScript脚本,默认false,不允许
        mWebSettings.setJavaScriptEnabled(true);

        // 设置WebView是否可以由JavaScript自动打开窗口,默认为false,通常与JavaScript的window.open()配合使用。
        mWebSettings.setJavaScriptCanOpenWindowsAutomatically(true);

3.WebSettings里与访问权限相关设置

        // 设置在WebView内部是否允许访问文件,默认允许访问。
        mWebSettings.setAllowFileAccess(true);
        // 设置在WebView内部是否允许访问ContentProvider,默认允许访问。
        mWebSettings.setAllowContentAccess(true);
        // 设置在WebView内部是否允许通过file url加载的 Js代码读取其他的本地文件
        // Android 4.1前默认允许,4.1后默认禁止
        mWebSettings.setAllowFileAccessFromFileURLs(false);
        // 设置WebView内部是否允许通过 file url 加载的 Javascript 可以访问其他的源(包括http、https等源)
        // Android 4.1前默认允许,4.1后默认禁止
        mWebSettings.setAllowUniversalAccessFromFileURLs(false);

访问权限控制不严格会导致漏洞
例如, A 应用可以通过 B 应用导出的 Activity 让 B 应用加载一个恶意的 file 协议的 url,从而可以获取 B 应用的内部私有文件,从而带来数据泄露威胁

具体:当其他应用启动此 Activity 时, intent 中的 data 直接被当作 url 来加载(假定传进来的 url 为 file:///data/local/tmp/attack.html ),其他 APP 通过使用显式 ComponentName 或者其他类似方式就可以很轻松的启动该 WebViewActivity 并加载恶意url。

解决方案
如果是 file 协议,禁用 javascript 可以很大程度上减小跨源漏洞对 WebView 的威胁。

  • 对于不需要使用 file 协议的应用,禁用 file 协议;
// 禁用 file 协议;
setAllowFileAccess(false); 
setAllowFileAccessFromFileURLs(false);
setAllowUniversalAccessFromFileURLs(false);
  • 对于需要使用 file 协议的应用,禁止 file 协议加载 JavaScript
// 需要使用 file 协议
setAllowFileAccess(true); 
setAllowFileAccessFromFileURLs(false);
setAllowUniversalAccessFromFileURLs(false);

// 禁止 file 协议加载 JavaScript
if (url.startsWith("file://") {
    setJavaScriptEnabled(false);
} else {
    setJavaScriptEnabled(true);
}

4.WebSettings里与屏幕显示相关设置

关于setUseWideViewPort,当该属性被设置为false时,加载页面的宽度总是适应WebView控件宽度;
当被设置为true,当前页面包含viewport属性标签,在标签中指定宽度值生效,如果页面不包含viewport标签,无法提供一个宽度值,这个时候该方法将被使用。

        // 设置WebView是否使用viewport,将图片调整到适合WebView的大小
        mWebSettings.setUseWideViewPort(true);
        // 设置WebView是否使用预览模式加载界面,即缩放至屏幕的大小
        mWebSettings.setLoadWithOverviewMode(true);
        // 设置WebView是否支持多窗口, 如果为true,必须要重写WebChromeClient的onCreateWindow方法
        mWebSettings.setSupportMultipleWindows(true);
        // 设置WebView是否需要设置一个节点获取焦点当WebView#requestFocus(int,android.graphics.Rect)被调用时,默认true
        mWebSettings.setNeedInitialFocus(true);

5.WebSettings里与屏幕缩放相关设置

        // 设置WebView是否支持使用屏幕控件或手势进行缩放,默认是true,支持缩放, 是前面方法的前提
        mWebSettings.setSupportZoom(true);
        // 设置WebView是否使用其内置的变焦机制,该机制集合屏幕缩放控件使用,默认是false,不使用内置变焦机制。
        mWebSettings.setBuiltInZoomControls(true);
        // 设置WebView使用内置缩放机制时,是否展现在屏幕缩放控件上,默认true,展现在控件上。
        mWebSettings.setDisplayZoomControls(true);

6.WebSettings里与显示字体相关设置

        // 设置WebView加载页面文本内容的编码,默认"UTF-8"。
        mWebSettings.setDefaultTextEncodingName("UTF-8");
        // 设置标准的字体族,默认"sans-serif"。
        mWebSettings.setStandardFontFamily("sans-serif");
        // 设置混合字体族,默认"monospace"。
        mWebSettings.setFixedFontFamily("monospace");
        // 设置默认填充字体大小,默认16,取值区间为[1-72],超过范围,使用其上限值。
        mWebSettings.setDefaultFixedFontSize(16);
        // 设置默认字体大小,默认16,取值区间[1-72],超过范围,使用其上限值。
        mWebSettings.setDefaultFontSize(16);

7.WebSettings里与资源加载相关设置

关于setBlockNetworkImage,需要注意的是,如果设置是从禁止到允许的转变的话,图片数据并不会在设置改变后立刻去获取,而是在WebView调用reload()的时候才会生效。
这个时候,需要确保这个app拥有访问Internet的权限,否则会抛出安全异常。
通常没有禁止图片加载的需求的时候,完全不用管这个方法,因为当我们的app拥有访问Internet的权限时,这个flag的默认值就是false。

        // 设置WebView代理字符串,如果String为null或为空,将使用系统默认值
        mWebSettings.setUserAgentString("");
        // 设置WebView是否以http、https方式访问从网络加载图片资源,默认false
        mWebSettings.setBlockNetworkImage(false);
        // 设置WebView是否从网络加载资源,Application需要设置访问网络权限,否则报异常
        mWebSettings.setBlockNetworkLoads(false);
        // 设置WebView是否加载图片资源,默认true,自动加载图片
        mWebSettings.setLoadsImagesAutomatically(true);

        // 特别注意:5.0以上默认禁止了https和http混用
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            // 设置当一个安全站点企图加载来自一个不安全站点资源时WebView的行为
            // Android 5.0以下,默认是MIXED_CONTENT_ALWAYS_ALLOW
            // Android 5.0已上,默认是MIXED_CONTENT_NEVER_ALLOW
            mWebSettings.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
        }

MixedContentMode说明:

  • MIXED_CONTENT_ALWAYS_ALLOW:允许从任何来源加载内容,即使起源是不安全的;
  • MIXED_CONTENT_NEVER_ALLOW:不允许Https加载Http的内容,即不允许从安全的起源去加载一个不安全的资源;
  • MIXED_CONTENT_COMPATIBILITY_MODE:当涉及到混合式内容时,WebView 会尝试去兼容最新Web浏览器的风格。

8.WebSettings里与缓存模式相关设置

        // 用来设置WebView的缓存模式。当我们加载页面或从上一个页面返回的时候,会按照设置的缓存模式去检查并使用(或不使用)缓存
        if (CommonUtils.isNetworkAvailable(getBaseContext())) {
            // 如果有网,则根据cache-control决定是否从网络上取数据
            mWebSettings.setCacheMode(WebSettings.LOAD_DEFAULT);
        } else {
            // 如果没网,则从本地获取,即离线加载
            mWebSettings.setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK);
        }

CacheMode说明:

  • LOAD_DEFAULT:默认的缓存使用模式。在进行页面前进或后退的操作时,如果缓存可用并未过期就优先加载缓存,否则从网络上加载数据。这样可以减少页面的网络请求次数。
  • LOAD_CACHE_ELSE_NETWORK:只要缓存可用就加载缓存,哪怕它们已经过期失效。如果缓存不可用就从网络上加载数据。
  • LOAD_NO_CACHE:不加载缓存,只从网络加载数据。
  • LOAD_CACHE_ONLY:不从网络加载数据,只从缓存加载数据。

9.WebSettings里与缓存机制相关设置

        // 开启Application Caches 功能
        mWebSettings.setAppCacheEnabled(true);
        // 设置Application Caches 缓存目录
        // 这个路径必须是可以让app写入文件的。该方法应该只被调用一次,重复调用会被无视~
        String cacheDirPath = getFilesDir().getAbsolutePath() + APP_CACAHE_DIRNAME;
        mWebSettings.setAppCachePath(cacheDirPath);
        // 开启 DOM storage API 功能
        mWebSettings.setDomStorageEnabled(true);
        // 开启 database storage API 功能
        mWebSettings.setDatabaseEnabled(true);

WebViewClient

从名字上不难理解,这个类就像WebView的委托人一样,是帮助WebView处理各种通知和请求事件的,我们可以称他为WebView的“内政大臣”。

1. 重载URL

shouldOverrideUrlLoading方法在WebView加载URL的时候可以截获这个动作。

当我们使用默认返回时,WebView如果要加载一个URL会向Activity寻求一个适合的处理者来加载该URL(比如系统自带浏览器),这通常是我们不想看到的。于是我们需要给WebView提供一个WebViewClient,并重写shouldOverrideUrlLoading方法,返回true or false。这时便可以实现在app内访问网页。

该方法的返回值:
1、 默认返回:return super.shouldOverrideUrlLoading(view, url); 这个返回的方法会调用父类方法,也就是跳转至手机浏览器,平时写WebView一般都在方法里面写 webView.loadUrl(url); 然后把这个返回值改成下面的false。
2、返回: return true; WebView处理url是根据程序来执行的。
3、返回: return false; WebView处理url是在WebView内部执行。

注意:post请求并不会回调这个函数

            // Android 5.0之前的方法
            @Override
            public boolean shouldOverrideUrlLoading(WebView view, String url) {
                view.loadUrl(url);
                return false;
            }

            // Android 5.0之后的方法
            @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
            @Override
            public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
                view.loadUrl(String.valueOf(request.getUrl()));
                return false;
            }

2. 拦截请求

当WebView需要请求某个数据时,shouldInterceptRequest方法可以拦截该请求来告知app并且允许app本身返回一个数据来替代我们原本要加载的数据。

比如你对web的某个js做了本地缓存,希望在加载该js时不再去请求服务器而是可以直接读取本地缓存的js,这个方法就可以帮助你完成这个需求。你可以写一些逻辑检测这个request,并返回相应的数据,你返回的数据就会被WebView使用,如果你返回null,WebView会继续向服务器请求。

注意:shouldInterceptRequest方法是在非UI线程中调用的,因此,不要在此方法中进行任何UI操作。

            // Android 5.0之前的方法
            @Override
            public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
                // 根据自定义逻辑,判断是否使用本地资源
                WebResourceResponse webResourceResponse = WebResourceHelper.getInstance().getCache(url);
                return webResourceResponse != null ? webResourceResponse : super.shouldInterceptRequest(view, url);
            }

            // Android 5.0之后的方法
            @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
            @Override
            public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
                if (request.getMethod().toLowerCase().equals("get")) {
                    return this.shouldInterceptRequest(view, request.getUrl().toString());
                } else {
                    return super.shouldInterceptRequest(view, request);
                }
            }

3. 页面监听

onPageStarted方法在WebView开始加载页面且仅在Main frame loading(即整页加载)时回调,一次Main frame的加载只会回调该方法一次。我们可以在这个方法里设定开启一个加载的动画,告诉用户程序在等待网络的响应。

onPageFinished方法只在WebView完成一个页面加载时调用一次(同样也只在Main frame loading时调用),我们可以可以在此时关闭加载动画,进行其他操作。

onPageStarted就是开始载入页面调用的,onPageFinished就是载入页面完成调用的,通常我们可以在这设定一个loading的页面,告诉用户页面正在加载中。

注意:由于URL可能会有重定向,导致onPageStarted和onPageFinished可能会被调用多次。
Solution: How to listen for a WebView finishing loading a URL?

    boolean loadingFinished = true;
    boolean redirect = false;
    private void initWebViewClient() {
        mWebView.setWebViewClient(new WebViewClient() {

            @Override
            public void onPageStarted(WebView view, String url, Bitmap favicon) {
                super.onPageStarted(view, url, favicon);
                loadingFinished = false;
                //SHOW LOADING IF IT ISN'T ALREADY VISIBLE
            }

            @Override
            public void onPageFinished(WebView view, String url) {
                super.onPageFinished(view, url);
                if (!redirect) {
                    loadingFinished = true;
                }

                if (loadingFinished && !redirect) {
                    //HIDE LOADING IT HAS FINISHED
                } else {
                    redirect = false;
                }
            }

            @Override
            public boolean shouldOverrideUrlLoading(WebView view, String url) {
                if (!loadingFinished) {
                    redirect = true;
                }

                loadingFinished = false;
                view.loadUrl(url);
                return false;
            }
        });
    }

4. 资源加载

onLoadResource方法在加载页面资源时会回调,每一个资源(比如图片)的加载都会调用一次。

            @Override
            public void onLoadResource(WebView view, String url) {
                super.onLoadResource(view, url);
            }

5. 错误处理

Android 6.0之前的onReceivedError方法,在web页面加载错误时回调,这些错误通常都是由无法与服务器正常连接引起的。

简单来说,只有在遇到不可用的(unrecoverable)错误时,才会被调用。

不可用情况包括:

  • 没有网络连接
  • 连接超时
  • 找不到页面

不可用情况不包括:

  • 网页内引用其他资源加载错误,比如图片、css不可用
  • js执行错误

Android 6.0之后的onReceivedError方法,在页面局部加载发生错误时也会被调用(比如页面里两个子Tab或者一张图片),这就意味着该方法的调用频率可能会更加频繁,所以我们应该在该方法里执行尽量少的操作。

当遇到错误后,可以显示一个自定义的错误页,优化用户体验,而不应该将默认的网页错误样式直接显示。

            // Android 6.0之前的方法
            @Override
            public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
                super.onReceivedError(view, errorCode, description, failingUrl);
                // 断网或者网络连接超时
                if (errorCode == ERROR_HOST_LOOKUP || errorCode == ERROR_CONNECT || errorCode == ERROR_TIMEOUT) {
                    view.loadUrl("about:blank"); // 避免出现默认的错误界面
                    // 在这里显示自定义错误页
                    showErrorPage();
                }
            }

            // Android 6.0之后的方法
            @RequiresApi(api = Build.VERSION_CODES.M)
            @Override
            public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) {
                super.onReceivedError(view, request, error);
                // 如果当前网络请求是为main frame创建的,则显示错误页
                if (request.isForMainFrame()) {
                    this.onReceivedError(view, error.getErrorCode(), error.getDescription().toString(), request.getUrl().toString());
                } else {
                    // main frame没有出错,正常显示
                }
            }

上一个方法提到onReceivedError并不会在服务器返回错误码时被回调,那么当我们需要捕捉HTTP ERROR并进行相应操作时应该怎么办呢?API23便引入了onReceivedHttpError方法。当服务器返回一个HTTP ERROR并且它的status code>=400时,该方法便会回调。这个方法的作用域并不局限于Main Frame,任何资源的加载引发HTTP ERROR都会引起该方法的回调,所以我们也应该在该方法里执行尽量少的操作,只进行非常必要的错误处理等。

            @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
            @Override
            public void onReceivedHttpError(WebView view, WebResourceRequest request, WebResourceResponse errorResponse) {
                super.onReceivedHttpError(view, request, errorResponse);
                // 这个方法在6.0才出现
                int statusCode = errorResponse.getStatusCode();
                Log.d(TAG, "onReceivedHttpError code = " + statusCode);
                if (404 == statusCode || 500 == statusCode) {
                    view.loadUrl("about:blank");// 避免出现默认的错误界面
                    showErrorPage();
                }
            }

当Android 6.0以下,WebView想要判断HTTP ERROR 404、500,则可以通过在WebChromeClient()子类中可以重写他的onReceivedTitle()方法。

            @Override
            public void onReceivedTitle(WebView view, String title) {
                super.onReceivedTitle(view, title);
                // android 6.0 以下通过title获取
                if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
                    if (title.contains("404") || title.contains("500") || title.contains("Error")) {
                        view.loadUrl("about:blank"); // 避免出现默认的错误界面
                        // 在这里显示自定义错误页
                        showErrorPage();
                    }
                }
            }

当WebView加载某个资源引发SSL错误时会回调onReceivedSslError方法,这时WebView要么执行handler.cancel()取消加载,要么执行handler.proceed()方法继续加载(默认为cancel)。

注意:这个决定可能会被保留并在将来再次遇到SSL错误时执行同样的操作。
当遇到SSL错误时,较好的solution为,给用户一个Dialog提示。
Solution: Webview avoid security alert from google play upon implementation of onReceivedSslError

            @Override
            public void onReceivedSslError(WebView view, final SslErrorHandler handler, SslError error) {
                final AlertDialog.Builder builder = new AlertDialog.Builder(WebViewActivity.this);
                builder.setMessage("SSL证书无效");
                builder.setPositiveButton("continue", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        handler.proceed();
                    }
                });
                builder.setNegativeButton("cancel", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        handler.cancel();
                    }
                });
                final AlertDialog dialog = builder.create();
                dialog.show();
            }

6. 页面缩放

当WebView得页面Scale值发生改变时回调onScaleChanged方法。

getScale()在sdk 17中已经被弃用,建议使用WebViewClient中的onScaleChanged获取缩放比例

            @Override
            public void onScaleChanged(WebView view, float oldScale, float newScale) {
                super.onScaleChanged(view, oldScale, newScale);
            }

7. 按键监听

shouldOverrideKeyEvent方法默认值为false,重写此方法并return true可以让我们在WebView内处理按键事件。

            @Override
            public boolean shouldOverrideKeyEvent(WebView view, KeyEvent event) {
                return super.shouldOverrideKeyEvent(view, event);
            }

WebChromeClient

如果说WebViewClient是帮助WebView处理各种通知、请求事件的“内政大臣”的话,那么WebChromeClient就是辅助WebView处理Javascript的对话框,网站图标,网站title,加载进度等偏外部事件的“外交大臣”。

1. 处理JS回调

由于WebView只是载体,内容的渲染需要使用WebChromeClient类去实现,因此如果不重写以下这几个方法的话,在JS里调用Alert()并没有效果。

前提:setJavaScriptCanOpenWindowsAutomatically(true)
注意:JS代码调用一定要在 onPageFinished()回调之后才能调用,否则不会调用。

  • onJsAlert 处理Javascript中的Alert对话框。
            @Override
            public boolean onJsAlert(WebView view, String url, String message, final JsResult result) {
                Log.d(TAG, "onJsAlert");
                final AlertDialog.Builder builder = new AlertDialog.Builder(WebViewActivity.this);
                builder.setTitle("对话框")
                        .setMessage(message)
                        .setPositiveButton("确定", null);

                // 不需要绑定按键事件
                // 屏蔽keycode等于84之类的按键
                builder.setOnKeyListener(new DialogInterface.OnKeyListener() {
                    public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) {
                        Log.d(TAG, "onJsAlert keyCode==" + keyCode + "event=" + event);
                        return true;
                    }
                });
                // 禁止响应按back键的事件
                builder.setCancelable(false);
                AlertDialog dialog = builder.create();
                dialog.show();
                result.confirm();// 因为没有绑定事件,需要强行confirm,否则页面会变黑显示不了内容。
                return true;
            }
  • onJsConfirm 处理Javascript中的Confirm对话框。
            @Override
            public boolean onJsConfirm(WebView view, String url, String message, final JsResult result) {
                Log.d(TAG, "onJsConfirm");
                final AlertDialog.Builder builder = new AlertDialog.Builder(WebViewActivity.this);
                builder.setTitle("对话框")
                        .setMessage(message)
                        .setPositiveButton("确定", new DialogInterface.OnClickListener() {
                            public void onClick(DialogInterface dialog, int which) {
                                result.confirm();
                            }
                        })
                        .setNeutralButton("取消", new DialogInterface.OnClickListener() {
                            public void onClick(DialogInterface dialog, int which) {
                                result.cancel();
                            }
                        });

                builder.setOnCancelListener(new DialogInterface.OnCancelListener() {
                    @Override
                    public void onCancel(DialogInterface dialog) {
                        result.cancel();
                    }
                });

                // 屏蔽keycode等于84之类的按键,避免按键后导致对话框消息而页面无法再弹出对话框的问题
                builder.setOnKeyListener(new DialogInterface.OnKeyListener() {
                    @Override
                    public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) {
                        Log.d(TAG, "onJsConfirm keyCode==" + keyCode + "event=" + event);
                        return true;
                    }
                });

                // 禁止响应按back键的事件
                builder.setCancelable(false);
                AlertDialog dialog = builder.create();
                dialog.show();
                return true;
            }
  • onJsPrompt 处理Javascript中的Prompt对话框。
            @Override
            public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, final JsPromptResult result) {
                Log.d(TAG, "onJsPrompt");
                final AlertDialog.Builder builder = new AlertDialog.Builder(WebViewActivity.this);
                builder.setTitle("对话框").setMessage(message);
                final EditText et = new EditText(view.getContext());
                et.setSingleLine();
                et.setText(defaultValue);
                builder.setView(et)
                        .setPositiveButton("确定", new DialogInterface.OnClickListener() {
                            public void onClick(DialogInterface dialog, int which) {
                                result.confirm(et.getText().toString());
                            }

                        })
                        .setNeutralButton("取消", new DialogInterface.OnClickListener() {
                            public void onClick(DialogInterface dialog, int which) {
                                result.cancel();
                            }
                        });

                // 屏蔽keycode等于84之类的按键,避免按键后导致对话框消息而页面无法再弹出对话框的问题
                builder.setOnKeyListener(new DialogInterface.OnKeyListener() {
                    public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) {
                        Log.v(TAG, "onJsPrompt keyCode==" + keyCode + "event=" + event);
                        return true;
                    }
                });

                // 禁止响应按back键的事件
                builder.setCancelable(false);
                AlertDialog dialog = builder.create();
                dialog.show();
                return true;
            }
  • onJsBeforeUnload 处理Javascript中的beforeUnload对话框,即监听关闭浏览器。
            @Override
            public boolean onJsBeforeUnload(WebView view, String url, String message, JsResult result) {
                return super.onJsBeforeUnload(view, url, message, result);
            }

2. 加载进度

当页面加载的进度发生改变时回调,onProgressChanged用来告知主程序当前页面的加载进度。

            @Override
            public void onProgressChanged(WebView view, int newProgress) {
                super.onProgressChanged(view, newProgress);
                Log.d(TAG, "newProgress = " + newProgress);
                if (newProgress == 100) {
                    //加载完毕进度条消失
                    mProgressView.setVisibility(View.GONE);
                } else {
                    //更新进度
                    if (mProgressView.getVisibility() != View.VISIBLE) {
                        mProgressView.setVisibility(View.VISIBLE);
                    }
                    mProgressView.setProgress(newProgress);
                }
            }

自定义顶部进度条

public class ProgressView extends View {
    private Paint mPaint;
    private int mWidth, mHeight;
    private int progress;//加载进度

    public ProgressView(Context context) {
        this(context, null);
    }

    public ProgressView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public ProgressView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        //初始化画笔
        mPaint = new Paint();
        mPaint.setDither(true);
        mPaint.setAntiAlias(true);
        mPaint.setStrokeWidth(10);
        mPaint.setColor(Color.RED);
    }

    @Override
    protected void onSizeChanged(int w, int h, int ow, int oh) {
        mWidth = w;
        mHeight = h;
        super.onSizeChanged(w, h, ow, oh);
    }


    @Override
    protected void onDraw(Canvas canvas) {
        canvas.drawRect(0, 0, mWidth * progress / 100, mHeight, mPaint);
        super.onDraw(canvas);
    }

    /**
     * 设置新进度 重新绘制
     *
     * @param newProgress 新进度
     */
    public void setProgress(int newProgress) {
        this.progress = newProgress;
        invalidate();
    }

    /**
     * 设置进度条颜色
     *
     * @param color 色值
     */
    public void setColor(int color) {
        mPaint.setColor(color);
    }
}

在WebViewActivity中初始化自定义顶部进度条

    private void initProgressView() {
        //初始化进度条
        mProgressView = new ProgressView(getBaseContext());
        LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
                ViewGroup.LayoutParams.MATCH_PARENT, CommonUtils.dp2px(getBaseContext(), 4));

        mProgressView.setLayoutParams(params);
        mProgressView.setColor(Color.BLUE);
        mProgressView.setProgress(10);
        //把进度条加到Webview中
        mLlWebView.addView(mProgressView);
    }

3. 接收信息

  • onReceivedIcon 用来接收web页面的icon,我们可以在这里将该页面的icon设置到Toolbar。
  • onReceivedTitle 用来接收web页面的title,我们可以在这里将页面的title设置到Toolbar。
            @Override
            public void onReceivedIcon(WebView view, Bitmap icon) {
                super.onReceivedIcon(view, icon);
                mToolbar.setLogo(new BitmapDrawable(WebViewActivity.this.getResources(), icon));
            }

            @Override
            public void onReceivedTitle(WebView view, String title) {
                super.onReceivedTitle(view, title);
                mToolbar.setTitle(title);
            }

4. 全屏模式

  • onShowCustomView 在当前页面进入全屏模式时回调,主程序必须提供一个包含当前web内容(视频 or Something)的自定义的View。
  • onHideCustomView 该方法在当前页面退出全屏模式时回调,主程序应在这时隐藏之前show出来的View。

            @Override
            public void onShowCustomView(View view, CustomViewCallback callback) {
                super.onShowCustomView(view, callback);
            }

            @Override
            public void onHideCustomView() {
                super.onHideCustomView();
            }

5. 视频处理

  • getDefaultVideoPoster 当我们的Web页面包含视频时,我们可以在HTML里为它设置一个预览图,WebView会在绘制页面时根据它的宽高为它布局。

注意:当我们处于弱网状态下时,我们没有比较快的获取该图片,那WebView绘制页面时的gitWidth()方法就会报出空指针异常。这时我们就需要重写该方法,在我们尚未获取web页面上的video预览图时,给予它一个本地的图片,避免空指针的发生。

  • getVideoLoadingProgressView 重写该方法可以在视频loading时给予一个自定义的View,可以是加载圆环 or something。
            @Override
            public Bitmap getDefaultVideoPoster() {
                return super.getDefaultVideoPoster();
            }

            @Override
            public View getVideoLoadingProgressView() {
                return super.getVideoLoadingProgressView();
            }

6. 文件选择

  • onShowFileChooser 该方法在用户进行了web上某个需要上传文件的操作时回调。我们应该在这里打开一个文件选择器,如果要取消这个请求我们可以调用该方法在用户进行了web上某个需要上传文件的操作时回调。我们应该在这里打开一个文件选择器,如果要取消这个请求我们可以调用filePathCallback.onReceiveValue(null)并返回true。
            @Override
            public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams) {
                return super.onShowFileChooser(webView, filePathCallback, fileChooserParams);
            }

7. 权限处理

  • onPermissionRequest 该方法在web页面请求某个尚未被允许或拒绝的权限时回调,主程序在此时调用grant(String [])或deny()方法。如果该方法没有被重写,则默认拒绝web页面请求的权限。
  • onPermissionRequestCanceled 该方法在web权限申请权限被取消时回调,这时应该隐藏任何与之相关的UI界面。
            @Override
            public void onPermissionRequest(PermissionRequest request) {
                super.onPermissionRequest(request);
            }

            @Override
            public void onPermissionRequestCanceled(PermissionRequest request) {
                super.onPermissionRequestCanceled(request);
            }

8. 窗口管理

  • onCreateWindow 该方法在web调用window.open的时候回调,申请新建一个窗口。如果允许新建,则返回true。默认返回值为false。
  • onCloseWindow 该方法在web调用window.close的时候回调,申请关闭该窗口。

注意:调用这两个方法的前提:
setJavaScriptEnabled(true);
setJavaScriptCanOpenWindowsAutomatically(true);
setSupportMultipleWindows(true);

            @Override
            public boolean onCreateWindow(WebView view, boolean isDialog, boolean isUserGesture, Message resultMsg) {
                return super.onCreateWindow(view, isDialog, isUserGesture, resultMsg);
            }

            @Override
            public void onCloseWindow(WebView window) {
                super.onCloseWindow(window);
            }

Android与JavaScript交互

二者沟通的桥梁是WebView

注意:交互前提,需要设置与Js交互的权限setJavaScriptEnabled(true);

Android调用JavaScript方法

为了方便测试,使用以下本地HTML

<html>
<head>
    <meta charset="utf-8"/>
    <title>JeremySun Js Test</title>
    <script>
        function testLoadUrl1(){
            var element=document.getElementById("header1");
            element.innerHTML="Test LoadUrl without Params Successfully";
        }

        function testLoadUrl2(message){
            var element=document.getElementById("header2");
            element.innerHTML=message;
        }

        function testEvaluateJavascript(){
            var element=document.getElementById("header3");
            element.innerHTML="Test EvaluatedJavaScript Successfully";
            return "Successfully";
        }
    </script>
</head>
<body>
<h1>Android调用JS方法:</h1>
<h1>方法1:loadUrl</h1>
<h2 id="header1">点击menu,测试无参数的loadUrl</h2>
<h2 id="header2">点击menu,测试带参数的loadUrl</h2>

<h1>方法2:evaluateJavascript</h1>
<h2 id="header3">点击menu,测试待返回的evaluateJavascript</h2>
</body>
</html>
1.通过WebView的loadUrl()
case R.id.android_to_js1:
    Log.d(TAG, "loadUrl with no params");
    mWebView.loadUrl("javascript:testLoadUrl1()");
    break;
case R.id.android_to_js2:
    Log.d(TAG, "loadUrl with params");
    String parameters = "Test LoadUrl With Params Successfully";
    mWebView.loadUrl("javascript:testLoadUrl2(\"" + parameters + "\")");
    break;
2.通过WebView的evaluateJavascript()
case R.id.android_to_js3:
    Log.d(TAG, "evaluateJavascript with return");
    mWebView.evaluateJavascript("javascript:testEvaluateJavascript()", new ValueCallback<String>() {

        @Override
        public void onReceiveValue(String value) {
            Toast.makeText(WebViewActivity.this, "onReceiveValue = " + value, Toast.LENGTH_SHORT).show();
        }
    });
    break;
3.方法对比
调用方法 优点 缺点 其他
loadUrl 方便简洁,兼容性强 获取返回值麻烦 执行会使页面刷新
evaluateJavascript 效率更高,容易获取返回值 仅支持Android 4.4以上 执行不会使页面刷新
4.使用建议
// 因为该方法在 Android 4.4 版本才可使用,所以使用时需进行版本判断
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
    mWebView.loadUrl("javascript:callJS()");
} else {
    mWebView.evaluateJavascript("javascript:callJS()", new ValueCallback<String>() {
        @Override
        public void onReceiveValue(String value) {
            //此处为 js 返回的结果
        }
    });
}

JavaScript调用Android方法

为了方便测试,使用以下本地HTML

<html>
<head>
    <meta charset="utf-8"/>
    <title>JeremySun Js Test</title>
    <script>
        function testJavascriptInterfaceWithParams() {
            test.jsiTestToast("Test JavascriptInterface With Params Successfully");
        }

        function testJavascriptInterfaceWithoutParams() {
            test.jsiTestToast();
        }

        function testJavascriptInterfaceWithoutReturn() {
            var result = test.jsiGetToastMessage();
            alert(result);
        }

        function testShouldOverrideUrlLoading() {
            document.location="js://webview?arg1=111&arg2=222";
        }

        function testPromptInterface() {
            var result=prompt("js://webview?arg1=111&arg2=222");
            alert("demo " + result);
        }

        function testAlert() {
            alert("I am an alert box!!");
        }

        function testConfirm() {
            var r = confirm("Press a button");
            var element=document.getElementById("confirm_result");
            if (r == true) {
                element.innerHTML="You pressed OK!";
            } else {
                element.innerHTML="You pressed Cancel!";
            }
        }

        function testPrompt() {
            var name = prompt("Please enter your name", "");
            var element=document.getElementById("prompt_result");
            if (name != null && name != "") {
                element.innerHTML="Hello " + name + "!";
            }
        }
    </script>
</head>
<body>
<h1>JS调用Android方法:</h1>
<h1>方法1:addJavascriptInterface</h1>
<button style="width:600px;height:60px;font-size:30px;"
        onclick="testJavascriptInterfaceWithParams()"><strong>test JavascriptInterface with
    params</strong></button>
<br><br>
<button style="width:600px;height:60px;font-size:30px;"
        onclick="testJavascriptInterfaceWithoutParams()"><strong>test JavascriptInterface without
    params</strong></button>
<br><br>
<button style="width:600px;height:60px;font-size:30px;"
        onclick="testJavascriptInterfaceWithoutReturn()"><strong>test JavascriptInterface with
    return</strong></button>
<br><br>

<h1>方法2:shouldOverrideUrlLoading拦截</h1>
<button style="width:600px;height:60px;font-size:30px;" onclick="testShouldOverrideUrlLoading()">
    <strong>test shouldOverrideUrlLoading</strong></button>
<br><br>

<h1>方法3:onAlert, onConfirm, onPrompt</h1>
<button style="width:350px;height:60px;font-size:30px;" onclick="testPromptInterface()"><strong>test Prompt Interface</strong></button>
<br><br>

<button style="width:230px;height:60px;font-size:30px;" onclick="testAlert()"><strong>test
    Alert</strong></button>
<br><br>

<button style="width:230px;height:60px;font-size:30px;" onclick="testConfirm()"><strong>test
    Confirm</strong></button>
<br><br>
<h2 id="confirm_result">Confirm点击结果</h2>

<button style="width:230px;height:60px;font-size:30px;" onclick="testPrompt()"><strong>test
    Prompt</strong></button>
<br><br>
<h2 id="prompt_result">Prompt点击结果</h2>
</body>
</html>
1.通过WebView的addJavascriptInterface()进行对象映射
    private void initJavaScriptInterface() {
        if (mWebView != null) {
            // 通过addJavascriptInterface()将Java对象映射到JS对象
            // 参数1:Javascript对象名
            // 参数2:Java对象名
            mWebView.addJavascriptInterface(new JsTestInterface(), "test");
        }
    }

    private class JsTestInterface {
        public JsTestInterface() {
        }

        // 定义JS需要调用的方法
        // Google 在Android 4.2 版本中规定对被调用的函数以 @JavascriptInterface进行注解从而避免漏洞攻击
        @JavascriptInterface
        public void jsiTestToast(String message) {
            Toast.makeText(WebViewActivity.this, "jsiTestToast: params = " + message, Toast.LENGTH_SHORT).show();
        }


        @JavascriptInterface
        public void jsiTestToast() {
            Toast.makeText(WebViewActivity.this, "jsiTestToast: no params", Toast.LENGTH_SHORT).show();
        }


        @JavascriptInterface
        public String jsiGetToastMessage() {
            Toast.makeText(WebViewActivity.this, "jsiGetToastMessage", Toast.LENGTH_SHORT).show();
            return "Toast Message";
        }

    }

注意:addJavascriptInterface在Android 4.2以下存在严重漏洞:
当JS拿到Android这个对象后,就可以调用这个Android对象中所有的方法,包括系统类(java.lang.Runtime 类),从而进行任意代码执行。

2.通过 WebViewClient 的shouldOverrideUrlLoading ()方法回调拦截 url
            @Override
            public boolean shouldOverrideUrlLoading(WebView view, String url) {
                Log.d(TAG, "shouldOverrideUrlLoading");

                // 根据协议的参数,判断是否是所需要的url
                // 一般根据scheme(协议格式) & authority(协议名)判断(前两个参数)
                // 假定传入进来的 url = "js://webview?arg1=111&arg2=222"(同时也是约定好的需要拦截的)
                Uri uri = Uri.parse(url);
                // 如果url的协议 = 预先约定的 js 协议
                // 就解析往下解析参数
                if (uri.getScheme().equals("js")) {
                    // 如果 authority  = 预先约定协议里的 WebView,即代表都符合约定的协议
                    // 所以拦截url,下面JS开始调用Android需要的方法
                    if (uri.getAuthority().equals("webview")) {
                        // 可以在协议上带有参数并传递到Android上
                        HashMap<String, String> params = new HashMap<>();
                        Set<String> collection = uri.getQueryParameterNames();
                        String arg1 = uri.getQueryParameter("arg1");
                        String arg2 = uri.getQueryParameter("arg2");
                        Toast.makeText(WebViewActivity.this, "arg1 = " + arg1 + ", arg2 = " + arg2, Toast.LENGTH_SHORT).show();
                    }
                    return true;
                }
                return super.shouldOverrideUrlLoading(view, url);
            }

传递返回值的方法:

// Android:MainActivity.java
mWebView.loadUrl("javascript:returnResult(" + result + ")");

// JS:javascript.html
function returnResult(result){
    alert("result is" + result);
}
3.通过 WebChromeClient 的onJsAlert()、onJsConfirm()、onJsPrompt()方法回调拦截JS对话框alert()、confirm()、prompt() 消息

常用的是拦截onJsPrompt()方法,因为可以返回任意类型的值,操作最全面方便、更加灵活。

            @Override
            public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, final JsPromptResult result) {
                Log.d(TAG, "onJsPrompt");
                Uri uri = Uri.parse(message);
                // 如果url的协议 = 预先约定的 js 协议
                // 就解析往下解析参数
                if (uri.getScheme().equals("js")) {
                    // 如果 authority  = 预先约定协议里的 WebView,即代表都符合约定的协议
                    // 所以拦截url,下面JS开始调用Android需要的方法
                    if (uri.getAuthority().equals("webview")) {
                        String arg1 = uri.getQueryParameter("arg1");
                        String arg2 = uri.getQueryParameter("arg2");
                        result.confirm("arg1 = " + arg1 + ", arg2 = " + arg2);
                    }
                    return true;
                } else {
                    return super.onJsPrompt(view, url, message, defaultValue, result);
                }
            }
4.方法对比
调用方法 优点 缺点
addJavascriptInterface 使用简单 Android 4.2以下存在严重的漏洞问题
shouldOverrideUrlLoading 不存在上面的漏洞 获取返回值复杂,需要进行协议约束
onJsAlert 不存在上面的漏洞 使用复杂,需要进行协议约束

源码

Github: https://github.com/JeremySun823/MyWebViewTest

参考链接

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

推荐阅读更多精彩内容