Android 干货分享:WebView 优化(2)—— 桥接设计、独立进程、跨进程通信

lxf2023-11-25 21:50:01

“我报名参加1期挑战——瓜分10万奖池,这是我的第2篇文章,点击查看活动详情”

目录

  • Android 干货分享:WebView 优化(1)—— 缓存管理、回收复用、网页秒开、白屏检测

  • Android 干货分享:WebView 优化(2)—— 桥接设计、独立进程、跨进程通信

前言

下面接着上篇博客的 Demo 继续完善

桥接

WebView 的桥接,也就是 App 可以调用 Web 中的 js 方法,js 也可以调用 App 中的方法

普通调用

js 调用 App 方法

以实现 js 调用 App 实现展示 toast 为例,Demo 为了方便直接在 BaseWebView 中增加以下代码

open class BaseWebView @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null
) : WebView(context, attrs), LifecycleEventObserver {

    init {
        // 省略其他代码...
        // 添加桥接
        addJavascriptInterface(this, "bridge")
    }
    
    // 添加注解 表示 js 可以调用该方法
    @JavascriptInterface
    fun showToast(message: String){
        Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
    }
}

新建 test_default_bridge.html 文件

<!DOCTYPE html>
<html>
<head>
    <meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" name="viewport">
    <script src='../js/bridge.js'></script>
    <style type="text/css">
   .bn {
      padding: 8px 20px;
      width: 100%;
      height: auto;
      margin: 0 auto;
      text-align: center;
      margin-top: 20px;
   }
    </style>
</head>
<body>
<div class="detail-content" id="app-vote">
    <div style="display: flex; flex-direction: column;">
        <button class="bn" onclick="showToast()">调用原生 App 展示 Toast</button>
    </div>
</div>
</body>
<script type='text/javascript'>
function showToast() {
   // bridge 要和 BaseWebView 中 addJavascriptInterface 第二个参数对应
   window.bridge.showToast('hello world')
}
</script>

让 WebView 加载 test_default_bridge.html 后测试,效果图:

Android 干货分享:WebView 优化(2)—— 桥接设计、独立进程、跨进程通信

App 调用 js 方法

App 调用 js 就比较简单了直接利用 WebView 的 evaluateJavascript 方法即可:

// 写正确 方法名 和 参数 即可
mWebView.evaluateJavascript("javascript:showToast('hello world')") {}

命令模式

作为开发者,肯定不希望写好的基类(BaseWebView)被频繁改动,如果采用上述 Demo 中的方式实现桥接通信,那每增加一个桥接都需要修改 BaseWebView 类,这里就需要用设计模式来进行重构。 这里为什么用命令模式?

  1. 通信双发都符合 请求方发出请求,要求执行某个操作;接收方收到请求,执行对应操作
  2. 并且都符合单命令单接收者
  3. 请求方和接收方可以独立开来,不用知道对方的命令接口(经过封装后达成,调用同一方法实现不同命令)
  4. 降低耦合,新命令(桥接方法)很容易加入到项目中(下面会结合 APT 实现)

实现过程主要考虑几个方面:

  1. 一般桥接肯定要 Android iOS 同时实现,要考虑易用性。
  2. 新增桥接方法时不能频繁改动已有代码,低耦合。
  3. 支持回调,WebView 的 evaluateJavascript 方法支持回调,那么 js 调用 Android 方法也要支持回调。
  4. 桥接参数传递选择了 json 格式,增删参数方便。

桥接实现

流程图

Android 干货分享:WebView 优化(2)—— 桥接设计、独立进程、跨进程通信

App 提供发送命令桥接

首先新增桥接参数实体类:

data class JsBridgeMessage(
    @SerializedName("command")
    val command: String?, // 命令
    @SerializedName("params")
    val params: JsonObject?, // 参数
)

新增命令接口:

interface IBridgeCommand {
    fun exec(params: JsonObject?)
}

上述示例中展示 toast 的桥接改为命令,新增 ToastCommand 类:

class ToastCommand : IBridgeCommand {
    override fun exec(params: JsonObject?) {
        if (params != null && params["message"] != null) {
            ToastUtils.showShort(params["message"].asString)
        }
    }
}

BaseWebView 中添加 sendCommond 桥接方法:

