phonegap的主要亮点就是它能通过js与native进行通信,一个基本的phonegap插件的执行过程一般是这样:
js接口 --> native代码 --> js回调
所以要明白phonegap的原理,就要弄明白两件事:(1)native如何调用js,(2)js如何调用native
1. native调用js
native调用js非常简洁方便,只需要
[webView stringByEvaluatingJavascriptFromString:@"alert('hello world!')"];
并且该方法是同步的。所以phonegap解决的主要是js调用native的问题。
2. js调用native
phonegap原理的主要难点就是js如何调用native,下面我以项目中用到的一个插件为例,通过设置断点的方法一步步理解:
(1).页面上调用插件的js代码
navigator.fixedInput.showAndFocus(function(content){
alert(content);
}, 'hello world', '发送');
第一个参数为回调函数
(2).通过断点我们可以知道,js后面执行了这个方法
exec(sendCallback, null, "FixedInput", "showAndFocus", [defaultVal, btnText]);
跳入exec函数,继续执行
(3).断点执行到了cordova.js的iOSExec函数,该函数主要是获取调用参数,进队列,选择调用模式。重要的是这段代码:
switch (bridgeMode) {
case jsToNativeModes.XHR_NO_PAYLOAD:
case jsToNativeModes.XHR_WITH_PAYLOAD:
case jsToNativeModes.XHR_OPTIONAL_PAYLOAD:
pokeNativeViaXhr();
break;
default:
pokeNativeViaIframe();
}
从上面的代码,我们知道cordova会采用以下2种方式的一种,来与ios native交互
通过iframe
cordova.exec往当前的html中插入一个不可见的iframe,从而向UIWebView请求加载一个特殊的URL,这个URL里包含了要调用的native plugin的类名,方法名,参数,回调函数等信息。接下来,由于被请求加载URL,于是UIWebViewDelegate的这个方法被调用:
- (BOOL)webView:(UIWebView*)theWebView shouldStartLoadWithRequest:(NSURLRequest*)request navigationType:(UIWebViewNavigationType)navigationType
这样就进入了native侧代码,通过request参数就拿到了js端传过来的信息,然后调用到native plugin
通过XHR
cordova.exec里直接发起一个XHR请求,被native侧的NSURLProtocol拦截,于是调用native的这个方法:
+ (BOOL)canInitWithRequest:(NSURLRequest*)theRequest
也进入了native侧代码,然后以同样的方式调用到native plugin
在2种方式中,cordova会优先选择XHR方式,只有当XHR方式不可用时,才会使用iframe的方式。
native代码
下面以iframe方式为例,看native的执行过程
我们知道js会创建一个iframe并发送gap://ready这个指令来告诉native开始执行操作。首先是CDVViewController的shouldStartLoadWithRequest方法
CDVViewController
- (BOOL)webView:(UIWebView*)theWebView shouldStartLoadWithRequest:(NSURLRequest*)request navigationType:(UIWebViewNavigationType)navigationType
{
NSURL* url = [request URL];
if ([[url scheme] isEqualToString:@"gap"]) {
/**
* 从js端拉取command,即存储在js端commandQueue数组中的数据
*/
[_commandQueue fetchCommandsFromJs];
[_commandQueue executePending];
return NO;
}
...
}
看下CDVCommandQueue中的fetchCommandsFromJs方法与executePending方法中做的事。
CDVCommandQueue
- (void)fetchCommandsFromJs
{
NSString* queuedCommandsJSON = [_viewController.webView stringByEvaluatingJavascriptFromString:
@"cordova.require('cordova/exec').nativeFetchMessages()"];
[self enqueueCommandBatch:queuedCommandsJSON];
}
etchCommandsFromJs方法比较简单,就是从js端队列取出一条command,并转成cordova格式的对象。
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);这块代码,设置断点看下参数
可以看到里面的参数都是都是与插件有关的,代码接着会执行具体的插件代码,路由过程我们就不去看了,代码最终执行了FixedInput.m里的showAndFocus方法
最后的运行效果:
还有一个问题,就是我们设置了回调函数,那么我们点击发送的时候,怎么将内容通过回调函数传给js呢?
看这段代码:
-(void) sendContent {
NSString *inputText = textField.text;
CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString : inputText];
[pluginResult setKeepCallback:[NSNumber numberWithBool:YES]];
[self.commandDelegate sendPluginResult:pluginResult callbackId:showCommand.callbackId];
[textField resignFirstResponder];
textField.text = @"";
}
我们给按钮绑定了这个函数,我们可以看到,这个方法通过下面的代码调用回调函数
[self.commandDelegate sendPluginResult:pluginResult callbackId:showCommand.callbackId]
继续跟踪代码:
- (void)sendPluginResult:(CDVPluginResult*)result callbackId:(NSString*)callbackId
{
CDV_EXEC_LOG(@"Exec(%@): Sending result. Status=%@", callbackId, result.status);
if ([@"INVALID" isEqualToString : callbackId]) {
return;
}
if (![self isValidCallbackId:callbackId]) {
NSLog(@"Invalid callback id received by sendPluginResult");
return;
}
int status = [result.status intValue];
BOOL keepCallback = [result.keepCallback boolValue];
NSString* argumentsAsJSON = [result argumentsAsJSON];
NSString* js = [NSString stringWithFormat:@"cordova.require('cordova/exec').nativeCallback('%@',%d,%@,%d)", callbackId, status, argumentsAsJSON, keepCallback];
[self evalJsHelper:js];
}
这段代码整合参数拼凑出一句js代码,最后通过evalJsHelper:js执行,设置断点我们可以知道最后js变量的内容为:
cordova.require('cordova/exec').nativeCallback('FixedInput158086966',1,\n \"hello world\"\n,1)
所以最后native会调用cordova.require(‘cordova/exec’).nativeCallback()执行我们设置的回调函数。
phonegap ios版的原理分析就到此,有理解不到位的地方欢迎指出,一起探讨。^_^
参考文章:
http://itindex.net/detail/50630-cordova-ios-native
http://www.cocoachina.com/industry/20140623/8919.html