一、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.场景

像上面那样运行的结果,将会是下面这样:

点击按钮后,页面没有变化

android app与h5交互 安卓与h5交互_js

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

android app与h5交互 安卓与h5交互_es6_02


并没有出现想象中的,点击按钮就出现loading,然后2秒后值被修改。

因为按照代码逻辑,我们想要的结果是:

  1. OFFICE号初始化是qqqq
  2. 执行back() 函数,进入back后,首先会修改defaultOffice为start。此时页面上的OFFICE号显示为start
  3. 然后调用calLocal方法,进入此方法后,首先会加在loading
  4. 然后调用安卓的方法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