在上一节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)
}
@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,这就涉及到了进程间的通信,将会在下一小节中介绍,拜拜~
附录流程图