跨平台PhoneGap技術架構三:PhoneGap底層原理(中)


越來越發現把自己的思路理清楚並且能夠給別人講明白是件很困難的事情,技術博客不等於小說、散文,不能天馬行空,思維必須嚴謹,思路必須清晰。

寫技術博客要隨時審視自己的思路是不是還連貫,要不斷返回去看代碼,再從代碼理清思路。有時還要查找資料來把某個點解釋清楚,當然其中會有很多自己不太清楚的地方,所以也希望博友能一起交流、討論,給指出不正確的地方,博客確實是一個互相進步的很好的手段。

好了,不多說了,進入正題。

昨天講解了DroidGap類,DroidGap繼承Activity,在DroidGap的init()初始化方法中設置了WebView,今天主要講解CordovaWebView類以及CordovaWebviewClient類和CordovaChromeClient類。

CordovaWebView類

public class CordovaWebView extends WebView {...}

CordovaWebView類繼承WebView類,在上一篇博文中已經對WebView類做了簡要的介紹。PhoneGap針對不同平台的WebView做了擴展和封裝,使WebView這個組件變成可訪問設備本地API的強大瀏覽器,所以開發人員在PhoneGap框架下可通過JavaScript訪問設備本地API。

可以說,CordovaWebView是整個PhoneGap的核心組件

 1   /**
 2      * Constructor.
 3      *
 4      * @param context
 5      */
 6     public CordovaWebView(Context context) {
 7         super(context);
 8         if (CordovaInterface.class.isInstance(context))
 9         {
10             this.cordova = (CordovaInterface) context;
11         }
12         else
13         {
14             Log.d(TAG, "Your activity must implement CordovaInterface to work");
15         }
16         this.loadConfiguration();
17         this.setup();
18     }

CordovaWebView類的構造函數中,需要傳入Context,並且Context必須是CordovaInterface的實現類(這里需要特別注意)。構造函數里調用loadConfiguration()和Setup()方法。在CordovaWebView類的構造函數重載方法中,還有setWebChromeClient和setWebViewClient對CordovaWebView的設置,原因在上篇最后也有講到,這兩個類的具體講解在下面。

loadConfiguration
 1     /**
 2      * Load Cordova configuration from res/xml/cordova.xml.
 3      * Approved list of URLs that can be loaded into DroidGap
 4      *      <access origin="http://server regexp" subdomains="true" />
 5      * Log level: ERROR, WARN, INFO, DEBUG, VERBOSE (default=ERROR)
 6      *      <log level="DEBUG" />
 7      */
 8     private void loadConfiguration() {
 9         int id = getResources().getIdentifier("cordova", "xml", this.cordova.getActivity().getPackageName());
10          // int id = getResources().getIdentifier("cordova", "xml", this.cordova.getPackageName());
11         if (id == 0) {
12             LOG.i("CordovaLog", "cordova.xml missing. Ignoring...");
13             return;
14         }
15         XmlResourceParser xml = getResources().getXml(id);
16         int eventType = -1;
17         while (eventType != XmlResourceParser.END_DOCUMENT) {
18             if (eventType == XmlResourceParser.START_TAG) {
19                 String strNode = xml.getName();
20                 if (strNode.equals("access")) {
21                     String origin = xml.getAttributeValue(null, "origin");
22                     String subdomains = xml.getAttributeValue(null, "subdomains");
23                     if (origin != null) {
24                         this.addWhiteListEntry(origin, (subdomains != null) && (subdomains.compareToIgnoreCase("true") == 0));
25                     }
26                 }
27                 else if (strNode.equals("log")) {
28                     String level = xml.getAttributeValue(null, "level");
29                     LOG.i("CordovaLog", "Found log level %s", level);
30                     if (level != null) {
31                         LOG.setLogLevel(level);
32                     }
33                 }
34                 else if (strNode.equals("preference")) {
35                     String name = xml.getAttributeValue(null, "name");
36                     String value = xml.getAttributeValue(null, "value");
37 
38                     LOG.i("CordovaLog", "Found preference for %s=%s", name, value);
39                     Log.d("CordovaLog", "Found preference for " + name + "=" + value);
40 
41                     // Save preferences in Intent
42                     this.cordova.getActivity().getIntent().putExtra(name, value);
43                 }
44             }
45             try {
46                 eventType = xml.next();
47             } catch (XmlPullParserException e) {
48                 e.printStackTrace();
49             } catch (IOException e) {
50                 e.printStackTrace();
51             }
52         }
53 
54         // Init preferences
55         if ("true".equals(this.getProperty("useBrowserHistory", "false"))) {
56             this.useBrowserHistory = true;
57         }
58         else {
59             this.useBrowserHistory = false;
60         }
61 
62         if ("true".equals(this.getProperty("fullscreen", "false"))) {
63             this.cordova.getActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN);
64             this.cordova.getActivity().getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
65         }
66     }

