当前位置 : 主页 > 手机开发 > 其它 >

Hybrid优化之两个WebView中的H5通信

来源:互联网 收集:自由互联 发布时间:2021-06-12
0.前言 前段时间,我的一个ios同事兼大学同学,提出了一个Hybrid开发中的优化方案,人很帅,可能是我见过最帅的程序猿了,下次有机会po他的照片。那么这个优化方案是什么呢?待会

0.前言

前段时间,我的一个ios同事兼大学同学,提出了一个Hybrid开发中的优化方案,人很帅,可能是我见过最帅的程序猿了,下次有机会po他的照片。那么这个优化方案是什么呢?待会再说吧,其实我想表达的是Android和ios同事应该多沟通,其实很多设计思想都很像,可以相互借鉴对方一些优秀的点,就像现在新出的Android机和苹果机也越来越像了。

1.需求场景

就像下图(以Android为例,IOS把Activity替换成ViewControll),在Activity A中的H5 A里打开一个新的网页H5 B,是使用启动了一个Activity B,然后用其中的Webview B去加载H5 B,当用户在H5 B中进行了某些操作,需要让H5 A知道时,就会很麻烦,因为它们是被两个不同的Webview所加载。最常见的例子就是登录、支付了,用户在B页面完成登录或支付行为,返回到A时,希望H5 A刷新。这篇文章,就是用于解决这个问题:实现Hybrid中两个WebView中H5的通信。
这里写图片描述
额外说明:
1. 打开新的H5,采用了启动新的Activity B去加载,是为了提升用户的体验感,避免出现用户回退时,H5页面刷新的问题。
2. H5里采用document.addEventListener(‘visibilitychange’,function(){})是可以做到监听回到H5 A的事件,但是有些手机会无效,比如:华为KIW-AL10、魅族MX4
3. 以上方式解决的,不限于两个相邻的H5,也不限于1:1的更新,可以支持1:N的刷新,只要保证注册的名字一致,后面会讲到。

2.实现原理

