一、H5页面和安卓如何交互的
1.为什么会有h5和安卓的交互?
在手机app中,有时候需要在app中嵌入h5网页,能增加app的跨平台性,也就是相同的h5也可以嵌在ios平台。减少跨平台的开发成本。
同时,也能增强响应速度,减少内存消耗等优点。
2.如何交互?
主要在app中镶嵌webview的方式。webview可以看做一个内置浏览器,在webview中通过链接进入页面。
h5调移动端:在app中定义一个全局方法A,h5去调用。
移动端调h5:相同的,在h5 中定义一个全局方法B,然后app中调用。
一次完整的交互:由于h5调用app中的方法A没有返回值,需要app再主动调用一次h5的方法B,去拿app的返回值。
整个流程可以简化为:H5 --A()–> app --B()–> H5
app端代码:
前提:在安卓中引入webview。下面的代码在基于webview的框架下写的
通过移动端的@JavascriptInterface 在webview中定义一个全局的方法(window.xxx())。然后h5中直接通过window.xxx()去调用移动端方法。(代码1)
// 入参分别为service(需要被调用的服务名),data(调用服务的参数),funcIndex(回调方法的唯一标识)@JavascriptInterfacefun callMobile(service:String, data: String, funcIndex: String) {// 睡2秒,模拟被调用时的运行时间Thread.sleep(2000)println("调用")handler.post {// 调用js中定义的方法webview.loadUrl("javascript:callbackFun('$data', '$uuid')");}}
H5代码:
定一个公共方法:(代码2)
/*** 用于缓存回调方法,使用map形式,每个回调方法都有对应的key*/
const funcIndexMap = new Map();/*** 封装的用于调用安卓方法的通用性方法* @param service 需要被调用的服务名* @param data 调用服务的参数* @param func 回调方法,用于前端处理安卓的返回值*/
export const callLocal = (service: string,data: string,func: any
) => {// 添加一个loading框,用来演示页面渲染的结果Toast.loading({duration: 0,message: "加载中...",forbidClick: true,});// 添加回调函数到mapconst uuidNo: string = uuid();funcIndexMap.set(uuidNo, func);// 调用安卓if (/(Android)/i.test(navigator.userAgent)) {//判断是安卓用户的时候执行调用安卓中定义的方法window.android.callMobile("xxxx",data, uuidNo);}
};
在页面调用(我使用的时vue3+ts的框架),给页面一个按钮定义一个事件,让该事件去调用上面的公共方法,该事件的代码为:(代码3)
defaultOffice = 'qqqq'
const back = () => {defaultOffice = "start";// 调用方法callLocal("",defaultOffice + "123",(res:any) => {defaultOffice = res;},true);};// h5中定义的回调函数,用于被安卓调用
window.callbackFun = (data: string, uuidNo: string, inBack: boolean) => {const getFunc = funcIndexMap.get(uuidNo);getFunc(data);funcIndexMap.delete(uuidNo);
};
二、问题描述
1.场景
像上面那样运行的结果,将会是下面这样:
点击按钮后,页面没有变化

2秒后,才出现loading,而且对应的值被修改

并没有出现想象中的,点击按钮就出现loading,然后2秒后值被修改。
因为按照代码逻辑,我们想要的结果是:
-
OFFICE号初始化是qqqq
-
执行back() 函数,进入back后,首先会修改defaultOffice为start。此时页面上的OFFICE号显示为start
-
然后调用calLocal方法,进入此方法后,首先会加在loading
-
然后调用安卓的方法window.android.callMobile,等待安卓方法执行完后(2秒后执行完),会执行回调函数,将值改为over。
简化对比实际和预计的结果就是:
-
实际:点击 —> 2s后 —> loading —> 改为over
-
预计:点击 --> loading --> 2s后 —> 改为over
三、问题分析
先一句话概括:就是运行安卓的这个方法(window.android.callMobile)会阻塞js的执行
通过上述场景,我们发现,页面重新渲染是在安卓中的方法执行完成后。执行安卓方法的关键代码虽然就一句
window.android.callMobile("xxxx",data, uuidNo);,但是在它之前的代码被执行后并没有将页面渲染。即使修改了defaultOffice的值,页面也没有变化。只有在等到最后安卓的方法被执行后,页面才会被重新渲染。
这一现象启发了我,让我重温了js的执行原理。最终找到了真相。
最终原因是js是单线程的,它会等上一句代码解析执行完成后,才会执行下一行。具体原理可参考:https://www.jianshu.com/p/1368d375aa66。
我们这里就是因为js执行到window.android.callMobile时,在里面有2s的时间阻塞,导致整个callLocal方法不能执行完成,从而导致整个back方法不能执行完成,所以对于页面来说你这个back方法还没执行完,我的defaultOffice参数被改成什么了我也不知道,所以页面才没被重新渲染。
为什么我们平时在方法中修改变量不会有这个问题?
因为我们在平时的js方法中修改外部变量时,方法很快被执行完毕,我们感受不到,但如果我们像下面这样写,在defaultOffice被修改后,执行一个稍微耗时的for循环,就会发现页面上office的值会在点击事件后隔一段时间才被修改为start
const back = () => {defaultOffice = "start";for (let i = 1; i<10000; i++) {console.log("i:", i)}
};
四、问题解决
一句话:使用setTiimeout()
问题根本找到了,我们需要解决的就是不要让
window.android.callMobile方法的执行阻塞住我们的js线程,我们需要让它异步执行。
而根据setTimeout(fun,time)的原理:setTimeout和setInterval会将指定的代码移出本轮事件循环,得到下一轮事件循环,再检查是否到了指定时间,如果到了就执行对应代码,如果不到,就继续等待。所以它们都会等到本轮事件循环的所有同步任务都执行完,才开始执行。对于setTimeout来说,我们哪怕设置他的时间为0,它也会等到下一轮执行。
所以我们需要将callLocal方法修改为:
export const callLocal = (service: string,data: string,func: any
) => {Toast.loading({duration: 0,message: "加载中...",forbidClick: true,});const uuidNo: string = uuid();funcIndexMap.set(uuidNo, func);if (/(Android)/i.test(navigator.userAgent)) {//修改的地方setTimeout(() => {window.android.callMobile("xxxx",data, uuidNo);},0)}
};
修改之后即可完美实现。
五、优化同步异步调用
异步方法定义:
经过修改后我们使用
callLocal()方法就可以像使用普通的,但是在方法的定义中去添加回调函数显得比较乱,阅读性不强,我们稍稍进行一些优化,使用Promise将它的回调函数转移到then中。
/*** 异步调用安卓方法* @param service* @param data* @param inBack*/
const asyncCallLocal = (service: string,data: string,inBack: boolean
) => {return new Promise((resolve) => {console.log("进入promise");callLocal(service,data,(data: any) => {resolve(data);},inBack);});
};
页面使用:
const back = () => {defaultOffice = "start";// 使用asyncCallLocal("",defaultOffice + "123",true).then((res:any) => {defaultOffice = res})defaultOffice = "end"};
由于这是异步方法,如果像上面那样调用,defaultOffice显示的结果将会是:
start —> end —> start123
同步方法定义:
如果想要让他同步进行修改想让defaultOffice最后被改为end,则需要定义一个同步的方法,让上面的asyncCallLocal()被同步执行即可。
定义如下:
/*** 同步调用安卓* @param service* @param data* @param inBack*/
const syncCallLocal = async (service: string,data: string,inBack: boolean
) => {let result: any = null;await asyncCallLocal(service, data, inBack).then((res) => {result = res;});return result;
};
使用:
const back = async () => {defaultOffice = "start";personalData.defaultOffice = await syncCallLocal("",defaultOffice + "123",true);defaultOffice = await syncCallLocal("",defaultOffice + "aaa",true);};
执行结果:start —> start123 —> start123aaa

















