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

发布于:2025-02-11 ⋅ 阅读:(26) ⋅ 点赞:(0)

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>

实现要点

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

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


网站公告

今日签到

点亮在社区的每一天
去签到