在上一节Android进阶宝典 – 从0到1搭建高效webview框架中,介绍了webview的基础使用场景,搭建的基础的webview框架,那么如何将我们的框架做的高效、可靠、易扩展,在本章就会着重介绍。

1 Android与JS通信

因为webview很复杂,不是像我们简单地加载一个url就能显示网页,而且能展示的网页参差不齐,网页崩溃的可能性很高,那么如何做到一个高可靠的webview框架?

1 内存限制:如果熟悉Binder底层的伙伴会了解,系统分配给每个app的进程内存是有限的,况且每个webview占用的内存会有几十兆

2 独立进程:选择将webview独立到单独的一个进程,即便是网页崩溃了,但并不影响app进程崩溃

首先是否需要跨进程,得看具体的场景,如果在原生页面中某处使用了webview,那么就没有必要单独起一个进程,跨进程实现的成本太高了;如果整个独立的页面都是webview,而且打开的频率很高,那么就建议将这个页面单独起一个进程处理

单独起一个进程是非常简单的,四大组件都支持process属性,当设置WebViewActivity进程名为myweb并启动,我们可以看到已经有一个独立的进程。

如果要涉及到跨进程通信,百度网页显然我们是通信不了的,那么就需要一个本地的html,在service中添加一个打开本地网页的路由

/** * 打开本地的html */fun startLocalHtml(context: Context)

1.1 单独进程内Android与JS通信

看下面这张图

在单独进程内,WebViewActivity嵌入一个WebView,在webview中加载html,真正与原神交互的就是script,如果你看过js的代码,就会看到一个 script 标签,其中就是主要的代码逻辑

假设这里有一个网页,有一个按钮,点击之后,需要调用Android端的方法弹出一个吐司,这里面就涉及到了H5和原生的交互

