• 跨平台PhoneGap技术架构四:PhoneGap底层原理(下)


    晚上被朋友拉去看了海上烟花,烟花易冷,热情难却。散场时候人太多,车都打不着,回来都23点了,差点把写博客计划给耽搁了。

    废话不多说,上篇博客讲到了PhoneGap的核心组件WebView,如果园友有觉得讲得不够清楚的地方,欢迎提出您的意见。

    本篇主要讲解PhoneGap的两个重要的类,PluginManager类和CallbackServer类。

    PluginManager类

    PluginManager类实现了PhoneGap的插件管理机制,并在CordovaWebView提供出JavaScript接口。在CordovaChromeClient类的onJsPrompt()方法中截获JavaScript消息,根据消息具体执行某一个插件。代码如下,

    String r = this.appView.pluginManager.exec(service, action, callbackId, message, async);

    在上篇博客讲解CordovaWebView类时曾提到,PluginManager的实例化是在CordovaWebView的初始化init()函数调用setup()方法时执行的。

    1         //Start up the plugin manager
    2         try {
    3             this.pluginManager = new PluginManager(this, this.cordova);
    4         } catch (Exception e) {
    5             // TODO Auto-generated catch block
    6             e.printStackTrace();
    7         }

    我们首先看一下PluginManager类的构造函数:

    View Code
     1     /**
     2      * Constructor.
     3      *
     4      * @param app
     5      * @param ctx
     6      */
     7     public PluginManager(CordovaWebView app, CordovaInterface ctx) {
     8         this.ctx = ctx;
     9         this.app = app;
    10         this.firstRun = true;
    11     }

    传入参数有两个CordovaWebView类型的app和CordovaInterface类型的ctx。这两个参数比较重要,PluginManager是和CordovaWebView绑定的,要求必须是CordovaWebView类型,第二个参数必须是实现了CordovaInterface的Context。为什么是必须呢?这里留下一个悬念,如果你只做PhoneGap层的应用,可以不关心这个问题。初始化firstRun为true,这个Flag用来判断初始化时是否需要加载插件。

    并在CordovaWebView的loadUrlIntoView()方法中进行了初始化工作。

    this.pluginManager.init();

    那在初始化中到底完成了哪些工作?看一下代码就很好理解了。

    init
     1     /**
     2      * Init when loading a new HTML page into webview.
     3      */
     4     public void init() {
     5         LOG.d(TAG, "init()");
     6 
     7         // If first time, then load plugins from plugins.xml file
     8         if (this.firstRun) {
     9             this.loadPlugins();
    10             this.firstRun = false;
    11         }
    12 
    13         // Stop plugins on current HTML page and discard plugin objects
    14         else {
    15             this.onPause(false);
    16             this.onDestroy();
    17             this.clearPluginObjects();
    18         }
    19 
    20         // Start up all plugins that have onload specified
    21         this.startupPlugins();
    22     }

    首先判断firstRun是否为true,如果为true,则执行this.loadPlugins()加载插件,并把firstRun置为false。否则停止plugin工作并销毁对象。

    最后执行this.startupPlugins()启动插件。

    下面我们来看看如何加载插件的。

    loadPlugins
     1     /**
     2      * Load plugins from res/xml/plugins.xml
     3      */
     4     public void loadPlugins() {
     5         int id = this.ctx.getActivity().getResources().getIdentifier("plugins", "xml", this.ctx.getActivity().getPackageName());
     6         if (id == 0) {
     7             this.pluginConfigurationMissing();
     8         }
     9         XmlResourceParser xml = this.ctx.getActivity().getResources().getXml(id);
    10         int eventType = -1;
    11         String service = "", pluginClass = "";
    12         boolean onload = false;
    13         PluginEntry entry = null;
    14         while (eventType != XmlResourceParser.END_DOCUMENT) {
    15             if (eventType == XmlResourceParser.START_TAG) {
    16                 String strNode = xml.getName();
    17                 if (strNode.equals("plugin")) {
    18                     service = xml.getAttributeValue(null, "name");
    19                     pluginClass = xml.getAttributeValue(null, "value");
    20                     // System.out.println("Plugin: "+name+" => "+value);
    21                     onload = "true".equals(xml.getAttributeValue(null, "onload"));
    22                     entry = new PluginEntry(service, pluginClass, onload);
    23                     this.addService(entry);
    24                 } else if (strNode.equals("url-filter")) {
    25                     this.urlMap.put(xml.getAttributeValue(null, "value"), service);
    26                 }
    27             }
    28             try {
    29                 eventType = xml.next();
    30             } catch (XmlPullParserException e) {
    31                 e.printStackTrace();
    32             } catch (IOException e) {
    33                 e.printStackTrace();
    34             }
    35         }
    36     }

     PluginManager类加载插件是通过读取res/xml/plugins.xml文件实现的。首先取得xml文件id,然后通过id取得xml文件的内容。接下来是解析xml文件,每次都会读取出插件Plugin的service(如Camera、Notification等)、pluginClass(如org.apache.cordova.CameraLauncher、org.apache.cordova.Notification等)和onload(判断插件初始化时是否需要创建插件的flag)。最后通过addService将读取到的插件添加到PluginEntry的list中去。

    在插件实体类PluginEntry中有一个属性onload可能让人有些迷惑,在plugins.xml中并没有关于onload的节点,也就是说在解析xml文件时

    onload = "true".equals(xml.getAttributeValue(null, "onload"));

    这里的onload是被赋值为false的。个人猜测是为以后的插件扩展预留的接口,在初始化时需要先创建这个插件。

    在startupPlugins()函数中,主要是做了onload被设置成true时(目前还没有这种情况)的插件创建工作。

    startupPlugins
     1     /**
     2      * Create plugins objects that have onload set.
     3      */
     4     public void startupPlugins() {
     5         for (PluginEntry entry : this.entries.values()) {
     6             if (entry.onload) {
     7                 entry.createPlugin(this.app, this.ctx);
     8             }
     9         }
    10     }

    好了,插件初始化完毕,万事具备只欠东风了。这里的“东风”是什么?当然是插件的执行了。

    exec
     1     /**
     2      * Receives a request for execution and fulfills it by finding the appropriate
     3      * Java class and calling it's execute method.
     4      *
     5      * PluginManager.exec can be used either synchronously or async. In either case, a JSON encoded
     6      * string is returned that will indicate if any errors have occurred when trying to find
     7      * or execute the class denoted by the clazz argument.
     8      *
     9      * @param service       String containing the service to run
    10      * @param action        String containt the action that the class is supposed to perform. This is
    11      *                      passed to the plugin execute method and it is up to the plugin developer
    12      *                      how to deal with it.
    13      * @param callbackId    String containing the id of the callback that is execute in JavaScript if
    14      *                      this is an async plugin call.
    15      * @param args          An Array literal string containing any arguments needed in the
    16      *                      plugin execute method.
    17      * @param async         Boolean indicating whether the calling JavaScript code is expecting an
    18      *                      immediate return value. If true, either Cordova.callbackSuccess(...) or
    19      *                      Cordova.callbackError(...) is called once the plugin code has executed.
    20      *
    21      * @return              JSON encoded string with a response message and status.
    22      */
    23     public String exec(final String service, final String action, final String callbackId, final String jsonArgs, final boolean async) {
    24         PluginResult cr = null;
    25         boolean runAsync = async;
    26         try {
    27             final JSONArray args = new JSONArray(jsonArgs);
    28             final IPlugin plugin = this.getPlugin(service);
    29             //final CordovaInterface ctx = this.ctx;
    30             if (plugin != null) {
    31                 runAsync = async && !plugin.isSynch(action);
    32                 if (runAsync) {
    33                     // Run this on a different thread so that this one can return back to JS
    34                     Thread thread = new Thread(new Runnable() {
    35                         public void run() {
    36                             try {
    37                                 // Call execute on the plugin so that it can do it's thing
    38                                 PluginResult cr = plugin.execute(action, args, callbackId);
    39                                 int status = cr.getStatus();
    40 
    41                                 // If no result to be sent and keeping callback, then no need to sent back to JavaScript
    42                                 if ((status == PluginResult.Status.NO_RESULT.ordinal()) && cr.getKeepCallback()) {
    43                                 }
    44 
    45                                 // Check the success (OK, NO_RESULT & !KEEP_CALLBACK)
    46                                 else if ((status == PluginResult.Status.OK.ordinal()) || (status == PluginResult.Status.NO_RESULT.ordinal())) {
    47                                     app.sendJavascript(cr.toSuccessCallbackString(callbackId));
    48                                 }
    49 
    50                                 // If error
    51                                 else {
    52                                     app.sendJavascript(cr.toErrorCallbackString(callbackId));
    53                                 }
    54                             } catch (Exception e) {
    55                                 PluginResult cr = new PluginResult(PluginResult.Status.ERROR, e.getMessage());
    56                                 app.sendJavascript(cr.toErrorCallbackString(callbackId));
    57                             }
    58                         }
    59                     });
    60                     thread.start();
    61                     return "";
    62                 } else {
    63                     // Call execute on the plugin so that it can do it's thing
    64                     cr = plugin.execute(action, args, callbackId);
    65 
    66                     // If no result to be sent and keeping callback, then no need to sent back to JavaScript
    67                     if ((cr.getStatus() == PluginResult.Status.NO_RESULT.ordinal()) && cr.getKeepCallback()) {
    68                         return "";
    69                     }
    70                 }
    71             }
    72         } catch (JSONException e) {
    73             System.out.println("ERROR: " + e.toString());
    74             cr = new PluginResult(PluginResult.Status.JSON_EXCEPTION);
    75         }
    76         // if async we have already returned at this point unless there was an error...
    77         if (runAsync) {
    78             if (cr == null) {
    79                 cr = new PluginResult(PluginResult.Status.CLASS_NOT_FOUND_EXCEPTION);
    80             }
    81             app.sendJavascript(cr.toErrorCallbackString(callbackId));
    82         }
    83         return (cr != null ? cr.getJSONString() : "{ status: 0, message: 'all good' }");
    84     }

    本篇开始已经提到过,插件的执行是在CordovaChromeClient的onJsPrompt()方法中截获JavaScript消息,根据消息具体执行某一个插件来调用的。几个传入参数再说明一下

    service:需要执行的某一项服务,如Camera、Notification、Battery等等;

    action:服务执行的具体动作,如takePhone、getPicture等

    callbackId:如果插件是异步执行的话,那么插件执行完成之后回调执行的JavaScript代码的Id存放在callbackId中(好长的定语⊙﹏⊙b汗)。

    jsonArgs:即上篇提到过的message消息,存放着服务的调用信息。

    async:布尔变量,标明JavaScript端是否需要执行回调。如果是true,则Cordova.callbackSuccess(...)或者Cordova.callbackError(...)需要被执行。

        实际这个值传入时默认都是true的,插件是否是异步执行是通过plugin.isSynch(action)来判断的。估计开始时想让开发者指定插件是否要异步执行,后来添加了plugin.isSynch()的方法来判断。

    首先判断插件是否是异步执行,如果是ture的话,新起一个线程执行。

    PluginResult cr = plugin.execute(action, args, callbackId);

    通过PluginResult类型返回值cr的取得status和 cr.getKeepCallback()是否执行app.sendJavascript()方法。

    如果插件不是异步执行,则直接调用cr = plugin.execute(action, args, callbackId)方法执行插件服务。

    CallbackServer类

    如果说前面所讲的几个类都是为后台JAVA端服务的话,那么CallbackServer类就是为前台JavaScript服务的了。

     下面我们看看CallbackServer类官方文档的解释:

    CallbackServer 实现了 Runnable 接口,具体的功能就是维护一个数据的队列,并且建立一个服务器,用于 XHR 的数据传递,对数据的队列的维护利用的是 LinkedList<String>。

    XHR处理流程:

    1. JavaScript 发起一个异步的XHR 请求.

    2. 服务器保持该链接打开直到有可用的数据。

    3. 服务器把数据写入客户端并关闭链接。

    4. 服务器立即开始监听下一个XHR请求。

    5. 客户端接收到XHR回复,并处理该回复。

    6. 客户端发送新的异步XHR 请求。


    如果说手持设备设置了代理,那么XHR是不可用的,这时候需要使用Pollibng轮询模式。
    该使用哪种模式,可通过 CallbackServer.usePolling()获取。


    Polling调用流程:

    1.客户端调用CallbackServer.getJavascript()来获取要执行的Javascript 语句。
    2. 如果有需要执行的JS语句,那么客户端就会执行它。
    3. 客户端在循环中执行步骤1.

    前面已经讲到过,在CordovaWebViewClient类的onPageStarted方法中实现了CallbackServer的实例化和初始化。

    按照惯例,首先来看一下CallbalServer类的构造函数:

    CallbackServer
     1     /**
     2      * Constructor.
     3      */
     4     public CallbackServer() {
     5         //Log.d(LOG_TAG, "CallbackServer()");
     6         this.active = false;
     7         this.empty = true;
     8         this.port = 0;
     9         this.javascript = new LinkedList<String>();
    10     }

    构造函数比较简单,初始化的时候设置活动状态this.active = false;JavaScript队列为空this.empty = true;端口初始化为0 this.port = 0;新建一个LinkedList<String>类型的javascript队列。

    然后再看一下初始化方法

    init
     1    /**
     2      * Init callback server and start XHR if running local app.
     3     *
     4      * If Cordova app is loaded from file://, then we can use XHR
     5      * otherwise we have to use polling due to cross-domain security restrictions.
     6      *
     7      * @param url            The URL of the Cordova app being loaded
     8      */
     9     public void init(String url) {
    10         //System.out.println("CallbackServer.start("+url+")");
    11         this.active = false;
    12         this.empty = true;
    13         this.port = 0;
    14         this.javascript = new LinkedList<String>();
    15 
    16         // Determine if XHR or polling is to be used
    17         if ((url != null) && !url.startsWith("file://")) {
    18             this.usePolling = true;
    19             this.stopServer();
    20         }
    21         else if (android.net.Proxy.getDefaultHost() != null) {
    22             this.usePolling = true;
    23             this.stopServer();
    24         }
    25         else {
    26             this.usePolling = false;
    27             this.startServer();
    28         }
    29     }

    CallbalServer类支持两种执行模式,一种是Polling,另为一种是XHR(xmlHttpRequest)。在初始化init方法中先判断使用哪种模式。如果是打开的本地文件或者设置设置了代理,则使用polling的模式,否则,使用XHR模式,并启动Server。

    由于CallbalServer类实现的是 Runnable 接口,在 CallbackServer 中,最主要的方法就是 run() 方法,run() 方法的具体内容简介如下:

    run
     1     /**
     2      * Start running the server.  
     3      * This is called automatically when the server thread is started.
     4      */
     5     public void run() {
     6 
     7         // Start server
     8         try {
     9             this.active = true;
    10             String request;
    11             ServerSocket waitSocket = new ServerSocket(0);
    12             this.port = waitSocket.getLocalPort();
    13             //Log.d(LOG_TAG, "CallbackServer -- using port " +this.port);
    14             this.token = java.util.UUID.randomUUID().toString();
    15             //Log.d(LOG_TAG, "CallbackServer -- using token "+this.token);
    16 
    17             while (this.active) {
    18                 //Log.d(LOG_TAG, "CallbackServer: Waiting for data on socket");
    19                 Socket connection = waitSocket.accept();
    20                 BufferedReader xhrReader = new BufferedReader(new InputStreamReader(connection.getInputStream()), 40);
    21                 DataOutputStream output = new DataOutputStream(connection.getOutputStream());
    22                 request = xhrReader.readLine();
    23                 String response = "";
    24                 //Log.d(LOG_TAG, "CallbackServerRequest="+request);
    25                 if (this.active && (request != null)) {
    26                     if (request.contains("GET")) {
    27 
    28                         // Get requested file
    29                         String[] requestParts = request.split(" ");
    30 
    31                         // Must have security token
    32                         if ((requestParts.length == 3) && (requestParts[1].substring(1).equals(this.token))) {
    33                             //Log.d(LOG_TAG, "CallbackServer -- Processing GET request");
    34 
    35                             // Wait until there is some data to send, or send empty data every 10 sec 
    36                             // to prevent XHR timeout on the client 
    37                             synchronized (this) {
    38                                 while (this.empty) {
    39                                     try {
    40                                         this.wait(10000); // prevent timeout from happening
    41                                         //Log.d(LOG_TAG, "CallbackServer>>> break <<<");
    42                                         break;
    43                                     } catch (Exception e) {
    44                                     }
    45                                 }
    46                             }
    47 
    48                             // If server is still running
    49                             if (this.active) {
    50 
    51                                 // If no data, then send 404 back to client before it times out
    52                                 if (this.empty) {
    53                                     //Log.d(LOG_TAG, "CallbackServer -- sending data 0");
    54                                     response = "HTTP/1.1 404 NO DATA\r\n\r\n "; // need to send content otherwise some Android devices fail, so send space
    55                                 }
    56                                 else {
    57                                     //Log.d(LOG_TAG, "CallbackServer -- sending item");
    58                                     response = "HTTP/1.1 200 OK\r\n\r\n";
    59                                     String js = this.getJavascript();
    60                                     if (js != null) {
    61                                         response += encode(js, "UTF-8");
    62                                     }
    63                                 }
    64                             }
    65                             else {
    66                                 response = "HTTP/1.1 503 Service Unavailable\r\n\r\n ";
    67                             }
    68                         }
    69                         else {
    70                             response = "HTTP/1.1 403 Forbidden\r\n\r\n ";
    71                         }
    72                     }
    73                     else {
    74                         response = "HTTP/1.1 400 Bad Request\r\n\r\n ";
    75                     }
    76                     //Log.d(LOG_TAG, "CallbackServer: response="+response);
    77                     //Log.d(LOG_TAG, "CallbackServer: closing output");
    78                     output.writeBytes(response);
    79                     output.flush();
    80                 }
    81                 output.close();
    82                 xhrReader.close();
    83             }
    84         } catch (IOException e) {
    85             e.printStackTrace();
    86         }
    87         this.active = false;
    88         //Log.d(LOG_TAG, "CallbackServer.startServer() - EXIT");
    89     }

    首先利用 ServerSocket 监听端口,具体端口则自由分配。
    在 accept 后则是对 HTTP 协议的解析,和对应的返回 status code。
    在验证正确后,利用 getJavascript 方法得到维护的 LinkedList<String>() 中的保存的 js 代码,如果为空则返回 null。
    这些具体的 string 类型的 js 代码则利用 socket 作为 response 返回给前端。
    之后就是对队列维护的方法,这时理解之前的 sendJavaScript 则很简单,该方法与 getJavaScript 相反,一个是从 LinkedList 中取出 js 代码,一个则是加入。
    综上,CallbackServer 实现的是两个功能,一个是 XHR 的 SocketServer,一个是对队列的维护。

  • 相关阅读:
    数据库连接池
    JDBC事务
    oracle 11g
    python自动化办公1-os模块学习
    python模块学习1
    requests-post请求
    linux学习二-目录文件相关命令
    Linux学习一常见的7个命令及命令的信息查看
    python-文件操作
    异常以及异常处理
  • 原文地址:https://www.cnblogs.com/ever4ever/p/2588958.html
Copyright © 2020-2023  润新知