实现原理是依赖于加载H5的WebView之间的通信,借助了观察者模式的思想。这里需要分两步走:
1. H5 A触发WebView A订阅某类事件,以便H5 B发布该类事件时,能够接受到。
这里写图片描述
2. H5 B发布该类事件,触发WebView A收到消息,并调用H5 A中Js方法。
这里写图片描述
额外说明:
1. 这里的订阅和发布,用安卓中的广播可以实现,但是考虑其比较“重”,这里使用Handler来实现广播功能。原理如下:为每一个WebView创建了一个Handler对象,并创建了一个全局的Handler集合,用于保存WebView的Handler对象,当初始化WebView时,把Handler放入集合中,当WebView销毁,即调用了其onDetachedFromWindow方法时,把Handler从集合中移除。当WebView A订阅消息时,把订阅名记下来,作为当前WebView对象的属性;当WebView B发布消息时,遍历全局的Handler结合,逐个发送消息,该消息中携带了订阅名、回调方法名、参数等信息,然后在WebView中Handler的handleMessage方法中,对比当前WebView的订阅消息,是否包含WebView B发布的订阅名。如果包含,则用消息中包含的回调方法名和参数,调用当前WebView的js方法,这样就完全了通信。(文字看着晕的话,我们待会举个栗子look look!
2. 因为采用了Handler来实现广播,所以WebView A不能立即订阅H5 A传递过来的订阅名,这时候就要把该订阅名保存下来,这时候当WebView B需要发布消息的时候,msg.what可以指定一个特定的值,并用msg.obj携带订阅名等信息。WebView A中的handler只要去case到那个特定的值,然后判断msg.obj中是否与WebView中之前保存的订阅名是否匹配就可以了。
3. 订阅和发布的协议格式:
H5 A 订阅:协议名/register/订阅名
H5 B 发布:协议名/trigger/订阅名/回调方法名/参数1/参数2/参数N
以登录为例子:
订阅:josan://register/login
发布:josan://trigger/login/logined/张三/24
这里写图片描述

3.实现效果

当我们把App里的逻辑实现了以后,看看两个H5通信有多简单,只需要两步:
1.在需要刷新的H5页面完成订阅,并实现回调的Js方法

<script> function openH5B() { location.href = 'josan://register/login'; } function logined(name, age) { document.getElementById("login").innerHTML = name + ',年龄:' + age; document.getElementById("login").style.backgroundColor= "green"; } </script>

2.在另一个触发刷新的H5页面,通过协议发布事件

function triggerLogined(){
          location.href = 'josan://trigger/login/logined/张三/24';
    }

这样就会调用到前一个需要刷新H5的logined方法,并把张三和24作为参数传递过去。是不是很简单呢!

4.实现细节

1. H5 A触发WebView A订阅

这个页面很简单,先看第17行,这里点击进入H5 B时,触发了第9行的openH5B()方法,方法里很简单,第一行是通过协议去打开Activity B,不用关心,重点是关注第9行,这里其实就是调用了订阅的协议,其中login为订阅名。

<!DOCTYPE html>
<html>
<head>
    <meta charset=utf-8>
    <script> function openH5B() { location.href = 'josan://openSecondActivity/'; setTimeout(function () { location.href = 'josan://register/login'; }, 0); } </script>
</head>
<body>
    <div id="login" style="font-size: 30px;text-align:center;background-color: red" onclick="registerLogin()">未登录</div>
    <div style="font-size: 40px;text-align:center" onclick="openH5B()">进入H5 B</div>
</body>
</html>

接下来,会触发WebView A的WebViewClient中的shouldOverrideUrlLoading方法,在这里面,我们就要完成WebView A的订阅,我们先来看看客户端WebView的实现。

2. WebView A订阅

我们自定义了一个MyWebView,继承于WebView

public class MyWebView extends WebView{
    private static final String TAG = "MyWebView";
    /** 网页需要调用触发协议时,发送该消息 **/
    public static final int TRIGGER_INTERACTIVE = 1000;
    private List<String> registerKeyArr;
    private Context mContext;

    private MyHandler mHandler;

    public MyWebView(Context context) {
        this(context, null);
    }

    public MyWebView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MyWebView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        //初始化
        init();
    }

    /** * 初始化操作 */
    private void init() {
        mContext = this.getContext();
        //创建一个订阅名的数组,用户保存H5注册时的注册名
        registerKeyArr = new ArrayList<>();
        //允许使用Js
        getSettings().setJavaScriptEnabled(true);
        //创建一个Handler
        mHandler = new MyHandler(this);
        //将创建的handler加入到集合中,以便实现广播
        MessageUtils.getInstance().registerHandler(this.hashCode()+"", mHandler);
        //设置WebViewClient
        setWebViewClient(new WebViewClient(){
            @Override
            public boolean shouldOverrideUrlLoading(WebView view, String url) {
                Log.e(TAG, "shouldOverrideUrlLoading: url:" + url);
                //如果网页调用了注册协议,比如:josan://register/login
                if (url.contains("josan://register/")) {    //订阅协议,即H5 A中触发
                    //这里取出login,作为群发消息的key
                    String registerName = url.replace("josan://register/", "");
                    if (!registerKeyArr.contains(registerName)) {
                        //存入到当前WebView的订阅名集合中
                        registerKeyArr.add(registerName);
                    }
                } else if (url.contains("josan://trigger/")) {  //发布协议,即H5 B中触发
                    //如果网页调用了触发协议,比如:josan://trigger/login/logined
                    String triggerNameAndCallback = url.replace("josan://trigger/", "");
                    MessageUtils.getInstance().sendOverallMessage(TRIGGER_INTERACTIVE, triggerNameAndCallback);
                } else if (url.contains("josan://openSecondActivity/")){
                    //用协议启动第二个Activity
                    Intent intent = new Intent(mContext, SecondActivity.class);
                    mContext.startActivity(intent);
                } else {
                    view.loadUrl(url);
                }
                return true;
            }
        });

    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        //反注册handler
        MessageUtils.getInstance().unregisterHandler(this.hashCode()+"");
    }
}

