Cordova 工作原理(IOS篇)

Cordova 工作原理(IOS篇)

本文基于Cordova 6.2.0

Cordova作为Hybird的先驱者,假如有不熟悉的可以点击:Cordova官方文档

Architecture

cordova architecture

这是一张基于cordova的hybrid app架构图,官方拿的,之后的工作原理会结合这张图解释,大致就分为web端的JS工作原理以及native端的oc工作原理:

Web App端

  • config.xml
  • cordova.js核心代码分析

Native端

  • cordova webview 引擎具体实现
  • 容器初始化以及plugin初始化

Web App

config.xml

This container has a very crucial file - config.xml file that provides information about the app and specifies parameters affecting how it works, such as whether it responds to orientation shifts.

对于使用cordova cli初始化的web app 在主目录下会存在一个config.xml,其中包含了整个app的一些基本信息:比如appName、app入口文件、白名单、webview初始化的一些配置、plugin信息、图标资源信息

其中web app大致的目录结构可以参考如下:

myapp/
|-- config.xml
|-- hooks/
|-- merges/
| | |-- android/
| | |-- windows/
| | |-- ios/
|-- www/
|-- platforms/
| |-- android/
| |-- windows/
| |-- ios/
|-- plugins/
  |--cordova-plugin-camera/

cordova.js源码分析

以下源码基于platforms/ios/www/cordova.js

exec:
function iOSExec() {

var successCallback, failCallback, service, action, actionArgs;
var callbackId = null;
if (typeof arguments[0] !== 'string') {
    // FORMAT ONE
    successCallback = arguments[0];
    failCallback = arguments[1];
    service = arguments[2];
    action = arguments[3];
    actionArgs = arguments[4];

    // Since we need to maintain backwards compatibility, we have to pass
    // an invalid callbackId even if no callback was provided since plugins
    // will be expecting it. The Cordova.exec() implementation allocates
    // an invalid callbackId and passes it even if no callbacks were given.
    callbackId = 'INVALID';
} else {
    throw new Error('The old format of this exec call has been removed (deprecated since 2.1). Change to: ' +
        'cordova.exec(null, null, \'Service\', \'action\', [ arg1, arg2 ]);'
    );
}

// If actionArgs is not provided, default to an empty array
actionArgs = actionArgs || [];

// Register the callbacks and add the callbackId to the positional
// arguments if given.
if (successCallback || failCallback) {
    callbackId = service + cordova.callbackId++;
    cordova.callbacks[callbackId] =
        {success:successCallback, fail:failCallback};
}

actionArgs = massageArgsJsToNative(actionArgs);

var command = [callbackId, service, action, actionArgs];

// Stringify and queue the command. We stringify to command now to
// effectively clone the command arguments in case they are mutated before
// the command is executed.
commandQueue.push(JSON.stringify(command));

// If we're in the context of a stringByEvaluatingJavaScriptFromString call,
// then the queue will be flushed when it returns; no need for a poke.
// Also, if there is already a command in the queue, then we've already
// poked the native side, so there is no reason to do so again.
if (!isInContextOfEvalJs && commandQueue.length == 1) {
    pokeNative();
}
}

这是cordova ios中js端的核心执行代码,所有的plugin的执行入口

  • successCallback -- 成功的回调
  • failCallback -- 失败的回调
  • service -- 所调用native plugin的类
  • action -- 所调用native plugin的类下的具体method
  • actionArgs -- 具体参数

注册回调id,构建cordova.callbacks的map,其中key就是callbackId,value就是callBackFunction, 对具体参数做序列化,之后将 callbackId, service, action, actionArgs作为一个数组对象传入 commandQueue 等待native来获取

pokeNative:
function pokeNative() {
// CB-5488 - Don't attempt to create iframe before document.body is available.
if (!document.body) {
    setTimeout(pokeNative);
    return;
}

// 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 {
    execIframe = document.createElement('iframe');
    execIframe.style.display = 'none';
    execIframe.src = 'gap://ready';
    document.body.appendChild(execIframe);
}
// Use a timer to protect against iframe being unloaded during the poke (CB-7735).
// This makes the bridge ~ 7% slower, but works around the poke getting lost
// when the iframe is removed from the DOM.
// An onunload listener could be used in the case where the iframe has just been
// created, but since unload events fire only once, it doesn't work in the normal
// case of iframe reuse (where unload will have already fired due to the attempted
// navigation of the page).
failSafeTimerId = setTimeout(function() {
    if (commandQueue.length) {
        // CB-10106 - flush the queue on bridge change
        if (!handleBridgeChange()) {
            pokeNative();
         }
    }
}, 50); // Making this > 0 improves performance (marginally) in the normal case (where it doesn't fire).
}

js如何来通知native,调用native的方法呢,pokeNative就是提供这么一个方法 ,通过UIWebView相关的UIWebViewDelegate协议的拦截url (IOS7之后引入原生的Javascript Core之后有别的实现方式,这里暂不阐述),对js端发来的request做出响应,cordova使用的方法是创建一个iframe 并且设置iframe的src的方式来进行url的改变,之后所有的请求会根据是否已经存在这个iframe 来 通过改变location 的方式发起请求,避免前端的异步请求会创建多个iframe

nativeCallback nativeEvalAndFetch
iOSExec.nativeCallback = function(callbackId, status, message, keepCallback, debug) {
    return iOSExec.nativeEvalAndFetch(function() {
     var success = status === 0 || status === 1;
        var args = convertMessageToArgsNativeToJs(message);
        function nc2() {
         cordova.callbackFromNative(callbackId, success, status, args, keepCallback);
         }
         setTimeout(nc2, 0);
     });
};

