Android与WebView的同步和异步访问机制

大家都知道,通过WebView,我们可以在Android客户端,用Web开发的方式来开发我们的应用。

如果一个应用就是单纯一个WebView,所有的逻辑都只需要在网页上交互的话,那我们其实就只需要通过html和javascript来跟服务器交互就可以了。

但是很多情况下,我们的应用不是单纯一个WebView就可以了,有可能会需要运用到Android本身的应用,比如拍照,就需要调用Android本身的照像机等,要产生震动,在需要运用到手机特性的一些场景下,肯定需要这么一套机制在javascript和Android之间互相通信,包括同步和异步的方式,而这套机制就是本文中我想要介绍的。

一步一步来,我们先从最简单的地方讲起:

1)需要一个WebView去展现我们的页面,首先定义一个布局,非常简单,就是一个WebView,如下:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width= "match_parent"
    android:layout_height= "match_parent"
    android:orientation= "vertical">

    <WebView
        android:id="@+id/html5_webview"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</LinearLayout>

这个WebView就是承载我们页面展现的一个最基本的控件,所有在页面上的逻辑,需要跟Android原生环境交互的逻辑数据都是通过它来传输的。

2)在对应的Activity中,对WebView进行一些初始化

mWebView = (WebView) findViewById(R.id. html5_webview );
WebSettings webSettings = mWebView.getSettings();
webSettings.setJavaScriptCanOpenWindowsAutomatically( true );
webSettings.setJavaScriptEnabled( true );
webSettings.setLayoutAlgorithm(LayoutAlgorithm. NORMAL );

mWebView.setWebChromeClient( new WebServerChromeClient());
mWebView.setWebViewClient( new WebServerViewClient());

mWebView.setVerticalScrollBarEnabled( false );
mWebView.requestFocusFromTouch();

mWebView.addJavascriptInterface( new AppJavascriptInterface(), "nintf" );

在上面的代码中,主要是对WebView的一些初始化,但其中最重要的几句代码是这么几句:

2.1)webSettings.setJavaScriptEnabled( true );

告诉WebView,让它能够去执行JavaScript语句。在一个交互的网页上,javascript是没办法忽略的。

2.2)mWebView.setWebChromeClient( new WebServerChromeClient());
2.3)mWebView.setWebViewClient( new WebServerViewClient());

WebChromeClient和WebViewClient是WebView应用中的两个最重要的类。

通过这两个类,WebView能够捕获到Html页面中url的加载,javascript的执行等的所有操作,从而能够在Android的原生环境中对这些来自网页上的事件进行判断,解析,然后将对应的处理结果返回给html网页。

这两个类是html页面和Android原生环境交互的基础,所有通过html页面来跟后台交互的操作,都在这两个类里面实现,在后面我们还会详细说明。

2.4)mWebView.addJavascriptInterface( new AppJavascriptInterface(), "nintf" );

这个JavascriptInterface,则是Android原生环境和javascript交互的另一个窗口。

将我们自定义的AppJavascriptInterface类,调用mWebView的addJavascriptInterface方法,可以将这个对象传递给mWebView中Window对象的nintf属性("nintf"这个属性名称是自定义的)之后,

就可以直接在javascript中调用这个Java对象的方法。

3)接下来,我们就先来看看在Html中的javascript是如何跟Android原生环境来交互的。

我们按照事件发生的顺序机制来看,这样有个先后的概念,理解起来会容易一点。

在这套机制中,提供了两种访问Android原生环境的方法,一种是同步的,一种是异步的。

同步的概念就是说,我在跟你交流的时候,如果我还没有收到你的回复,我是不能跟其他人交流的,我必须等在那里,一直等着你。

异步的概念就是说,我在跟你交流的时候,如果你还没有回复我,我还能够去跟其他人交流,而当我收到你的回复的时候,再去看看你的回复,应该要干些什么。

3.1)同步访问

在Javascript中,我们定义了这样一个方法,如下:

