Android WebView 详解

2,818 阅读6分钟

WebView 是一个基于 WebKit 引擎,展现 Web 页面的控件,Android 的 WebView 在低版本和高版本采用了不同的 WebKit 版本内核。

简单使用

WebView 最简单的使用方式是直接显示网页内容

<!-- 添加网络权限 -->
<uses-permission android:name="android.permission.INTERNET" />
  <WebView
        android:id="@+id/webview"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
   webview.loadUrl(url)

常用方法

后退网页

//判断是否可以后退
webview.canGoBack()
//后退网页
webview.goBack()

销毁 Webview,先从父容器移除,再销毁。

father.removeView(webview)
webview.destroy() 

在 Activity 中处理 Back 键事件

  // 监听手机屏幕上的按键
  override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
        if ((keyCode == KEYCODE_BACK) && webview.canGoBack()) {
            webview.goBack()
            return true
        }
        return super.onKeyDown(keyCode, event)
    }

清除缓存

//由于内核缓存是全局的,因此这个方法不仅针对该 Webview 而是针对整个应用程序。
webview.clearCache(true)

//清除当前 Webview 访问的历史记录
webview.clearHistory()

//清除自动完成填充的表单数据
webview.clearFormData()

WebSettings

作用:对 WebView 进行配置和管理

下面是一些常规设置

        val webSettings = webview.settings
        with(webSettings) {
            javaScriptEnabled = true //支持 JS
            javaScriptCanOpenWindowsAutomatically = true //支持通过 JS 打开新窗口

            domStorageEnabled = true //支持 DOM Storage
            defaultTextEncodingName = "utf-8" //设置编码格式
            loadsImagesAutomatically = true //支持自动加载图片

            setSupportZoom(true) //支持缩放,默认为 true。是下面属性的前提
            builtInZoomControls = true //设置内置的缩放控件,若为 false,则该 WebView 不可缩放
            displayZoomControls = true //隐藏原生的缩放控件

            databaseEnabled = true //数据库存储 API 是否可用
            cacheMode = WebSettings.LOAD_CACHE_ELSE_NETWORK //设置缓存,只要本地有就使用缓存中的数据,本地没有才从网络上获取
            allowFileAccess = true //设置可以访问文件

            //下面两者合用,可设置自适应屏幕
            useWideViewPort = true //将图片调整到适合 WebView 的大小
            loadWithOverviewMode = true //缩放至屏幕大小
        }

WebViewClient

在影响 View 的事件到来时,会通过 WebViewClient 中的方法回调。

 webview.webViewClient = object : WebViewClient() {
            override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean {
                view.loadUrl(url)
                return true
            }

            override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
                //开始载入页面时调用此方法,在这里我们可以设定一个 loading 的页面
            }
            override fun onPageFinished(view: WebView?, url: String?) {
                //在页面加载结束时调用,在这里我们可以关闭 loading 
            }

            override fun onLoadResource(view: WebView?, url: String?) {
                //在加载页面资源时会调用,比如图片
            }
        }

WebChromeClient

当影响浏览器的事件到来时,会通过 WebChromeClient 中的方法回调。

        webview.webChromeClient = object : WebChromeClient() {
            override fun onProgressChanged(view: WebView?, newProgress: Int) {
                //获得网页的加载进度并显示
                progressbar.text = if (newProgress < 100) "${newProgress}%" else "100%"
            }

            override fun onReceivedTitle(view: WebView?, title: String?) {
                //获取Web页面中的标题
                title_text.text = title
            }
        }

WebView 与 JS 的交互

Android 通过 WebView 调用 JS 代码

通过 WebView 的 loadUrl

        // 先载入 JS 代码
        webview.loadUrl(url)
        // 调用 javascript 的 callJS() 方法,调用的 JS 的方法名要对应上。
        webview.loadUrl("javascript:callJS()")

也可以通过 WebView 的 evaluateJavascript,这个比上面的方法效率更高,因为该方法的执行不会使页面刷新,而 loadUrl 会,该方法在 Android 4.4 后才可使用,不过现在的安卓版本基本上都高于 4.4 了,所以这点不用在意了。如果需要返回值的话,请务必使用这个方法。

  webview.evaluateJavascript("javascript:callJS()", object : ValueCallback<String> {
            override fun onReceiveValue(p0: String?) {
                //此处为 js 返回的结果
            }
        })

