Cordova source learning (1) - JS calls Native

This article only reads the source code of the interaction between JS and Native. As for how Cordova develops plug-ins and so on, please refer to Cordova's official documentation: https://cordova.apache.org/docs/en/latest/

JS calls Native

Flow chart

  • Flow chart

analysis

  • index.html call
    • The participants were
      • successCallback: Successful callback
      • failCallback: Failed callback
      • service: class name
      • action: method name
      • Action Args: Enter
  • Cordova.js

    • The definition of cordova

      The main parts are intercepted:
      var cordova = {
          // Initial callbackId values are randomly generated
          callbackId: Math.floor(Math.random() * 2000000000),
          // Store callbackId corresponding to each call
          callbacks:  {},
          // state
          callbackStatus: {...},
      
          callbackFromNative: function(callbackId, isSuccess, status, args, keepCallback) {
              var callback = cordova.callbacks[callbackId];
              // callback calls based on status
          }
      }
      
    • 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];
    
            // Setting default callbackId
            callbackId = 'INVALID';
        }else {...}
    
        if (successCallback || failCallback) {
            // callbackId is actually a String with a unique id in the class name.
            callbackId = service + cordova.callbackId++;
            // Store corresponding callbacks
            cordova.callbacks[callbackId] =
                {success:successCallback, fail:failCallback};
        }
    
        // Entry serialization
        actionArgs = massageArgsJsToNative(actionArgs);
    
        // Wrapped as Command and passed into commandQueue for native to fetch
        var command = [callbackId, service, action, actionArgs];
        commandQueue.push(JSON.stringify(command));
    
        /* The judgment conditions here
        isInContextOfEvalJs: In the execution context, queue automatically refreshes after returning without further execution (as you can see from the method analysis below, executePending is still dropped at the end of each execution)
        commandQueue Empathy
        */
        if (!isInContextOfEvalJs && commandQueue.length == 1) {
            pokeNative();
        }
    }
    • Before we look at the pokeNative call, let's look at the definition of the global variables involved in it.
    /**
    * Creates a gap bridge iframe used to notify the native code about queued
    * commands.
    */
    <!-- The annotations here have been explained in great detail to create an invisible iframe,Used for notification Native Call queue -->
    var cordova = require('cordova'),
        execIframe,
        commandQueue = [], // Contains pending JS->Native messages.
        isInContextOfEvalJs = 0
    • pokeNative()
      • Take the key code and explain it.
    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 {
            <!-- Create invisible iframe Inject current html,src by gap://ready, the latter native intercepts the url whose scheme is gap 
            //Due to url loading, the UIWebViewDelegate protocol method is triggered to enter the Native call - >.
            execIframe = document.createElement('iframe');
            execIframe.style.display = 'none';
            execIframe.src = 'gap://ready';
            document.body.appendChild(execIframe);
        }
    }
  • Native

    • Before you start explaining Native code, look ahead at the call stack

    • CDVUIWebViewDelegate : NSObject <UIWebViewDelegate>
      CDVUI WebView Delegate implements the proxy method of UIWebView Delegate, forwards the request to CDVUI WebView Navigation Delegate, and intercepts the request.

    - (BOOL)webView:(UIWebView*)webView shouldStartLoadWithRequest:(NSURLRequest*)request navigationType:(UIWebViewNavigationType)navigationType
    {
        BOOL shouldLoad = YES;
        <!-- _delegate by CDVUIWebViewNavigationDelegate,stay CDVUIWebViewEngine-pluginInitialize Set in -->
        if ([_delegate respondsToSelector:@selector(webView:shouldStartLoadWithRequest:navigationType:)]) {
            shouldLoad = [_delegate webView:webView shouldStartLoadWithRequest:request navigationType:navigationType];
        }
    }
    • CDVUIWebViewNavigationDelegate
      Intercepting url with scheme as gap

      - (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.
          */
          // Intercepting url with scheme as gap
          if ([[url scheme] isEqualToString:@"gap"]) {
              [vc.commandQueue fetchCommandsFromJs];
              [vc.commandQueue executePending];
              return NO;
          }
      
          // In addition to this, there are some codes to reserve the way to process URLs for plug-ins.
          // Applications in some default plug-ins
      }
      
    • CDVCommandQueue: Command Queue

      • fetchCommandsFromJs: Get plug-in information for calls

        - (void)fetchCommandsFromJs
        {
            __weak CDVCommandQueue* weakSelf = self;
            // Get the plug-in information for the call, which is passed here
            NSString* js = @"cordova.require('cordova/exec').nativeFetchMessages()";
        
            /*
            The `nativeFetchMessages'method in cordova is executed through `webViewEngine-evaluateJavaScript'.
            In fact, look at the code in webView Engine. The implementation is `UIWebView-stringByEvaluating JavaScriptFromString:` Method
            */
            [_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 native FetchMessages key code
        iOSExec.nativeFetchMessages = function() {
            var json = '[' + commandQueue.join(',') + ']';
            commandQueue.length = 0;
            return json;
        };
      • enqueueCommandBatch: Put plug-in call information in commandQueue

        - (void)enqueueCommandBatch:(NSString*)batchJSON
        {
            if ([batchJSON length] > 0) {
                NSMutableArray* commandBatchHolder = [[NSMutableArray alloc] init];
                [_queue addObject:commandBatchHolder];
                // When the length of batch JSON is less than a specific value, the serialization-addition operation is performed directly in the main thread. Otherwise, add asynchronously to the global queue and manually trigger the executePending operation
                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: Traversing the pending plug-ins in the execution commandQueue
        Before looking at this part of the code, let's first look at the global parameter _startExecutionTime, which is involved to mark the time when the commandQueue is started to traverse, whether the function is being executed like a flag mark, and as a control over the execution time.

        //The plug-in to be executed in the execution command queue traverses the execution queue
        - (void)executePending
        {
            // Executing,
            if (_startExecutionTime > 0) {
                return;
            }
            @try {
                _startExecutionTime = [NSDate timeIntervalSinceReferenceDate];
        
                // Traversing _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];
                    }
        
                    // Traversing commandBatch
                    while ([commandBatch count] > 0) {
                        // Optimization of autorelease pool
                        @autoreleasepool {
                            NSArray* jsonEntry = [commandBatch cdv_dequeue];
                            if ([commandBatch count] == 0) {
                                [_queue removeObjectAtIndex:0];
                            }
        
                            // Create a CDVInvokedUrlCommand command object that contains a series of information needed to call Native
                            CDVInvokedUrlCommand* command = [CDVInvokedUrlCommand commandFromJson:jsonEntry];
        
                            // Call plug-in
                            [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;
            }
        }
        • Here the Yield if we're taking too long part is to optimize the execution time. When the execution time is too long, avoid blocking the main thread, and use the runloop feature to optimize. (There are many knowledge points involved here.) )

          • The longest time is half of 1/60. This involves the optimization of frame dropping. CPU processing time and GPU rendering time need to be considered. Half of the time taken here is enough time for GPU rendering.
            static const double MAX_EXECUTION_TIME = .008; // Half of a 60fps frame.
          • PerfmSelector: withObject: afterDelay registers the execution method into the timer of the current thread runloop, waiting for the runloop to be awakened by the timer to continue processing transactions
        • Here's a supplement to the CDVInvokedUrlCommand command object
          Looking at the code should be clear, without explanation.

          @interface CDVInvokedUrlCommand : NSObject {
              NSString* _callbackId;
              NSString* _className;
              NSString* _methodName;
              NSArray* _arguments;
          }
      • execute:: Plug-in executes, calling Native code
        This part of the code is very simple, the source notes are also very clear, get examples for invocation.

        - (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;
            }
        }

Keywords: JSON Apache less

Added by PHPSpirit on Thu, 16 May 2019 07:52:04 +0300