fred-ye / summary

my blog
43 stars 9 forks source link

[Android] Cordova学习 #56

Open fred-ye opened 8 years ago

fred-ye commented 8 years ago

Cordova

官网文档

常用命令

以下命令在项目根目录执行

private static void exposeJsInterface(WebView webView, CordovaBridge bridge) {
   //API Level 17以下
   if ((Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1)) {
       Log.i(TAG, "Disabled addJavascriptInterface() bridge since Android version is old.");
       return;
   }
   SystemExposedJsApi exposedJsApi = new SystemExposedJsApi(bridge);
   webView.addJavascriptInterface(exposedJsApi, "_cordovaNative");
}

这样前端的JS和原生连接便建立起来,接下来看原生如何向前端返回数据:

374ff3df-08e8-41ed-abe1-91f3bd036e0d

题外话:流程图的制作参看这里,下方是该流程图的代码

graph TB
SystemExposedJsApi.exec-->CordovaBridge.jsExec
CordovaBridge.jsExec --> PluginManager.exec
PluginManager.exec --> 具体的CordovaPlugin
具体的CordovaPlugin --> CallbackContext.sendPluginResult
 CallbackContext.sendPluginResult  -->NativeToJsMessageQueue.addPluginResult

 NativeToJsMessageQueue.addPluginResult -->BridgeMode.onNativeToJsMessageAvailable
 BridgeMode.onNativeToJsMessageAvailable --> CordovaWebViewEngine.loadUrl

 CordovaWebViewEngine.loadUrl-->WebView.loadUrl

插件是如何集成进来的

camera插件为例,为测试的工程添加了camer插件,于是项目的结构变成了如下这样子: structure

首先看官网上关于camera插件的使用是这样子的navigator.camera.getPicture(cameraSuccess, cameraError, cameraOptions); ,于是我们想知道navigatorcamera分别是什么是什么时候初始化的,为什么可以直接用。然后从cordova.js中找到了navigator的定义。从cordova_plugin.js中看到了这么一段代码

 {
     "file": "plugins/cordova-plugin-camera/www/Camera.js",
     "id": "cordova-plugin-camera.camera",
     "clobbers": [
         "navigator.camera"
     ]
 }

