一、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(回调方法的唯一标识)
@JavascriptInterface
fun 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,
});
// 添加回调函数到map
const 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