WebView 干货分享:独立进程、跨进程通信、桥接设计
本文作者
作者:孙先森Blog
链接:
https://juejin.cn/post/7143026094289977381
本文由作者授权发布。
前言
下面接着上篇博客Android 干货分享:WebView 优化(1)—— 缓存管理、回收复用、网页秒开、白屏检测 的 Demo 继续完善。
https://juejin.cn/post/7143025767268810759
普通调用
js 调用 App 方法
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()
}
}
<!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>
App 调用 js 方法
// 写正确 方法名 和 参数 即可
mWebView.evaluateJavascript("javascript:showToast('hello world')") {}
命令模式
通信双发都符合 请求方发出请求,要求执行某个操作;接收方收到请求,执行对应操作。 并且都符合单命令单接收者。 请求方和接收方可以独立开来,不用知道对方的命令接口。(经过封装后达成,调用同一方法实现不同命令) 降低耦合,新命令。(桥接方法)很容易加入到项目中(下面会结合 APT 实现)
一般桥接肯定要 Android iOS 同时实现,要考虑易用性。 新增桥接方法时不能频繁改动已有代码,低耦合。 支持回调,WebView 的 evaluateJavascript 方法支持回调,那么 js 调用 Android 方法也要支持回调。 桥接参数传递选择了 json 格式,增删参数方便。
桥接实现
流程图
App 提供发送命令桥接
data class JsBridgeMessage(
@SerializedName("command")
val command: String?, // 命令
@SerializedName("params")
val params: JsonObject?, // 参数
)
interface IBridgeCommand {
fun exec(params: JsonObject?)
}
class ToastCommand : IBridgeCommand {
override fun exec(params: JsonObject?) {
if (params != null && params["message"] != null) {
ToastUtils.showShort(params["message"].asString)
}
}
}
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?){
//...
}
}
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)
)
}
}
}
}
private fun excuteCommand(view: BaseWebView, message: JsBridgeMessage?){
BridgeCommandHandler.getInstance().handleBridgeInvoke(message.command, message.params)
}
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;
// 省略了无关代码 只展示了引入 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>
回调支持
流程图
Web 端实现
// 省略其他代码 只贴 新增 修改的代码
// 回调方法 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 端实现
data class JsBridgeMessage(
//省略...
@SerializedName("bridgeCallback")
val bridgeCallback: String? // 回调 key
)
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
}
}
interface IBridgeCommand {
fun exec(params: JsonObject?, callback: IBridgeCallbackInterface?)
}
fun handleBridgeInvoke(command: String?, params: String?, bridgeCallback: IBridgeCallbackInterface?) {
// ...
mCommandMap[command]!!.exec(
GsonUtils.fromJson(params, JsonObject::class.java),
bridgeCallback // 回调传递给 Command
)
}
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
)
}
}
class BaseWebView{
// 省略其他代码
fun postBridgeCallback(key: String?, data: String?) {
post {
evaluateJavascript("javascript:window.jsBridge.postBridgeCallback(`$key`, `$data`)") {}
}
}
}
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))
}
}
}
}
<script type='text/javascript'>
// ...
function showToastWithCallback() {
var params = {
'message': 'hello world'
}
window.jsBridge.sendCommand('showToast', params, function(data){
// 打印一下 回调中接受的参数
console.log('触发回调成功!data:', data)
})
}
</script>
利用 apt 自动注册桥接
注意
新建 Moudle
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"
implementation project(path: ':apt-annotations')
kapt project(path: ':apt-processor')
注解
@kotlin.annotation.Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.CLASS)
annotation class JsBridgeCommand(
val name: String // 命令名
)
注解处理器
@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)
//输出到文件...
}
}
注解使用
@JsBridgeCommand(name = "showToast")
class ToastCommand : IBridgeCommand {
// ...
}
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() }
}
后续在注册新桥接新建 Command 时只需要加上注解给予 name 命令名即可。
进程隔离,Web 进程发生异常不会导致主进程闪退。 分担主进程内存压力。
Application 每个进程启动都会初始化,造成多次初始化。 跨进程通信要注意的细节很多。(静态成员变量问题、sharedpreferences操作等等)
查看进程
adb shell ps -A |grep com.sunhy.demo
实现 Web 进程
<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>
class BaseApplication : Application() {
override fun onCreate() {
super.onCreate()
when(ProcessUtils.getCurrentProcessName()){
"com.sunhy.demo" -> {
// 主进程初始化...
}
"com.sunhy.demo:web" -> {
// :web 进程程初始化...
initWebViewPool()
}
}
}
}
Application 设计
利用抽象工厂模式,BaseApplication 中仅做生产调用,各个进程具体初始化逻辑写在各自的“孵化器”中。 和上面自动注册桥接方法一样利用 APT 实现,无非是把 map 中的桥接名替换为进程名,根据进程名取出“孵化器”进行初始化操作。
这不是关于 WebView 的重点就不贴代码了,仅分享下思路,下面是最后的重头戏了。
为什么要跨进程通信
object LoginUtils {
private var userInfo: UserInfo? = null
fun getUserInfo(): String{
return GsonUtils.toJson(userInfo)
}
// 模拟登陆
fun login(){
this.userInfo = UserInfo("孙先森@", "ASDJKLQJDKL12KLDKL3KLJ1234KL12KLLDA")
}
}
实现
增加登陆命令
@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);
}
})
}
分析
跨进程通信
// web 进程调用 主进程
// 也就是 js 调用 原生 桥接调用
// 表示web进程调用主进程
interface IBridgeInvokeMainProcess {
void handleBridgeInvoke(String command, String params, IBridgeCallbackInterface bridgeCallback);
}
// 方法没有变化,由 IBridgeCallbackInterface 改为了 IBridgeInvokeWebProcess
interface IBridgeInvokeWebProcess {
void handleBridgeCallback(String callback, String params);
}
class BridgeCommandHandler: IBridgeInvokeMainProcess.Stub() {
// 省略代码...
// 修改最后一参数类型
override fun handleBridgeInvoke(command: String?, params: String?, bridgeCallback: IBridgeInvokeWebProcess?) {
// 省略代码...
}
}
interface IBridgeCommand {
// IBridgeCallbackInterface 改为 IBridgeInvokeMainProcess
fun exec(params: JsonObject?, callback: IBridgeInvokeMainProcess?)
}
class BridgeCommandService: Service() {
override fun onBind(intent: Intent?): IBinder {
return BridgeCommandHandler.getInstance()
}
}
// AndroidManifest.xml 中注册
<service android:name=".service.BridgeCommandService"/>
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)
}
}
}
效果图
最后
Demo源码
源码地址:WebViewSimpleDemo
https://github.com/RDSunhy/WebViewSimpleDemo
如果我的博客分享对你有点帮助,不妨点个赞支持下!
最后推荐一下我做的网站,玩Android: wanandroid.com ,包含详尽的知识体系、好用的工具,还有本公众号文章合集,欢迎体验和收藏!
推荐阅读:
扫一扫 关注我的公众号
如果你想要跟大家分享你的文章,欢迎投稿~
┏(^0^)┛明天见!