Android进阶宝典 -- 从0到1搭建高效webview框架2

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

1 Android与JS通信

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

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

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

首先是否需要跨进程,得看具体的场景,如果在原生页面中某处使用了webview,那么就没有必要单独起一个进程,跨进程实现的成本太高了;如果整个独立的页面都是webview,而且打开的频率很高,那么就建议将这个页面单独起一个进程处理
Android进阶宝典 -- 从0到1搭建高效webview框架2_第1张图片

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

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

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

1.1 单独进程内Android与JS通信

看下面这张图
Android进阶宝典 -- 从0到1搭建高效webview框架2_第2张图片

在单独进程内,WebViewActivity嵌入一个WebView,在webview中加载html,真正与原神交互的就是script,如果你看过js的代码,就会看到一个 script 标签,其中就是主要的代码逻辑
Android进阶宝典 -- 从0到1搭建高效webview框架2_第3张图片

假设这里有一个网页,有一个按钮,点击之后,需要调用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)
    }

    @JavascriptInterface
    fun 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."}}

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

@JavascriptInterface
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()
            }
        }
    }
}

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? = null
        fun 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? = null
    private var context: Context? = null

    //启动服务
    fun initAidlConnection(context: Context) {
        this.context = context

        val 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? = null
        fun 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:String
    fun 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? = null
        fun getInstance(): MainProcessCommandManager {
            if (mainProcessCommandManager == null) {
                synchronized(this) {
                    if (mainProcessCommandManager == null) {
                        mainProcessCommandManager = MainProcessCommandManager()
                    }
                }
            }
            return mainProcessCommandManager!!
        }
    }
}

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

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

附录流程图

Android进阶宝典 -- 从0到1搭建高效webview框架2_第4张图片

你可能感兴趣的:(技术,android,webview,java,架构,kotlin)