本文设计到的源码是基于Cordova 4.2.1版本,Cordova官网。
CDVViewController
CDVViewController是Cordova最主要的类,它把所有模块整合在一起,直接初始化一个它的实例就可以使用。例如下面的代码:
1 | CDVViewController *vc = [[CDVViewController alloc] init]; |
CDVViewController主要实现的功能:
- 注册、初始化插件
- 读取、应用配置文件
- 初始化并配置WebView,设置其代理
- 管理js与原生的方法调用
管理应用与网页的生命周期
。。。它主要的属性有:
CDVWebViewEngineProtocol:webview相关的回调
- CDVCommandDelegate:js与原生插件交互方法,插件初始化
- CDVCommandQueue:命令执行队列
CDVCommandDelegate和CDVCommandQueue会在js调用原生插件与插件初始化提到,这里先不细说。CDVWebViewEngineProtocol定义了WebView引擎的抽象类,具体实现由插件提供,例如CDVUIWebViewEngine实现UIWebView的引擎。
CDVWebViewEngineProtocol协议定义
CDVWebViewEngineProtocol协议其实是对于WebView的一层封装,屏蔽了不同WebView接口的差异,现在iOS有UIWebView与WKWebView。
1 | @protocol CDVWebViewEngineProtocol <NSObject> |
engineWebView属性对外直接暴露了内部封装的WebView,其它方法都是对WebView方法的一层简单封装。
UIWebView引擎CDVUIWebViewEngine
我们以UIWebView的实现CDVUIWebViewEngine为例说明,它是以插件的形式实现的,主要作用是初始化UIWebView的配置,对UIWebView的方法和代理进行了一层封装。它实现了协议CDVWebViewEngineProtocol,主要有以下几个属性。
1 | // CDVUIWebViewEngine |
初始化从initWithFrame:方法开始,它创建了一个UIWebView,并赋值给了engineWebView,然后在插件初始化方法pluginInitialize中初始化UIWebView的代理和配置。
1 | - (void)pluginInitialize |
js调用原生插件解析
插件调用流程:
- js发起请求
gap:// - 实现
WebView的代理webView:shouldStartLoadWithRequest:navigationType:,拦截scheme为gap的请求 - 执行js方法
cordova.require('cordova/exec').nativeFetchMessages()获取需要执行的原生插件的信息(插件名,插件方法,回调ID,参数) - 将需要执行的原生插件信息放入命令队列等待执行
- 执行原生插件,并把结果回调给js
插件调用堆栈如图所示:

js请求拦截
CDVUIWebViewDelegate实现了UIWebView的webView:shouldStartLoadWithRequest:navigationType:代理,在页面加载前做一些处理。
1 | - (BOOL)webView:(UIWebView*)webView shouldStartLoadWithRequest:(NSURLRequest*)request navigationType:(UIWebViewNavigationType)navigationType |
拦截js调用原生插件请求的关键代码在CDVUIWebViewNavigationDelegate,它实现了CDVUIWebViewDelegate的代理,在CDVUIWebViewDelegate会把请求转发给它。
1 | - (BOOL)webView:(UIWebView*)theWebView shouldStartLoadWithRequest:(NSURLRequest*)request navigationType:(UIWebViewNavigationType)navigationType |
H5调用原生插件
Cordova调用原生插件的方式是通过拦截gap://的URL,然后执行js代码cordova.require('cordova/exec').nativeFetchMessages()获取参数,来实现调用。
我们来看关键代码:
1 | // CDVUIWebViewNavigationDelegate.m |
1 | - (void)fetchCommandsFromJs |
调用js方法cordova.require('cordova/exec').nativeFetchMessages(),获取调用的插件信息。
1 | // 插件信息示例 |
命令队列CDVCommandQueue
js的每次调用信息会封装被封装为一个命令CDVInvokedUrlCommand,CDVInvokedUrlCommand继承自NSObject,主要存储了下面的信息:
1 | // CDVInvokedUrlCommand |
CDVCommandQueue管理着所有的命令,实现了一个命令的队列。在js调用原生插件时,会调用CDVCommandQueue的enqueueCommandBatch:方法,将插件调用信息加到commandBatchHolder数组中,最后commandBatchHolder数组添加到CDVCommandQueue的queue。
1 | - (void)enqueueCommandBatch:(NSString*)batchJSON |
插件的执行由CDVCommandQueue管理,每个CDVViewController有自己的队列,有两个重要的成员变量。
1 | /* 二维数组,存储着所有插件调用的json */ |
executePending负责执行命令队列中待执行的插件,具体实现就是遍历执行二维数组queue。
1 | - (void)executePending |
用Runloop优化性能
Cordova对于插件的执行进行了优化,保证页面的流程度,运用了RunLoop,巧妙的将代码分割为多块分次执行,避免由于插件执行导致主线程阻塞,影响页面绘制,导致掉帧。具体代码如下:
1 | // CDVCommandQueue.m |
优化策略分析:
- 将队列中的插件分割为很多小块来执行
- 开始执行
executePending方法时,记录开始时间,每次执行完一个插件方法后,判断本次执行时间是否超过MAX_EXECUTION_TIME,如果没有超过,继续执行,如果超过了MAX_EXECUTION_TIME,调用performSelector:withObject:afterDelay,结束本次调用 - 如果要保证UI流畅,需要满足条件
CPU时间 + GPU时间 <= 1s/60, 为了给GPU留下足够的时间渲染,要尽量让CPU占用时间小于1s/60/2 - Runloop执行的流程如下图所示,系统在收到
kCFRunLoopBeforeWaiting(线程即将休眠)通知时,会触发一次界面的渲染,也就是在完成source0的处理后 source0在这里就是插件的执行代码,在kCFRunLoopBeforeWaiting通知之前,如果source0执行时间过长就会导致界面没有得到及时的刷新。- 函数
performSelector:withObject:afterDelay,会将方法注册到Timer,结束source0调用,开始渲染界面。界面渲染完成后,Runloop开始sleep,然后被timer唤醒又开始继续处理source0。

插件方法执行
方法最终的执行在方法execute:中,从command中取出要执行的插件类、方法、参数,然后执行方法。
1 | - (BOOL)execute:(CDVInvokedUrlCommand*)command |
原生回调js
原生方法执行完成后,会把结果返回给js,调用方法sendPluginResult:callbackId:,用CDVPluginResult来传递回调参数,用callbackId来区分是哪次调用(callbackId由js产生)。
1 | // CDVCommandDelegateImpl.m |
CDVPlugin注册与初始化
我们先看看配置文件中插件的定义:
1 | <!-- 定义插件名为HandleOpenUrl的插件 --> |
加载默认插件
在CDVViewController的viewDidLoad时,从Cordova的配置文件config.xml中,读取出需要默认加载的插件,遍历初始化。
CDVViewController中初始化默认插件代码。
1 | - (void)viewDidLoad { |
插件初始化
插件初始化的过程:
- 加载配置文件
config.xml - 根据插件名获取对应类名
- 根据类名从缓存中查找,如果命中直接返回
- 没有缓存重新创建一个实例,并写入缓存
插件初始化的入口是getCommandInstance,传入参数是插件名称,返回一个插件的实例对象。
1 | - (id)getCommandInstance:(NSString*)pluginName |
注册插件的关键方法registerPlugin:withClassName:
1 | - (void)registerPlugin:(CDVPlugin*)plugin withClassName:(NSString*)className |
Cordova还提供了插件名注册插件的方式,使用函数registerPlugin:withPluginName:,实现方式差不多,就不赘述了。
注册插件步骤
- 设置插件的
viewController和delegate - 将插件以
className为key放入pluginObjects中,pluginObjects是一个插件的缓存 - 调用插件的
pluginInitialize
插件销毁
插件销毁的时机是创建插件的CDVViewController释放的时候,因为插件实例被创建后被缓存map引用,对应的销毁代码。
1 | // CDVViewController.m |
小结
- 在
config.xml文件中配置插件,声明插件与类的映射关系,以及加载策略 - 插件的初始化时懒加载,除了
onload配置为YES的插件会默认加载,其它插件都是使用时加载 - 插件使用了两个Map来缓存,
pluginObjects建立了类名与插件实例对象的映射,pluginsMap建立了插件名与类名的映射。 - 在一个
CDVViewController中,同一个插件同时只会存在一个实例