Cordova源码学习(一)-JS调用Native

38 篇文章 1 订阅
2 篇文章 0 订阅

本文只对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唤醒继续处理事务
        • 这里补充看下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;
            }
        }
        
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值