如何理解苹果iOS版PhoneGap原理分析
PhoneGap,著名的跨平臺Hybrid框架,旨在讓開發者使用HTML、Javascript、CSS開發跨平臺的App。
最近的工作,就是做Hybrid方面的,很自然,方案就從PhoneGap入手。
下面就切入正題,分析下PhoneGap的原理,需要說明的是,我只針對iOS版本的PhoneGap做分析,android版本的原理大同小異。
安裝PhoneGap
現在使用PhoneGap非常方便,只需要安裝node,用簡單的命令就能完成安裝和使用的工作。
安裝PhoneGap:
sudo?npm?install?-g?phonegap創建phoneGap應用:
phonegap?create?my-appcd?my-app
phonegap?run?ios?
PhoneGap與Cordova的關系
Cordova是PhoneGap貢獻給Apache后的開源項目,是從PhoneGap中抽離出的核心代碼,是驅動PhoneGap的核心引擎。有點類似Webkit和Google Chrome的關系。
淵源就是:早在2011年10月,Adobe收購了Nitobi Software和它的PhoneGap產品,然后宣布這個移動Web開發框架將會繼續開源,并把它提交到Apache Incubator,以便完全接受ASF的管治。當然,由于Adobe擁有了PhoneGap商標,所以開源組織的這個PhoneGap v2.0版產品就更名為Apache Cordova。
為什么說這個?因為下面的文章中,會出現Cordova這個命令,大家不要覺得奇怪。
js與native通信的原理
但在切入正題前,需要先了解下iOS js與native通信的原理。了解這個原理,是理解PhoneGap代碼的關鍵。
?
js –> native
在iOS中,js調用native并沒有提供原生的實現,只能通過UIWebView相關的UIWebViewDelegate協議的
-?(BOOL)webView:(UIWebView?*)webView?shouldStartLoadWithRequest:(NSURLRequest?*)request?navigationType:(UIWebViewNavigationType)navigationType?方法來做攔截,并在這個方法中,根據url的協議或特征字符串來做調用方法或觸發事件等工作,如
/** 方法的返回值是BOOL值。
* 返回YES:表示讓瀏覽器執行默認操作,比如某個a鏈接跳轉
* 返回NO:表示不執行瀏覽器的默認操作,這里因為通過url協議來判斷js執行native的操作,肯定不是瀏覽器默認操作,故返回NO
* /
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
? ?NSURL *url = [request URL];
? ?if ([[url scheme] isEqualToString:@"callFunction") {
? ? ? ?//調用原生方法
? ? ? ?return NO;
? ?} else if (([[url scheme] isEqualToString:@"sendEvent") {
? ? ? ?//觸發事件
? ? ? ?return NO;
? ?} else {
? ? ? ?return YES;
? ?}
}
值得注意的是,通過這個方式,js調用native是異步的。
native –> js
native調用js非常簡潔方便,只需要
[webView?stringByEvaluatingJavaScriptFromString:@"alert('hello?world!')"];并且該方法是同步的。
native調用js非常簡單直接,所以PhoneGap解決的主要是js調用native的問題。
PhoneGap js –> native
我們通過一個js調用native的Dialog的例子做說明。
Dialog是一個PhoneGap的插件,可以看dialog 插件文檔,學習下載并使用該插件。
這里有個很重要的事需要說明一下:?目前PhoneGap的文檔更新非常不及時,特別是插件的使用方面,比如Dialog插件的使用,文檔中寫的是使用navigator.notification.alert,但是經過我的摸索,因為現在PhoneGap使用AMD的方式來管理插件,所以應該是使用cordova.require("cordova/plugin/notification").alert的方式來調用。?插件的合并方面,也有很多坑,主要是文檔不全?-?-|||js部分
在html上添加一個button,然后通過下列代碼調用:
function alertDismissed() {? ?// do something
}
function showAlert() {
? ?cordova.require("cordova/plugin/notification").alert(
? ? ? ?'You are the winner!', ?// message
? ? ? ?alertDismissed, ? ? ? ? // callback
? ? ? ?'Game Over', ? ? ? ? ? ?// title
? ? ? ?'Done' ? ? ? ? ? ? ? ? ?// buttonName
? ?);
}
再看下對應的cordova/plugin/notification的代碼:
var exec = cordova.require('cordova/exec');var platform = cordova.require('cordova/platform');
module.exports = {
? ?/**
? ? * Open a native alert dialog, with a customizable title and button text.
? ? *
? ? * @param {String} message ? ? ? ? ? ? ?Message to print in the body of the alert
? ? * @param {Function} completeCallback ? The callback that is called when user clicks on a button.
? ? * @param {String} title ? ? ? ? ? ? ? ?Title of the alert dialog (default: Alert)
? ? * @param {String} buttonLabel ? ? ? ? ?Label of the close button (default: OK)
? ? */
? ?alert: function(message, completeCallback, title, buttonLabel) {
? ? ? ?var _title = (title || "Alert");
? ? ? ?var _buttonLabel = (buttonLabel || "OK");
? ? ? ?exec(completeCallback, null, "Notification", "alert", [message, _title, _buttonLabel]);
? ?}
}
....
可以看到alert最終其實是調用了exec方法來調用native代碼的,exec方法非常關鍵,是PhoneGap js調用native的核心代碼。
然后在源碼中搜索exec對應的cordova/exec,查看exec方法的源碼。
因為對應的cordova/exec源碼非常長,我只能截取最關鍵的代碼并做說明:
define("cordova/exec", function(require, exports, module) {? ?...
? ?function iOSExec() {
? ? ? ?...
? ? ? ?var successCallback, failCallback, service, action, actionArgs, splitCommand;
? ? ? ?var callbackId = null;
? ? ? ?...
? ? ? ?// 格式化傳入參數
? ? ? ?successCallback = arguments[0]; //成功的回調函數
? ? ? ?failCallback = arguments[1]; ? ?//失敗的回調函數
? ? ? ?service = arguments[2]; ? ? ? ? //表示調用native類的類名
? ? ? ?action = arguments[3]; ? ? ? ? ?//表示調用native類的一個方法
? ? ? ?actionArgs = arguments[4]; ? ? ?//參數
? ? ? ?//默認callbackId為'INVALID',表示不需要回調
? ? ? ?callbackId = 'INVALID';
? ? ? ?...
? ? ? ?//如果傳入參數有successCallback或failCallback,說明需要回調,就設置callbackId,并存儲對應的回調函數
? ? ? ?if (successCallback || failCallback) {
? ? ? ? ? ?callbackId = service + cordova.callbackId++;
? ? ? ? ? ?cordova.callbacks[callbackId] =
? ? ? ? ? ? ? ?{success:successCallback, fail:failCallback};
? ? ? ?}
? ? ? ?//格式化傳入的service、action、actionArgs,并存儲,準備native代碼來調用
? ? ? ?actionArgs = massageArgsJsToNative(actionArgs);
? ? ? ?var command = [callbackId, service, action, actionArgs];
? ? ? ?commandQueue.push(JSON.stringify(command));
? ? ? ?...
? ? ? ?//通過創建一個iframe并設置src,給native代碼一個指令,開始執行js調用native的過程
? ? ? ?execIframe = execIframe || createExecIframe();
? ? ? ?if (!execIframe.contentWindow) {
? ? ? ? ? ?execIframe = createExecIframe();
? ? ? ?}
? ? ? ?execIframe.src = "gap://ready";
? ? ? ?...
? ?}
? ?module.exports = iOSExec;
});
為了調用native方法,exec方法做了大量初始化的工作,這么做的原因,還是因為iOS沒有提供直接的方法來執行js調用native,不能把參數直接傳遞給native,所以只能通過js端存儲對應操作的所有參數,然后通過指令來讓native代碼來回調的方式間接完成。
native部分
之后,就走到了native代碼的部分。
CDVViewController
前面js通過創建一個iframe并發送gap://ready這個指令來告訴native開始執行操作。native中對應的操作在CDVViewController.m文件中的webView:shouldStartLoadWithRequest:navigationType:方法:
- (BOOL)webView:(UIWebView*)theWebView shouldStartLoadWithRequest:(NSURLRequest*)request navigationType:(UIWebViewNavigationType)navigationType{
? ?NSURL* url = [request URL];
? ?/*
? ? * 判斷url的協議以"gap"開頭
? ? * 執行在js端調用cordova.exec()的command隊列
? ? * 注:這里的command表示js調用native
? ? */
? ?if ([[url scheme] isElaqualToString:@"gap"]) {
? ? ? //_commandQueue即CDVCommandQueue類
? ? ? ?//從js端拉取command,即存儲在js端commandQueue數組中的數據
? ? ? ?[_commandQueue fetchCommandsFromJs];
? ? ? ?//開始執行command
? ? ? ?[_commandQueue executePending];
? ? ? ?return NO;
? ?}
...
}
到這里,其實已經走完js調用native的主要過程了。
之后,讓我們再看下CDVCommandQueue中的fetchCommandsFromJs方法與executePending方法中做的事。
CDVCommandQueue
- (void)fetchCommandsFromJs{
? ?// 獲取js端存儲的command,并在native暫存
? ?NSString* queuedCommandsJSON = [_viewController.webView stringByEvaluatingJavaScriptFromString:
? ? ? ?@"cordova.require('cordova/exec').nativeFetchMessages()"];
? ?[self enqueueCommandBatch:queuedCommandsJSON];
}
fetchCommandsFromJs方法非常簡單,不細說了。
executePending方法稍微復雜些,因為js是單線程的,而iOS是典型的多線程,所以executePending方法做的工作主要是讓command一個一個執行,防止線程問題。
executePending方法其實與之后的execute方法緊密相連,這里一起列出,只保留關鍵代碼:
- (void)executePending{
? ?...
? ?//_queue即command隊列,依次執行
? ?while ([_queue count] > 0) {
? ? ? ?...
? ? ? ?//取出從js中獲取的command字符串,解析為native端的CDVInvokedUrlCommand類
? ? ? ?CDVInvokedUrlCommand* command = [CDVInvokedUrlCommand commandFromJson:jsonEntry];
? ? ? ?...
? ? ? ?//執行command
? ? ? ?[self execute:command])
? ? ? ?...
? ?}
}
- (BOOL)execute:(CDVInvokedUrlCommand*)command
{
? ?...
? ?BOOL retVal = YES;
? ?//獲取plugin對應的實例
? ?CDVPlugin* obj = [_viewController.commandDelegate getCommandInstance:command.className];
? ?//調用plugin實例的方法名
? ?NSString* methodName = [NSString stringWithFormat:@"%@:", command.methodName];
? ?SEL normalSelector = NSSelectorFromString(methodName);
? ?if ([obj respondsToSelector:normalSelector]) {
? ? ? ?//消息發送,執行plugin實例對應的方法,并傳遞參數
? ? ? ?objc_msgSend(obj, normalSelector, command);
? ?} else {
? ? ? ?// There's no method to call, so throw an error.
? ? ? ?NSLog(@"ERROR: Method '%@' not defined in Plugin '%@'", methodName, command.className);
? ? ? ?retVal = NO;
? ?}
? ?...
? ?return retVal;
}
可以看到js調用native plugin最終執行的是objc_msgSend(obj, normalSelector, command);這塊代碼,這里我們再拿js端的代碼來進行理解。
之前js中的showAlert方法中我們書寫了 exec(completeCallback, null, "Notification", "alert", [message, _title, _buttonLabel]);
故,這里的對應關系:
obj:“Notification”
normalSelector:“alert”
command:[message, title, buttonLabel]
CDVNotification
“Notification”真正對應的iOS類是CDVNotification。js端調用的插件名字”Notification”與真正的native類名并非完全對應,因為native因為平臺的不同,有不同的命名規范。
看下CDVNotification的代碼:
- (void)alert:(CDVInvokedUrlCommand*)command{
? ?NSString* callbackId = command.callbackId;
? ?NSString* message = [command argumentAtIndex:0];
? ?NSString* title = [command argumentAtIndex:1];
? ?NSString* buttons = [command argumentAtIndex:2];
? ?[self showDialogWithMessage:message title:title buttons:@[buttons] defaultText:nil callbackId:callbackId dialogType:DIALOG_TYPE_ALERT];
}
前面用objc_msgSend(obj, normalSelector, command);做消息發送,執行的便是這塊代碼,代碼很好理解,就是對command再做解析,并顯示。
最終效果:
點擊”Done”,native會再回調執行js端的成功回調,這里對應的就是js里設置的alertDismissed方法。
到此為止,我們已經走完從js端調用native alert的全部過程了。
列下過程的核心代碼:
js部分:cordova.js中的iOSExec()方法,指定js調用native的初始化工作,并發送開始執行的指令
native部分:CDVViewController:攔截js調用native的url協議,執行調用;CDVCommandQueue:執行js調用native的隊列,調用對應的plugin
時序圖
以上Dialog例子中,PhoneGap js調用native的時序圖:?
?
結語
PhoneGap還是很給力的,能做到主流平臺全兼容著實不容易。
iOS端因為沒有提供js調用native的直接方法,做的處理也算合理到位。
特別是插件化的支持做的很好,但是文檔著實不夠給力。
轉載于:https://www.cnblogs.com/lxf1016/p/4753938.html
總結
以上是生活随笔為你收集整理的如何理解苹果iOS版PhoneGap原理分析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: c++之带默认形参值的函数
- 下一篇: 你写的前端到底用没用到这些