本文只对JS与Native之间的交互进行源码阅读。至于Cordova如何开发插件等等,请参考Cordova官方文档:https://cordova.apache.org/docs/en/latest/
JS调用Native
流程图
- 流程图
解析
- index.html 调用
Cordova.js
cordova的定义
1
2
3
4
5
6
7
8
9
10
11
12
13
14截取了主要部分:
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()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37function 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的调用之前,先看一下里面将涉及的全局变量的定义说明
1
2
3
4
5
6
7
8
9/**
* 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 = 0pokeNative()
- 取关键代码进行说明
1
2
3
4
5
6
7
8
9
10
11
12
13function 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,对请求进行拦截1
2
3
4
5
6
7
8- (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的url1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19- (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
:获取调用的插件信息1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24- (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",[]]]1
2
3
4
5
6// Cordova.js nativeFetchMessages 关键代码
iOSExec.nativeFetchMessages = function() {
var json = '[' + commandQueue.join(',') + ']';
commandQueue.length = 0;
return json;
};enqueueCommandBatch
:将插件调用信息存在commandQueue中1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19- (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的时间,看功用在类似一个标志位标记是否正在执行,以及作为对执行时长的控制1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50//执行命令队列中待执行的插件,遍历执行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
命令对象
看代码应该很清晰了,无需解释1
2
3
4
5
6@interface CDVInvokedUrlCommand : NSObject {
NSString* _callbackId;
NSString* _className;
NSString* _methodName;
NSArray* _arguments;
}
execute:
:插件执行,调用Native代码
这部分代码很简单,看源码备注也很清晰,获取实例进行调用1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17- (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;
}
}