25、WebView 的定制与扩展:WebView 配置、JavaScript 接口注入、自定义协议拦截、WebViewClient 与 WebChromeClient

WebView 是 Android 平台上承载 Web 内容的容器。但说实话,默认的 WebView 就是个“毛坯房”——能跑,但不好用。真正让它发挥价值的,是定制与扩展能力。

这一章,我会带你深入 WebView 的四个核心扩展点:WebSettings(配置)、addJavascriptInterface(JS 接口注入)、自定义协议拦截,以及两个关键回调类——WebViewClientWebChromeClient

嗯,这些内容我几乎在每个项目里都会用到。从简单的 H5 混合开发,到复杂的浏览器内核定制,都绕不开它们。

WebView 定制与扩展核心体系 WebView WebSettings 配置 JS 接口注入 自定义协议拦截 WebViewClient / WebChromeClient JavaScript 开关 缓存策略 User-Agent 缩放/渲染 shouldInterceptRequest 自定义 scheme 页面加载 / 进度 / 弹窗 错误处理 / 权限请求

25.1 WebSettings:把毛坯房精装修

WebSettings 是 WebView 的配置中心。你想想看,一个浏览器要支持 JavaScript、要缓存、要缩放……这些开关全在这里。

我个人习惯在 WebView 初始化后,第一时间拿到 WebSettings 对象,然后按需配置。下面是我常用的配置模板:

WebSettings settings = webView.getSettings();

// JavaScript 必须开,否则 H5 动不了
settings.setJavaScriptEnabled(true);

// 缓存策略:有网用默认,没网用缓存
settings.setCacheMode(WebSettings.LOAD_DEFAULT);
settings.setDomStorageEnabled(true);

// 缩放相关
settings.setSupportZoom(true);
settings.setBuiltInZoomControls(true);
settings.setDisplayZoomControls(false); // 隐藏缩放按钮

// 渲染加速
settings.setRenderPriority(WebSettings.RenderPriority.HIGH);
settings.setLayoutAlgorithm(WebSettings.LayoutAlgorithm.SINGLE_COLUMN);

// User-Agent 定制(我经常用来区分客户端)
String ua = settings.getUserAgentString();
settings.setUserAgentString(ua + " MyApp/1.0");
⚠️ 注意:setJavaScriptEnabled(true) 必须在 WebView 加载任何 URL 之前调用。我曾经在项目里因为调用时机不对,导致页面白屏,排查了半天才发现是 JS 没开。

还有一个容易被忽略的点:混合内容。如果你的页面是 HTTPS,但引用了 HTTP 的资源,默认会被阻塞。这时候需要:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
    settings.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
}

嗯,这个坑我踩过。有一次对接第三方支付页面,HTTPS 页面里加载了 HTTP 的图片,结果图片全裂了。加上这行代码就好了。

25.2 JavaScript 接口注入:让 H5 和 Native 对话

addJavascriptInterface 是 WebView 最强大的功能之一。它允许你把 Java 对象暴露给 JavaScript 调用。说白了,就是搭了一座桥。

来看一个典型例子:

// 定义接口类
public class JsBridge {
    @JavascriptInterface
    public void share(String title, String url) {
        // 调用 Native 分享功能
        Intent intent = new Intent(Intent.ACTION_SEND);
        intent.putExtra(Intent.EXTRA_TEXT, title + " " + url);
        intent.setType("text/plain");
        context.startActivity(Intent.createChooser(intent, "分享到"));
    }

    @JavascriptInterface
    public String getToken() {
        return "your_token_here";
    }
}

// 注入到 WebView
webView.addJavascriptInterface(new JsBridge(), "NativeBridge");

然后在 H5 里这样调用:

<button onclick="NativeBridge.share('标题', 'https://example.com')">分享</button>
<script>
    var token = NativeBridge.getToken();
    console.log(token);
</script>
💡 关键点:被 @JavascriptInterface 注解的方法,必须运行在独立的线程中,不能阻塞 UI 线程。另外,这个方法在 Android 4.2 以下存在安全漏洞,建议 minSdk 至少设为 17。

我在项目中遇到过一个问题:H5 频繁调用 Native 方法获取数据,导致页面卡顿。后来我加了一层缓存,把高频数据(比如 token、设备信息)在注入时一次性传给 JS,而不是每次调用都走桥接。性能提升很明显。

25.3 自定义协议拦截:接管网络请求