var exec = function (service, action, args) {
        var json = {
               "service" : service,
               "action" : action
       };
        var result_str = prompt(JSON.stringify(json), args);

        var result;
        try {
              result = JSON.parse(result_str);
       } catch (e) {
              console.error(e.message);
       }

        var status = result.status;
        var message = result.message;
        if (status == 0) {

               return message;
       } else {
              console.error( "service:" + service + " action:" + action + " error:" + message);
       }
}

而对此方法的,典型的调用如下:

exec( "Toast", "makeTextShort" , JSON.stringify(text));

其中Toast和makeTextShort是要调用Android原生环境的服务和参数,这些都是在PluginManager中管理的,在下一篇文章中会提及到。

在这里,我们调用了prompt方法,通过这个方法,在WebView中定义的的WebChromeClient就会拦截到这样一个方法,具体代码如下:

class WebServerChromeClient extends WebChromeClient {
     @Override
    public boolean onJsPrompt(WebView view, String url, String message,
              String defaultValue, JsPromptResult result) {
         System.out.println( "onJsPrompt:defaultValue:" + defaultValue + "|" + url + "," + message);
         JSONObject args = null ;
         JSONObject head = null ;
         try {
              head = new JSONObject(message);
              args = new JSONObject(defaultValue);
              String execResult = mPluginManager.exec(head.getString(IPlugin.SERVICE),
                        head.getString(IPlugin.ACTION), args);

              result.confirm(execResult);
              return true;

         ...

    }
}

在这里,我们会重载WebChromeClient的onJsPrompt方法,当此方法返回true的时候,就说明WebChromeClient已经处理了这个prompt事件,不需要再继续分发下去;

而当返回false的时候,则此事件会继续传递给WebView,由WebView来处理。

由于我们这里是要利用这个Prompt方法,来实现Javascript跟Android原生环境之间的同步访问,所以我们在这里会拦截这个事件进行处理。

在这里,通过message和defaultValue,我们可以拿到javascript中prompt方法两个参数的值,在这里,它们是Json数据,在这里进行解析之后,由PluginManager来进行处理,最后将结果返回给JsPromptResult的confirm方法中。

此结果就是javascript中prompt的返回值了。

而除了JsPrompt,还有类似Javascript中的Alert方法等,我们知道浏览器弹出的Alert窗口跟我们手机应用中窗口风格样式是很不一样的,而作为一个应用,风格肯定要有一套统一的标准,所以一般情况下,我们也会拦截WebView中的Alert窗口,这个逻辑也同样会是在这里处理,如下:

@Override
public boolean onJsAlert(WebView view, String url, String message,
               final JsResult result) {
       System. out .println("onJsAlert : url:" + url + " | message:" + message);
        if (isFinishing()) {
               return true ;
       }
       CustomAlertDialog.Builder customBuilderres = new CustomAlertDialog.Builder(DroidHtml5.this );
       customBuilderres.setTitle( "信息提示" ).setMessage(message)
                     .setPositiveButton( "确定" , new DialogInterface.OnClickListener() {
                            public void onClick(DialogInterface dialog, int which) {
                                  dialog.dismiss();
                                  result.confirm();
                           }
                     }).create().show();
        return true ;
}

上面描述的都是同步访问Android原生环境的方式,那么,异步的访问方式是怎么样的呢?

3.2)异步访问

同样的,我们会在Javascript中定义如下一个方法:

var exec_asyn = function(service, action, args, success, fail) {
       var json = {
               "service" : service,
               "action" : action
       };
       var result = AndroidHtml5.callNative(json, args, success, fail);
}

我们会调用AndroidHtml5的callNative,此方法有四个参数:

a)json:是调用的服务和操作

b)args: 对应的参数数

c)success : 成功时的回调方

d)fail:失败时的回调方

典型的调用如下:

var success = function(data){};
var fail = functio(data){};
exec_asyn( "Contacts", "openContacts" , '{}', success, fail);

在这里,AndroidHtml5是在Javascript中定义的一个对象,它提供了访问Android原生环境的方法,以及回调的队列函数。它的定义如下:

var AndroidHtml5 = {
       idCounter : 0,                 // 参数序列计数器
       OUTPUT_RESULTS : {},      // 输出的结果
       CALLBACK_SUCCESS : {},  // 输出的结果成功时调用的方法
       CALLBACK_FAIL : {},       // 输出的结果失败时调用的方法

       callNative : function (cmd, args, success, fail) {
              var key = "ID_" + (++ this.idCounter);

              window.nintf.setCmds(cmd, key);
              window.nintf.setArgs(args, key);

              if (typeof success != 'undefined'){
                    AndroidHtml5.CALLBACK_SUCCESS[key] = success;
              } else {
                    AndroidHtml5.CALLBACK_SUCCESS[key] = function (result){};
              }

              if (typeof fail != 'undefined'){
                    AndroidHtml5.CALLBACK_FAIL[key] = fail;
              } else {
                    AndroidHtml5.CALLBACK_FAIL[key] = function (result){};
              }

              //下面会定义一个Iframe,Iframe会去加载我们自定义的url,以androidhtml:开头
              var iframe = document.createElement("IFRAME" );
              iframe.setAttribute( "src" , "androidhtml://ready?id=" + key);
              document.documentElement.appendChild(iframe);
              iframe.parentNode.removeChild(iframe);
              iframe = null ;

              return this .OUTPUT_RESULTS[key];
       }, 

       callBackJs : function (result,key) {
               this .OUTPUT_RESULTS[key] = result;
               var obj = JSON.parse(result);
               var message = obj.message;
               var status = obj.status;
               if (status == 0) {
                      if (typeof this.CALLBACK_SUCCESS[key] != "undefined"){
                           setTimeout( "AndroidHtml5.CALLBACK_SUCCESS['" +key+"']('" + message + "')", 0);
                     }
              } else {
                      if (typeof this.CALLBACK_FAIL != "undefined") {
                           setTimeout( "AndroidHtml5.CALLBACK_FAIL['" +key+"']('" + message + "')" , 0);
                     }
              }
       }
};

在AndroidHtml5中,有几个地方我们需要注意的。

a)大家还记得我们在WebView初始化时设置的AppJavascriptInterface吗?当时自定义的名称就是"nintf",而在此时,在javascript中,我们就可以直接来运用这个对象所有的方法。

window.nintf.setCmds(cmd, key);
window.nintf.setArgs(args, key);

我们也看一下这个AppJavascriptInterface中的方法,如下:

public class AppJavascriptInterface implements java.io.Serializable {

        private static Hashtable<String,String> CMDS = new Hashtable<String,String>();
        private static Hashtable<String,String> ARGS = new Hashtable<String,String>();

        @JavascriptInterface
        public void setCmds(String cmds, String id) {
               CMDS .put(id, cmds);
       }      

        @JavascriptInterface
        public void setArgs(String args, String id) {
               ARGS .put(id, args);
       }

        public static String getCmdOnce(String id) {
              String result = CMDS .get(id);
               CMDS .remove(id);
               return result;
       }

        public static String getArgOnce(String id) {
              String result = ARGS .get(id);
               ARGS .remove(id);
               return result;
       }
}

这个类是简洁而不简单,通过在Javascript中调用类中的set方法,将对应的cmd和args参数给保存起来,目的是为了保存异步请求中多次的命令和操作,然后在Android原生环境中再取出来。

b)第二步呢,也是最重要的一步,会创建一个Iframe,在Iframe中申明一个url,而且是以androidhtml: 开头的。

在上面我们提过,WebView在初始化的时候,会设置一个WebViewClient,这个类的主要作用就是,当在html页面中发生url加载的时候,我们可以拦截这个加载事件,进行处理,重写这次加载事件。

而我们正好是利用了这一点,利用一个Iframe来触发一次Url的拦截事件。

我们来看一下WebViewClient中是如何实现这个异步请求的实现的。

class WebServerViewClient extends WebViewClient {

       Handler myHandler = new Handler() {
               ...
       };