class BaseWebView{
    // 省略其他代码...
    init{
        addJavascriptInterface(this, "bridge")
    }
        
    @JavascriptInterface
    fun sendCommand(json: String?) {
        if (json.isNullOrEmpty()) {
            // 异常调用处理
            return
        }
        try {
            val message = GsonUtils.fromJson(json, JsBridgeMessage::class.java)
            // 成功拿到参数后 待会在这里分发命令
            // ...
        } catch (e: JsonSyntaxException) {
            e.printStackTrace()
        }
    }
}

命令分发器

分发器主要负责命令参数合法性校验、执行命令,这里用单例模式实现:

class JsBridgeInvokeDispatcher {
    companion object {
        // 省略不必要的代码...
        // 单例
        fun getInstance(): JsBridgeInvokeDispatcher {
            // ...
        }
    }
    
    // 暴露给外部方法 分发调用
    fun sendCommand(view: BaseWebView, message: JsBridgeMessage?) {
        LogUtils.d(TAG, "sendCommand()", "message: $message")
        if (checkMessage(message)){
            // 校验命令通过后 执行命令
            excuteCommand(view, message)
        }
    }

    // 校验命令、参数 合法性
    private fun checkMessage(message: JsBridgeMessage?): Boolean{
        if (message == null) {
            return false
        }
        // ...
        return true
    }

    //执行命令
    private fun excuteCommand(view: BaseWebView, message: JsBridgeMessage?){
        //...
    }
}

执行命令再抽出一个类来,BridgeCommandHandler 主要负责命令的注册、命令的逻辑执行:

class BridgeCommandHandler {

    companion object {
        // 省略不必要的代码...
        // 单例
        fun getInstance(): BridgeCommandHandler {
            // ...
        }
    }

    // 用于切线程
    private val mHandle = Handler(Looper.getMainLooper())
    
    // 命令注册 暂时用 map 手动添加 后续修改
    private val mCommandMap by lazy {
        val map = ArrayMap<String, IBridgeCommand>().apply {
            put("showToast", ToastCommand())
        }
        return@lazy map
    }

    // 暴露给外部方法 分发调用
    fun handleBridgeInvoke(command: String?, params: String?) {
        // map 中存在命令 则执行
        if (mCommandMap.contains(command)) {
            mHandle.post { // 切换到主线程 获取命令 执行
                mCommandMap[command]!!.exec(
                    GsonUtils.fromJson(params, JsonObject::class.java)
                )
            }
        }
    }
}

再回到 JsBridgeInvokeDispatcher 的 excuteCommand 执行方法里调用一下 BridgeCommandHandler :

private fun excuteCommand(view: BaseWebView, message: JsBridgeMessage?){
    BridgeCommandHandler.getInstance().handleBridgeInvoke(message.command, message.params)
}

到这里 App 端已经为 js 端做好了调用准备,这里毕竟是个 Demo 下面我们自己来搞一下 js 那边的封装。

js 文件封装

还是在 assets 的 js 目录下,新建 bridge.js 文件:

Android 干货分享:WebView 优化(2)—— 桥接设计、独立进程、跨进程通信

下面是 js 文件内容,算是一个工具类吧,Android 开发其实不用特别关注 js 代码,就不详细解释了,代码中都有注释,直接贴代码:

var jsBridge = {};

// 系统判断
jsBridge.os = {
    'isAndroid': Boolean(navigator.userAgent.match(/android/ig)),
    'isIOS': Boolean(navigator.userAgent.match(/iphone|ipod|iOS/ig))
};

// 发送命令 参数解释:command 命令;params 参数json格式
jsBridge.sendCommand = function(command, params) {
   // 构建 message 对象
   var message = {
      'command': command
   }
   if (params && typeof params === 'object') { // 支持传参
       message['params'] = params // 参数
   }
   if (jsBridge.os.isAndroid) { // android 桥接调用
      window.bridge.sendCommand(JSON.stringify(message))
   } else if (jsBridge.os.isIOS) { // ios 桥接调用 偷来的代码 不用太关注
      window.webkit.messageHandlers.bridge.sendCommand(JSON.stringify(message))
   }
}

window.jsBridge = jsBridge;