WebView 默认处理 http/https 协议。但如果你想拦截自定义协议(比如 myapp://openPage),或者拦截特定请求做本地处理,就需要用到 shouldInterceptRequest。

这个回调在 WebViewClient 里。我经常用它来做两件事:

  • 拦截自定义协议:比如 myapp://scan,唤起扫码页面
  • 本地资源替换:把远程图片替换为本地资源,加速加载

代码示例:

webView.setWebViewClient(new WebViewClient() {
    @Override
    public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
        String url = request.getUrl().toString();

        // 拦截自定义协议
        if (url.startsWith("myapp://")) {
            handleCustomProtocol(url);
            return new WebResourceResponse(null, null, null);
        }

        // 替换本地资源
        if (url.contains("logo.png")) {
            try {
                InputStream is = context.getAssets().open("logo_local.png");
                return new WebResourceResponse("image/png", "utf-8", is);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        return super.shouldInterceptRequest(view, request);
    }
});
📌 提示:shouldInterceptRequest 运行在后台线程,不要在这里做 UI 操作。另外,返回 null 表示不拦截,让 WebView 自己处理。

我曾经用这个功能做过离线包方案:把 H5 的静态资源打包到 APK 里,拦截请求时优先返回本地文件。这样页面加载速度从 3 秒降到了 200 毫秒。效果非常显著。

25.4 WebViewClient 与 WebChromeClient:两个关键回调

这两个类经常被混淆。我简单总结一下:

回调类 主要职责 常用方法
WebViewClient 页面加载、请求拦截、错误处理 onPageStarted, onPageFinished, shouldOverrideUrlLoading, shouldInterceptRequest, onReceivedError
WebChromeClient 浏览器 UI 相关:进度、标题、弹窗、权限 onProgressChanged, onReceivedTitle, onJsAlert, onGeolocationPermissionsShowPrompt

说白了,WebViewClient 管的是“页面本身”,WebChromeClient 管的是“浏览器外壳”。

来看一个完整的例子:

// WebViewClient:控制页面行为
webView.setWebViewClient(new WebViewClient() {
    @Override
    public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
        String url = request.getUrl().toString();
        // 拦截 tel: 协议,调用系统拨号
        if (url.startsWith("tel:")) {
            Intent intent = new Intent(Intent.ACTION_DIAL, Uri.parse(url));
            context.startActivity(intent);
            return true;
        }
        return false;
    }

    @Override
    public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) {
        // 加载失败时显示自定义错误页
        view.loadUrl("file:///android_asset/error.html");
    }
});

// WebChromeClient:控制浏览器 UI
webView.setWebChromeClient(new WebChromeClient() {
    @Override
    public void onProgressChanged(WebView view, int newProgress) {
        // 更新进度条
        progressBar.setProgress(newProgress);
        if (newProgress == 100) {
            progressBar.setVisibility(View.GONE);
        }
    }

    @Override
    public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
        // 用 Native 弹窗代替 JS 弹窗
        new AlertDialog.Builder(context)
                .setMessage(message)
                .setPositiveButton("确定", (d, w) -> result.confirm())
                .show();
        return true;
    }
});
🔑 核心原则:WebViewClient 的 shouldOverrideUrlLoading 返回 true 表示“我处理了,你别管”;返回 false 表示“让 WebView 自己加载”。这个逻辑一定要搞清楚,否则会出现页面跳转异常。

我记得有一次,用户反馈点击 H5 里的链接没反应。排查后发现是 shouldOverrideUrlLoading 里对所有 URL 都返回了 true,但没有做任何处理。结果就是所有链接都被“吃掉”了。嗯,这种低级错误,犯过一次就不会再犯了。

25.5 综合实践:一个可复用的 WebView 封装

最后,我分享一个我在项目中常用的封装思路。把配置、注入、拦截、回调整合到一个类里:

public class CustomWebViewClient extends WebViewClient {
    @Override
    public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
        String url = request.getUrl().toString();
        if (url.startsWith("myapp://")) {
            // 处理自定义协议
            return true;
        }
        return false;
    }

    @Override
    public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
        // 本地资源替换逻辑
        return null;
    }
}

// 使用
WebView webView = findViewById(R.id.webview);
WebSettings settings = webView.getSettings();
settings.setJavaScriptEnabled(true);
// ... 其他配置

webView.addJavascriptInterface(new JsBridge(), "NativeBridge");
webView.setWebViewClient(new CustomWebViewClient());
webView.setWebChromeClient(new MyWebChromeClient());
webView.loadUrl("https://example.com");

这样做的好处是:配置和逻辑分离,代码清晰,便于维护。你想想看,如果所有代码都堆在 Activity 里,后期改起来多痛苦。

好了,这一章的内容就到这里。WebView 的定制与扩展,说白了就是四个字:配置、注入、拦截、回调。把这四个点吃透,你就能应对绝大多数混合开发场景。


公众号:蓝海资料掘金营,微信 deep3321