这里主要关注的是第22行,在这个构造方法里,调用了init方法,去进行了初始化,我们重点关注这个方法。注释写的很详细了,就不做过多解释了,主要关注shouldOverrideUrlLoading方法里的内容,当我们在H5 A中执行了location.href = ‘josan://register/login’;时,就会触发WebView A的WebViewClient对象的这个方法,我们在第44行用josan://register来拦截该请求,然后取出订阅名,即login,并把它存入到当前WebView A的订阅名的集合中,这样就完成了H5 A中的订阅步骤。

3. H5 B触发WebView B发布协议

<!DOCTYPE html>
<html>
<head>
    <meta charset=utf-8>
    <script> function triggerLogined(){ location.href = 'josan://trigger/login/logined/张三/24'; } </script>
</head>
<body>
    <div style="font-size: 40px;text-align:center" onclick="triggerLogined()">点击登陆</div>
</body>
</html>

这里代码很简单,就是第7行,执行location.href = ‘josan://trigger/login/logined/张三/24’;去触发WebView B发布login事件,其中login为订阅名,loinged为回调方法名(是H5 A中的Js方法),张三/24为参数,这个时候,我们再看看本地WebView拦截到josan://trigger/login/logined/张三/24会执行什么操作。

4. WebView B发布事件

还是回到自定义的WebVie中的shouldOverrideUrlLoading方法

//设置WebViewClient
        setWebViewClient(new WebViewClient(){
            @Override
            public boolean shouldOverrideUrlLoading(WebView view, String url) {
                Log.e(TAG, "shouldOverrideUrlLoading: url:" + url);
                //如果网页调用了注册协议,比如:josan://register/login
                if (url.contains("josan://register/")) {    //订阅协议,即H5 A中触发
                    //这里取出login,作为群发消息的key
                    String registerName = url.replace("josan://register/", "");
                    if (!registerKeyArr.contains(registerName)) {
                        //存入到当前WebView的订阅名集合中
                        registerKeyArr.add(registerName);
                    }
                } else if (url.contains("josan://trigger/")) {  //发布协议,即H5 B中触发
                    //如果网页调用了触发协议,比如:josan://trigger/login/logined
                    String triggerNameAndCallback = url.replace("josan://trigger/", "");
                    MessageUtils.getInstance().sendOverallMessage(TRIGGER_INTERACTIVE, triggerNameAndCallback);
                } else if (url.contains("josan://openSecondActivity/")){
                    //用协议启动第二个Activity
                    Intent intent = new Intent(mContext, SecondActivity.class);
                    mContext.startActivity(intent);
                } else {
                    view.loadUrl(url);
                }
                return true;
            }
        });

直接看第14行,当WebView B拦截到josan://trigger/login/logined/张三/24时,会先取出其中的订阅名、回调方法名以及参数,即logined/张三/24,然后调用MessageUtils中的sendOverallMessage方法实现广播功能,通知WebView A中的Handler,我们来看看消息是如何实现的。

5. Handler实现广播

public class MessageUtils {

    private static Map<String, Handler> sHandlerMap;
    private volatile static MessageUtils instance;

    private MessageUtils() {
        if (sHandlerMap == null) {
            sHandlerMap = new HashMap<>();
        }
    }

    public static MessageUtils getInstance() {
        if (instance == null ) {
            synchronized (MessageUtils.class) {
                if (instance == null) {
                    instance = new MessageUtils();
                }
            }
        }
        return instance;
    }

    /** * 注册handler * @param key 注册handler使用的key * @param handler */
    public void registerHandler(String key, Handler handler) {
        sHandlerMap.put(key, handler);
    }

    /** * 反注册handler * @param key */
    public void unregisterHandler(String key) {
        if (sHandlerMap.containsKey(key)) {
            sHandlerMap.remove(key);
        }

    }