修改最初事例中展示 toast 的 html 文件:

// 省略了无关代码 只展示了引入 bridge.js 和 方法调用
// ...
<head>
    <script src='./js/bridge.js'></script>
</head>
// ...
<script type='text/javascript'>
function showToast() {
   // window.bridge.showToast('hello world')
   // 上面的桥接调用修改为
   var params = {
     'message': 'hello world'
   }
   window.jsBridge.sendCommand('showToast', params)
}
</script>

测试效果和普通调用是一样的,这里就不放效果图了,为什么呢?因为这条流程还没搞定,还有一个很重要的细节 ———— 回调

回调支持

流程图

Android 干货分享:WebView 优化(2)—— 桥接设计、独立进程、跨进程通信

Web 端实现

这次先来修改 js 文件部分,在 bridge.js 中维护一个回调 map,并且桥接调用支持传递回调方法:

// 省略其他代码 只贴 新增 修改的代码
// 回调方法 map
jsBridge.mapCallbacks = {}

// 回调处理 提供给 App 命令执行完成后调用此方法 根据 key 从 map 中取出 触发回调
jsBridge.postBridgeCallback = function(key, data){
    var obj = jsBridge.mapCallbacks[key]; // 从 map 中拿出 function
    if(obj.callback){ // 存在则调用
        obj.callback(data); // 调用 有参数则传递参数 这里回调参数也设计为 JSON 格式
        delete jsBridge.mapCallbacks[key]; // 从 map 中移除
    }else{ // 不存在 异常处理
        console.log('jsBridge postBridgeCallback', '回调不存在: ' + key)
    }
}

// 生成回调map key 的方法 采用 固定前缀+时间戳+随机码 的方式 
// 防止短时间内并发调用出现重复的key
function generateCallbackKey(){
    return "jsBridgeCallback_" + new Date().getTime() + "_" + randomCode();
}

// 生成随机码 防止并发重复
function randomCode(){
    var code = ""
    for(var i = 0; i < 6; i++){
        code += Math.floor(Math.random() * 10)
    }
    return code;
}

// 发送命令 方法修改 增加 callback 回调方法参数
jsBridge.sendCommand = function(command, params, callback) {
   var message = {
      'command': command
   }
   if (params && typeof params === 'object') { // 支持传参
       message['params'] = params
   }
   // sendCommand 方法主要增加了这个 if 判断
   // 回调 key 固定放在 bridgeCallback 字段中,app 客户端判断 callback 是否有值即可
   if (callback && typeof callback === 'function') { // 支持回调 判断是否是回调方法
        var key = generateCallbackKey() // 生成回调key
        jsBridge.mapCallbacks[key] = { 'callback': callback } // 回调方法放入 map 中
        message['params']['bridgeCallback'] = key // 参数新增字段 bridgeCallback
   }
   if (jsBridge.os.isAndroid) { // android 桥接调用
      window.bridge.sendCommand(JSON.stringify(message))
   } else if (jsBridge.os.isIOS) { // ios 桥接调用 偷来的代码 不用太关注
      window.webkit.messageHandlers.bridge.sendCommand(JSON.stringify(message))
   }
}

App 端实现

首先 JsBridgeMessage 添加 bridgeCallback 字段:

data class JsBridgeMessage(
    //省略...
    @SerializedName("bridgeCallback")
    val bridgeCallback: String? // 回调 key
)

新增回调接口 IBridgeCallbackInterface :

interface IBridgeCallbackInterface {
    /**
     * callback 回调key
     * params   参数 json 格式
     */
    fun handleBridgeCallback(callback: String, params: String)
    
    // 从参数中获取回调 key 的方法
    fun getCallbackKey(params: JsonObject?): String? {
        if (params == null) {
            return null
        }
        if (params["bridgeCallback"] == null) {
            return null
        }
        return params["bridgeCallback"].asString
    }
}

修改 IBridgeCommand 中 exec 方法参数,增加回调接口参数

interface IBridgeCommand {
    fun exec(params: JsonObject?, callback: IBridgeCallbackInterface?)
}

修改 BridgeCommandHandler 的 handleBridgeInvoke 方法参数,增加回调接口参数:

fun handleBridgeInvoke(command: String?, params: String?, bridgeCallback: IBridgeCallbackInterface?) {
    // ... 
    mCommandMap[command]!!.exec(
        GsonUtils.fromJson(params, JsonObject::class.java),
        bridgeCallback // 回调传递给 Command
    )
}

最后修改 JsBridgeInvokeDispatcher 的 excuteCommand 方法:

class JsBridgeInvokeDispatcher {
    // ...
    private fun excuteCommand(view: BaseWebView, message: JsBridgeMessage?){
        // 实现 IBridgeCallbackInterface
        val callback = object : IBridgeCallbackInterface{
            override fun handleBridgeCallback(callback: String, params: String) {
                view.postBridgeCallback(callback, params)
            }
        }
        BridgeCommandHandler.getInstance().handleBridgeInvoke(
            message?.command, 
            GsonUtils.toJson(message?.params), 
            callback
        )
    }
}

BaseWebView 中提供触发回调方法:

class BaseWebView{
    // 省略其他代码
    fun postBridgeCallback(key: String?, data: String?) {
        post {
          evaluateJavascript("javascript:window.jsBridge.postBridgeCallback(`$key`, `$data`)") {}
        }
    }
}

最后修改一下 ToastCommand 中的代码:

class ToastCommand : IBridgeCommand {
    override fun exec(params: JsonObject?) {
        if (params != null && params["message"] != null) {
            ToastUtils.showShort(params["message"].asString)
            
            //回调 测试 返回一个 message 给 web 端
            val key = getCallbackKey(params)
            if (!key.isNullOrEmpty()) {
                val data = mapOf("message" to "showToast is success!!")
                callback?.handleBridgeCallback(key, GsonUtils.toJson(data))
            }
        }
    }
}

为了测试回调效果 js 中新增一个带回调的调用:

<script type='text/javascript'>
// ...
function showToastWithCallback() {
    var params = {
        'message': 'hello world'
    }
    window.jsBridge.sendCommand('showToast', params, function(data){
       // 打印一下 回调中接受的参数
       console.log('触发回调成功! data:', data)
   })
}
</script>

效果图:

Android 干货分享:WebView 优化(2)—— 桥接设计、独立进程、跨进程通信

toast 一加成功展示,看一下回调输出的日志

Android 干货分享:WebView 优化(2)—— 桥接设计、独立进程、跨进程通信

到这里为止用命令模式封装的桥接设计就大功告成了!

利用 apt 自动注册桥接

目前的实现后续每个命令的增删都需要在 BridgeCommandHandler 类中手动修改 mCommandMap 的初始化代码,这肯定是不合理的。网上也有很多博客利用 @AutoService 实现自动注册,这里分享一个我个人的方案,我是利用 APT 去扫描自定义注解生成一个工具类,BridgeCommandHandler 调用工具类方法实现自动注册的,下面简单说一下实现过程。

注意

Demo 中 APT 相关代码也是用 Kotlin 写的,Demo 整体也是个完完全全的 Kotlin 项目,那么 APT 生成的文件也自然是 Kotlin 文件最好

新建 Moudle

首先新建两个 Java or Kotlin Library

Android 干货分享:WebView 优化(2)—— 桥接设计、独立进程、跨进程通信

Android 干货分享:WebView 优化(2)—— 桥接设计、独立进程、跨进程通信

apt-annotations : 用来写自定义注解

apt-processor :用来写自定义注解处理器

apt-processor 添加依赖:

implementation project(path: ':apt-annotations')
// kotlin 文件生成库
implementation "org.jetbrains.kotlin:kotlin-stdlib:1.6.10"
implementation "com.squareup:kotlinpoet:1.8.0"
implementation "com.google.auto.service:auto-service:1.0"
kapt "com.google.auto.service:auto-service:1.0"

moudle_web 引入两个 apt 模块:

implementation project(path: ':apt-annotations')
kapt project(path: ':apt-processor')

注解

apt-annotations 中新建自定义注解:

@kotlin.annotation.Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.CLASS)
annotation class JsBridgeCommand(
    val name: String // 命令名
)

注解处理器

apt-processor 中新建自定义注解处理器 JsBridgeCommandProcessor, APT 不是重点,就简单贴一下部分代码,详情可以去 Demo 中查看:

@AutoService(Processor::class)
class JsBridgeCommandProcessor : AbstractProcessor() {
    // 省略其他代码...
    
    // 生成代码 路径
    val packageName = "com.sunhy.demo.apt"
    
    // 生成类方法
    val registerMethodBuilder = FunSpec.builder("autoRegist")
        .addComment("web jsbridge command auto load")
    
    // 定义局部变量
    val arrayMap = ClassName("android.util", "ArrayMap")
    val iBridgeCommand = ClassName("com.sunhy.demo.web.bridge", "IBridgeCommand")
    val arrayMapCommand = arrayMap.parameterizedBy(String::class.asTypeName(), iBridgeCommand)
    registerMethodBuilder.addStatement("val commandMap = %L()", arrayMapCommand)
    
    commandMap.forEach { (key, value) ->
        registerMethodBuilder.addStatement("commandMap[%S] = $value()", key)
    }
    
    // 方法返回类型
    registerMethodBuilder.returns(arrayMapCommand)
    registerMethodBuilder.addStatement("return commandMap")
    
    // 生成伴生对象
    val companionObject = TypeSpec.companionObjectBuilder()
        .addFunction(registerMethodBuilder.build())
        .build()
    
    // 生成类
    val clazzBuilder = TypeSpec.classBuilder("JsBridgeUtil")
        .addType(companionObject)
    
    //输出到文件...
    }
}

注解使用

给 ToastCommand 添加注解:

@JsBridgeCommand(name = "showToast")
class ToastCommand : IBridgeCommand {
    // ...
}

到这里先编译一下项目,让 APT 生成 JsBridgeUtil 文件:

Android 干货分享:WebView 优化(2)—— 桥接设计、独立进程、跨进程通信

文件生成成功后修改 BridgeCommandHandler 中 mCommandMap 初始化代码:

class BridgeCommandHandler {
    //    private val mCommandMap by lazy {
    //        val map = ArrayMap<String, IBridgeCommand>().apply {
    //            put("showToast", ToastCommand())
    //        }
    //        return@lazy map
    //    }
    // 之前初始化代码 替换为 自动注册
    private val mCommandMap: ArrayMap<String, IBridgeCommand> by lazy { JsBridgeUtil.autoRegist() }
}

运行测试:

Android 干货分享:WebView 优化(2)—— 桥接设计、独立进程、跨进程通信

后续在注册新桥接新建 Command 时只需要加上注解给予 name 命令名即可。

独立进程

WebView 独立进程,就是让 Web 相关功能单独使用一个进程。

优点:

  1. 进程隔离,Web 进程发生异常不会导致主进程闪退
  2. 分担主进程内存压力

缺点:

  1. Application 每个进程启动都会初始化,造成多次初始化
  2. 跨进程通信要注意的细节很多(静态成员变量问题、sharedpreferences操作等等)

虽然有一定的缺点,当 App 越做业务越繁杂,内存占用变高,让其中一些功能模块独立进程运行能够有效解决内存占用高的问题。

查看进程

命令:

adb shell ps -A |grep com.sunhy.demo
Android 干货分享:WebView 优化(2)—— 桥接设计、独立进程、跨进程通信

可以看出目前 Demo 只有一个进程。

实现 Web 进程

让 WebView 相关页面在一个新的进程运行非常简单,只需要在 AndroidManifest.xml 给对应的 Activity 添加 process :

<application>
    <activity
        android:name=".activity.NewsDetailActivity"
        android:exported="false" 
        android:process=":web"/> // 进程名
    <activity
        android:name=".activity.WebActivity"
        android:exported="false"
        android:process=":web"/> // 所有 WebView 相关页面都在 :web 进程中运行
</application>

运行 App 并且启动其中的 WebActivity 再次用命令行查看进程信息:

Android 干货分享:WebView 优化(2)—— 桥接设计、独立进程、跨进程通信

现在 Demo 已经有两个进程了,独立进程就已经实现了!

但是注意!!!多进程带来的缺点不能忽视,Application 进行了多次初始化,也就意味着之前在 Application 中写的 WebViewPool 初始化的代码初始化了两次,WebView 相关既然已经独立到新进程,那么主进程的 Application 还需要对 WebViewPool 初始化吗?

