简介:这份资源是面向Android开发者的X5WebView基础封装代码,适合需要快速集成腾讯X5内核、处理H5混合开发场景的初中级工程师。包内围绕WebView常用能力做了系统整理:通过OkHttp拦截器与自定义CookieJar实现客户端与H5端Cookie同步管理,监听网页加载进度,灵活设置滚动条隐藏或显示及内外侧位置,并借助IntentService优化X5WebView预加载以规避风险。资源共76个文件,以29个java源码、20个xml布局与配置、12个png图片资源为主,另含gradle构建脚本、jar与so库等,压缩包约578KB,结构紧凑便于直接导入Android Studio参考。目前已有1425人学习下载,读者可从中获取Cookie同步、进度监听、滚动条控制与预加载优化的完整实现思路,适合作为混合开发项目的起步模板。
1. 从一次 WebView 内存泄漏说起:这套 X5 封装到底值不值得拆
去年帮一个做资讯聚合的团队排查线上 OOM,堆栈翻到最后发现是com.tencent.smtt.sdk.WebView持有 Activity 引用没释放。他们项目里 WebView 是直接 new 出来的,加载完 H5 页面后 Activity 销毁了,WebView 还挂在一条静态的List<WebView>里做"复用池"。这种写法在原生 WebView 上勉强能跑,换成 X5 内核后因为内部多了几层代理和回调,泄漏路径直接翻倍。后来我把他们的 WebView 层换成了带生命周期绑定和引用计数回收的封装,问题才压下去。
这份 Android-X5WebView 基本封装,解决的就是这类"X5 能用但用不稳"的问题。它把 X5 内核的初始化、WebView 实例管理、WebViewClient/WebChromeClient的常用回调、JS 桥接、进度与错误处理收进一套可复用的基类里。适合两类人:一是刚接手一个已经引入 X5 但代码散落各处的项目,需要快速理清调用链;二是准备从系统 WebView 迁到 X5、想先看一套能跑通的骨架再决定要不要上。下面按"它封装了什么 → 怎么接进项目 → 哪些参数必须改 → 坑在哪"的顺序拆。
2. X5 内核初始化与 WebView 实例管理:封装层到底包了哪几层
2.1 为什么不能直接 new X5 的 WebView
X5 的WebView和系统android.webkit.WebView是两个完全不同的类,包名在com.tencent.smtt.sdk下。直接 new 会踩三个问题:第一,内核没初始化时构造出来的 WebView 会退化成系统内核,你调 X5 独有 API 直接抛异常;第二,X5 的 WebView 内部持有WebViewProxy和TbsListener,不主动 destroy 会一直挂在QbSdk的实例表里;第三,多进程场景下每个进程都要单独初始化,主进程初始化完子进程不生效。
这套封装的思路是:用一个X5WebViewManager单例管初始化状态,用一个X5WebViewWrapper管实例生命周期,业务层只跟 Wrapper 打交道。初始化部分核心是QbSdk.initX5Environment,它是个异步过程,回调里会告诉你内核是"已安装""需下载"还是"加载失败"。
// X5WebViewManager.java public class X5WebViewManager { private static volatile boolean sInitDone = false; public static void init(Context context) { if (sInitDone) return; // 允许在非 WiFi 下也下载内核,测试期建议打开,上线按需关 QbSdk.setDownloadWithoutWifi(true); QbSdk.initX5Environment(context.getApplicationContext(), new QbSdk.PreInitCallback() { @Override public void onCoreInitFinished() { // 内核初始化完成,此时还不一定能用,要等 onViewInitFinished } @Override public void onViewInitFinished(boolean success) { // success=true 表示 X5 内核可用;false 表示回退到系统内核 sInitDone = success; Log.i("X5", "view init finished, x5=" + success); } }); } public static boolean isReady() { return sInitDone; } }逻辑说明:initX5Environment的回调分两段,onCoreInitFinished只代表 so 加载完,onViewInitFinished才代表 WebView 可创建。参数上setDownloadWithoutWifi(true)在调试期很有用,否则模拟器或测试机没连 WiFi 时内核永远下不下来,你会误以为封装有问题。上线后建议改回 false,避免用户在移动网络下被静默下载几十兆内核。
2.2 Wrapper 怎么管住实例和生命周期
业务层最容易翻车的地方是 WebView 和 Activity 生命周期不同步。封装的 Wrapper 做了三件事:构造时绑定 Context(用WeakReference存,避免泄漏)、提供destroy()统一回收、内部维护一个isDestroyed标志防止 destroy 后还被调用。
// X5WebViewWrapper.java public class X5WebViewWrapper { private final WeakReference<Context> ctxRef; private com.tencent.smtt.sdk.WebView webView; private boolean destroyed = false; public X5WebViewWrapper(Context context) { this.ctxRef = new WeakReference<>(context); if (!X5WebViewManager.isReady()) { // 内核没就绪时先不创建,交给业务层决定是等待还是降级 throw new IllegalStateException("X5 core not ready"); } webView = new com.tencent.smtt.sdk.WebView(context); WebSettings settings = webView.getSettings(); settings.setJavaScriptEnabled(true); settings.setDomStorageEnabled(true); settings.setAllowFileAccess(false); // 按需,见避坑章 } public com.tencent.smtt.sdk.WebView get() { if (destroyed) throw new IllegalStateException("webview destroyed"); return webView; } public void destroy() { if (destroyed) return; destroyed = true; if (webView != null) { webView.stopLoading(); webView.loadUrl("about:blank"); webView.removeAllViews(); webView.destroy(); webView = null; } } }参数说明:setAllowFileAccess(false)是安全基线,X5 默认允许 file 协议访问,配合setAllowFileAccessFromFileURLs打开时会有本地文件读取风险,封装里默认关掉,业务确实要读本地 HTML 再单独开。destroy()里先loadUrl("about:blank")再 destroy 是血泪经验,直接 destroy 有时会触发内核线程还在跑导致的偶发崩溃。
2.3 初始化时机与多进程的取舍
初始化放在Application.onCreate里最稳,但要注意别在主线程做重活。initX5Environment本身是异步的,但第一次调用会触发 so 加载,耗时在几十到几百毫秒。常见做法是放到一个延迟任务里,或者等首页渲染完再 init。多进程场景下,如果你的 WebView 只在主进程用,子进程不用调 init;如果子进程也要用,得在每个进程的 Application 里各调一次,且内核下载是进程独立的,这点很多人不知道,以为主进程下完子进程就能用。
3. WebViewClient 与 WebChromeClient 回调封装:把散落的回调收成一条链
3.1 为什么要把两个 Client 包一层
原生用法里,业务代码经常在shouldOverrideUrlLoading里写一堆 if-else 判断 URL scheme,在onReceivedError里弹 Toast,在onProgressChanged里更新进度条。这些逻辑散在 Activity 里,换一个页面就要复制一遍。封装的思路是定义一个X5WebViewClient继承 X5 的WebViewClient,把可复用的行为(拦截非 http scheme、错误码归一化、进度回调)抽出来,业务只实现一个接口。
// X5WebViewClient.java public class X5WebViewClient extends com.tencent.smtt.sdk.WebViewClient { public interface PageListener { void onPageStarted(String url); void onPageFinished(String url); void onError(int errorCode, String description); boolean onScheme(String url); // 返回 true 表示业务自己处理了 } private final PageListener listener; public X5WebViewClient(PageListener listener) { this.listener = listener; } @Override public boolean shouldOverrideUrlLoading(com.tencent.smtt.sdk.WebView view, String url) { if (url.startsWith("http://") || url.startsWith("https://")) { return false; // 交给 WebView 自己加载 } // 非 http scheme,交给业务判断,比如 tel:、mailto:、自定义 scheme return listener != null && listener.onScheme(url); } @Override public void onPageStarted(com.tencent.smtt.sdk.WebView view, String url, Bitmap favicon) { if (listener != null) listener.onPageStarted(url); } @Override public void onPageFinished(com.tencent.smtt.sdk.WebView view, String url) { if (listener != null) listener.onPageFinished(url); } @Override public void onReceivedError(com.tencent.smtt.sdk.WebView view, int errorCode, String description, String failingUrl) { // X5 的错误码和系统 WebView 不完全一致,这里做一次归一化 if (listener != null) listener.onError(errorCode, description); } }逻辑说明:shouldOverrideUrlLoading里只拦非 http scheme,http/https 一律返回 false 让 WebView 自己处理,这是最省心的做法。很多人在这里对所有 URL 都返回 true 然后自己loadUrl,结果导致重定向链断掉、POST 请求丢失。onReceivedError里 X5 的错误码是WebViewClient.ERROR_*常量,但部分内核版本会返回 -1 或 -16 这类非标准值,封装里建议做一层映射,把 -1 归为"未知错误"、-16 归为"网络错误"。
3.2 WebChromeClient 处理进度、标题和文件选择
WebChromeClient负责的是"浏览器外壳"相关的东西:进度条、页面标题、JS 的 alert/confirm、文件选择器。封装里最值得抄的是文件选择器的处理,因为 X5 的onShowFileChooser和系统 WebView 的签名不一样,参数类型是com.tencent.smtt.sdk.ValueCallback。
// X5WebChromeClient.java public class X5WebChromeClient extends com.tencent.smtt.sdk.WebChromeClient { private ValueCallback<Uri[]> fileCallback; private static final int REQ_FILE = 1001; @Override public void onProgressChanged(com.tencent.smtt.sdk.WebView view, int newProgress) { // 进度回调,业务层可以在这里更新 ProgressBar } @Override public void onReceivedTitle(com.tencent.smtt.sdk.WebView view, String title) { // 页面标题,可用于更新 Toolbar } @Override public boolean onShowFileChooser(com.tencent.smtt.sdk.WebView view, ValueCallback<Uri[]> callback, FileChooserParams params) { this.fileCallback = callback; // 这里要拿到 Activity 才能 startActivityForResult,封装里通过接口回调出去 return true; } public void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == REQ_FILE && fileCallback != null) { Uri[] results = null; if (resultCode == Activity.RESULT_OK && data != null) { results = new Uri[]{data.getData()}; } fileCallback.onReceiveValue(results); fileCallback = null; } } }参数说明:onShowFileChooser返回 true 表示业务自己处理文件选择,返回 false 则 WebView 用默认行为(通常什么都不做)。ValueCallback必须且只能回调一次,回调后置 null,否则第二次选文件会没反应。FileChooserParams里有getAcceptTypes()和isCaptureEnabled(),做拍照上传时要读这两个参数来决定是开相机还是开相册。
3.3 JS 桥接的封装边界
JS 桥接是 X5 封装里最容易出安全问题的地方。addJavascriptInterface在 API 17 之前有反射漏洞,X5 内核虽然做了修补,但封装里还是建议只暴露必要的方法,且方法上加@JavascriptInterface注解。封装的常见做法是提供一个JsBridge类,内部用@JavascriptInterface标注的方法转发到业务接口,而不是把整个业务对象暴露给 JS。
public class JsBridge { public interface Callback { void onResult(String data); } private final Callback callback; public JsBridge(Callback callback) { this.callback = callback; } @JavascriptInterface public void postMessage(String json) { // 只暴露一个入口,具体协议在业务层解析 if (callback != null) callback.onResult(json); } }逻辑说明:只暴露postMessage一个方法,JS 侧所有调用都走这个入口,用 JSON 里的action字段区分。这样既减少了暴露面,也方便做统一的参数校验和日志。注意@JavascriptInterface是android.webkit包下的注解,X5 也认这个注解,别引错包。
4. 避坑与排查:X5 封装里最容易翻车的五个点
4.1 内核没就绪就创建 WebView,页面白屏
现象:Activity 打开后 WebView 区域一片白,日志里没有明显报错,onPageFinished也不回调。原因:initX5Environment还没回调onViewInitFinished就 new 了 WebView,此时内核是系统内核,但代码里调了 X5 独有 API 导致内部状态错乱。解决:在 Wrapper 构造时检查X5WebViewManager.isReady(),没就绪就等回调,或者降级用系统 WebView 并给用户一个提示。我一般会在 Application 里 init,然后在首页用一个空 View 占位,等 ready 后再创建真正的 WebView。
4.2 destroy 后回调还在跑,偶发 NullPointerException
现象:退出页面后偶发崩溃,堆栈指向onPageFinished或onProgressChanged里访问了已置空的 View。原因:WebView destroy 是异步的,内核线程可能还在回调,而业务代码在回调里直接用了已经置空的成员变量。解决:Wrapper 里维护destroyed标志,所有回调入口先判断这个标志;同时 Client 里对 view 参数做判空。更彻底的做法是在 destroy 前把 Client 置为 null,但要注意 X5 某些版本对 null Client 处理不完善,建议用空实现代替。
4.3 文件上传回调不触发,选完文件没反应
现象:H5 页面点"选择文件"没弹选择器,或者弹了选完文件后页面没收到。原因:onShowFileChooser返回了 false,或者ValueCallback被回调了两次。解决:确认返回 true,确认onActivityResult里 requestCode 对得上,确认fileCallback回调后置 null。还有一个隐蔽点:如果 Activity 的launchMode是 singleTask 或 singleInstance,startActivityForResult可能拿不到结果,这种情况要用ActivityResultLauncher或者改成 standard。
4.4 本地 HTML 加载失败,file 协议被拦
现象:loadUrl("file:///android_asset/demo.html")加载不出来,控制台报Not allowed to load local resource。原因:封装里默认关了setAllowFileAccess,或者 H5 里又去请求了本地其他文件被setAllowFileAccessFromFileURLs拦住。解决:如果确实要加载本地 HTML,在创建 WebView 后单独开setAllowFileAccess(true),但不要同时开setAllowFileAccessFromFileURLs和setAllowUniversalAccessFromFileURLs,这两个开了等于把本地文件系统暴露给任意页面。更安全的做法是把本地 HTML 放到 assets 里用WebViewAssetLoader走 https 域名加载。
4.5 多进程下内核重复下载,流量被偷跑
现象:用户反馈没打开几个页面但流量用了很多,查下来是 X5 内核在多个进程里各下载了一份。原因:X5 内核下载是按进程隔离的,如果 App 有多个进程都调了initX5Environment且开了setDownloadWithoutWifi(true),每个进程都会尝试下载。解决:只在主进程 init,子进程如果要用 WebView,通过 IPC 把渲染结果传回来,或者干脆子进程不用 X5。上线前把setDownloadWithoutWifi关掉,让内核只在 WiFi 下更新。
5. 进阶:用引用计数和降级策略把 WebView 复用做稳
前面讲的 Wrapper 是"一个页面一个实例"的用法,但资讯类 App 经常需要预加载和复用 WebView 来减少白屏时间。直接搞一个全局List<WebView>复用池就是开头那个团队踩的坑,正确做法是引用计数加降级。
引用计数的核心是:每个 WebView 实例维护一个refCount,业务层acquire()时加一,release()时减一,减到零才真正 destroy。这样预加载的 WebView 在页面退出后不会被立刻销毁,下次打开同域名页面可以直接复用。但要注意,复用时必须loadUrl("about:blank")清空上一个页面的状态,否则 JS 全局变量、cookie、localStorage 会串。
public class WebViewPool { private static final int MAX_IDLE = 2; private final Deque<X5WebViewWrapper> idle = new ArrayDeque<>(); public X5WebViewWrapper acquire(Context context) { X5WebViewWrapper wrapper = idle.pollFirst(); if (wrapper == null) { wrapper = new X5WebViewWrapper(context); } wrapper.retain(); // refCount++ return wrapper; } public void release(X5WebViewWrapper wrapper) { if (wrapper.release() == 0) { // refCount-- if (idle.size() < MAX_IDLE) { wrapper.get().loadUrl("about:blank"); idle.offerFirst(wrapper); } else { wrapper.destroy(); } } } }参数说明:MAX_IDLE控制空闲池大小,设太大内存扛不住,设 2 是常见折中。retain和release要成对出现,漏掉 release 会导致 WebView 永远不销毁,多调 release 会导致提前销毁后还被使用。建议在 Wrapper 里加断言,release 到负数时打日志。
降级策略是另一层保险:当 X5 内核加载失败(onViewInitFinished(false))时,封装要能无缝切到系统 WebView。做法是定义一个IWebView接口,X5 和系统 WebView 各实现一份,业务层面向接口编程。切换时只换实现类,Client 和 ChromeClient 的回调签名差异在适配层里消化。这套下来代码量会翻倍,但如果你的 App 有低端机用户,这层降级能救命。
验证方法上,我习惯用三个场景压:一是弱网下反复进出页面,看 WebView 实例数是否稳定;二是连续打开二十个不同域名的 H5,看内存曲线是否收敛;三是模拟内核下载失败,看降级是否生效。从那以后我每次接 X5 封装,都强制走一遍这三个场景,不再靠"看起来能跑"下结论。希望帮到你。
本文还有配套的精品资源,点击获取