        @Override
        public boolean shouldOverrideUrlLoading(WebView view, String url) {

               if (url != null && url.startsWith( "androidhtml")) {
                    String id = url.substring(url.indexOf( "id=" ) + 3);
                    JSONObject cmd = null ;
                    JSONObject arg = null ;
                     try {
                           String cmds = AppJavascriptInterface.getCmdOnce(id);
                           String args = AppJavascriptInterface.getArgOnce(id);
                           cmd = new JSONObject(cmds);
                           arg = new JSONObject(args);
                    } catch (JSONException e1) {
                           e1.printStackTrace();
                            return false ;
                    }
                    //另起线程处理请求
                     try {
                           AsynServiceHandler asyn = new AsynServiceHandlerImpl();
                           asyn.setKey(id);
                           asyn.setService(cmd.getString( "service" ));
                           asyn.setAction(cmd.getString( "action" ));
                           asyn.setArgs(arg);
                           asyn.setWebView( mWebView);
                           asyn.setMessageHandler( myHandler );
                           Thread thread = new Thread(asyn, "asyn_" + (threadIdCounter ++));
                           thread.start();
                    } catch (Exception e) {
                           e.printStackTrace();
                            return false;
                    }
                     return true ;
              }
              //如果url不是以Androidhtml开头的,则由WebView继续去处理。
              view.loadUrl(url);
              return true ;
       }

}

我们可以看到,在这方法中,首先只有以androidhtml开头的url才会被拦截处理,而其他的url则还是由WebView进行处理。

而通过AppJavascriptInterface,我们将在Javascript中保存的cmds和args等数据都拿出来了,并由AsynServiceHandler新启一个线程去处理。

我们再来看看AsynServiceHandlerImpl是怎么实现的,