当然不需要!可以通过判断当前进程名字进行不同的初始化操作:

class BaseApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        when(ProcessUtils.getCurrentProcessName()){
            "com.sunhy.demo" -> {
                // 主进程初始化...
            }
            "com.sunhy.demo:web" -> {
                // :web 进程程初始化...
                initWebViewPool()
            }
        }
    }
}

Application 设计

上面这种写法呢并不友好,当进程变多,或者因业务逻辑修改需要修改各个进程初始化逻辑时,又是会发生频繁修改 BaseApplication 文件的问题,尤其多人协作大家都负责不同的模块同时修改一个文件难免冲突。

这里分享两个个人时间过的思路:

  1. 利用抽象工厂模式,BaseApplication 中仅做生产调用,各个进程具体初始化逻辑写在各自的“孵化器”中。
  2. 和上面自动注册桥接方法一样利用 APT 实现,无非是把 map 中的桥接名替换为进程名,根据进程名取出“孵化器”进行初始化操作。

这不是关于 WebView 的重点就不贴代码了,仅分享下思路,下面是最后的重头戏了。

跨进程通信

为什么要跨进程通信

思考一个问题,就以当前 Demo 来说,有一个登陆工具类:

object LoginUtils {

    private var userInfo: UserInfo? = null

    fun getUserInfo(): String{
        return GsonUtils.toJson(userInfo)
    }

    // 模拟登陆
    fun login(){
        this.userInfo = UserInfo("孙先森@", "ASDJKLQJDKL12KLDKL3KLJ1234KL12KLLDA")
    }
}

App 主进程初始化时进行登陆,那么在 web进程中通过调用 LoginUtils.getUserInfo() 能够获取到用户信息吗?

Android 干货分享:WebView 优化(2)—— 桥接设计、独立进程、跨进程通信

并不能,原因很简单,在上面 webview 独立进程后,用户登陆只在主进程进行,web 进程中对 LoginUtils 的调用属于一个全新的对象。

需要解决类似这种问题就要动手实现进程间通信,将主进程的 userInfo 传递给 web 进程。

实现

增加登陆命令

@JsBridgeCommand(name = "getUserInfo")
class UserInfoCommand : IBridgeCommand{
    override fun exec(params: JsonObject?, callback: IBridgeCallbackInterface?) {
        val userInfoJson = LoginUtils.getUserInfo()
        val key = getCallbackKey(params)
        if (!key.isNullOrEmpty()) {
            callback?.handleBridgeCallback(key, userInfoJson)
        }
    }
}

js 调用

function getUserInfo() {
    var params = {}
    window.jsBridge.sendCommand('getUserInfo', params, function(data){
       console.log('用户信息:', data)
       if(data){
           $('.user').text(data);
       }
   })
}

分析

以上述 getUserInfo 从 web 进程调用为例,目前的调用流程图:

Android 干货分享:WebView 优化(2)—— 桥接设计、独立进程、跨进程通信

黑色框代表 web 进程调用,红色框代表 main 进程调用。可以看出分发器执行分发命令后就要进行跨进程通信。

跨进程通信

基础工作整完,下一步就是实现跨进程通信。这里采用 Android 中的 Binder 来实现跨进程通信。首先新建 AIDL 文件:

// web 进程调用 主进程
// 也就是 js 调用 原生 桥接调用
// 表示web进程调用主进程
interface IBridgeInvokeMainProcess {
    void handleBridgeInvoke(String command, String params, IBridgeCallbackInterface bridgeCallback);
}

IBridgeCallbackInterface 之前已经定义为普通接口,主要用于处理桥接回调,那么同样改写为 AIDL 文件,Demo 没有删除 IBridgeCallbackInterface 文件为了避免命名冲突 AIDL 文件命名为 IBridgeInvokeWebProcess :

// 方法没有变化,由 IBridgeCallbackInterface 改为了 IBridgeInvokeWebProcess
interface IBridgeInvokeWebProcess {
    void handleBridgeCallback(String callback, String params);
}

先编译一下,没问题后接着修改 BridgeCommandHandler,它主要负责处理 js 调用原生方法,所以首先继承 IBridgeInvokeMainProcess.Stub(),对应修改其handleBridgeInvoke 方法参数:

class BridgeCommandHandler: IBridgeInvokeMainProcess.Stub() {
    // 省略代码...
    
    // 修改最后一参数类型
    override fun handleBridgeInvoke(command: String?, params: String?, bridgeCallback: IBridgeInvokeWebProcess?) {
        // 省略代码...
    }
}

接着修改 IBridgeCommand 中 exec 方法回调参数:

interface IBridgeCommand {
    // IBridgeCallbackInterface 改为 IBridgeInvokeMainProcess
    fun exec(params: JsonObject?, callback: IBridgeInvokeMainProcess?)
}

相关命令子类一并修改,这里就不贴了。

根据上述分析 BridgeCommandHandler 的调用在主进程,那么主进程需要一个 service 向外暴露:

class BridgeCommandService: Service() {
    override fun onBind(intent: Intent?): IBinder {
        return BridgeCommandHandler.getInstance()
    }
}
// AndroidManifest.xml 中注册
<service android:name=".service.BridgeCommandService"/>

下一步就轮到修改命令分发器JsBridgeInvokeDispatcher ,首先继承 ServiceConnection 并且提供绑定 service 方法:

class JsBridgeInvokeDispatcher : ServiceConnection{
private var iBridgeInvokeMainProcess: IBridgeInvokeMainProcess? = null
    //省略其他代码...
    // 获取 IBinder 对象
    fun bindService() {
        LogUtils.d(TAG, "bindService()")
        if (iBridgeInvokeMainProcess == null) {
            val i = Intent(BaseApplication.getInstance(), BridgeCommandService::class.java)
            BaseApplication.getInstance().bindService(i, this, Context.BIND_AUTO_CREATE)
        }
    }
    
    fun unbindService() {
        LogUtils.d(TAG, "unbindService()")
        iBridgeInvokeMainProcess = null
        BaseApplication.getInstance().unbindService(this)
    }
    
    override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
        iBridgeInvokeMainProcess = IBridgeInvokeMainProcess.Stub.asInterface(service)
    }
    
    override fun onServiceDisconnected(name: ComponentName?) {
        iBridgeInvokeMainProcess = null
    }
    
    // excuteCommand 方法修改
    // callback 改为 IBridgeInvokeWebProcess.Stub
    // 通过 iBridgeInvokeMainProcess 跨进程调用 handleBridgeInvoke
    private fun excuteCommand(view: BaseWebView, message: JsBridgeMessage?) {
        val callback = object : IBridgeInvokeWebProcess.Stub() {
            override fun handleBridgeCallback(callback: String, params: String) {
                LogUtils.e(TAG, "当前进程: ${ProcessUtils.getCurrentProcessName()}")
                view.postBridgeCallback(callback, params)
            }
        }
        if (iBridgeInvokeMainProcess != null){
            iBridgeInvokeMainProcess?.handleBridgeInvoke(message?.command, parseParams(message?.params), callback)
        }
    }
}

效果图

Android 干货分享:WebView 优化(2)—— 桥接设计、独立进程、跨进程通信

最后

Demo源码

源码地址:WebViewSimpleDemo

Demo源码均为手写,参考文献已在第一篇文末说明。

THE END

如果我的博客分享对你有点帮助,不妨点个赞支持下!

本网站是一个以CSS、JavaScript、Vue、HTML为核心的前端开发技术网站。我们致力于为广大前端开发者提供专业、全面、实用的前端开发知识和技术支持。 在本网站中,您可以学习到最新的前端开发技术,了解前端开发的最新趋势和最佳实践。我们提供丰富的教程和案例,让您可以快速掌握前端开发的核心技术和流程。 本网站还提供一系列实用的工具和插件,帮助您更加高效地进行前端开发工作。我们提供的工具和插件都经过精心设计和优化,可以帮助您节省时间和精力,提升开发效率。 除此之外,本网站还拥有一个活跃的社区,您可以在社区中与其他前端开发者交流技术、分享经验、解决问题。我们相信,社区的力量可以帮助您更好地成长和进步。 在本网站中,您可以找到您需要的一切前端开发资源,让您成为一名更加优秀的前端开发者。欢迎您加入我们的大家庭,一起探索前端开发的无限可能!