    /** * 发送全局的消息,实现类似广播的机制 */
    public void sendOverallMessage(int what, Object obj) {
        Set<Map.Entry<String, Handler>> set = sHandlerMap.entrySet();
        for (Map.Entry<String, Handler> entry : set) {
            Handler handler = entry.getValue();
            Message msg = Message.obtain();
            msg.what = what;
            msg.obj = obj;
            handler.sendMessage(msg);
        }
    }
}

这就是一个消息通信的管理类,其中用Map存了所有WebView的Handler对象,是在WebView的init方法中存入的,在WebView的onDetachedFromWindow方法中移除的。广播机制的原理很简单,直接看第48行,其实就是遍历注册到Map中的所有Handler,然后逐个给handler发消息,其中的消息包含了订阅名、回调方法和参数,这里的obj就是”logined/张三/24”。

6. 回调H5 A中的Js方法

因为WebView A和WebView B中的handler都注册了,所以,这两个handler都会收到该消息,这时候就需要通过步骤2中存下来的订阅名来区分,判断前WebView的Handler对象是否订阅了该消息,我们看看Handler中handleMessage方法中的逻辑,它是MyWebView中的一个静态内部类。

private static class MyHandler extends Handler {
        private WeakReference<WebView> weakWebView;

        public MyHandler(WebView webView) {
            weakWebView = new WeakReference<WebView>(webView);
        }

        @Override
        public void handleMessage(Message msg) {
            try {
                super.handleMessage(msg);
                switch (msg.what) {
                    case TRIGGER_INTERACTIVE:   //网页调用触发协议时,格式为: registerName/callbackName/params1/param2/param...
                        String triggerNameAndCallback = (String) msg.obj;
                        String[] nameAndCallbackArr = triggerNameAndCallback.split("/");
                        if (nameAndCallbackArr.length >= 2) {
                            String triggerName = nameAndCallbackArr[0];
                            String triggerCall = nameAndCallbackArr[1];
                            MyWebView webview = (MyWebView) weakWebView.get();
                            if (webview != null && webview.registerKeyArr.contains(triggerName)) {
                                String callbackUrl = "javascript:" + triggerCall + "(";
                                for (int i = 2; i < nameAndCallbackArr.length; i++) {
                                    callbackUrl += "'" + nameAndCallbackArr[i] + "',";
                                }
                                if (nameAndCallbackArr.length > 2) {
                                    callbackUrl = callbackUrl.substring(0, callbackUrl.length() - 1);
                                }
                                callbackUrl += ")";
                                Log.e(TAG, "callbackUrl:" + callbackUrl);
                                webview.loadUrl(callbackUrl);
                            }
                        }
                        break;
                    default:
                        break;
                }

            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

逻辑很简单,就是先从广播中取出订阅名,这里为login,然后判断当前WebView中的订阅名集合registerKeyArr中是否包含了当前发布的订阅名,显然WebView A中包含,而WebView B中不包含。所以只有WebView A中的handler会满足第20行if条件,继续执行。然后根据回调名和参数(参数可以支持多个,只要跟H5 A中定义的Js方法保持一致)来构造出回调的url,这里其实就是:javascript:logined(‘张三’,‘24‘),最后通过webview.loadUrl方法,触发H5 A 中的logined方法

7. H5 A中定义回调方法

/** * 登录成功的回调方法,用于test2.html中通过josan://trigger/login/logined触发 */
  function logined(name, age) {
      document.getElementById("login").innerHTML = name + ',年龄:' + age;
      document.getElementById("login").style.backgroundColor= "green";
  }

这里就是响应WebView A调用的loadUrl,取出其中的名字和年龄现实出来,并把按钮的背景色由原来的红色改成绿色。
这样就完成了H5 B登陆以后,刷新了H5 A页面的数据。
下面是上述实现细节的流程图:
这里写图片描述

源码地址

https://gitee.com/josan24/WebView_Interactive.git

网友评论