JS 通过 WebView 调用 Android 代码

通过 WebView 的 addJavascriptInterface 进行对象映射

    //定义一个内部类
    inner class AndroidJSInterFace{
        // 定义 JS 需要调用的方法,被 JS 调用的方法要加上 @JavascriptInterface 注解
        @JavascriptInterface
        fun hello(msg: String?) {
            runOnUiThread{
                title_text.text = msg
            }
        }
    }
        //AndroidJSInterFace 类对象映射到 js 的 test 对象
        webview.addJavascriptInterface(AndroidJSInterFace(), "test") 

调起系统相机和相册

    private var uploadMessageAboveL: ValueCallback<Array<Uri>>? = null
    private var cameraFielPath: String? = null
    private var uploadMessage: ValueCallback<Uri>? = null
   webview.webChromeClient = object :WebChromeClient(){
            override fun onShowFileChooser(
                webView: WebView?,
                filePathCallback: ValueCallback<Array<Uri>>?,
                fileChooserParams: FileChooserParams?
            ): Boolean {
                uploadMessageAboveL = filePathCallback
                openImageChooserActivity()
                return true
            }
            // For Android < 3.0
            fun openFileChooser(valueCallback: ValueCallback<Uri>) {
                uploadMessage = valueCallback
                openImageChooserActivity()
            }
        }
    // 回调方法触发本地选择文件
    private fun openImageChooserActivity() {
        //拍照
        val imageStorageDir = File(
            Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES),
            "heart_image"
        )
        if (!imageStorageDir.exists()) {
            imageStorageDir.mkdirs()
        }
        cameraFielPath = imageStorageDir.toString() + File.separator + "IMG_" + System.currentTimeMillis()
            .toString() + ".jpg"
        val file = File(cameraFielPath)

        //需要显示应用的意图列表,这个 list 的顺序和选择菜单上的图标顺序是相关的
        val cameraIntents: MutableList<Intent> = ArrayList()
        val captureIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
        val packageManager: PackageManager = packageManager
        //获取手机里所有注册相机接收意图的应用程序,放到意图列表里
        val listCam: List<ResolveInfo> = packageManager.queryIntentActivities(captureIntent, 0)
        for (res in listCam) {
            val packageName: String = res.activityInfo.packageName
            val i = Intent(captureIntent)
            i.component = ComponentName(res.activityInfo.packageName, res.activityInfo.name)
            i.setPackage(packageName)
            i.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(file))
            cameraIntents.add(i)
        }

        //相册
        val i = Intent(Intent.ACTION_GET_CONTENT)
        i.action = Intent.ACTION_PICK
        i.type = "image/*"
        val chooserIntent = Intent.createChooser(i, "选择")
        chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, cameraIntents.toTypedArray())
        startActivityForResult(
            chooserIntent,
            101
        )
    }
    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if (requestCode == 101) {
            val result = if (data == null || resultCode != Activity.RESULT_OK) null else data.data
            if (uploadMessageAboveL != null) {
                onActivityResultAboveL(requestCode, resultCode, data)
            } else if (uploadMessage != null) {
                uploadMessage?.onReceiveValue(result)
                uploadMessage = null
            }
            if (resultCode != RESULT_OK) {
                //这里 uploadMessage 和 uploadMessageAboveL 在不同系统版本下分别持有了
                //WebView 对象,在用户取消文件选择器的情况下,需给 onReceiveValue 传 null 返回值
                //否则 WebView 在未收到返回值的情况下,无法进行任何操作,文件选择器会失效
                if (uploadMessage != null) {
                    uploadMessage?.onReceiveValue(null)
                    uploadMessage = null
                } else if (uploadMessageAboveL != null) {
                    uploadMessageAboveL?.onReceiveValue(null)
                    uploadMessageAboveL = null
                }
            }
        }
    }
    // 选择内容回调到 Html 页面
    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    fun onActivityResultAboveL(requestCode: Int, resultCode: Int, intent: Intent?) {
        if (requestCode != 101 || uploadMessageAboveL == null)
            return
        var results = arrayOf<Uri>()
        var result: Uri? = null
        if (resultCode == Activity.RESULT_OK) {
            if (intent != null) {
                var dataString = intent.dataString
                var clipData = intent.clipData
                if (clipData != null) {
                    for (i in 0 until clipData.itemCount) {
                        var itemAt = clipData.getItemAt(i)
                        results[i] = itemAt.uri
                    }
                }
                if (dataString != null) {
                    results = arrayOf<Uri>(Uri.parse(dataString))
                }
                uploadMessageAboveL?.onReceiveValue(results)
                uploadMessageAboveL = null
            } else { 
                if (result == null && File(cameraFielPath).exists()) {
                    result = Uri.fromFile(File(cameraFielPath))
                }
                uploadMessageAboveL?.onReceiveValue(arrayOf(result!!))
                uploadMessageAboveL = null
            }
        }
    }