loadConfiguration()的作用是從res/xml/cordova.xml文件中加載Cordova配置信息,包括允許加載本地網頁等,xml文件沒幾行,不細說。

setup
 1    /**
 2      * Initialize webview.
 3      */
 4     @SuppressWarnings("deprecation")
 5     private void setup() {
 6 
 7         this.setInitialScale(0);
 8         this.setVerticalScrollBarEnabled(false);
 9         this.requestFocusFromTouch();
10 
11         // Enable JavaScript
12         WebSettings settings = this.getSettings();
13         settings.setJavaScriptEnabled(true);
14         settings.setJavaScriptCanOpenWindowsAutomatically(true);
15         settings.setLayoutAlgorithm(LayoutAlgorithm.NORMAL);
16 
17         //Set the nav dump for HTC
18         settings.setNavDump(true);
19 
20         // Enable database
21         settings.setDatabaseEnabled(true);
22         String databasePath = this.cordova.getActivity().getApplicationContext().getDir("database", Context.MODE_PRIVATE).getPath();
23         settings.setDatabasePath(databasePath);
24 
25         // Enable DOM storage
26         settings.setDomStorageEnabled(true);
27 
28         // Enable built-in geolocation
29         settings.setGeolocationEnabled(true);
30 
31         //Start up the plugin manager
32         try {
33             this.pluginManager = new PluginManager(this, this.cordova);
34         } catch (Exception e) {
35             // TODO Auto-generated catch block
36             e.printStackTrace();
37         }
38     }

setup()方法初始化WebView配置,包括設置WebView初始化視圖大小、是否允許垂直滾動條、觸摸焦點信息。

因為PhoneGap的強大之處在於可以在Web端直接調用底層API,包括照相機、指南針、GPS等設備,其實現這些功能是通過JavaScript與底層交互實現的,所以在接下來設置settings.setJavaScriptEnabled(true)也就理所當然的事情了。

然后設置了數據庫database、DOM storage和地理位置應用geolocation等信息,這里也不細講。

需要着重提到的一點是最后PhoneGap插件的初始化。沒錯,PhoneGap插件的初始化是在這里進行的。PhoneGap插件管理機制是PhoneGap實現跨平台的基礎。pluginManager類在后面也會講到。

 

在上篇中講到了DroidGap類loadUrl()方法,我們可以把它看成是整個PhoneGap應用的入口函數。實際DroidGap類的loadUrl()方法調用了CordovaWebView類的loadUrl()方法:

loadUrl
 1    /**
 2      * Load the url into the webview.
 3      *
 4      * @param url
 5      */
 6     @Override
 7     public void loadUrl(String url) {
 8         if (url.equals("about:blank") || url.startsWith("javascript:")) {
 9             this.loadUrlNow(url);
10         }
11         else {
12 
13             String initUrl = this.getProperty("url", null);
14 
15             // If first page of app, then set URL to load to be the one passed in
16             if (initUrl == null || (this.urls.size() > 0)) {
17                 this.loadUrlIntoView(url);
18             }
19             // Otherwise use the URL specified in the activity's extras bundle
20             else {
21                 this.loadUrlIntoView(initUrl);
22             }
23         }
24     }

代碼不多,也比較好理解,自己看吧,不細講了。O(∩_∩)O~