iOSExec.nativeEvalAndFetch = function(func) {
// This shouldn't be nested, but better to be safe.
isInContextOfEvalJs++;
try {
    func();
    return iOSExec.nativeFetchMessages();
} finally {
    isInContextOfEvalJs--;
}
};

之后分析OC代码的时候会讲到,native这边处理完动作之后的触发回调的统一入口就是 nativeCallBack, 且是以同步的方式来触发native -> js 的callBack

callbackFromNative
cordova.callbackFromNative: function(callbackId, isSuccess, status, args, keepCallback) {
    try {
        var callback = cordova.callbacks[callbackId];
        if (callback) {
            if (isSuccess && status == cordova.callbackStatus.OK) {
                callback.success && callback.success.apply(null, args);
            } else if (!isSuccess) {
                callback.fail && callback.fail.apply(null, args);
            }
            /*
            else
                Note, this case is intentionally not caught.
                this can happen if isSuccess is true, but callbackStatus is NO_RESULT
                which is used to remove a callback from the list without calling the callbacks
                typically keepCallback is false in this case
            */
            // Clear callback if not expecting any more results
            if (!keepCallback) {
                delete cordova.callbacks[callbackId];
            }
        }
    }
    catch (err) {
        var msg = "Error in " + (isSuccess ? "Success" : "Error") + " callbackId: " + callbackId + " : " + err;
        console && console.log && console.log(msg);
        cordova.fireWindowEvent("cordovacallbackerror", { 'message': msg });
        throw err;
    }
}

这个就是js这边回调真正执行的地方,根据cordova.callBacks的map以及回调的callBackId 还有状态(success 或者 fail)来执行相应的回调函数,之后根据keepCallback来决定是否将该回调从callBacks的map中移除

Native

cordova webview 引擎具体实现

首先说说几个主要的类

  • cordovaLib.xcodeproj/Public/CDVViewController
  • cordovaLib.xcodepro/Private/Plugins/CDVUIWebViewEngine/*

CDVViewController

  • init --- 初始化程序,
  • loadSettings --- 解析config.xml 将pluginsMap startplugin settings startPage等变量初始化到容器controller中,初始化plugin字典
  • viewDidLoad --- 先loadSettings,之后创建特殊存储空,根据CDVUIWebViewEngine初始化Webview,然后获取appURL加载index.html

CDVUIWebViewEngine

  • initWithFrame --- 创建webview
  • pluginInitialize --- 初始化webView中的一系列设置,创建delegate(CDVUIWebViewDelegate)
  • getConmmandInstance --- 获取command的实例

初始化一系列的东西可以自己打个debug 看下源代码的流程下面看几个类的核心源代码

cordovaLib.xcodepro/Private/Plugins/CDVUIWebViewEngine/CDVUIWebViewNavigationDelegate

- (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.
 */
if ([[url scheme] isEqualToString:@"gap"]) {
    [vc.commandQueue fetchCommandsFromJs];
    // The delegate is called asynchronously in this case, so we don't have to use
    // flushCommandQueueWithDelayedJs (setTimeout(0)) as we do with hash changes.
    [vc.commandQueue executePending];
    return NO;
}

/*
 * Give plugins the chance to handle the url
 */
BOOL anyPluginsResponded = NO;
BOOL shouldAllowRequest = NO;

for (NSString* pluginName in vc.pluginObjects) {
    CDVPlugin* plugin = [vc.pluginObjects objectForKey:pluginName];
    SEL selector = NSSelectorFromString(@"shouldOverrideLoadWithRequest:navigationType:");
    if ([plugin respondsToSelector:selector]) {
        anyPluginsResponded = YES;
        shouldAllowRequest = (((BOOL (*)(id, SEL, id, int))objc_msgSend)(plugin, selector, request, navigationType));
        if (!shouldAllowRequest) {
            break;
        }
    }
}

if (anyPluginsResponded) {
    return shouldAllowRequest;
}

/*
 * Handle all other types of urls (tel:, sms:), and requests to load a url in the main webview.
 */
BOOL shouldAllowNavigation = [self defaultResourcePolicyForURL:url];
if (shouldAllowNavigation) {
    return YES;
} else {
    [[NSNotificationCenter defaultCenter] postNotification:[NSNotification notificationWithName:CDVPluginHandleOpenURLNotification object:url]];
}

return NO;
}

这个就是js通知native的所有入口,所有的js调用native都需要经过这个重新实现了uiwebView中的 UIWebWiewDelegate协议中的 shouldStartLoadWithRequest,对于所有的请求做一个拦截,对于url scheme中带有gap的都会执行 CDVViewController中的commandQueue

- (void)fetchCommandsFromJs
{
__weak CDVCommandQueue* weakSelf = self;
NSString* js = @"cordova.require('cordova/exec').nativeFetchMessages()";

[_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];
    }
}];
}

注意:我这里插一句,之前我看到有人说js native通信的用url拦截的方式不优雅,参数放在url中传递很不好之类的,很low,但是我想说的是你是没有看到设计的精髓。cordova中对于参数的传递,管理各个异步的js-native call 以及同步的执行callback。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 162,158评论 4 370
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,600评论 1 307
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 111,785评论 0 254
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,655评论 0 220
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,075评论 3 295
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 41,002评论 1 225
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,146评论 2 318
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,918评论 0 211
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,671评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,838评论 2 254
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,318评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,636评论 3 263
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,343评论 3 244
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,187评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,982评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,126评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,934评论 2 279

推荐阅读更多精彩内容