ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

Android混合开发:AgentWeb封装WebView,解决H5容器痛点

Android混合开发:AgentWeb封装WebView,解决H5容器痛点 做过Android混合开发的兄弟应该都有这种感受项目里H5页面越来越多活动页、商品详情、运营配置页全往WebView里塞结果自己却要跟原生WebView的进度条、文件下载、JS通信、生命周期来回较劲。后来我把项目里的WebView相关逻辑整个换成了AgentWeb这个三方封装库混合开发才终于变得像正常开发。这篇是系列第一篇先聊清楚为什么选它、怎么正确接入、以及那些文档里不怎么写但实际项目中绕不开的配置细节。不管你是刚接触混合开发的新手还是被原生WebView折腾过几次的老手这篇都适合作为一份可以直接参考的落地笔记。1. 放着原生 WebView 不用为啥非要选 AgentWeb1.1 原生 WebView 的几个老毛病先说说我最早用原生WebView踩坑的经历。第一是内存泄漏Activity销毁了结果WebView还在后台加载页面排查的时候发现是WebView持有Activity的引用没释放每次都泄漏一部分内存页面开多了应用直接卡顿。第二是生命周期管理WebView的onResume、onPause、onDestroy都得自己找时机调漏一个就可能导致播放视频没声音、页面不刷新这类诡异问题。第三是样板代码太多进度条要自己写、文件下载要自己接管、JS调用要自己搭桥一套下来小一百行代码起步。第四是厂商定制内核带来的兼容性问题同一套代码在不同机型上行为不一样有些机型的WebView甚至不按标准回调走。这些问题的本质是原生WebView给的是基础能力混合开发场景里的容器逻辑都要自己补。一旦你负责的业务模块多了这类重复代码会翻好几倍而且测试成本非常高。AgentWeb能流行起来不是因为它做了什么天顶星科技而是它把这堆通用需求做成了开箱即用的配置项开发者只需要关注自己的业务逻辑。1.2 AgentWeb 到底帮我们解决了什么AgentWeb严格来说不是一个浏览器内核它是基于系统WebView做的一个轻量级封装库官方定位就是让Android开发者用更少的代码搞定WebView容器。它内部帮我们做了几件非常实在的事自动绑定Activity或Fragment的生命周期页面销毁时自动释放资源从根源上解决绝大多数内存泄漏问题。内置了进度条、错误页、文件下载、文件上传这些高频能力开几个开关就能用。对WebViewClient、WebChromeClient、JavascriptInterface做了更好的封装JS和原生通信的逻辑可以写得更干净。统一了不同Android版本和厂商ROM之间的行为差异减少适配成本。提示AgentWeb只是容器层的封装内核还是系统WebView。如果你追求极致的加载速度和渲染效果后期可以考虑接入腾讯X5或系统内核的WebView但那是另一个话题系列后面有机会专门聊。我实际用下来的感受是接入AgentWeb之后WebView相关的代码量能少一半以上而且很多以前需要盯着手机一台台适配的问题库已经帮我们处理掉了。这也是我推荐它的核心原因。2. 集成前的准备工作和最小工程结构2.1 依赖引入与初始化在项目级的build.gradle中确认已经配置好仓库然后在模块级的build.gradle里加上AgentWeb的依赖。版本号建议直接以官方最新Release为准不要盲目用老版本因为WebView的适配和Android系统版本强相关新版本通常会修复一些厂商兼容问题和系统安全策略变更带来的坑。dependencies { implementation com.just.agentweb:agentweb:最新版本号 }如果你的项目用到了AndroidX选对应支持AndroidX的版本如果项目还在用旧的支持库则要选对应的老版本。依赖坐标具体怎么写每个版本略有差别这里就不写死版本号了防止误导。引入依赖之后建议在Application的onCreate里做一次初始化。AgentWeb其实不需要手动初始化但我会习惯性地在这里做一些全局配置比如public class App extends Application { Override public void onCreate() { super.onCreate(); // 这里可以放置全局的WebView相关配置 } }2.2 权限声明与混淆规则在AndroidManifest.xml里需要声明网络权限。如果后面要支持下载文件、定位等功能记得把对应权限一并加上uses-permission android:nameandroid.permission.INTERNET / uses-permission android:nameandroid.permission.ACCESS_NETWORK_STATE /混淆配置这一块必须配好。虽然AgentWeb本身是开源库但它内部用了大量反射和动态代理混淆不过会导致页面白屏或者JS调用失效。在proguard-rules.pro里加上以下几行-keep class com.just.agentweb.** { *; } -keep class com.just.agentweb.core.** { *; } -keep class com.just.agentweb.widget.** { *; }如果项目里还混淆了WebView相关的类记得把你自己写的JavascriptInterface接口类也加上keep规则否则混淆之后JS那边找不到对应的方法名通信链路直接断裂。2.3 页面承载方式的选择接入AgentWeb之前建议先想清楚一个基础问题用Activity承载还是用Fragment承载。我的经验是能用独立Activity就用独立Activity每个H5业务模块一个Activity对应一个AgentWeb实例。原因有两个一是Activity的生命周期清晰AgentWeb自动绑定生命周期时不容易出错二是当H5页面进入二级三级页面时返回栈管理在Activity层面更可控。如果你必须在一个Activity里切换多个H5页面那么建议用Fragment来做容器每个Fragment持有自己的AgentWeb实例。但这样要特别注意Fragment的隐藏和显示时机处理不当会出现多个WebView同时存活的内存问题。3. 5分钟接入一个能上线的H5容器页3.1 核心代码AgentWeb链式创建接入AgentWeb的代码非常简洁核心就是链式调用。先看一个最基础的示例后面再逐个方法拆开讲public class WebActivity extends AppCompatActivity { private AgentWeb mAgentWeb; private FrameLayout mContainer; Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_web); mContainer findViewById(R.id.web_container); mAgentWeb AgentWeb.with(this) .setAgentWebParent(mContainer, new FrameLayout.LayoutParams(-1, -1)) .useDefaultIndicator(getResources().getColor(R.color.colorPrimary)) .defaultWebClient() .openParallelDownload() .setWebChromeClient(mWebChromeClient) .setWebViewClient(mWebViewClient) .createAgentWeb() .ready() .go(https://example.com/h5/page); } private WebChromeClient mWebChromeClient new WebChromeClient() { Override public void onReceivedTitle(WebView view, String title) { super.onReceivedTitle(view, title); // 更新标题栏 } }; private WebViewClient mWebViewClient new WebViewClient() { Override public boolean shouldOverrideUrlLoading(WebView view, String url) { // 拦截URL return super.shouldOverrideUrlLoading(view, url); } }; }这段代码执行完一个带默认进度条的WebView容器就能用了。布局文件里只需要放一个FrameLayout作为容器根节点?xml version1.0 encodingutf-8? FrameLayout xmlns:androidhttp://schemas.android.com/apk/res/android android:idid/web_container android:layout_widthmatch_parent android:layout_heightmatch_parent /3.2 每个方法是什么意思setAgentWebParent把AgentWeb创建的WebView塞到这个ViewGroup中。这里要注意容器必须是ViewGroup否则WebView没有挂载点。布局参数建议直接填-1、-1也就是match_parent因为WebView一般就是撑满整个容器。useDefaultIndicator开启默认的顶部进度条。可以传入颜色值颜色最好跟App主题色一致。如果你觉得默认进度条太细也可以自定义一个这个后面在配置章节单独说。defaultWebClient这个是AgentWeb的关键方法相当于一键开启一套默认的WebViewClient和WebChromeClient配置比如辅助处理文件上传、页面加载错误等行为。如果不需要完全自定义直接用默认实现就够了。openParallelDownload开启并行下载能力也就是说用户长按图片或者点击下载链接时AgentWeb可以接管下载任务而不是让WebView自己尝试打开下载文件。这一步在原生WebView里是要自己写DownloadListener的这里一行搞定。createAgentWeb().ready().go(url)创建AgentWeb实例并加载最终URL。ready方法必须调用它负责绑定生命周期监听器。go方法传入URL字符串。3.3 生命周期绑定漏了这一步必出大问题AgentWeb的自动生命周期管理是它最大的优势但前提是你得正确调用。在Activity中把下面这几行加上Override protected void onResume() { if (mAgentWeb ! null) { mAgentWeb.getWebLifeCycle().onResume(); } super.onResume(); } Override protected void onPause() { if (mAgentWeb ! null) { mAgentWeb.getWebLifeCycle().onPause(); } super.onPause(); } Override protected void onDestroy() { if (mAgentWeb ! null) { mAgentWeb.getWebLifeCycle().onDestroy(); } super.onDestroy(); }getWebLifeCycle()是AgentWeb暴露出来的生命周期控制器。onResume里让WebView恢复加载onPause里暂停所有动画、定位、视频等耗时任务onDestroy里释放WebView资源。很多刚开始用AgentWeb的人会漏掉这三行后果是页面返回之后WebView还在后台加载App内存持续上涨甚至出现声音残留。注意onDestroy里不要直接在agentWeb对象上调用destroy方法一定要走getWebLifeCycle().onDestroy()。AgentWeb内部会对WebView做递归销毁把WebView的引用置空并且移除父容器上的WebView这一步是防止内存泄漏的关键。3.4 加载本地HTML和Assets资源开发阶段如果后端接口还没好可以先用本地HTML调试。AgentWeb支持直接加载assets目录下的文件mAgentWeb.getUrlLoader().loadUrl(file:///android_asset/local_demo.html);这里有个细节Android WebView默认是允许加载本地文件和远程资源混用的但出于安全考虑很多团队会把setAllowFileAccessFromFileURLs关掉。AgentWeb的默认配置在这方面比较严格如果遇到本地HTML里引用了远端资源导致加载失败需要确认一下安全策略配置不要为了方便把安全开关全打开。4. 混合开发中最离不开的三类配置4.1 JS与原生互相调用混合开发的核心之一就是JS和原生通信。AgentWeb在这块提供了比较清晰的封装本质上还是基于WebView的JavascriptInterface机制。原生注入JS接口public class JsInterface { JavascriptInterface public void showToast(String msg) { runOnUiThread(() - Toast.makeText(WebActivity.this, msg, Toast.LENGTH_SHORT).show()); } } mAgentWeb.getJsAccessEntrace().quickCallJs(showToast, hello from native);这里的quickCallJs是AgentWeb封装好的方法可以在原生侧主动调用H5页面里的JS函数。H5侧调用原生方法则很简单在页面里直接调用window对象上挂载的方法即可。实际项目里JS通信有个高频坑线程问题。JavascriptInterface的方法是在WebView的私有线程里执行的不是UI线程如果你在这个方法里直接更新UI轻则崩溃重则界面闪烁。正确做法是切到主线程再操作UI上面代码里用了runOnUiThread。还有一个坑是方法名混淆前面提过JavascriptInterface接口类一定不能被混淆。4.2 文件下载与上传的配置混合开发里最常见的下载场景是用户长按H5页面里的图片保存到相册或者点击某个文件链接开始下载。原生WebView默认会把下载任务交给系统浏览器处理体验比较割裂。AgentWeb的openParallelDownload可以解决下载问题它会自动创建一个下载任务并且通过通知栏显示进度。对于文件上传也就是H5页面的input typefile需要配合WebChromeClient的onShowFileChooser来处理。AgentWeb的defaultWebClient已经封装了默认逻辑如果你要定制上传行为可以重写private WebChromeClient mWebChromeClient new WebChromeClient() { Override public boolean onShowFileChooser(WebView webView, ValueCallbackUri[] filePathCallback, FileChooserParams fileChooserParams) { // 保存回调打开系统文件选择器 mFilePathCallback filePathCallback; Intent intent new Intent(Intent.ACTION_GET_CONTENT); intent.addCategory(Intent.CATEGORY_OPENABLE); intent.setType(*/*); startActivityForResult(intent, FILE_CHOOSER_REQUEST_CODE); return true; } };onActivityResult里记得把选择结果回传给WebViewOverride protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode FILE_CHOOSER_REQUEST_CODE) { if (mFilePathCallback ! null) { Uri result data ! null ? data.getData() : null; mFilePathCallback.onReceiveValue(result ! null ? new Uri[]{result} : null); mFilePathCallback null; } return; } super.onActivityResult(requestCode, resultCode, data); }这里的回调对象必须在收到结果后置空否则下一次上传会失效。这也是网上问得比较多的问题我自己第一次这个坑也踩过。4.3 进度条、标题同步与错误页处理默认进度条是一根细线很多App会定制成有圆角的胶囊进度条或者直接改成仿Safari风格的加载动画。AgentWeb是支持自定义进度条样式的核心方式是通过createAgentWeb方法链上的.progressBar替换默认实现。我一般偷懒的做法是用useDefaultIndicator加颜色值就够线上用了因为大多数用户对进度条样式并不敏感。标题同步这个功能建议通过onReceivedTitle回调来做。H5页面切换标题时WebView会回调这个方法你在里面动态更新Activity的标题栏即可。要注意的是有些页面会动态修改document.title这个回调触发时机不固定极端情况下标题会上下跳动。错误页处理比较重要。在WebViewClient的onReceivedError里可以看到加载失败的URL和错误码。AgentWeb默认会显示一个错误页但样式比较简陋。我的建议是线上环境直接用默认错误页不要过度定制因为定制错误页需要额外维护一套静态资源而且错误页毕竟只是兜底方案频次不该太高。如果发现某个环境下错误页频繁出现优先排查网络策略和HTTPS证书问题而不是纠结错误页样式。5. 实际项目里那些文档没写的坑5.1 常见问题速查表问题现象可能原因解决思路页面白屏但URL能正常打开WebView加载失败或资源被拦截检查网络请求是否被代理拦截查看onReceivedError回调JS调用原生方法无反应混淆导致方法名丢失给JavascriptInterface类和接口方法加keep规则返回上级页面时WebView直接退出返回键事件被Activity消费用AgentWeb的onKey事件拦截优先回退WebView内部页面栈上传文件时点击按钮没反应文件选择回调被系统回收确认onShowFileChooser正确调用并且回调对象没有泄漏加载HTTPS页面提示证书错误系统时间不对或证书链不完整检查系统时间或与后端确认证书链是否完整从列表页进入H5第二次加载变慢WebView缓存策略不合适使用默认缓存模式耗尽性能换内存5.2 返回键处理与物理按键拦截Android的返回键默认逻辑是直接结束Activity但在WebView场景下用户预期是返回上一个页面。AgentWeb提供了返回键事件处理机制推荐写法如下Override public boolean onKeyDown(int keyCode, KeyEvent event) { if (mAgentWeb ! null mAgentWeb.handleWebEvent(keyCode, event)) { return true; } return super.onKeyDown(keyCode, event); }handleWebEvent会先检查WebView内部是否有可回退的历史记录如果有就返回上一页没有才交给系统处理。这个虽然是个小细节但直接影响体验很多混合开发的新手会漏掉。5.3 UserAgent 拼接与客户端标识很多H5页面会通过UserAgent来判断当前环境比如区分是App内打开还是普通浏览器打开从而展示不同的运营位。WebView的默认UserAgent里没有App标识需要在加载页面之前手动拼接。String defaultUA mAgentWeb.getWebCreator().getWebView().getSettings().getUserAgentString(); String customUA defaultUA AppName/2.1.0; mAgentWeb.getWebCreator().getWebView().getSettings().setUserAgentString(customUA);拼接的位置建议放在createAgentWeb之前通过WebView的settings直接设置。注意UserAgent不能设置得太长有些服务端解析UA的正则对超长UA处理不友好实测下来在默认UA后面追加App名和版本号是最稳的。5.4 与业务线多页面复用时的单例问题如果App里同时打开了多个AgentWeb实例一定要确认每个页面持有的是独立的WebView对象。AgentWeb本身没有全局单例的限制但如果你在Activity里用了静态变量去引用AgentWeb就会出大问题页面A关了静态变量还指着它的WebView页面B拿到这个引用后操作的就是一个已销毁的对象各种诡异崩溃接踵而至。我的建议是凡是Activity或Fragment持有的AgentWeb一律做成成员变量不要静态持有。实在要在多个页面共享登录态、Cookie尽量通过WebView的CookieManager同步机制来做。5.5 关于后续系列的一些想法这篇先讲到AgentWeb的基础接入和核心配置主要是帮大家把混合开发的底座打稳。后面我会继续写H5与原生通信的安全校验怎么做、如何用AgentWeb实现一个支持离线包预加载的H5容器、视频播放控制、以及从AgentWeb容器平滑切换到X5内核的实战思路。如果项目里WebView页面的启动速度一直是痛点离线包和预加载这部分值得重点关注。在自己的实际项目中我最大的体会是混合开发的技术门槛并不在WebView本身而在工程化的细节管理上。AgentWeb帮我们封住了大部分底层的坑但如果接入姿势不正确返而会引入新问题。建议第一次接入时先按这篇文章的最小步骤跑通一个简单页面再逐步叠加功能需求。按这个顺序走项目基本不会出现大方向上的偏差。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表