loadUrlIntoView
 1     /**
 2      * Load the url into the webview.
 3      *
 4      * @param url
 5      */
 6     public void loadUrlIntoView(final String url) {
 7         LOG.d(TAG, ">>> loadUrl(" + url + ")");
 8 
 9         this.url = url;
10         if (this.baseUrl == null) {
11             int i = url.lastIndexOf('/');
12             if (i > 0) {
13                 this.baseUrl = url.substring(0, i + 1);
14             }
15             else {
16                 this.baseUrl = this.url + "/";
17             }
18 
19  //           this.pluginManager.init();
20 
21             if (!this.useBrowserHistory) {
22                 this.urls.push(url);
23             }
24         }
25 
26         // Create a timeout timer for loadUrl
27         final CordovaWebView me = this;
28         final int currentLoadUrlTimeout = me.loadUrlTimeout;
29         final int loadUrlTimeoutValue = Integer.parseInt(this.getProperty("loadUrlTimeoutValue", "20000"));
30 
31         // Timeout error method
32         final Runnable loadError = new Runnable() {
33             public void run() {
34                 me.stopLoading();
35                 LOG.e(TAG, "CordovaWebView: TIMEOUT ERROR!");
36                 if (viewClient != null) {
37                     viewClient.onReceivedError(me, -6, "The connection to the server was unsuccessful.", url);
38                 }
39             }
40         };
41 
42         // Timeout timer method
43         final Runnable timeoutCheck = new Runnable() {
44             public void run() {
45                 try {
46                     synchronized (this) {
47                         wait(loadUrlTimeoutValue);
48                     }
49                 } catch (InterruptedException e) {
50                     e.printStackTrace();
51                 }
52 
53                 // If timeout, then stop loading and handle error
54                 if (me.loadUrlTimeout == currentLoadUrlTimeout) {
55                     me.cordova.getActivity().runOnUiThread(loadError);
56                 }
57             }
58         };
59 
60         // Load url
61         this.cordova.getActivity().runOnUiThread(new Runnable() {
62             public void run() {
63                 Thread thread = new Thread(timeoutCheck);
64                 thread.start();
65                 me.loadUrlNow(url);
66             }
67         });
68     }

這里需要細講一下。Android WebView里並沒有對加載超時的處理,PhoneGap自己實現了加載超時處理的方法。雖然看不看這里並不會影響對PhoneGap整個機制的理解,但拿出來講一下對以后可能也會很有用處。

從代碼的第26行注釋看起,CordovaWebViewClient類有一個屬性LoadUrlTimeout,這里不要被其定義為int類型迷惑,實際這個變量完全可以是boolean布爾值,因為它只有兩個值:0和1。在頁面開始加載時currentLoadUrlTimeout初始化為me.loadUrlTimeout(這個值初始化為0),在頁面加載完成(注意:這里CordovaWebViewClient派上用場了),即CordovaWebViewClient類里OnpageFinished()方法中appView.LoadUrlTimeout被置成1。定義了loadError和runOnUiThread兩個線程。然后起了一個在Timeout之后執行的線程,具體執行那個線程就看頁面是否加載完成,即OnpageFinished()方法中appView.LoadUrlTimeout是否被置成了1。

 

CordovaWebViewClient類

CordovaWebViewClient類主要處理各種通知、請求事件的,重寫了WebView類的一些方法,這里主要講解其中onPageStarted()和onPageFinished()兩個方法。

onPageStarted
 1     /**
 2      * Notify the host application that a page has started loading. 
 3      * This method is called once for each main frame load so a page with iframes or framesets will call onPageStarted 
 4      * one time for the main frame. This also means that onPageStarted will not be called when the contents of an 
 5      * embedded frame changes, i.e. clicking a link whose target is an iframe. 
 6      * 
 7      * @param view          The webview initiating the callback.
 8      * @param url           The url of the page.
 9      */
10     @Override
11     public void onPageStarted(WebView view, String url, Bitmap favicon) {
12         // Clear history so history.back() doesn't do anything.
13         // So we can reinit() native side CallbackServer & PluginManager.
14         if (!this.appView.useBrowserHistory) {
15             view.clearHistory();
16             this.doClearHistory = true;
17         }
18 
19         // Create callback server and plugin manager
20         if (this.appView.callbackServer == null) {
21             this.appView.callbackServer = new CallbackServer();
22             this.appView.callbackServer.init(url);
23         }
24         else {
25             this.appView.callbackServer.reinit(url);
26         }
27 
28         // Broadcast message that page has loaded
29         this.appView.postMessage("onPageStarted", url);
30     }

