这两年一直在作Cordova工程的项目,目前咱们基于Cordova的jsBridge进行两端的交互,经过加载本地JS优化渲染时间和白屏问题,Cordova给咱们带来了交互的插件化,可配置等优势,能够说Cordova为咱们进行Hybrid应用的构建提供了一个优秀的平台,总结一下Cordova实现,下面主要基于native端部分的源代码进行一下分析和学习,本篇不具体分析Cordova的源代码,旨在总结Cordova的编码思想。html
cordova入口web
- (void)viewDidLoad
{
[super viewDidLoad];
1.加载配置在config.xml中的配置文件,具体作了哪些下面分析。
// Load settings
[self loadSettings];
2.这一块主要是对cordova的一些配置
NSString* backupWebStorageType = @"cloud"; // default value
id backupWebStorage = [self.settings cordovaSettingForKey:@"BackupWebStorage"];
if ([backupWebStorage isKindOfClass:[NSString class]]) {
backupWebStorageType = backupWebStorage;
}
[self.settings setCordovaSetting:backupWebStorageType forKey:@"BackupWebStorage"];
[CDVLocalStorage __fixupDatabaseLocationsWithBackupType:backupWebStorageType];
// // Instantiate the WebView ///////////////
3.配置Cordova的Webview,具体怎么配置的下面分析
if (!self.webView) {
[self createGapView];
}
4.对config.xml文件中,配置了onload为true的插件提早加载
if ([self.startupPluginNames count] > 0) {
[CDVTimer start:@"TotalPluginStartup"];
for (NSString* pluginName in self.startupPluginNames) {
[CDVTimer start:pluginName];
[self getCommandInstance:pluginName];
[CDVTimer stop:pluginName];
}
[CDVTimer stop:@"TotalPluginStartup"];
}
// /////////////////
5.配置url
NSURL* appURL = [self appUrl];
6.配置webView的userAgent,加锁,加载url
[CDVUserAgentUtil acquireLock:^(NSInteger lockToken) {
...
加载url的代码省略了
...
}];
}
复制代码
viewDidload里面已经将整个调用过程走完了,到这里咱们也就可以看到咱们在cordova上面加载的页面了,因此咱们在使用的时候能够直接继承自CDVViewController来实现咱们本身的逻辑,而后咱们对这一过程进行逐步分析,viewDidload里面究竟具体作了哪些工做。objective-c
首先加载配置文件,仍是看代码:编程
- (void)loadSettings
{
1.config.xml配置文件解析具体实现类
CDVConfigParser* delegate = [[CDVConfigParser alloc] init];
[self parseSettingsWithParser:delegate];
2.将解析后的结果给self,也就是CDVViewController,其中pluginsMap的存储全部咱们在xml中配置的插件字典,
key为咱们配置的feature,value为插件类名。startupPluginNames存储了咱们全部配置了onload为true的插件,用来干吗的后面说,
settings存储了咱们在xml中对web的一些配置,后续也会用到。
// Get the plugin dictionary, whitelist and settings from the delegate.
self.pluginsMap = delegate.pluginsDict;
self.startupPluginNames = delegate.startupPluginNames;
self.settings = delegate.settings;
3.默认wwwFolderName为www,wwwFolderName干什么用后面会说。
// And the start folder/page.
if(self.wwwFolderName == nil){
self.wwwFolderName = @"www";
}
4.startPage外面有没有设置,若是没有设置就在xml里面取,
若是配置文件没有配置默认为index.html。
if(delegate.startPage && self.startPage == nil){
self.startPage = delegate.startPage;
}
if (self.startPage == nil) {
self.startPage = @"index.html";
}
// Initialize the plugin objects dict.
self.pluginObjects = [[NSMutableDictionary alloc] initWithCapacity:20];
}
复制代码
初始化咱们在config.xml配置的类名、插件提早加载仍是使用的时候再建立等信息。json
配置Cordova的webview,这一块比较重要着重分析。设计模式
- (UIView*)newCordovaViewWithFrame:(CGRect)bounds
{
1.默认的webView抽象类,实际上CDVViewController中是没有webView的具体实现等代码的,
他们的实现都是在这个抽象类里面。固然这个抽象类也能够咱们本身去配置,而后在咱们本身的抽象类里面去作具体实现,
好比说咱们如今项目使用的是UIWebView那么就彻底可使用框架内不提供的默认实现,若是咱们升级WKWebView,就能够直接修改了。
NSString* defaultWebViewEngineClass = @"CDVUIWebViewEngine";
NSString* webViewEngineClass = [self.settings cordovaSettingForKey:@"CordovaWebViewEngine"];
if (!webViewEngineClass) {
webViewEngineClass = defaultWebViewEngineClass;
}
2.寻找咱们配置的webView
if (NSClassFromString(webViewEngineClass)) {
self.webViewEngine = [[NSClassFromString(webViewEngineClass) alloc] initWithFrame:bounds];
3.若是webEngine返回nil,没有遵循protocol,不能加载配置的url,知足其一,都会加载框架默认的。
// if a webView engine returns nil (not supported by the current iOS version) or doesn't conform to the protocol, or can't load the request, we use UIWebView
if (!self.webViewEngine || ![self.webViewEngine conformsToProtocol:@protocol(CDVWebViewEngineProtocol)] || ![self.webViewEngine canLoadRequest:[NSURLRequest requestWithURL:self.appUrl]]) {
self.webViewEngine = [[NSClassFromString(defaultWebViewEngineClass) alloc] initWithFrame:bounds];
}
} else {
self.webViewEngine = [[NSClassFromString(defaultWebViewEngineClass) alloc] initWithFrame:bounds];
}
4.初始化webView
if ([self.webViewEngine isKindOfClass:[CDVPlugin class]]) {
[self registerPlugin:(CDVPlugin*)self.webViewEngine withClassName:webViewEngineClass];
}
5.返回webView
return self.webViewEngine.engineWebView;
}
复制代码
这一块稍微有点抽象,其实是基于面向协议的编程思想对接口和试图作了一个抽离,id webViewEngine,实际上它指向的是一个id类型而且遵循了CDVWebViewEngineProtocol协议的对象,也就是说它能够实现CDVWebViewEngineProtocol报漏出来的接口,这样咱们只要让抽象类遵循了这个协议,那么就能够实现协议里面定义的方法和属性,从而实现接口分离,若是哪天咱们使用WKWebView那么就能够直接再定义一套接口出来彻底不须要修改框架,同理webViewEngine抽象类表面上看是个webview其实是将webView抽离出来,实现试图分离,达到解耦合。数组
webViewEngine其实是webView的一层抽象类,为何封装了webViewEngine做为中间层上面也提到了再也不分析了,下面主要看一下它的具体实现。安全
- (instancetype)initWithFrame:(CGRect)frame
{
self = [super init];
if (self) {
Class WebClass = NSClassFromString(@"DLPanableWebView");
if ([[WebClass class] isSubclassOfClass:[UIWebView class]]) {
self.engineWebView = [[WebClass alloc] initWithFrame:frame];
} else {
self.engineWebView = [[UIWebView alloc] initWithFrame:frame];
}
NSLog(@"Using UIWebView");
}
return self;
}
复制代码
这里就是刚才说的抽离具体的WebView,因此说框架不须要关心具体使用的是哪个webView,好比说DLPanableWebView就是咱们自定义的webView,那么咱们彻底能够将web的工做拿到DLPanableWebView里面去作,彻底不会影响框架功能。数据结构
webViewEngine初始化配置app
- (void)pluginInitialize
{
// viewController would be available now. we attempt to set all possible delegates to it, by default
1.首先拿到咱们上面配置的web。
UIWebView* uiWebView = (UIWebView*)_engineWebView;
2.看一下咱们外面配置的实现Controller是否本身实现了UIWebView的代理,
若是实现了,那么配置一下,在web回调的时候会传到咱们本身的controller里面作
一下咱们本身的事情。
if ([self.viewController conformsToProtocol:@protocol(UIWebViewDelegate)]) {
self.uiWebViewDelegate = [[CDVUIWebViewDelegate alloc] initWithDelegate:(id <UIWebViewDelegate>)self.viewController];
uiWebView.delegate = self.uiWebViewDelegate;
} else {
3.若是外部controller没有实现,那么配置代理具体实现。
好比说这里咱们在项目里配置了HWebViewDelegate,那么我
们web拦截的时候其余处理就能够在子类里面作了,好比添加白
名单设置等。
self.navWebViewDelegate = [[CDVUIWebViewNavigationDelegate alloc] initWithEnginePlugin:self];
Class TheClass = NSClassFromString(@"HWebViewDelegate");
if ([TheClass isSubclassOfClass:[CDVUIWebViewDelegate class]]) {
self.uiWebViewDelegate = [[TheClass alloc] initWithDelegate:self.navWebViewDelegate];
} else {
self.uiWebViewDelegate = [[CDVUIWebViewDelegate alloc] initWithDelegate:self.navWebViewDelegate];
}
// end
uiWebView.delegate = self.uiWebViewDelegate;
}
[self updateSettings:self.commandDelegate.settings];
}
复制代码
到这里为止,咱们插件配置与加载完成了,webView的具体实现与代理的设置也完成了,那么接下来讲一下native与js的具体交互吧,主要说一下native端都作了什么。这是在CDVUIWebViewNavigationDelegate类中对web代理的实现,也是在上面配置webView的时候将它配置为代理的。这里的实现就是交互的重中之重了,那么详细看下。
- (BOOL)webView:(UIWebView*)theWebView shouldStartLoadWithRequest:(NSURLRequest*)request navigationType:(UIWebViewNavigationType)navigationType
{
1.拿到url
NSURL* url = [request URL];
2.拿到咱们的实现类
CDVViewController* vc = (CDVViewController*)self.enginePlugin.viewController;
3.看url的scheme是否是gap,gap来源于cordova.js,
cordova.js里面建立了一个不可见的iframe注入当前html,
src为gap://ready,用于webView拦截
if ([[url scheme] isEqualToString:@"gap"]) {
4.若是是就进行拦截,具体拦截后干了啥下面说。
[vc.commandQueue fetchCommandsFromJs];
[vc.commandQueue executePending];
return NO;
}
...省略了一些代码
return NO;
}
复制代码
到这里native已经收到了js的调用了,webView的拦截能够看到只传递了一个gap://ready并无详细的参数,详细的参数其实是在fetchCommandsFromJs中取到的,具体的下面说,这样一来shouldStartLoadWithRequest:就不须要关心js端传的是什么,要怎么解析等状况,它只负责拦截,而后将任务交给commandQueue去处理,比如快递员给你打电话说你快递到了去取一下,那么你取回来的多是鞋子,也多是衣服,可是具体是什么快递员并不关心,他只须要告诉你你快递到了,他的任务就结束了。
到这里着重分析两个方法,fetchCommandsFromJs:和executePending:,也是咱们拦截的具体实现。
- (void)fetchCommandsFromJs
{
__weak CDVCommandQueue* weakSelf = self;
NSString* js = @"cordova.require('cordova/exec').nativeFetchMessages()";
1.经过jsBridge调用js方法,js端会以字符串的形式返回插件信息
[_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);
2.解析字符串。
[weakSelf enqueueCommandBatch:queuedCommandsJSON];
// this has to be called here now, because fetchCommandsFromJs is now async (previously: synchronous)
3.调用插件
[self executePending];
}
}];
}
复制代码
evaluateJavaScript:作了一个native调用js的操做,实际上cordova.js里面也有一个commandQueue这样一个数据结构,在调用native以前,就已经将本次调用全部的参数存储在了commandQueue中,比如快递柜同样,快递已经放到柜子里了,本身来拿。native调用js仍是基于jsBridge的stringByEvaluatingJavaScriptFromString:实现的。到这里,js调用native和native调用js已经所有结束了,接下来就是native拿到参数调用native端的插件代码了。
- (void)enqueueCommandBatch:(NSString*)batchJSON
{
1.作个保护。
if ([batchJSON length] > 0) {
NSMutableArray* commandBatchHolder = [[NSMutableArray alloc] init];
2.添加到queue中。
[_queue addObject:commandBatchHolder];
3.若是json串小于4M同步执行,若是大于就放到子线程中异步执行。
if ([batchJSON length] < JSON_SIZE_FOR_MAIN_THREAD) {
4.将字典存入commandBatchHolder数据中。
[commandBatchHolder addObject:[batchJSON cdv_JSONObject]];
} else {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^() {
NSMutableArray* result = [batchJSON cdv_JSONObject];
5.由于异步执行可能会发生线程安全的问题因此加互斥锁作个线程保护。
@synchronized(commandBatchHolder) {
[commandBatchHolder addObject:result];
}
6.回调到主线程执行executePending
[self performSelectorOnMainThread:@selector(executePending) withObject:nil waitUntilDone:NO];
});
}
}
}
复制代码
到这里为止咱们拿到了配置好的插件,webView,js端传递过来的参数,还剩下最后一步,参数拿到了怎么调用到插件的呢?
- (void)executePending
{
1.由于executePending函数会在多个地方调用,避免重复调用。
if (_startExecutionTime > 0) {
return;
}
@try {
_startExecutionTime = [NSDate timeIntervalSinceReferenceDate];
2.遍历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];
}
3.遍历queue中的第一个插件。
while ([commandBatch count] > 0) {
4.内存优化。
@autoreleasepool {
5.返回插件数组并删除,目的让遍历只走一次。
NSArray* jsonEntry = [commandBatch cdv_dequeue];
if ([commandBatch count] == 0) {
6.从队列中删除此插件。
[_queue removeObjectAtIndex:0];
}
7.将参数存储在CDVInvokedUrlCommand类型的实例对象中,这也就是咱们定义插件的时候
为何形参类型为CDVInvokedUrlCommand的缘由了。
CDVInvokedUrlCommand* command = [CDVInvokedUrlCommand commandFromJson:jsonEntry];
CDV_EXEC_LOG(@"Exec(%@): Calling %@.%@", command.callbackId, command.className, command.methodName);
8.执行插件具体函数。
if (![self execute:command]) {
#ifdef DEBUG
NSString* commandJson = [jsonEntry cdv_JSONString];
static NSUInteger maxLogLength = 1024;
NSString* commandString = ([commandJson length] > maxLogLength) ?
[NSString stringWithFormat : @"%@[...]", [commandJson substringToIndex:maxLogLength]] :
commandJson;
DLog(@"FAILED pluginJSON = %@", commandString);
#endif
}
}
9.利用runloop作的优化,具体能够参考一下runloop的知识,目的是为了保证UI流畅进行了优化。
// 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的知识,若是执行的时间过长,避免主线程堵塞,形成卡顿,向runloop中添加了一个timer来唤醒runloop继续干活,防止休眠。
- (BOOL)execute:(CDVInvokedUrlCommand*)command
{
if ((command.className == nil) || (command.methodName == nil)) {
NSLog(@"ERROR: Classname and/or methodName not found for command.");
return NO;
}
1.找到native端的类并返回实例对象。
CDVPlugin* obj = [_viewController.commandDelegate getCommandInstance:command.className];
2.是否继承与CDVPlugin。
if (!([obj isKindOfClass:[CDVPlugin class]])) {
NSLog(@"ERROR: Plugin '%@' not found, or is not a CDVPlugin. Check your plugin mapping in config.xml.", command.className);
return NO;
}
BOOL retVal = YES;
double started = [[NSDate date] timeIntervalSince1970] * 1000.0;
// Find the proper selector to call.
NSString* methodName = [NSString stringWithFormat:@"%@:", command.methodName];
3.生成对应的选择子。
SEL normalSelector = NSSelectorFromString(methodName);
4.发消息执行。
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;
}
double elapsed = [[NSDate date] timeIntervalSince1970] * 1000.0 - started;
if (elapsed > 10) {
NSLog(@"THREAD WARNING: ['%@'] took '%f' ms. Plugin should use a background thread.", command.className, elapsed);
}
return retVal;
}
复制代码
到这里,整个插件的调用过程就结束了,生成plugin这里,框架是基于工厂的设计模式,经过不一样的类名返回继承了CDVPlugin的不一样对象,而后在对应的plugin对象上执行对应的方法,到这里整个调用过程所有结束了。
注:本文属于原创,转载注明出处。图片资源来源互联网。