<div class="item" style="font-size: 20px; color: #ffffff" onclick="callAppToast()">点击</div>
<script>function callAppToast(){console.log("callAppToast.");</script>

js的代码其实大概能够看懂,其实跟Android的没啥区别,有一个按钮点击事件是callAppToast,然后在script里实现这个方法,打印了一行日志,那我想看js中打印的日志,该怎么查看

在上一节中介绍过,WebChromClient是真正跟js交互的,所以这里会有一些回调的方法能够看到js端打印的日志,就是onConsoleMessage方法

override fun onConsoleMessage(consoleMessage: ConsoleMessage" />): Boolean {Log.e("TAG", "${consoleMessage?.message()}")return super.onConsoleMessage(consoleMessage)}

当点击按钮时,打印的日志就可以在控制台查看

接下来就是H5调用原生的方法弹出吐司,两者之间的桥梁就是JavaScriptInterface,所以只有一个html网页是万万不行的,还需要注入一个js文件

var layjs = {};layjs.os = {};layjs.os.isIOS = /iOS|iPhone|iPad|iPod/i.test(navigator.userAgent);layjs.os.isAndroid = !layjs.os.isIOS;window.layjs = layjs;

其实js文件就是来区分平台,定义与原生交互的规则,因为不管是Android还是ios都会与h5交互,但是两者的交互方式是有区别的,因此通过isIOS isAndroid区分,我们主要看Android,如果需要与Android交互,那么就引入这个js文件;

<script src="js/lay.js" charset="utf-8"></script>

1.2 JavaScriptInterface

在上一节中,我们对于webview的配置都是在WebFragment中,其实后续的js函数注入等,UI层其实是不会去关心这些的,应该是webview内核层去做的事情,因此可以抽出单独的一个WebView组件来处理

class BaseWebView : WebView{constructor(context: Context):super(context){init(context)}constructor(context: Context,attributeSet: AttributeSet):super(context,attributeSet){init(context)}private fun init(context: Context) {WebViewDefaultSettings.getInstance().setSettings(this)}fun registerWebViewCallback(callback: IWebViewCallback){webViewClient = MyWebViewClient(callback)webChromeClient = MyWebChromeClient(callback)}}

这里就可以注入与js交互的函数,调用addJavascriptInterface,第一个参数就是JavascriptInterface接口所在的类,第二个参数就是对象名,然后callAndroidAction就是方法名

class BaseWebView : WebView{constructor(context: Context):super(context){init(context)}constructor(context: Context,attributeSet: AttributeSet):super(context,attributeSet){init(context)}@SuppressLint("JavascriptInterface")private fun init(context: Context) {WebViewDefaultSettings.getInstance().setSettings(this)addJavascriptInterface(this,"lay")}fun registerWebViewCallback(callback: IWebViewCallback){webViewClient = MyWebViewClient(callback)webChromeClient = MyWebChromeClient(callback)}@JavascriptInterfacefun callAndroidAction(msg:String){}}

拉回到之前的js文件,如果想要调用callAndroidAction方法,就是通过window.lay.callAndroidAction的方式调用,传入的值就是callAndroidAction中接收的参数

var layjs = {};layjs.os = {};layjs.os.isIOS = /iOS|iPhone|iPad|iPod/i.test(navigator.userAgent);layjs.os.isAndroid = !layjs.os.isIOS;layjs.callAndroidAction = function(commandname, parameters){console.log("lay takenativeaction")var request = {};request.name = commandname;request.param = parameters;if(window.layjs.os.isAndroid){console.log("android take native action" + JSON.stringify(request));window.lay.callAndroidAction(JSON.stringify(request));} else {window.webkit.messageHandlers.lay.postMessage(JSON.stringify(request))}}window.layjs = layjs;

所以在js中定义了一个方法,这个方法会传入两个参数,一个就是commandname,命令(因为不止一个弹toast命令,可能还会有其他的),另一个就是传入的参数,然后将传入的参数拼接为json字符串传给Android。

<script>function callAppToast(){console.log("callAppToast.");layjs.callAndroidAction("showToast", {message: "this is a message from html."});}</script>

在按钮的点击事件中,调用这个js方法,在Android端就接收到了返回值\

callAndroidAction -- {"name":"showToast","param":{"message":"this is a message from html."}}

将接受到的参数解析出来之后,根据命令来进行相应的操作

@JavascriptInterfacefun callAndroidAction(msg: String) {Log.e("TAG", "callAndroidAction --$msg")if (!TextUtils.isEmpty(msg)) {val jsBean = Gson().fromJson(msg, JSBean::class.java)if (jsBean.name == "showToast") {jsBean.param.message" />.let {Toast.makeText(context, it, Toast.LENGTH_SHORT).show()}}}}

2 跨进程通信方案

在上一小节中,简单介绍了进程内Android与H5的交互,但是webview毕竟是一个组件,如果在JavascriptInterface接口中,通过判断命令来进行对应的操作,显然是违背了开闭原则;因此当webview接受到命令后,应该往外抛到主线程,由主线程来执行这些操作,这其中就涉及到了跨进程的通信。

2.1 aidl跨进程通信

跨进程的方式其实有多种,现阶段常用的就是aidl

interface IWebprocessToMainprocessInterface { void handleCommend(in String commandName,in String jsonParams);}

定义一个aidl接口,作用就是向外抛出命令,已经回调的数据;那么既然有了接口,那么就需要一个命令管理器,继承了Stub类

class MainProcessCommandManager private constructor() : IWebprocessToMainprocessInterface.Stub() {override fun handleCommend(commandName: String?, jsonParams: String?) {}companion object {private var mainProcessCommandManager: MainProcessCommandManager? = nullfun getInstance(): MainProcessCommandManager {if (mainProcessCommandManager == null) {synchronized(this) {if (mainProcessCommandManager == null) {mainProcessCommandManager = MainProcessCommandManager()}}}return mainProcessCommandManager!!}}}

当web进程启动之后,需要启动一个服务,主进程可以绑定这个服务接收命令的发送

class MainProcessService : Service() {override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {return super.onStartCommand(intent, flags, startId)}override fun onBind(intent: Intent?): IBinder? {return MainProcessCommandManager.getInstance()}}
class WebProcessCommandDispatchers private constructor() : ServiceConnection {private var iWebprocessToMainprocessInterface: IWebprocessToMainprocessInterface? = nullprivate var context: Context? = null//启动服务fun initAidlConnection(context: Context) {this.context = contextval intent = Intent(context, MainProcessService::class.java)context.bindService(intent, this, Context.BIND_AUTO_CREATE)} //执行命令fun executeCommand(commandName: String, jsonParams: String) {iWebprocessToMainprocessInterface?.handleCommend(commandName, jsonParams)}companion object {@SuppressLint("StaticFieldLeak")private var mainProcessCommandManager: WebProcessCommandDispatchers? = nullfun getInstance(): WebProcessCommandDispatchers {if (mainProcessCommandManager == null) {synchronized(this) {if (mainProcessCommandManager == null) {mainProcessCommandManager = WebProcessCommandDispatchers()}}}return mainProcessCommandManager!!}}override fun onServiceConnected(name: ComponentName?, service: IBinder?) {iWebprocessToMainprocessInterface =IWebprocessToMainprocessInterface.Stub.asInterface(service)}override fun onServiceDisconnected(name: ComponentName?) {iWebprocessToMainprocessInterface = null//重新连接context?.let {initAidlConnection(it)}}}

在webview初始化的时候,就开启这个服务

@SuppressLint("JavascriptInterface")private fun init(context: Context) {WebViewDefaultSettings.getInstance().setSettings(this)addJavascriptInterface(this, "lay")//启动服务WebProcessCommandDispatchers.getInstance().initAidlConnection(context)}

当我们再次点击按钮的时候,调用了WebProcessCommandDispatchers的executeCommand方法,最终在
MainProcessCommandManager主进程命令管理器中接收到了回调,这也意味着服务已经通了。

fun callAndroidAction(msg: String) {Log.e("TAG", "callAndroidAction --$msg")if (!TextUtils.isEmpty(msg)) {val jsBean = Gson().fromJson(msg, JSBean::class.java)if (jsBean.name == "showToast") {jsBean.param.message?.let {Toast.makeText(context, it, Toast.LENGTH_SHORT).show()}WebProcessCommandDispatchers.getInstance().executeCommand(jsBean.name,Gson().toJson(jsBean.param))}}}

到这一步,我们再次回到前面的话题,对于js端命令的发送,在webview接收没有问题,但是不能在webview处理,作为一个组件,处理业务逻辑是不对的,而且应该是跟业务解耦的,所以,我们前面看到的在JavascriptInterface弹吐司是错误的,所以跨进程的目的就是webview接受到命令后抛出给主线程

2.2 命令模式

主线程用于接收命令,那么命令的种类有很多,如果通过if – else的方式来判断,耦合度太高,而且不易于扩展,如果某个命令发生变化,需要抠一部分代码修改,很麻烦,因此采用一种命令设计模式

interface Command {var commandName:Stringfun execute(json:String)}

在web组件层定义一个Command接口,其中commandName代表命令的名称,execute方法用于执行命令;

在主进程中实现这个接口,execute用来实现之前在JavascriptInterface中的操作

@AutoService(Command::class)class ShowToastCommand : Command {override var commandName: String = "showToast"override fun execute(json: String) {val map = Gson().fromJson(json, Map::class.java)map?.let {val message = it.get("message").toString()Toast.makeText(MyApp.context, message, Toast.LENGTH_SHORT).show()}}}

记不记得我们之前使用过AutoService,一看到接口的实现类就一定要想到它,基础的配置可以看看之前的文章

/** * 主进程命令管理器 */class MainProcessCommandManager : IWebprocessToMainprocessInterface.Stub() {private val map: MutableMap<String, Command> by lazy {mutableMapOf()}init {val iterator = ServiceLoader.load(Command::class.java).iterator()if (iterator.hasNext()) {//获取所有的实现类val command = iterator.next()//注册if(!map.containsKey(command.commandName)){map[command.commandName] = command}}}override fun handleCommend(commandName: String?, jsonParams: String?) {if(map.containsKey(commandName)){jsonParams?.let {map[commandName]?.execute(it)}}}companion object {private var mainProcessCommandManager: MainProcessCommandManager? = nullfun getInstance(): MainProcessCommandManager {if (mainProcessCommandManager == null) {synchronized(this) {if (mainProcessCommandManager == null) {mainProcessCommandManager = MainProcessCommandManager()}}}return mainProcessCommandManager!!}}}

因此在MainProcessCommandManager初始化的时候,就拿到所有的命令的实现类,注册到一个map中,当命令回调过来的时候,就判断这个命令是不是注册过,如果注册过,就执行相应的操作

其实讲到这里,还只是在进程内进行通信,但是进程间通信的雏形已经产生,具体的场景像在登录之后,将用户信息返给JS,这就涉及到了进程间的通信,将会在下一小节中介绍,拜拜~

附录流程图