從文檔注釋我們可以看出,onPageStarted方法用於通知主應用程序Web頁面開始加載了。其中在頁面加載時比較重要的一步是創建CallbackServer對象並初始化。(CallbackServer類可是實現插件異步執行的重頭戲啊,好緊張啊,寫了這么久CallbackServer終於出現了啊o(╯□╰)o)

onPageFinished
 1     /**
 2      * Notify the host application that a page has finished loading.
 3      * This method is called only for main frame. When onPageFinished() is called, the rendering picture may not be updated yet.
 4      * 
 5      *
 6      * @param view          The webview initiating the callback.
 7      * @param url           The url of the page.
 8      */
 9     @Override
10     public void onPageFinished(WebView view, String url) {
11         super.onPageFinished(view, url);
12         LOG.d(TAG, "onPageFinished(" + url + ")");
13 
14         /**
15          * Because of a timing issue we need to clear this history in onPageFinished as well as
16          * onPageStarted. However we only want to do this if the doClearHistory boolean is set to
17          * true. You see when you load a url with a # in it which is common in jQuery applications
18          * onPageStared is not called. Clearing the history at that point would break jQuery apps.
19          */
20         if (this.doClearHistory) {
21             view.clearHistory();
22             this.doClearHistory = false;
23         }
24 
25         // Clear timeout flag
26         this.appView.loadUrlTimeout++;
27 
28         // Try firing the onNativeReady event in JS. If it fails because the JS is
29         // not loaded yet then just set a flag so that the onNativeReady can be fired
30         // from the JS side when the JS gets to that code.
31         if (!url.equals("about:blank")) {
32             this.appView.loadUrl("javascript:try{ cordova.require('cordova/channel').onNativeReady.fire();}catch(e){_nativeReady = true;}");
33             this.appView.postMessage("onNativeReady", null);
34         }
35 
36         // Broadcast message that page has loaded
37         this.appView.postMessage("onPageFinished", url);
38 
39         // Make app visible after 2 sec in case there was a JS error and Cordova JS never initialized correctly
40         if (this.appView.getVisibility() == View.INVISIBLE) {
41             Thread t = new Thread(new Runnable() {
42                 public void run() {
43                     try {
44                         Thread.sleep(2000);
45                         cordova.getActivity().runOnUiThread(new Runnable() {
46                             public void run() {
47                                 appView.postMessage("spinner", "stop");
48                             }
49                         });
50                     } catch (InterruptedException e) {
51                     }
52                 }
53             });
54             t.start();
55         }
56 
57         // Shutdown if blank loaded
58         if (url.equals("about:blank")) {
59             if (this.appView.callbackServer != null) {
60                 this.appView.callbackServer.destroy();
61             }
62             appView.postMessage("exit", null);
63         }
64     }

上文已經提到在onPageFinished方法中會執行this.appView.loadUrlTimeout++,定時器loadUrlTimeout的值被置為1,表明頁面加載完成。如果沒有執行到此,說明加載超時,在CordovaWebView類的loadUrlIntoView()方法中判斷if(currentLoadUrlTimeout ==me.loadUrlTimeout),則執行loadError線程。

CordovaChromeClient類

WebChromeClient是輔助WebView處理Javascript的對話框,網站圖標,網站title,加載進度等,一般的WebView可以不設置ChromeClient,比如你的WebView不需要處理JavaScript腳本。前面已經提到為什么PhoneGap要設置ChromeClient,不再多說。

引用某位大俠的話說,關於Java/JS互調,在android sdk文檔中,也有用JsInterfaceloadUrl做到交互的示例。PhoneGap並沒有選擇用JsInterface,而是使用攔截prompt這種hack做法。

對JavaScript消息的攔截是在onJsPrompt()方法中實現並處理的。