响应下载事件

 webview.setDownloadListener { url, userAgent, contentDisposition, mimetype, contentLength ->
            val uri = Uri.parse(url)
            val intent = Intent(Intent.ACTION_VIEW, uri)
            startActivity(intent)
        }

问题与优化

WebView 资源加载速度优化,主要针对图片。

Html 下载到 WebView 后,webkit 开始解析网页各个节点,发现有外部样式文件或外部脚本文件时,会异步发起网络请求下载文件,但如果在这之前也有解析到 image 节点,那势必也会发起网络请求下载相应的图片。在网络较差的情况下,过多的网络请求就会造成带宽紧张,影响到 css 或 js 文件加载完成的时间,造成页面空白过久。

优化建议: 告诉 WebView 先不要自动加载图片,等页面 finish 后再发起图片加载

        webview.settings.loadsImagesAutomatically = Build.VERSION.SDK_INT < KITKAT
        webview.webViewClient = object : WebViewClient() {
            override fun onPageFinished(view: WebView?, url: String?) {//页面完成之后再加载图片
                super.onPageFinished(view, url)
                if (!webview.settings.loadsImagesAutomatically) {
                    webview.settings.loadsImagesAutomatically = true
                }
            }
        }

自定义加载异常的状态页面

当 WebView 加载页面出错时,比如 404 NOT FOUND,WebView 会默认显示一个出错界面,这个页面我们可以自定义比较好看的界面。

        webview.webViewClient = object : WebViewClient() {
            override fun onReceivedHttpError(
                view: WebView?,
                request: WebResourceRequest?,
                errorResponse: WebResourceResponse?
            ) {
                super.onReceivedHttpError(view, request, errorResponse)
                if (errorResponse!!.statusCode == 404) {
                    webview.loadUrl(errUrl)
                }
            }
        }

WebView 加载网页不显示图片问题

WebView从 Android 5 开始默认不允许混合模式,,https 当中不能加载 http 资源,但是在开发的时候可能使用的是 https 的链接,链接中的图片又是 http 的,导致显示图片失败,这时就需要设置混合模式了。

        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP) {
            webview.settings.mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
        }
        webview.settings.blockNetworkImage = false
        }

WebView 无法唤起其他 app 问题

需要通过 url 链接唤起其他 App 的话,可以使用以下方式

    private fun isInstall(intent: Intent): Boolean {
        return App.getInstance().packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY).size > 0
    }

    private fun openApp(url: String): Boolean {
        if (TextUtils.isEmpty(url)) return false
        try {
            if (!url.startsWith("http") || !url.startsWith("https") || !url.startsWith("ftp")) {
                val uri = Uri.parse(url)
                val host = uri.host
                val scheme = uri.scheme
                if (!TextUtils.isEmpty(host) && !TextUtils.isEmpty(scheme)) {
                    val intent = Intent(Intent.ACTION_VIEW, uri)
                    if (isInstall(intent)) {
                        startActivity(intent)
                        return true
                    }
                }
            }
        } catch (e: Exception) {
            return false
        }
        return false
    }

最后在 shouldOverrideUrlLoading 中调用 openApp 方法即可