Android WebView 与 H5 双向通信实现详解
背景介绍
在 Android 原生应用中,WebView 与 H5 页面的双向通信是一个常见需求。本文将详细介绍如何实现一个完整的通信方案。
核心实现
1. H5 端实现
与之前类似,但需要针对 Android 环境做适配:
window.ntalkCallbacks = {
callbacks: {},
register: function(type, successCallback, errorCallback) {
const callbackId = type + Date.now().toString();
// Promise方式
if (!successCallback && !errorCallback) {
const promise = new Promise((resolve, reject) => {
this.callbacks[callbackId] = {
success: resolve,
error: reject,
};
});
return {
promise,
callbackId
};
}
// 回调方式
this.callbacks[callbackId] = {
success: successCallback,
error: errorCallback,
};
return callbackId;
},
execute: function(callbackId, type, data) {
const callback = this.callbacks[callbackId];
if (callback) {
if (type === 'success') {
callback.success && callback.success(data);
} else {
callback.error && callback.error(data);
}
delete this.callbacks[callbackId];
}
}
};
// Android桥接对象
window.ntalkNative = {
getUserInfo: function(successCallback, errorCallback) {
const callbackId = ntalkCallbacks.register('getUserInfo', successCallback, errorCallback);
// 调用Android注入的对象
window.androidBridge.postMessage(JSON.stringify({
type: 'getUserInfo',
callbackId,
data: null
}));
},
getUserInfoAsync: async function() {
const { promise, callbackId } = ntalkCallbacks.register('getUserInfo');
window.androidBridge.postMessage(JSON.stringify({
type: 'getUserInfo',
callbackId,
data: null
}));
return promise;
}
};
2. Android 端实现
class WebViewBridge(private val webView: WebView) {
// JavaScript接口类
@JavascriptInterface
class AndroidBridge(private val handler: Handler, private val bridge: WebViewBridge) {
@JavascriptInterface
fun postMessage(message: String) {
// 切换到主线程处理
handler.post {
bridge.handleMessage(message)
}
}
}
init {
setupWebView()
}
private fun setupWebView() {
webView.apply {
settings.apply {
javaScriptEnabled = true
domStorageEnabled = true
}
// 注入JavaScript接口
addJavascriptInterface(
AndroidBridge(Handler(Looper.getMainLooper()), this@WebViewBridge),
"androidBridge"
)
}
}
private fun handleMessage(message: String) {
try {
val jsonObject = JSONObject(message)
val type = jsonObject.getString("type")
val callbackId = jsonObject.getString("callbackId")
val data = jsonObject.opt("data")
when (type) {
"getUserInfo" -> handleGetUserInfo(callbackId)
"setAppCache" -> handleSetAppCache(callbackId, data)
else -> sendCallback(callbackId, "error", "Unknown type")
}
} catch (e: Exception) {
Log.e("WebViewBridge", "Handle message error", e)
}
}
private fun sendCallback(callbackId: String, type: String, data: Any?) {
val script = """
window.ntalkCallbacks.execute(
'$callbackId',
'$type',
${Gson().toJson(data)}
);
""".trimIndent()
webView.evaluateJavascript(script, null)
}
private fun handleGetUserInfo(callbackId: String) {
try {
// 示例:获取用户信息
val userInfo = mapOf(
"userId" to "123",
"name" to "张三",
"avatar" to "https://example.com/avatar.png"
)
sendCallback(callbackId, "success", userInfo)
} catch (e: Exception) {
sendCallback(callbackId, "error", e.message)
}
}
private fun handleSetAppCache(callbackId: String, data: Any?) {
try {
// 处理缓存逻辑
sendCallback(callbackId, "success", true)
} catch (e: Exception) {
sendCallback(callbackId, "error", e.message)
}
}
}
3. Activity 中使用
class WebViewActivity : AppCompatActivity() {
private lateinit var webView: WebView
private lateinit var webViewBridge: WebViewBridge
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_webview)
webView = findViewById(R.id.webView)
webViewBridge = WebViewBridge(webView)
setupWebView()
}
private fun setupWebView() {
webView.apply {
webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView?, url: String?) {
super.onPageFinished(view, url)
// 页面加载完成后的处理
}
}
webChromeClient = object : WebChromeClient() {
override fun onConsoleMessage(message: ConsoleMessage): Boolean {
Log.d("WebView", "${message.message()} -- From line ${message.lineNumber()} of ${message.sourceId()}")
return true
}
}
// 加载网页
loadUrl("https://example.com")
}
}
override fun onDestroy() {
webView.destroy()
super.onDestroy()
}
}
4. 布局文件
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<WebView
android:id="@+id/webView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
实现要点
安全性考虑
- 使用
@JavascriptInterface
注解标记可供 JS 调用的方法 - 处理跨线程调用问题
- 注意 WebView 内存泄漏问题
- 使用
性能优化
- 使用 Handler 确保在主线程处理 UI 操作
- 避免频繁的 JSON 解析和字符串操作
- 合理管理 WebView 生命周期
调试功能
- 实现 WebChromeClient 以捕获 console 日志
- 添加详细的错误日志
- 支持开发环境调试
兼容性处理
- 处理不同 Android 版本的兼容性问题
- 确保 JavaScript 接口注入的稳定性
- 处理 WebView 内核版本差异
最佳实践建议
- 添加通信超时机制
- 实现错误重试策略
- 添加网络状态监听
- 实现页面加载进度提示
- 处理 WebView 内存管理
- 添加安全域名白名单
- 实现离线缓存策略
注意事项
- 在 AndroidManifest.xml 中添加网络权限:
<uses-permission android:name="android.permission.INTERNET" />
- 在 proguard-rules.pro 中添加混淆规则:
-keepclassmembers class com.example.webview.WebViewBridge$AndroidBridge {
public *;
}
- 注意处理 WebView 的生命周期,避免内存泄漏。
这个实现方案提供了一个完整的 Android WebView 与 H5 通信框架,可以根据实际项目需求进行扩展和优化。