本文只对JS与Native之间的交互进行源码阅读。至于Cordova如何开发插件等等,请参考Cordova官方文档:https://cordova.apache.org/docs/en/latest/
JS调用Native
流程图
- 流程图
解析
- index.html 调用
- 入参分别为
- successCallback:成功回调
- failCallback:失败回调
- service:类名
- action:方法名
- actionArgs:入参
- 入参分别为
-
Cordova.js
- cordova的定义
截取了主要部分: var cordova = { // callbackId初始值是随机生成的 callbackId: Math.floor(Math.random() * 2000000000), // 存储每次调用的callbackId对应的回调 callbacks: {}, // 状态 callbackStatus: {...}, callbackFromNative: function(callbackId, isSuccess, status, args, keepCallback) { var callback = cordova.callbacks[callbackId]; // 根据status进行callback调用 } }
- iOSExec()
function iOSExec() { var successCallback, failCallback, service, action, actionArgs; var callbackId = null; if (typeof arguments[0] !== 'string') { successCallback = arguments[0]; failCallback = arguments[1]; service = arguments[2]; action = arguments[3]; actionArgs = arguments[4]; // 设置默认callbackId callbackId = 'INVALID'; }else {...} if (successCallback || failCallback) { // cordova.callbackId使用后自增,callbackId实际上是一个类名带唯一id的String callbackId = service + cordova.callbackId++; // 存储对应的回调 cordova.callbacks[callbackId] = {success:successCallback, fail:failCallback}; } // 入参序列化 actionArgs = massageArgsJsToNative(actionArgs); // 包装成Command传入commandQueue,待native来取 var command = [callbackId, service, action, actionArgs]; commandQueue.push(JSON.stringify(command)); /* 这里的判断条件 isInContextOfEvalJs:在执行上下文,queue会在返回后自动刷新,无需继续执行(这个在下文ative的方法分析中可看出,每次执行结束仍然会继续掉executePending) commandQueue同理 */ if (!isInContextOfEvalJs && commandQueue.length == 1) { pokeNative(); } }
- 再看pokeNative的调用之前,先看一下里面将涉及的全局变量的定义说明
/** * Creates a gap bridge iframe used to notify the native code about queued * commands. */ <!-- 这里的注释已经说明的很详细了,创建一个不可见的iframe,用于通知Native调用队列 --> var cordova = require('cordova'), execIframe, commandQueue = [], // Contains pending JS->Native messages. isInContextOfEvalJs = 0
- pokeNative()
- 取关键代码进行说明
function pokeNative() { // Check if they've removed it from the DOM, and put it back if so. if (execIframe && execIframe.contentWindow) { execIframe.contentWindow.location = 'gap://ready'; } else { <!-- 创建不可见的iframe注入当前html,src为gap://ready,后面native会拦截scheme为gap的url 由于url加载,触发UIWebViewDelegate协议方法,进入Native调用--> execIframe = document.createElement('iframe'); execIframe.style.display = 'none'; execIframe.src = 'gap://ready'; document.body.appendChild(execIframe); } }
-
Native
-
开始说明Native代码前,先提前看下调用堆栈
-
CDVUIWebViewDelegate : NSObject <UIWebViewDelegate>
CDVUIWebViewDelegate实现了UIWebViewDelegate的代理方法,将请求转发给CDVUIWebViewNavigationDelegate,对请求进行拦截
- (BOOL)webView:(UIWebView*)webView shouldStartLoadWithRequest:(NSURLRequest*)request navigationType:(UIWebViewNavigationType)navigationType { BOOL shouldLoad = YES; <!-- _delegate为CDVUIWebViewNavigationDelegate,在CDVUIWebViewEngine-pluginInitialize中设置 --> if ([_delegate respondsToSelector:@selector(webView:shouldStartLoadWithRequest:navigationType:)]) { shouldLoad = [_delegate webView:webView shouldStartLoadWithRequest:request navigationType:navigationType]; } }
CDVUIWebViewNavigationDelegate
拦截scheme为gap的url
- (BOOL)webView:(UIWebView*)theWebView shouldStartLoadWithRequest:(NSURLRequest*)request navigationType:(UIWebViewNavigationType)navigationType { NSURL* url = [request URL]; CDVViewController* vc = (CDVViewController*)self.enginePlugin.viewController; /* * Execute any commands queued with cordova.exec() on the JS side. * The part of the URL after gap:// is irrelevant. */ // 拦截scheme为gap的url if ([[url scheme] isEqualToString:@"gap"]) { [vc.commandQueue fetchCommandsFromJs]; [vc.commandQueue executePending]; return NO; } // 除此下面还有些代码,给插件预留处理url的方法 // 在一些默认插件中有应用 }
-
CDVCommandQueue:命令队列
-
fetchCommandsFromJs
:获取调用的插件信息- (void)fetchCommandsFromJs { __weak CDVCommandQueue* weakSelf = self; // 获取调用的插件信息,此处会通过 NSString* js = @"cordova.require('cordova/exec').nativeFetchMessages()"; /* 此处会通过`webViewEngine-evaluateJavaScript:`执行cordova中`nativeFetchMessages`方法 实际上看下webViewEngine里的代码,具体实现就是`UIWebView-stringByEvaluatingJavaScriptFromString:`方法 */ [_viewController.webViewEngine evaluateJavaScript:js completionHandler:^(id obj, NSError* error) { if ((error == nil) && [obj isKindOfClass:[NSString class]]) { NSString* queuedCommandsJSON = (NSString*)obj; CDV_EXEC_LOG(@"Exec: Flushed JS->native queue (hadCommands=%d).", [queuedCommandsJSON length] > 0); [weakSelf enqueueCommandBatch:queuedCommandsJSON]; // this has to be called here now, because fetchCommandsFromJs is now async (previously: synchronous) [self executePending]; } }]; } // queuedCommandsJSON: // [["LocationPlugin859162834","LocationPlugin","location",[]]]
// Cordova.js nativeFetchMessages 关键代码 iOSExec.nativeFetchMessages = function() { var json = '[' + commandQueue.join(',') + ']'; commandQueue.length = 0; return json; };
-
enqueueCommandBatch
:将插件调用信息存在commandQueue中- (void)enqueueCommandBatch:(NSString*)batchJSON { if ([batchJSON length] > 0) { NSMutableArray* commandBatchHolder = [[NSMutableArray alloc] init]; [_queue addObject:commandBatchHolder]; // batchJSON长度小于特定值时,直接在主线程执行序列化-添加操作。否则在全局队列异步添加,并手动触发executePending操作 if ([batchJSON length] < JSON_SIZE_FOR_MAIN_THREAD) { [commandBatchHolder addObject:[batchJSON cdv_JSONObject]]; } else { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^() { NSMutableArray* result = [batchJSON cdv_JSONObject]; @synchronized(commandBatchHolder) { [commandBatchHolder addObject:result]; } [self performSelectorOnMainThread:@selector(executePending) withObject:nil waitUntilDone:NO]; }); } } }
-
executePending
:遍历执行commandQueue中的待执行插件
在看这部分代码之前,先来看一个涉及到的全局参数_startExecutionTime
,来标记开始遍历执行commandQueue的时间,看功用在类似一个标志位标记是否正在执行,以及作为对执行时长的控制//执行命令队列中待执行的插件,遍历执行queue - (void)executePending { // 正在执行,return if (_startExecutionTime > 0) { return; } @try { _startExecutionTime = [NSDate timeIntervalSinceReferenceDate]; // 遍历_queue while ([_queue count] > 0) { NSMutableArray* commandBatchHolder = _queue[0]; NSMutableArray* commandBatch = nil; @synchronized(commandBatchHolder) { // If the next-up command is still being decoded, wait for it. if ([commandBatchHolder count] == 0) { break; } commandBatch = commandBatchHolder[0]; } // 遍历commandBatch while ([commandBatch count] > 0) { // autoreleasepool的优化 @autoreleasepool { NSArray* jsonEntry = [commandBatch cdv_dequeue]; if ([commandBatch count] == 0) { [_queue removeObjectAtIndex:0]; } // 创建CDVInvokedUrlCommand命令对象,包含调用Native需要的一系列信息 CDVInvokedUrlCommand* command = [CDVInvokedUrlCommand commandFromJson:jsonEntry]; // 调用插件 [self execute:command] } // Yield if we're taking too long. if (([_queue count] > 0) && ([NSDate timeIntervalSinceReferenceDate] - _startExecutionTime > MAX_EXECUTION_TIME)) { [self performSelector:@selector(executePending) withObject:nil afterDelay:0]; return; } } } } @finally { _startExecutionTime = 0; } }
-
这里Yield if we’re taking too long部分是对执行时长的优化,当时间过长时,避免阻塞主线程,利用runloop特性来优化(这里涉及好多知识点啊…)
- 最长时间为1/60的一半。这里涉及针对掉帧的优化,需要考虑CPU处理时间及GPU的渲染时间,这里取一半也是为GPU渲染留足时间
static const double MAX_EXECUTION_TIME = .008; // Half of a 60fps frame.
performSelector:withObject:afterDelay
将执行方法注册到当前线程runloop的timer中,等待runloop被timer唤醒继续处理事务
- 最长时间为1/60的一半。这里涉及针对掉帧的优化,需要考虑CPU处理时间及GPU的渲染时间,这里取一半也是为GPU渲染留足时间
-
这里补充看下
CDVInvokedUrlCommand
命令对象
看代码应该很清晰了,无需解释@interface CDVInvokedUrlCommand : NSObject { NSString* _callbackId; NSString* _className; NSString* _methodName; NSArray* _arguments; }
-
-
execute:
:插件执行,调用Native代码
这部分代码很简单,看源码备注也很清晰,获取实例进行调用- (BOOL)execute:(CDVInvokedUrlCommand*)command { // Fetch an instance of this class CDVPlugin* obj = [_viewController.commandDelegate getCommandInstance:command.className]; // Find the proper selector to call. NSString* methodName = [NSString stringWithFormat:@"%@:", command.methodName]; SEL normalSelector = NSSelectorFromString(methodName); if ([obj respondsToSelector:normalSelector]) { // [obj performSelector:normalSelector withObject:command]; ((void (*)(id, SEL, id))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; } }
-
-