Cordova工作原理(IOS篇)
本文基于Cordova 6.2.0
Cordova作为Hybird的先驱者,假如有不熟悉的可以点击:Cordova官方文档
建筑
科尔多瓦建筑
这是一张基于cordova的混合app架构图,官方拿的,之后的工作原理会结合这张图解释,大致就分为web端的JS工作原理以及native端的oc工作原理:
Web App端
- config.xml中
- cordova.js核心代码分析
本地端
- cordova webview引擎具体实现
- 容器初始化以及插件初始化
网络应用
config.xml中
此容器有一个非常关键的file- config.xml文件,该文件提供有关应用程序的信息,并指定影响其工作方式的参数,例如它是否响应方向转换。
对于使用cordova cli初始化的web app在主目录下会存在一个config.xml,其中包含了整个app的一些基本信息:比如appName,app入口文件,白名单,webview初始化的一些配置,插件信息,图标资源信息
其中web app大致的目录结构可以参考如下:
myapp/
|-- config.xml
|-- hooks/
|-- merges/
| | |-- android/
| | |-- windows/
| | |-- ios/
|-- www/
|-- platforms/
| |-- android/
| |-- windows/
| |-- ios/
|-- plugins/
|--cordova-plugin-camera/
cordova.js源码分析
以下源码基于平台/ 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端的核心执行代码,所有的插件的执行入口
- successCallback - 成功的回调
- failCallback - 失败的回调
- service - 所调用native plugin的类
- action - 所调用native plugin的类下的具体方法
- 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端发来的请求做出响应,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的地图以及回调的callBackId还有状态(成功或者失败)来执行相应的回调函数,之后根据keepCallback来决定是否将该回调从callBacks的地图中移除
本地人
cordova webview引擎具体实现
首先说说几个主要的类
- cordovaLib.xcodeproj /公共/ CDVViewController
- cordovaLib.xcodepro /个人/插件/ CDVUIWebViewEngine / *
CDVViewController
- init ---初始化程序,
- loadSettings ---解析config.xml将pluginsMap startplugin设置startPage等变量初始化到容器controller中,初始化插件字典
- viewDidLoad ---先loadSettings,之后创建特殊存储空,根据CDVUIWebViewEngine初始化Webview,然后获取appURL加载index.html
CDVUIWebViewEngine
- initWithFrame ---创建webview
- pluginInitialize ---初始化webView中的一系列设置,创建委托(CDVUIWebViewDelegate)
- getConmmandInstance ---获取命令的实例
初始化一系列的东西可以自己打个调试看下源代码的流程下面看几个类的核心源代码
cordovaLib.xcodepro /个人/插件/ 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];
}
}];
}
作者:神秘老王
链接:https://www.jianshu.com/p/364f9320da8b
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。