AndroidとWebViewの同期と非同期アクセスメカニズム


WebViewを通じて、AndroidクライアントでWeb開発でアプリケーションを開発できることはよく知られています.
もし1つのアプリケーションが単純なWebViewであれば、すべての論理がWebページで対話するだけであれば、htmlとjavascriptでサーバと対話するだけでいいのです.
しかし、多くの場合、私たちのアプリケーションは単純なWebViewではなく、Android自体のアプリケーションを運用する必要がある可能性があります.例えば、写真を撮るには、Android自体のカメラを呼び出す必要があります.振動を起こすには、携帯電話の特性を運用する必要があるシーンでは、javascriptとAndroidの間で通信するメカニズムが必要です.同期と非同期の方法も含まれています.このメカニズムは本稿で紹介したいものです.
一歩一歩、私たちはまず最も簡単なところから話します:1)Web Viewが私たちのページを表示する必要があります.まずレイアウトを定義します.非常に簡単です.Web Viewです.以下のようにします.
<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文を実行できるように伝えます.インタラクティブなWebページではjavascriptは無視できません.
2.2)mWebView.setWebChromeClient( new WebServerChromeClient());
2.3)mWebView.setWebViewClient( new WebServerViewClient());
WebChromeClientとWebViewClientは、WebViewアプリケーションの2つの最も重要なクラスです.
この2つのクラスにより、WebViewは、Htmlページにおけるurlのロード、javascriptの実行などのすべての操作をキャプチャすることができ、Androidのオリジナル環境でこれらのWebページからのイベントを判断し、解析し、対応する処理結果をhtmlネットワークページに返すことができる.
この2つのクラスはhtmlページとAndroidのオリジナル環境のインタラクションの基礎であり、htmlページを通じてバックグラウンドとインタラクションするすべての操作は、この2つのクラスで実現されており、後で詳しく説明します.
2.4)mWebView.addJavascriptInterface( new AppJavascriptInterface(), "nintf" );

このJavascriptInterfaceは、Androidのオリジナル環境とjavascriptが対話する別のウィンドウです.
私たちがカスタマイズしたAppJavascriptInterfaceクラスをmWebViewのaddJavascriptInterfaceメソッドを呼び出し、このオブジェクトをmWebViewのWindowオブジェクトのnintfプロパティ(「nintf」というプロパティ名はカスタマイズされています)に渡した後、
JavascriptでこのJavaオブジェクトを直接呼び出す方法です.
3)次に、HtmlのjavascriptがAndroidのオリジナル環境とどのように対話しているかを見てみましょう.
私たちは事件が発生する順序のメカニズムから見ると、このように前後の概念があり、理解しやすい.
このメカニズムでは、Androidのオリジナル環境にアクセスする2つの方法が提供されています.1つは同期であり、1つは非同期です.
同期の概念は、私があなたと交流している間に、もし私がまだあなたの返事を受け取っていないならば、私は他の人と交流することができなくて、私はそこで待たなければならなくて、ずっとあなたを待っています.
非同期の概念は、私があなたと交流している間に、もしあなたがまだ私に返事をしていなければ、私は他の人と交流することができますが、私があなたの返事を受け取ったとき、あなたの返事を見て、何をすべきかを見ることができます.
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メソッドの2つのパラメータの値を得ることができます.ここでは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);  
}

AndroidHtml 5のcallNativeを呼び出します.このメソッドには4つのパラメータがあります.
a)json:呼び出されたサービスと操作
b)args:対応するパラメータ数
c)success:成功時のコールバック
d)fail:失敗時のコールバック
典型的な呼び出しは次のとおりです.
var success = function(data){};
var fail = functio(data){};
exec_asyn( "Contacts", "openContacts" , '{}', success, fail);

ここで、Android Html 5は、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);
                     }
              }
       }
};

Android Html 5では、いくつか注意すべき点があります.
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)第2のステップは、最も重要なステップであり、Iframeを作成し、Iframeでurlを宣言し、android html:で始まる.
前述したように、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 ;
       }
       
}
この方法では、まずandroid htmlで始まる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メソッドでAndroidHtml 5のcallBackJsメソッドが実行されることがわかります.
key値により,Android Html 5のcallBackJsメソッドで対応するコールバックメソッドを取り戻し,処理を行うことができる.
したがって,一度のIframeの構築によりandroidで始まるurlをロードし,WebViewのWebViewClientインタフェースオブジェクトを再利用することで,HtmlページでAndroidネイティブ環境と非同期でインタラクションできるようになった.
この記事では、HTMLとAndroidのオリジナル環境インタフェースを管理するクラスであるPluginManagerというクラスについていくつかお話しします.
すべての論理をWebViewClientかWebChromeClientの2つに置いて処理すれば、これは合理的ではなく、散らかっていて、複雑で、読めないからです.
だから私たちは論理の実現とインタラクティブを分ける必要があります.このメカニズムはきれいで、実用的で、操作しやすいように見えます.