前段代码没有做太多深究,但估计应该是在这里把camera对象挂载到navigator上。当执行navigator.camera.getPicture方法的时候,会调用Camera.js中的cameraExport.getPicture = function(successCallback, errorCallback, options), 最终调用exec(successCallback, errorCallback, "Camera", "takePicture", args); 。再看一下exec方法到底是什么,还是倒推。首先看到Camera.js中有exec = require('cordova/exec'), 再发现在cordova.js中有 define("cordova/exec", function(require, exports, module), 最终发现其流程是exec --> androidExec ----> 获取原生对象,并执行其 exec。原生对象对应的是SystemExposedJsApi, 其exec方法会调用CordovaBridge对象的jsExec方法,在CordovaBridge对象的jsExec方法中会去调用PluginManagerexec方法,最终由交给具体插件的execute方法去执行。

插件何时加载CordovaActivity中的loadConfig会取加载配置文件,然后获得插件的信息,最终会把这些信息保存到PluginManager实例的一个Map中,此时插件对象并没有被初始化。当前端调用到ExposedJsApi对象的方法时,PluginManager会去从那个Map查找对应的这plugin, 此时如果插件还未实例化,就利用反射实例化。

Cordova应用的启动

Cordova应用会首先启动主Activity, 在主Activity的 onCreate方法中会去做两件事:

  1. 调用父类(CordovaActivity)的onCreate方法
  2. 加载网页loadUrl(launchUrl);

先看第一步做的操作:

再看loadUrl做的操作(直接看loadUrl中的init方法):

  1. 调用makeWebView生成CordovaWebView:
  2. 通过调用CordovaWebViewImpl的构造方法生成一个CordovaWebView, 在调用CordovaWebViewImpl的构造方法时会调用makeWebViewEngine, 然后调用CordovaWebViewImpl.createEngine方法生成一个CordovaWebViewEngine对象,该对象的具体实现类是SystemWebViewEngine 。所以CordovaWebView中会持有一个CordovaWebViewEngine实例。
  3. 需要注意的是CordovaWebView和Android中的WebView没有什么关系,它只是一个接口。
  4. 调用createViewsCordovaWebView中的WebView视图设置相应的UI属性,同时调用setContentViewWebView设置到屏幕上。
  5. appView.init(cordovaInterface, pluginEntries, preferences): 初始化CordovaWebView
  6. 生成PluginManager
  7. 生成CordovaResourceApi
  8. 生成NativeToJsMessageQueue
  9. nativeToJsMessageQueue添加BridgeMode: 当前定义的只有两个BridgeMode, 分别是NativeToJsMessageQueue.NoOpBridgeMode, NativeToJsMessageQueue.LoadUrlBridgeMode 但是NoOpBridgeMode中的onNativeToJsMessageAvailable实现为空, 没有对消息进行处理,是因为在这种模式下会依赖前端js的轮询。
    • 执行CordovaWebViewEngine的初始化方法init:        在其实现类SystemWebViewEngine中往nativeToJsMessageQueue添加一个OnlineEventsBridgeMode, 至此共有三种不同的BridgeMode。
    • 调用PluginManageraddService方法
    • 调用PluginManagerinit方法
    • cordovaInterface.onCordovaInit(appView.getPluginManager()): 发送插件CoreAndroid resume事件。 到此处Cordova相关的组件已经准备就绪
    • 设置audio模式

      相关类的理解

    • CordovaBridge 在初始化SystemWebViewEngine的时候会构造一个CordovaBridge实例。前端js中的对象_cordovaNative对应的原生类是SystemExposedJsApi, 当前端在调用exec, setNativeToJsBridgeMode 等方法时,通过CordovaBridgejsExec, setNativeToJsBridgeMode, 再通过PluginManager定位到具体的插件上。
    • NativeToJsMessageQueue 用来保存将会发送到WebView的消息 每一个插件执行完后会调用CallbackContext 中的sendPluginResultsuccess, error方法,最终都会调用CordovaWebView中的sendPluginResult, 该方法中会调用nativeToJsMessageQueue.addPluginResult(cr, callbackId);, 将执行后的结果放到MessageQueue中。在NativeToJsMessageQueueenqueueMessage方法,其实现如下:
private void enqueueMessage(JsMessage message) {
   synchronized (this) {
      if (activeBridgeMode == null) {
          Log.d(LOG_TAG, "Dropping Native->JS message due to disabled bridge");
          return;
      }
      queue.add(message);
      if (!paused) {
          activeBridgeMode.onNativeToJsMessageAvailable(this);
      }
  }
}

当向队列中加一条消息的同时,也会触发activeBridgeMode.onNativeToJsMessageAvailable(this);操作。在onNativeToJsMessageAvailable中会有取消息,并向webView传递的操作。如LoadUrlBridgeMode

public void onNativeToJsMessageAvailable(final NativeToJsMessageQueue queue) {
   cordova.getActivity().runOnUiThread(new Runnable() {
       public void run() {
           String js = queue.popAndEncodeAsJs();
           if (js != null) {
               System.out.println("[Fred]javascript:" + js);
               engine.loadUrl("javascript:" + js, false);
           }
       }
   });
}

关于BridgeMode

BridgeMode分两种,一种是NativeToJsBridgeMode,另一种是JsToNativeBridgeMode.

NativeToJsBridgeMode

默认情况下是ONLINE_EVENT模式,在cordova.js中有这么一段代码:

nativeToJsModes = {
    // Polls for messages using the JS->Native bridge.
    POLLING: 0,
    // For LOAD_URL to be viable, it would need to have a work-around for
    // the bug where the soft-keyboard gets dismissed when a message is sent.
    LOAD_URL: 1,
    // For the ONLINE_EVENT to be viable, it would need to intercept all event
    // listeners (both through addEventListener and window.ononline) as well
    // as set the navigator property itself.
    ONLINE_EVENT: 2
},
jsToNativeBridgeMode,  // Set lazily.
nativeToJsBridgeMode = nativeToJsModes.ONLINE_EVENT,

可以解决loadUrl 隐藏键盘的问题:当你的焦点在输入,如果这时loadUrl调用js,会导致键盘隐藏 see

JsToNative

这部份内容参看这篇博文

从Java到JS通信有4种方式, 经常使用的是Polling方法和ONLINE_EVENT

webView的addJavascriptInterface可能会导致安全漏洞,因为js可能会包含恶意代码。解决方案

if (Build.VERSION.SDK_INT >= 11) {
    brwView.removeJavascriptInterface("searchBoxJavaBridge_");
    brwView.removeJavascriptInterface("accessibility");
    brwView.removeJavascriptInterface("accessibilityTraversal");
}

一篇和这个相关的文章

Cordova中的线程

Cordova中存在着主线程和WebCore两种类别线程,WebCore指的是子线程,插件的execute方法都执行在子线程中。我们来验证一下:

public class MainActivity extends CordovaActivity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        System.out.println("[Thread]: MainActivity " + Thread.currentThread().getName() + "  " + Thread.currentThread().getId());
        // Set by <content src="index_back.html" /> in config.xml
        loadUrl(launchUrl);
    }
}
public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException {
      this.callbackContext = callbackContext;
      System.out.println("[Thread]: camera " + Thread.currentThread().getName() + "  " + Thread.currentThread().getId());
      ...
}
@Override
public void loadUrl(final String url, boolean clearNavigationStack) {
    System.out.println("[Thread]: SystemWebViewEngine " + Thread.currentThread().getName() + "  " + Thread.currentThread().getId());
    webView.loadUrl(url);
}

最终运行时,发现打印的结果如下:

08-04 10:25:58.224 11711-11711/com.example.hello I/System.out: [Thread]: SystemWebViewEngine main  1
08-04 10:25:59.165 11711-11799/com.example.hello I/System.out: [Thread]: SystemExposedJsApi JavaBridge  9171
08-04 10:25:59.171 11711-11799/com.example.hello I/System.out: [Thread]: SystemExposedJsApi JavaBridge  9171
08-04 10:25:59.179 11711-11799/com.example.hello I/System.out: [Thread]: SystemExposedJsApi JavaBridge  9171
08-04 10:25:59.182 11711-11799/com.example.hello I/System.out: [Thread]: SystemExposedJsApi JavaBridge  9171
08-04 10:25:59.227 11711-11799/com.example.hello I/System.out: [Thread]: SystemExposedJsApi JavaBridge  9171
08-04 10:25:59.227 11711-11799/com.example.hello I/System.out: [Thread]: camera JavaBridge  9171

由此可以看到前端发一个请求到原生这边来是在子线程接收,Camera插件的execute方法也是执行在子线程中,执行完成后会调用loadUrl方法,而该方法是执行在主线程中的。此处的线程切换是在NativeToJsMessageQueue来做。在BridgeMode中的onNativeToJsMessageAvailable都会调用runOnUiThread将相关的操作放到主线程中去做。