Android WebView 与 H5 双向通信实现详解

Android WebView 与 H5 双向通信实现详解

背景介绍

在 Android 原生应用中,WebView 与 H5 页面的双向通信是一个常见需求。本文将详细介绍如何实现一个完整的通信方案。

核心实现

1. H5 端实现

与之前类似,但需要针对 Android 环境做适配:

javascript:src/www/bridge.js 复制代码
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 端实现

kotlin:src/main/java/com/example/webview/WebViewBridge.kt 复制代码
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 中使用

kotlin:src/main/java/com/example/webview/WebViewActivity.kt 复制代码
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:src/main/res/layout/activity_webview.xml 复制代码
<?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>

实现要点

  1. 安全性考虑

    • 使用 @JavascriptInterface 注解标记可供 JS 调用的方法
    • 处理跨线程调用问题
    • 注意 WebView 内存泄漏问题
  2. 性能优化

    • 使用 Handler 确保在主线程处理 UI 操作
    • 避免频繁的 JSON 解析和字符串操作
    • 合理管理 WebView 生命周期
  3. 调试功能

    • 实现 WebChromeClient 以捕获 console 日志
    • 添加详细的错误日志
    • 支持开发环境调试
  4. 兼容性处理

    • 处理不同 Android 版本的兼容性问题
    • 确保 JavaScript 接口注入的稳定性
    • 处理 WebView 内核版本差异

最佳实践建议

  1. 添加通信超时机制
  2. 实现错误重试策略
  3. 添加网络状态监听
  4. 实现页面加载进度提示
  5. 处理 WebView 内存管理
  6. 添加安全域名白名单
  7. 实现离线缓存策略

注意事项

  1. 在 AndroidManifest.xml 中添加网络权限:
xml:AndroidManifest.xml 复制代码
<uses-permission android:name="android.permission.INTERNET" />
  1. proguard-rules.pro 中添加混淆规则:
proguard:proguard-rules.pro 复制代码
-keepclassmembers class com.example.webview.WebViewBridge$AndroidBridge {
    public *;
}
  1. 注意处理 WebView 的生命周期,避免内存泄漏。

这个实现方案提供了一个完整的 Android WebView 与 H5 通信框架,可以根据实际项目需求进行扩展和优化。

相关推荐
踢球的打工仔2 小时前
PHP面向对象(7)
android·开发语言·php
安卓理事人2 小时前
安卓socket
android
安卓理事人8 小时前
安卓LinkedBlockingQueue消息队列
android
万能的小裴同学9 小时前
Android M3U8视频播放器
android·音视频
q***577410 小时前
MySql的慢查询(慢日志)
android·mysql·adb
JavaNoober10 小时前
Android 前台服务 "Bad Notification" 崩溃机制分析文档
android
城东米粉儿11 小时前
关于ObjectAnimator
android
zhangphil12 小时前
Android渲染线程Render Thread的RenderNode与DisplayList,引用Bitmap及Open GL纹理上传GPU
android
火柴就是我13 小时前
从头写一个自己的app
android·前端·flutter
lichong95114 小时前
XLog debug 开启打印日志,release 关闭打印日志
android·java·前端