public class AsynServiceHandlerImpl implements AsynServiceHandler {
        @Override
        public void run() {
           try {
              final String responseBody = PluginManager.getInstance().exec(service,  action,args);
              handler.post( new Runnable() {
                      public void run() {
                           webView .loadUrl( "javascript:AndroidHtml5.callBackJs('"+responseBody+ "','" +key +"')" );
                      }
              });
           } catch (PluginNotFoundException e) {
               e.printStackTrace();
           }
       }

可以看到,当调用PluginManager操作完对应的命令和数据之后,会通过WebView的loadUrl方法,去执行AndroidHtml5的callBackJs方法。

通过key值,我们就可以在AndroidHtml5中的callBackJs方法中找回到对应的回调方法,进行处理。

因此,通过一次Iframe的构建,加载以androidhtml开头的url,再利用WebView的WebViewClient接口对象,我们就能够在Html页面中和Android原生环境进行异步的交互了。

在这一篇文章中,我们几处地方讲到了PluginManager这个类,这是一个管理HTML和Android原生环境交互接口的类。

因为如果把所有的逻辑都放在WebViewClient或者WebChromeClient这两个都来处理,这是不合理的,乱,复杂,看不懂。

所以我们需要把逻辑实现跟交互给分开来,这个机制才显得漂亮,实用,易操作。

Android与WebView的同步和异步访问机制

时间: 2024-11-05 21:40:36

Android与WebView的同步和异步访问机制的相关文章

使用ab.exe监测100个并发/100次请求情况下同步/异步访问数据库的性能差异

ab.exe介绍 ab.exe是apache server的一个组件,用于监测并发请求,并显示监测数据 具体使用及下载地址请参考:http://www.cnblogs.com/gossip/p/4398784.html 本文的目的 通过webapi接口模拟100个并发请求下,同步和异步访问数据库的性能差异 创建数据库及数据 --创建表结构 CREATE TABLE dbo.[Cars] ( Id INT IDENTITY(1000,1) NOT NULL, Model NVARCHAR(50) 

Android 异步消息处理机制 让你深入理解 Looper、Handler、Message三者关系

转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/38377229 ,本文出自[张鸿洋的博客] 很多人面试肯定都被问到过,请问Android中的Looper , Handler , Message有什么关系?本篇博客目的首先为大家从源码角度介绍3者关系,然后给出一个容易记忆的结论. 1. 概述 Handler . Looper .Message 这三者都与Android异步消息处理线程相关的概念.那么什么叫异步消息处理线程呢?异步

Android异步消息处理机制——handle与Looper,AsyncTask

Android线程间的通讯采用异步消息处理机制,主要由四部分组成,包括Message,Handler,MessageQueue和Looper. 一个线程只有一个Looper与Messagequeue,但可以有多个handler实例. 例:线程A发消息Message,线程B处理消息Message. 需要在线程B中新建一个Handler实例handler,在A线程中通过该handler发送消息到线程B中的Messagequeue中, 通过B中的Looper以及先进先出的原则取出该消息并处理消息,所以

Android 异步消息处理机制

转载自博客:http://blog.csdn.net/lmj623565791/article/details/38377229/ 1. 概述 Handler . Looper .Message 这三者都与Android异步消息处理线程相关的概念.那么什么叫异步消息处理线程呢?异步消息处理线程启动后会进入一个无限的循环体之中,每循环一次,从其内部的消息队列中取出一个消息,然后回调相应的消息处理函数,执行完成一个消息后则继续循环.若消息队列为空,线程则会阻塞等待. 说了这一堆,那么和Handler

【Java&amp;Android开源库代码剖析】のandroid-async-http(如何设计一个优雅的Android网络请求框架,同时支持同步和异步请求)开篇

在<[Java&Android开源库代码剖析]のandroid-smart-image-view>一文中我们提到了android-async-http这个开源库,本文正式开篇来详细介绍这个库的实现,同时结合源码探讨如何设计一个优雅的Android网络请求框架.做过一段时间Android开发的同学应该对这个库不陌生,因为它对Apache的HttpClient API的封装使得开发者可以简洁优雅的实现网络请求和响应,并且同时支持同步和异步请求. 网络请求框架一般至少需要具备如下几个组件:1

Android异步消息处理机制详解及源码分析

PS一句:最终还是选择CSDN来整理发表这几年的知识点,该文章平行迁移到CSDN.因为CSDN也支持MarkDown语法了,牛逼啊! [工匠若水 http://blog.csdn.net/yanbober 转载烦请注明出处,尊重分享成果] 最近相对来说比较闲,加上养病,所以没事干就撸些自己之前的知识点为博客,方便自己也方便别人. 1 背景 之所以选择这个知识点来分析有以下几个原因: 逛GitHub时发现关注的isuss中有人不停的在讨论Android中的Looper , Handler , Me

【Mocha.js 101】同步、异步与 Promise

前情提要 在上一篇文章<[Mocha.js 101]Mocha 入门指南>中,我们提到了如何用 Mocha.js 进行前端自动化测试,并做了几个简单的例子来体验 Mocha.js 给我们带来的便利. 在本篇文章中,我们将了解到 Mocha.js 的同步/异步测试,以及如何测试 Promise. 同步代码测试 在上一篇文章中,其实我们已经学会了如何测试同步代码.今天,我们 BDD 风格编写一个测试: var should = require( 'should' ); var Calculator

socket阻塞与非阻塞,同步与异步、I/O模型,select与poll、epoll比较

1. 概念理解 在进行网络编程时,我们常常见到同步(Sync)/异步(Async),阻塞(Block)/非阻塞(Unblock)四种调用方式: 同步/异步主要针对C端: 同步:      所谓同步,就是在c端发出一个功能调用时,在没有得到结果之前,该调用就不返回.也就是必须一件一件事做,等前一件做完了才能做下一件事. 例如普通B/S模式(同步):提交请求->等待服务器处理->处理完毕返回 这个期间客户端浏览器不能干任何事 异步:      异步的概念和同步相对.当c端一个异步过程调用发出后,调

同步与异步

在高性能的I/O设计中,有两个比较著名的模式Reactor 和 Proactor 模式,其中 Reactor模式用于同步 I/O ,而Proactor 运用于异步 I/O操作. 在比较这两个模式之前,我们首先的搞明白几个概念,什么是阻塞和非阻塞,什么是同步和异步,同步和异步是针对应用程序和内核的交互而言的,同步指的是用户进程触发      IO操作并等待或者轮询的去查看IO操作是否就绪,而异步是指用户进程触发IO操作以后便开始做自己的事情,而当IO操作已经完成的时候会得到IO完成的通知.而阻塞和