onJsPrompt
 1     /**
 2      * Tell the client to display a prompt dialog to the user.
 3      * If the client returns true, WebView will assume that the client will
 4      * handle the prompt dialog and call the appropriate JsPromptResult method.
 5      *
 6      * Since we are hacking prompts for our own purposes, we should not be using them for
 7      * this purpose, perhaps we should hack console.log to do this instead!
 8      *
 9      * @param view
10      * @param url
11      * @param message
12      * @param defaultValue
13      * @param result
14      */
15     @Override
16     public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
17 
18         // Security check to make sure any requests are coming from the page initially
19         // loaded in webview and not another loaded in an iframe.
20         boolean reqOk = false;
21         if (url.startsWith("file://") || url.indexOf(this.appView.baseUrl) == 0 || this.appView.isUrlWhiteListed(url)) {
22             reqOk = true;
23         }
24 
25         // Calling PluginManager.exec() to call a native service using 
26         // prompt(this.stringify(args), "gap:"+this.stringify([service, action, callbackId, true]));
27         if (reqOk && defaultValue != null && defaultValue.length() > 3 && defaultValue.substring(0, 4).equals("gap:")) {
28             JSONArray array;
29             try {
30                 array = new JSONArray(defaultValue.substring(4));
31                 String service = array.getString(0);
32                 String action = array.getString(1);
33                 String callbackId = array.getString(2);
34                 boolean async = array.getBoolean(3);
35                 String r = this.appView.pluginManager.exec(service, action, callbackId, message, async);
36                 result.confirm(r);
37             } catch (JSONException e) {
38                 e.printStackTrace();
39             }
40         }
41 
42         // Polling for JavaScript messages 
43         else if (reqOk && defaultValue != null && defaultValue.equals("gap_poll:")) {
44             String r = this.appView.callbackServer.getJavascript();
45             result.confirm(r);
46         }
47 
48         // Do NO-OP so older code doesn't display dialog
49         else if (defaultValue != null && defaultValue.equals("gap_init:")) {
50             result.confirm("OK");
51         }
52 
53         // Calling into CallbackServer
54         else if (reqOk && defaultValue != null && defaultValue.equals("gap_callbackServer:")) {
55             String r = "";
56             if (message.equals("usePolling")) {
57                 r = "" + this.appView.callbackServer.usePolling();
58             }
59             else if (message.equals("restartServer")) {
60                 this.appView.callbackServer.restartServer();
61             }
62             else if (message.equals("getPort")) {
63                 r = Integer.toString(this.appView.callbackServer.getPort());
64             }
65             else if (message.equals("getToken")) {
66                 r = this.appView.callbackServer.getToken();
67             }
68             result.confirm(r);
69         }
70 
71         // Show dialog
72         else {
73             final JsPromptResult res = result;
74             AlertDialog.Builder dlg = new AlertDialog.Builder(this.cordova.getActivity());
75             dlg.setMessage(message);
76             final EditText input = new EditText(this.cordova.getActivity());
77             if (defaultValue != null) {
78                 input.setText(defaultValue);
79             }
80             dlg.setView(input);
81             dlg.setCancelable(false);
82             dlg.setPositiveButton(android.R.string.ok,
83                     new DialogInterface.OnClickListener() {
84                         public void onClick(DialogInterface dialog, int which) {
85                             String usertext = input.getText().toString();
86                             res.confirm(usertext);
87                         }
88                     });
89             dlg.setNegativeButton(android.R.string.cancel,
90                     new DialogInterface.OnClickListener() {
91                         public void onClick(DialogInterface dialog, int which) {
92                             res.cancel();
93                         }
94                     });
95             dlg.create();
96             dlg.show();
97         }
98         return true;
99     }

onJsPrompt到底攔截了哪些信息呢?

從傳入參數來說,比較重要的有兩個:message和defaultValue。

message字符串存放了插件的應用信息,如Camera插件的圖片質量、圖片是否可編輯,圖片返回類型等。

defaultValue字符串存放了插件信息:service(如Camera)、action(如getPicture())、callbackId、async等。

攔截到這些信息后就會調用this.appView.pluginManager.exec(service, action, callbackId, message, async)去執行。

當然onJsPrompt不只實現插件調用信息,還有JavaScript polling輪詢信息、CallbackServer調用信息等處理。

 

呃,今天寫到這里吧,歡迎大家批評指正,也可以相互交流。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM