phonegap原理分析-ios版

来源:互联网 发布:linux shell 嵌套 编辑:程序博客网 时间:2024/06/05 01:52

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: // iframe-based.        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];     /*       * 判断url的协议以"gap"开头       * 执行在js端调用cordova.exec()的command队列       * 注:这里的command表示js调用native       */     if ([[url scheme] isEqualToString:@"gap"]) {        /**         * 从js端拉取command,即存储在js端commandQueue数组中的数据          */        [_commandQueue fetchCommandsFromJs];        [_commandQueue executePending];        return NO;    }    ...}

看下CDVCommandQueue中的fetchCommandsFromJs方法与executePending方法中做的事。

CDVCommandQueue

- (void)fetchCommandsFromJs {     // 获取js端存储的command,并在native暂存     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);    // This occurs when there is are no win/fail callbacks for the call.    if ([@"INVALID" isEqualToString : callbackId]) {        return;    }    // This occurs when the callback id is malformed.    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

0 0
原创粉丝点击