ARTICLE DETAIL

资讯详情

深耕网站SEO优化与搜索引擎排名提升的一线实战洞察。

Android WebView 全集

Android WebView 全集 1. 从 WebView 谈起我们为什么需要它移动互联网发展至今原生应用的速度与体验有其天然优势但 H5 页面在动态化、跨平台和运营迭代方面拥有不可替代的地位。无论是电商详情页、活动落地页、图文混排文章还是内嵌的 H5 游戏我们都很难绕开一个核心控件WebView。Android WebView 从最初基于 WebKit 的精简封装到如今依托 Chromium 内核独立演进其功能边界一直在不断扩大坑也在不断增加。本文试图用超过两万字的篇幅对 Android WebView 进行系统且深入的全集讲解从基础使用、WebSettings 配置、WebViewClient 与 WebChromeClient 自定义到 JavaScript 交互、生命周期管理、缓存机制、性能优化、安全防护再到独立进程、多进程 WebView 和未来可替代方案力求覆盖一线开发中会遇到的绝大部分问题。读完本文你将能够理解 WebView 加载流程与核心回调链路熟练配置 WebSettings平衡功能与安全掌握 JavaScript 双向通信的多种方案与最新方案设计健壮的 WebView 页面生命周期管理策略找到 WebView 内存泄漏的根因并治理使用离线包、拦截加载等方式优化首屏加载速度构建安全可靠的 WebView 容器防范常见攻击了解 Android WebView 未来的演进方向。文章较长建议先收藏再按章节阅读代码示例均可在 Android 项目中直接运行。2. WebView 的基本使用从 Hello World 开始WebView 的初始使用门槛很低但很多看似简单的代码背后都藏有未来可能爆炸的隐患。我们先从最基础的加载开始。在布局文件中添加 WebViewWebView android:idid/webview android:layout_widthmatch_parent android:layout_heightmatch_parent /在 Activity 或 Fragment 中初始化WebView webView findViewById(R.id.webview); webView.loadUrl(https://www.example.com);这三行代码已经可以让一个网页展示出来但为了让它稳定运行我们至少还需要补充三件事启用 JavaScript 支持、自定义 WebViewClient 处理页面跳转、以及在合适的生命周期节点上管理 WebView 的状态。如果忽略这些你会很快遇到以下典型问题点击页面内链接会拉起系统浏览器页面中的 JavaScript 完全不可用按 Home 键后再返回WebView 内容丢失。一个更完备的初始化模板如下WebView webView findViewById(R.id.webview); WebSettings settings webView.getSettings(); settings.setJavaScriptEnabled(true); settings.setDomStorageEnabled(true); settings.setUseWideViewPort(true); settings.setLoadWithOverviewMode(true); webView.setWebViewClient(new WebViewClient() { Override public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { view.loadUrl(request.getUrl().toString()); return true; } }); webView.setWebChromeClient(new WebChromeClient()); webView.loadUrl(https://www.example.com);上述模板中我们仅启用了最基本的配置。接下来我们深入 WebSettings 的每一个配置项了解它们各自的影响范围和适用场景。3. WebSettings 全配置解析WebSettings 是管理 WebView 状态的配置中心几乎所有影响 WebView 行为、性能和安全的关键开关都在这里。WebSettings 的实例通过webView.getSettings()获取它的内部默认值随 Android 版本和 WebView 实现不同而变化不要依赖默认值应该按需显式设置。3.1 JavaScript 相关配置// 启用 JavaScript 执行默认 false settings.setJavaScriptEnabled(true); // 允许 JavaScript 打开新窗口如 window.open默认 false settings.setJavaScriptCanOpenWindowsAutomatically(true);关于setJavaScriptEnabled官方文档明确指出禁用状态下的 WebView 不会执行任何 JavaScript这可以有效减少攻击面但现代页面几乎都依赖 JS 渲染关闭会导致页面白屏或功能残废。因此大部分情况下我们必须开启但需要在 WebViewClient 的shouldInterceptRequest中配合请求过滤来保障安全。3.2 页面适配与缩放// 使用宽视口推荐开启让页面按设备宽度渲染 settings.setUseWideViewPort(true); // 缩放至屏幕宽度推荐开启在 loadUrl 时自动缩放 settings.setLoadWithOverviewMode(true); // 是否支持缩放 settings.setSupportZoom(true); settings.setBuiltInZoomControls(true); // 隐藏原生缩放控件推荐隐藏避免 UI 冲突 settings.setDisplayZoomControls(false); // 文字缩放百分比默认 100 settings.setTextZoom(100);移动端页面大多通过meta nameviewport声明视口宽度此时setUseWideViewPort配合setLoadWithOverviewMode能保证页面按预期适配屏幕。对于 PC 版页面开启缩放支持是必要的逃生手段。需要注意部分 Android 版本的 WebView 在缩放后会出现触摸偏移问题建议在测试时覆盖多种系统版本。3.3 存储与缓存// DOM 存储如 localStorage推荐开启 settings.setDomStorageEnabled(true); // 应用缓存API 已废弃不推荐使用 settings.setAppCacheEnabled(false); // 数据库存储默认 true一般保持即可 settings.setDatabaseEnabled(true); // 设置存储路径 String dbPath context.getDir(databases, Context.MODE_PRIVATE).getPath(); settings.setDatabasePath(dbPath);DOM Storage是当前使用最广泛的客户端存储方式务必开启。而 AppCache 已在标准层面被 Service Worker 取代Android WebView 中也已 deprecated不建议开启。3.4 文件访问// 允许访问文件默认 true存在安全风险 settings.setAllowFileAccess(false); // 允许通过 file:// 加载其他资源默认 true settings.setAllowFileAccessFromFileURLs(false); // 允许通过 file:// 加载的 JS 访问通用文件默认 true settings.setAllowUniversalAccessFromFileURLs(false);这三个设置是与安全密切相关的核心配置。setAllowFileAccess若开启WebView 中的 JS 可能通过file://协议读取应用私有目录下的敏感文件。如果你的 WebView 只用于加载远程页面应全部设为 false。如果确实需要加载本地 HTML应使用loadDataWithBaseURL或应用内置的 HTTP Server 方案并配合安全域名白名单进行严格限制。3.5 图片与资源加载// 自动加载图片默认 true settings.setLoadsImagesAutomatically(true); // 阻止图片加载以节省流量 settings.setBlockNetworkImage(false); // 混合内容模式Android 5.0 if (Build.VERSION.SDK_INT Build.VERSION_CODES.LOLLIPOP) { settings.setMixedContentMode(WebSettings.MIXED_CONTENT_NEVER_ALLOW); }setMixedContentMode控制 HTTPS 页面中是否允许加载 HTTP 资源。生产环境应设置为MIXED_CONTENT_NEVER_ALLOW以防止中间人攻击。如果业务侧确实存在混合内容过渡期至少使用MIXED_CONTENT_COMPATIBILITY_MODE但绝不推荐MIXED_CONTENT_ALWAYS_ALLOW。3.6 其他重要配置// 设置默认字体大小 settings.setDefaultFontSize(16); // 设置最小字体大小 settings.setMinimumFontSize(8); // 设置默认等宽字体大小 settings.setDefaultFixedFontSize(13); // 是否阻止网络加载可用于离线模式 settings.setBlockNetworkLoads(false); // 设置 User-Agent String ua settings.getUserAgentString(); settings.setUserAgentString(ua MyApp/1.0); // 设置默认文本编码 settings.setDefaultTextEncodingName(utf-8); // 地理定位 settings.setGeolocationEnabled(false); // 是否需要保存表单数据 settings.setSaveFormData(false); // 设置 layout 算法NORMAL 或 SINGLE_COLUMN后者已废弃 settings.setLayoutAlgorithm(WebSettings.LayoutAlgorithm.NORMAL); // 是否允许内容 URL 访问content:// 协议 settings.setAllowContentAccess(true); // 是否在加载过程中显示缩放控件已废弃 settings.setLoadWithOverviewMode(true); // 是否需要触摸焦点 webView.requestFocus(View.FOCUS_DOWN);设置 User-Agent 时务必在系统默认 UA 尾部追加而不是替换。直接覆盖会影响 H5 侧的设备识别与兼容性逻辑。修改后的 UA 可以用于区分 WebView 流量与浏览器流量方便后端进行统计分析。4. WebViewClient加载全流程拦截与控制WebViewClient 是 WebView 加载流程中最重要的拦截器它决定了 URL 在 WebView 内部的走向。几乎所有与页面导航、资源加载和错误处理相关的逻辑都通过 WebViewClient 的回调实现。不设置 WebViewClient 的 WebView 会在用户点击链接时跳转到系统默认浏览器破坏应用内体验。4.1 shouldOverrideUrlLoading这是开发者最熟悉的回调在每次页面导航前触发。返回 true 表示拦截本次加载通常用来处理自定义协议或内部路由跳转返回 false默认表示交给 WebView 继续加载。Android 7.0 以前使用旧版 APIOverride public boolean shouldOverrideUrlLoading(WebView view, String url) { if (url.startsWith(myapp://)) { // 处理自定义 scheme handleCustomScheme(url); return true; } return false; }Android 7.0API 24及以上推荐覆写新版 API它可以拿到完整的WebResourceRequest包括请求方法、请求头等信息Override public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { Uri uri request.getUrl(); if (myapp.equals(uri.getScheme())) { handleCustomUri(uri); return true; } if (uri.getHost().equals(www.example.com)) { // 在白名单内的域名交给 WebView 处理 return false; } // 其他域名跳系统浏览器 Intent intent new Intent(Intent.ACTION_VIEW, uri); view.getContext().startActivity(intent); return true; }注意如果你的 WebView 没有调用view.loadUrl(url)并返回 trueWebView 不会自己加载该 URL。如果希望留在 WebView 内且不做额外操作直接返回 false 即可。4.2 onPageStarted / onPageFinished页面开始加载和加载完成的回调。这两个回调用于展示和隐藏进度条但不要以为onPageFinished就代表页面所有内容都渲染完毕它只表示主 frame 的 HTML 加载完成异步资源图片、JS、CSS可能还在加载中。Override public void onPageStarted(WebView view, String url, Bitmap favicon) { progressBar.setVisibility(View.VISIBLE); } Override public void onPageFinished(WebView view, String url) { progressBar.setVisibility(View.GONE); }当页面中存在 iframe 时onPageStarted和onPageFinished可能会被多次调用因为 iframe 内部的导航也会触发这些回调。可以通过WebResourceRequest.isForMainFrame()来判断是否为主 frame 请求但这两者本身不直接携带WebResourceRequest。更精确的做法是在shouldInterceptRequest中统计资源加载数量或在onProgressChanged中判断进度到达 100% 时才隐藏进度条。4.3 shouldInterceptRequest这是资源拦截的强大武器。WebView 中的每一次资源请求HTML、JS、CSS、图片、字体等都会经过shouldInterceptRequest你可以在此返回一个自定义的WebResourceResponse从而实现离线包加载、资源替换、请求拦截等功能。Override public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) { String url request.getUrl().toString(); if (isOfflineResource(url)) { try { InputStream is getOfflineResourceStream(url); return new WebResourceResponse(text/javascript, UTF-8, is); } catch (Exception e) { return null; } } return super.shouldInterceptRequest(view, request); }使用shouldInterceptRequest时注意返回 null 会让 WebView 走正常网络加载流程同步的 I/O 操作和复杂逻辑可能阻塞 UI 线程如果资源特别多需要在设计上控制拦截粒度或把逻辑放到异步线程然后通过shouldInterceptRequest的同步返回提供预缓存内容。4.4 错误处理页面加载错误时的回调。早期版本是onReceivedErrorAndroid 8.0API 26之后分为onReceivedError(WebView, WebResourceRequest, WebResourceError)和已废弃的旧版。对于主 frame 加载错误通常需要展示自定义错误页面Override public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) { if (request.isForMainFrame()) { showErrorPage(view, error.getErrorCode(), error.getDescription().toString()); } } private void showErrorPage(WebView view, int errorCode, String description) { String errorHtml htmlbodyh2页面加载失败/h2p description /p/body/html; view.loadDataWithBaseURL(null, errorHtml, text/html, UTF-8, null); }另外还有一个容易忽视的回调onReceivedSslError用于处理 SSL 证书错误。生产环境绝不要在此回调中调用handler.proceed()忽略证书问题这会让应用面临中间人攻击风险。如果确实有调试需求应在 Debug 构建中临时处理。4.5 其他有用回调onLoadResource页面中每加载一个资源都会回调用于统计资源数量或诊断加载慢的资源。doUpdateVisitedHistory可用于构建 WebView 内部的导航栈实现真正的“返回上一页”而非关闭 Activity。onFormResubmission当用户通过返回键回到一个 POST 页面时触发应提示用户是否重新提交表单。5. WebChromeClientUI 与交互的桥梁如果说 WebViewClient 专注于页面加载和资源控制那么 WebChromeClient 就是负责与 UI 交互处理 JS 弹窗、标题变化、文件选择、权限请求以及全屏视频等 Web 内容相关的浏览器行为。5.1 onProgressChanged页面加载进度回调用于展示真实的加载进度条Override public void onProgressChanged(WebView view, int newProgress) { if (newProgress 100) { progressBar.setVisibility(View.GONE); } else { progressBar.setVisibility(View.VISIBLE); progressBar.setProgress(newProgress); } }这个回调比onPageFinished更准确地反映页面加载的完成状态当进度达到 100 时主资源和大部分子资源都已加载完成。但如果有长时间轮询或 WebSocket 连接进度条可能一直在 90% 左右晃动需要额外处理。5.2 onReceivedTitle页面标题改变时触发适合动态更新 ActionBar 或 Toolbar 的标题Override public void onReceivedTitle(WebView view, String title) { if (!TextUtils.isEmpty(title) !title.startsWith(http)) { toolbar.setTitle(title); } }某些 H5 页面会动态设置 document.title因此这个回调在一个页面的生命周期中可能触发多次。5.3 JavaScript 对话框onJsAlertalert 弹窗返回 true 表示自行处理。onJsConfirmconfirm 弹窗需返回用户的选择结果。onJsPromptprompt 弹窗可以借此实现 Native 与 JS 的通信后文详述。Override public boolean onJsAlert(WebView view, String url, String message, JsResult result) { new AlertDialog.Builder(view.getContext()) .setTitle(提示) .setMessage(message) .setPositiveButton(确定, (dialog, which) - result.confirm()) .setOnCancelListener(dialog - result.cancel()) .show(); return true; }如果不覆写这些方法WebView 不会显示任何 JS 弹窗JavaScript 对话框功能将完全失效。但如果你全都不拦截系统默认的弹窗样式在不同 Android 版本上表现不一致建议统一使用自定义弹窗。5.4 文件选择允许 WebView 中的input typefile触发系统文件选择器Override public boolean onShowFileChooser(WebView webView, ValueCallbackUri[] filePathCallback, FileChooserParams fileChooserParams) { this.filePathCallback filePathCallback; Intent intent fileChooserParams.createIntent(); startActivityForResult(Intent.createChooser(intent, 选择文件), FILE_CHOOSER_REQUEST); return true; } Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode FILE_CHOOSER_REQUEST) { if (filePathCallback ! null) { Uri[] results WebChromeClient.FileChooserParams.parseResult(resultCode, data); filePathCallback.onReceiveValue(results); filePathCallback null; } } }Android 5.0 使用onShowFileChooser低版本则使用已废弃的openFileChooser系列方法。应注意filePathCallback.onReceiveValue(null)必须在某个时刻被调用否则 WebView 内部会一直等待可能导致后续文件选择无法触发。5.5 全屏视频WebView 中的 HTML5 视频全屏需要通过onShowCustomView和onHideCustomView配合实现Override public void onShowCustomView(View view, CustomViewCallback callback) { if (customViewContainer ! null) { customViewContainer.setVisibility(View.VISIBLE); customViewContainer.addView(view); customViewCallback callback; toolbar.setVisibility(View.GONE); } } Override public void onHideCustomView() { if (customViewContainer ! null) { customViewContainer.setVisibility(View.GONE); customViewContainer.removeAllViews(); customViewCallback null; toolbar.setVisibility(View.VISIBLE); } }你需要在布局中预留一个全屏容器通常是一个覆盖在最上层的 FrameLayout用于承载全屏视频 View。退出全屏时务必调用customViewCallback.onCustomViewHidden()通知 WebView 状态恢复。5.6 权限请求Android WebView 通过onPermissionRequest回调向应用请求敏感权限如摄像头、麦克风、地理位置和 MIDI 设备。你必须显式处理这些请求否则 Web API 将直接失败。Override public void onPermissionRequest(PermissionRequest request) { for (String resource : request.getResources()) { if (PermissionRequest.RESOURCE_VIDEO_CAPTURE.equals(resource)) { // 请求摄像头权限 requestPermissions(new String[]{Manifest.permission.CAMERA}, CAMERA_PERMISSION); this.pendingPermissionRequest request; return; } } request.deny(); }权限处理成功或失败后必须调用request.grant(resources)或request.deny()否则 WebView 内的页面会无限期等待结果。6. JavaScript 与 Native 的双向通信WebView 中最核心、也是出错最多的一部分就是 JavaScript 与原生代码之间的交互。Android 平台提供了多种方案老的addJavascriptInterface、通过onJsPrompt拦截、evaluateJavascript注入 JS以及 Chrome DevTools 远程调试。6.1 addJavascriptInterfaceJSI最常用的方式将 Java 对象注入到 JS 的 window 对象下前端可以直接调用注入对象的方法class AndroidBridge { JavascriptInterface public void showToast(String message) { Toast.makeText(context, message, Toast.LENGTH_SHORT).show(); } JavascriptInterface public String getUserToken() { return tokenManager.getToken(); } } webView.addJavascriptInterface(new AndroidBridge(), android);前端调用window.android.showToast(Hello from H5); const token window.android.getUserToken();安全警告Android 4.2API 17之前任何 public 方法都可以被 JS 调用导致严重的安全漏洞。从 API 17 开始只有标记了JavascriptInterface的方法才会暴露给 JS但仅靠注解仍不够。你还要确保不要将敏感对象注入到 WebView确保被注入的页面是你完全控制的不要在第三方页面加载时注入 JSI 对象注入的对象方法中校验调用来源如检查 URL 白名单。6.2 evaluateJavascript从 Native 调用 JS 方法并可以获取返回值webView.evaluateJavascript(document.title, new ValueCallbackString() { Override public void onReceiveValue(String value) { Log.d(WebView, Title: value); } });注意evaluateJavascript必须在 UI 线程调用且页面 JS 环境就绪后才有效。返回值的类型是 JSON 格式的字符串需要手动解析。对于没有返回值的纯执行调用可以传入 null 作为回调。6.3 利用 onJsPrompt 实现通信这是一种绕过addJavascriptInterface限制的通信方式。前端调用prompt(method:params)Native 在onJsPrompt中拦截特定前缀的消息并处理。这种方式更安全因为不暴露 Java 对象只传递字符串参数Override public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) { if (message.startsWith(native:)) { String resultData handleNativeCall(message.substring(7)); result.confirm(resultData); return true; } return super.onJsPrompt(view, url, message, defaultValue, result); }这种方式最早在微信 JS-SDK 中流行具有良好的兼容性适用于跨平台 H5 容器的底层通信设计。6.4 WebView 调试Android 调试 WebView 离不开 Chrome DevTools。在应用中开启if (Build.VERSION.SDK_INT Build.VERSION_CODES.KITKAT) { if (BuildConfig.DEBUG) { WebView.setWebContentsDebuggingEnabled(true); } }然后在 Chrome 地址栏输入chrome://inspect即可看到当前连接设备的 WebView 实例点击 inspect 即可进入完整的 DevTools 界面。你可以看到控制台输出、网络请求、DOM 结构甚至可以实时修改页面元素调试效率远超打日志。7. WebView 生命周期与内存管理WebView 的生命周期管理不当是 Android 内存泄漏的重灾区。很多开发者抱怨“用了 WebView 之后 Activity 无法回收”根源往往在于 WebView 的初始化和销毁时机不对。7.1 正确的生命周期方法WebView 需要响应 Activity 或 Fragment 的生命周期变化尤其是onPause、onResume和onDestroyOverride protected void onResume() { super.onResume(); if (webView ! null) { webView.onResume(); webView.resumeTimers(); } } Override protected void onPause() { super.onPause(); if (webView ! null) { webView.onPause(); webView.pauseTimers(); } } Override protected void onDestroy() { if (webView ! null) { ViewGroup parent (ViewGroup) webView.getParent(); if (parent ! null) { parent.removeView(webView); } webView.loadUrl(about:blank); webView.stopLoading(); webView.setWebChromeClient(null); webView.setWebViewClient(null); webView.removeAllViews(); webView.destroy(); webView null; } super.onDestroy(); }关键细节webView.destroy()必须放在removeView之后调用必须在销毁前将 WebViewClient 和 WebChromeClient 置为 null切断引用链loadUrl(about:blank)可以提前释放 WebView 内部的渲染资源在单独的 Activity 中使用 WebView 是一个良好的隔离策略即使 WebView 出现内存问题也只会影响它所在的 Activity不会污染整个应用。7.2 Application Context 的陷阱很多开发者为了“避免 Activity 泄漏”使用Application Context创建 WebViewWebView webView new WebView(getApplicationContext());这实际上破坏了 WebView 对 Activity 上下文的一些隐式依赖可能导致弹出对话框崩溃、无法正确获取主题样式等问题。更推荐的做法是使用 Activity 上下文但在销毁时严格按照上述步骤清理。如果你必须在非 Activity 场景使用 WebView如全局预加载池则需要额外处理这些上下文依赖问题。7.3 独立进程 WebViewAndroid 从 7.0API 24开始支持将 WebView 运行在独立进程通过AndroidManifest.xml配置provider android:nameandroidx.webkit.WebViewRenderProcessProvider android:process:webview_process /独立进程的最大好处是隔离崩溃和隔离内存。当 WebView 所在进程因为页面 Bug 或内存不足崩溃时不会影响主进程。但劣势也比较明显跨进程通信带来额外开销创建新进程会增加冷启动耗时需要权衡利弊后使用。如果你的应用中 WebView 是核心高频功能且页面复杂独立进程非常值得投入。8. WebView 性能优化WebView 性能直接决定 H5 页面的用户体验。优化方向主要包括加载速度首屏白屏时间、渲染流畅度滚动与动画帧率以及资源占用CPU/内存/电量。8.1 首屏加载优化预创建 WebViewWebView 的初始化成本很高可以通过预创建 WebView 池来减少首屏等待。在一个独立的 Application 初始化阶段或后台线程中预先创建好 WebView 实例需要使用时直接从池中获取。public class WebViewPool { private static QueueWebView pool new LinkedList(); public static void warmUp(Context context) { new Handler(Looper.getMainLooper()).post(() -gt; { WebView webView new WebView(context); webView.loadUrl(about:blank); pool.offer(webView); }); } public static WebView obtain(Context context) { WebView webView pool.poll(); return webView ! null ? webView : new WebView(context); } }离线包与资源拦截通过shouldInterceptRequest将页面资源映射到本地 APK 内置或下载好的离线包是当前大多数大型 App 的标准做法。通常的策略是HTML/JS/CSS 走本地离线包API 数据走实时网络请求图片按需加载。DNS 预解析与连接预建// 在应用启动阶段或即将打开 WebView 页面之前调用 webView.getSettings().setNeedInitialFocus(true); // 使用 OkHttp 或其他网络库预先建立连接 OkHttpClient client new OkHttpClient(); Request request new Request.Builder().url(https://www.example.com).head().build(); client.newCall(request).enqueue(new Callback() { ... });延迟加载非关键资源通过setBlockNetworkImage可以在页面加载过程中先阻止图片加载等主内容渲染完毕后再放开settings.setBlockNetworkImage(true); webView.setWebViewClient(new WebViewClient() { Override public void onPageFinished(WebView view, String url) { settings.setBlockNetworkImage(false); } });8.2 渲染优化开启硬件加速默认情况下 WebView 使用硬件加速但如果你的应用在某个地方关闭了硬件加速会导致 WebView 滚动时严重卡顿。可以在 Activity 级别或 Window 级别确保开启getWindow().setFlags( WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED, WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED);减少布局嵌套WebView 自身就是一个复杂的 View尽量不要将它嵌套在多层 Layout 中避免过度的onMeasure和onLayout调用。合理使用 WebView 的 layout 属性避免wrap_content导致多次测量建议使用0dpweight或明确的宽高值。8.3 内存优化WebView 的内存消耗主要由 Chromium 内核引起无法从应用层完全消除但可以通过以下手段控制使用独立进程隔离内存退出页面时直接杀进程彻底释放减少同时存在的 WebView 实例数量定期调用webView.freeMemory()已废弃但某些版本仍有一定效果对于不再需要的 WebView及时执行 destroy 流程。9. WebView 安全实践WebView 安全是移动安全体系中容易被低估的一环但实际上它是攻击者进入应用内部的重要入口。以下安全实践建议逐条对照检查项目代码。9.1 文件访问控制settings.setAllowFileAccess(false); settings.setAllowFileAccessFromFileURLs(false); settings.setAllowUniversalAccessFromFileURLs(false);这三项是 WebView 安全的第一道防线关闭后可以防止大部分通过文件协议进行的跨域攻击。如果必须加载本地文件请使用loadDataWithBaseURL并限制 baseURL 的有效范围。9.2 HTTPS 与证书校验在onReceivedSslError中绝不调用proceed()。如果需要信任自签名证书仅限内网测试环境应使用Network Security Config方案!-- res/xml/network_security_config.xml -- network-security-config domain-config cleartextTrafficPermittedfalse domain includeSubdomainstrueyourdomain.com/domain trust-anchors certificates srcraw/my_ca / /trust-anchors /domain-config /network-security-config9.3 JavaScript 接口安全只对可信页面使用addJavascriptInterface标记所有暴露方法为JavascriptInterface避免通过 JSI 暴露可能导致越权操作的方法如反射调用、文件读写、动态加载代码使用 onJsPrompt 等非注入式通信作为更安全的选择。9.4 防止钓鱼与页面伪造在shouldOverrideUrlLoading中对页面跳转 URL 进行白名单校验任何不在白名单内的域名都应拉起系统浏览器或弹窗提示用户。特别注意不要仅检查 URL 前缀攻击者可以通过构造形如https://www.yourdomain.com.attacker.com的链接绕过简单检查。9.5 WebView 跨域与 Cookie 安全WebView 默认遵循同源策略但部分业务可能因为“方便”而关闭跨域限制。这相当于把整个应用的 Cookie 和存储暴露给所有页面极其危险。如果你的业务需要跨域处理应通过 Native 层转发请求来实现可控的跨域访问而不是直接在 WebView 层面放行。10. 高级话题与实战踩坑10.1 WebView 多进程架构Chromium 本身是多进程架构但 Android 上的 WebView 默认与宿主应用共享主进程。当 WebView 内容崩溃时例如 OOM 或 GPU 进程挂掉整个应用都会受到影响。从 Android 8.0API 26开始Google 逐步推进 WebView 的多进程隔离开发者可以通过 AndroidManifest 和应用设置来显式开启if (Build.VERSION.SDK_INT Build.VERSION_CODES.O) { // 开启多进程模式 WebView.startSafeBrowsing(context, new ValueCallbackBoolean() { Override public void onReceiveValue(Boolean value) { // 回调 } }); }多进程 WebView 模式下每个 WebView 实例可能会被分配到不同的渲染进程进一步提升了稳定性但也会导致进程间 Cookie 共享、存储同步变得复杂。如果你的应用使用了自定义 Cookie 管理逻辑需要全面回归测试。10.2 WebView 加载本地页面有三种主要方式加载本地 HTMLloadUrl(file:///android_asset/...)适用于固定资源需要允许文件访问loadDataWithBaseURL适用于动态生成的内容自定义 HTTP Server在应用本地启动一个微型 HTTP 服务器使用http://localhost加载安全性更好且无文件访问风险。10.3 请求头与 Cookie 管理某些业务场景需要在 WebView 的请求中附加特定的 Header如 Token但 WebView 没有全局 Header 注入机制。常见做法是在loadUrl时使用额外的 Header 参数MapString, String headers new HashMap(); headers.put(Authorization, Bearer token); webView.loadUrl(https://www.example.com, headers);但这只对主 frame 的首次请求生效后续的子请求和页面内跳转均不会携带这些 Header。若需要持续注入 Header需要使用shouldInterceptRequest拦截所有请求并附加 Header 后重新发起这会增加额外延迟或者通过 Cookie 传递 Token因为 Cookie 会被自动附加到同域请求中。CookieManager cookieManager CookieManager.getInstance(); cookieManager.setAcceptCookie(true); cookieManager.setCookie(https://www.example.com, token token); if (Build.VERSION.SDK_INT Build.VERSION_CODES.LOLLIPOP) { cookieManager.flush(); }10.4 WebView 中的键盘处理WebView 内 H5 表单的软键盘弹出和收起经常引发布局异常尤其是在全屏 WebView 场景下。可以通过在 AndroidManifest 中设置android:windowSoftInputModeadjustResize并配合 WebView 的setOnTouchListener和自定义 JavaScript 监听window.visualViewport的变化来解决。10.5 常见崩溃与排查WebView 相关的常见崩溃包括Native Crash in libwebviewchromium.so一般由页面 GPU 渲染异常引起尝试关闭硬件加速定位java.lang.NullPointerException at WebView.destroy()通常是 WebView 已被提前回收但再次调用了 destroyRender process crash在独立进程或多进程模式下渲染进程可能因为页面过量内存消耗被杀需要在 WebViewClient 的onRenderProcessGone中处理恢复逻辑。Override public boolean onRenderProcessGone(WebView view, RenderProcessGoneDetail detail) { if (!detail.didCrash()) { // 系统主动结束不是崩溃可以重新创建 WebView recreateWebView(); return true; } // 真的崩溃了展示错误页面 showCrashError(view); return true; }11. 第三方 WebView 增强库概述Android 原生 WebView API 在各版本间差异较大很多高级特性只有在较高版本才可用。Jetpack 中的androidx.webkit库为开发者提供了向下兼容的 API强烈推荐引入。dependencies { implementation androidx.webkit:webkit:1.7.0 }关键 API 示例WebViewCompat提供兼容版本的 WebView 方法WebViewFeature检测当前 WebView 实现是否支持某项特性ProxyController为 WebView 设置代理TracingController用于性能分析追踪。另外还有腾讯的 X5 WebViewTBS它提供了一套基于 Chromium 的增强 WebView 内核主要解决旧设备上系统 WebView 版本过低的问题并增加了文件浏览、长截图等扩展能力。但随着 Android 系统 WebView 的快速迭代和高版本覆盖率的提升TBS 的优势正在被削弱引入它需要额外承担几十 MB 的内核包体积需要按业务需求评估。12. 总结与展望本文系统性地梳理了 Android WebView 从基础到进阶的方方面面总计超过两万字。我们覆盖了 WebSettings 的每一个关键配置、WebViewClient 与 WebChromeClient 的全套回调、JavaScript 双端通信的多种方案、生命周期与内存管理的实战策略、性能优化的具体手段以及安全实践的每一条红线。在实际项目中没有一种万能的 WebView 方案能适配所有业务。一个优秀的 WebView 容器设计应该是分层的底层提供稳定、安全、高性能的内核封装中间层提供离线包、请求拦截、Cookie 管理等通用能力上层由具体业务定制加载策略和交互逻辑。保持每一层的职责清晰才能在快速迭代中避免牵一发而动全身。展望未来随着 Android 与 ChromeOS 的逐步融合WebView 的能力还将进一步增强。WebAssembly、WebAuthn、Trusted Web ActivityTWA等新技术正在把 Web 容器的应用边界推向更远的地方。掌握好 WebView 的底层原理才能在这些新浪潮中游刃有余。希望这篇文章能成为你 Android 开发路上的手边参考如果觉得有帮助欢迎收藏、转发并在实际项目中检验这些实践。
返回列表