封装从url 拉取 HTML 并加载到 WebView 的完整流程
在 Android 中从 URL 拉取 HTML 并加载到 WebView 的完整流程
下面我将详细介绍在 Android 应用中从 URL 获取 HTML 内容并加载到 WebView 的完整实现流程,包括必要的权限、网络请求处理和 WebView 配置。
基本流程概述
- 1.
添加网络权限
- 2.
创建 WebView 并配置基本设置
- 3.
使用 HttpURLConnection 或第三方库(如 OkHttp)从 URL 获取 HTML 内容
- 4.
将获取的 HTML 内容加载到 WebView 中
- 5.
处理加载过程中的各种状态和错误
问题思考:
原始 url 会重定向到目标 url1。关键是要拿到最终重定向后的地址作为 baseUrl,否则相对资源路径会404或跨域受限。
关键处理点
- 使用 OkHttp 的最终请求地址作为 baseUrl:response.request().url().toString()
- 确保跟随重定向已开启(默认是开启的),必要时显式设置
- 同步 Cookie 到 WebView,保持同域资源请求一致性
- 传递合理的 UA、Referer,避免服务端按 UA 返回不同内容
- 若页面依赖 JS 动态渲染,仅抓 HTML 无法还原,需要考虑直接 loadUrl 或注入必要资源
下面给出“重定向取最终地址 + 编码稳妥解析 + Cookie 同步 + 优先用 loadDataWithBaseURL 渲染,必要时回退到 loadUrl”的完整实现,已逐行注释,并在后面附上简要逻辑整理。
private void getAsyncOkHttp(final String newsUrl) { // 异步获取 HTML 并加载到 WebView 的入口// 1) 兜底判空,避免空 URL 触发异常if (newsUrl == null || newsUrl.trim().length() == 0) { // 判断入参有效性LogUtils.loge("getAsyncOkHttp() newsUrl is empty"); // 记录日志if (handler != null) handler.sendMessage(handler.obtainMessage(400)); // 通知错误return; // 提前返回} // endtry { // 捕获整体流程异常,保证不崩溃// 2) 复用/延迟初始化 OkHttpClient(连接池/缓存/拦截器集中配置)// 注意:生产请不要使用全信任证书;此处保留你原项目的 SSL 定制入口,但默认走系统信任更安全final long CACHE_SIZE = 50L * 1024 * 1024; // 50MB 缓存大小if (OkHttpHolder.sClient == null) { // 单例初始化,避免每次 new 带来性能与资源浪费synchronized (OkHttpHolder.class) { // 双重检查锁,线程安全if (OkHttpHolder.sClient == null) { // 二次判空Cache cache = null; // 本地 HTTP 缓存try { // 创建缓存目录File cacheDir = new File(getCacheDir(), "http_cache"); // App 缓存目录cache = new Cache(cacheDir, CACHE_SIZE); // 实例化缓存对象} catch (Throwable e) { // 缓存创建失败不影响主流程LogUtils.loge("create http cache fail: " + e.getMessage()); // 记录异常} // endOkHttpClient.Builder b = new OkHttpClient.Builder() // 构建器.followRedirects(true) // 允许 HTTP 重定向.followSslRedirects(true) // 允许 HTTPS 重定向.connectTimeout(10, java.util.concurrent.TimeUnit.SECONDS) // 连接超时.readTimeout(15, java.util.concurrent.TimeUnit.SECONDS) // 读取超时.writeTimeout(15, java.util.concurrent.TimeUnit.SECONDS) // 写入超时.callTimeout(20, java.util.concurrent.TimeUnit.SECONDS); // 整体调用最大耗时if (cache != null) b.cache(cache); // 挂载缓存(可提升重复请求性能)// 统一 Header:UA/语言。Referer/Cookie 由具体请求决定(或在需要时追加)b.addInterceptor(chain -> { // 应用层拦截器,给所有请求加通用头Request origin = chain.request(); // 原始请求Request.Builder rb = origin.newBuilder() // 构建新请求.header("User-Agent", getDefaultUA()) // 统一 UA(使用 WebView UA 更贴近站点判断).header("Accept-Language", "zh-CN,zh;q=0.9"); // 统一语言偏好return chain.proceed(rb.build()); // 继续链路}); // end interceptor// 如你的项目必须自定义 SSL(不建议全信任),在此启用;默认注释更安全// b.sslSocketFactory(SSLSocketClient.createSSLSocketFactory(), new SSLSocketClient.TrustAllCerts());// b.hostnameVerifier(SSLSocketClient.getHostnameVerifier());OkHttpHolder.sClient = b.build(); // 构建单例客户端} // end second null-check} // end synchronized} // end if client null// 3) 构建请求(不在此加 Referer,避免与最终 baseUrl 不一致;必要时在回退 loadUrl 再补)Request request = new Request.Builder() // 新建请求.get() // GET 方法.url(newsUrl) // 目标 URL(可能发生重定向).build(); // 构建完成// 4) 发起异步请求OkHttpHolder.sClient.newCall(request).enqueue(new Callback() { // 异步回调@Overridepublic void onFailure(Call call, IOException e) { // 网络/IO 失败runOnUiThread(() -> { // 回到主线程LogUtils.loge("getAsyncOkHttp onFailure: " + e.getMessage()); // 记录异常if (handler != null) handler.sendMessage(handler.obtainMessage(400)); // 通知错误}); // end UI} // end onFailure@Overridepublic void onResponse(Call call, Response response) throws IOException { // 成功拿到响应try { // 处理响应体的异常保护// 5) 状态码检查(OkHttp 已跟随重定向;此处判断最终状态)if (!response.isSuccessful()) { // 非 2xxthrow new IOException("HTTP " + response.code()); // 抛出异常走统一兜底} // end// 6) 取最终 URL(重定向后的地址,作 baseUrl 解析相对资源)final String finalUrl = response.request().url().toString(); // 最终有效地址// 7) 读取响应体字节,注意先判空再取长度,避免 NPEResponseBody body = response.body(); // 响应体byte[] bytes = (body != null) ? body.bytes() : null; // 取字节数组if (bytes == null || bytes.length == 0) { // 判空throw new IOException("empty body"); // 统一异常处理} // end// 8) 基于 Content-Type 推断 charset,缺省回退 UTF-8(更稳)MediaType ct = (body != null) ? body.contentType() : null; // 内容类型java.nio.charset.Charset charset = (ct != null && ct.charset() != null)? ct.charset(): java.nio.charset.StandardCharsets.UTF_8; // 字符集final String htmlSource = new String(bytes, charset); // 以 charset 解码为字符串// 9) 简单“空页面/壳页面”启发式判断(SPA 可能首屏很少内容)final boolean looksEmpty = isProbablyEmptyHtml(htmlSource); // 启发式判断// 10) 将 Cookie 同步到 WebView(以便后续资源/XHR 保持会话一致)final java.util.List<String> setCookies = response.headers("Set-Cookie"); // 取服务端下发的 cookie 列表// 11) 回到主线程,进行 WebView 加载runOnUiThread(() -> { // UI 线程更新try { // UI 层异常保护if (webview == null) { // WebView 可能已销毁LogUtils.loge("webview is null (destroyed?)"); // 记录日志if (handler != null) handler.sendMessage(handler.obtainMessage(400)); // 通知错误return; // 结束} // end// 12) 基础 WebView 设置(只做关键能力开启,避免覆盖外部定制)enableWebViewBasics(webview); // 开启 JS/DOMStorage/混合内容/第三方 Cookie// 13) 同步服务端 Set-Cookie 到 WebView 的 CookieManagertry { // Cookie 同步过程保护if (setCookies != null && !setCookies.isEmpty()) { // 有服务端下发 cookieandroid.webkit.CookieManager cm = android.webkit.CookieManager.getInstance(); // Cookie 管理