这两年一直在做Cordova工程的项目,目前我们基于Cordova的jsBridge进行两端的交互,通过加载本地JS优化渲染时间和白屏问题,Cordova给我们带来了交互的插件化,可配置等优点,总结一下Cordova实现,下面主要基于主要部分的源代码进行一下分析和学习。
1.先看一下viewDidLoad里面做了什么:
- (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]; } // / /* * Fire up CDVLocalStorage to work-around WebKit storage limitations: on all iOS 5.1+ versions for local-only backups, but only needed on iOS 5.1 for cloud backup. With minimum iOS 7/8 supported, only first clause applies. */ if ([backupWebStorageType isEqualToString:@"local"]) { NSString* localStorageFeatureName = @"localstorage"; if ([self.pluginsMap objectForKey:localStorageFeatureName]) { // plugin specified in config [self.startupPluginNames addObject:localStorageFeatureName]; } } 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) { _userAgentLockToken = lockToken; [CDVUserAgentUtil setUserAgent:self.userAgent lockToken:lockToken]; if (appURL) { NSURLRequest* appReq = [NSURLRequest requestWithURL:appURL cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:20.0]; [self.webViewEngine loadRequest:appReq]; } else { NSString* loadErr = [NSString stringWithFormat:@"ERROR: Start Page at '%@/%@' was not found.", self.wwwFolderName, self.startPage]; NSLog(@"%@", loadErr); NSURL* errorUrl = [self errorURL]; if (errorUrl) { errorUrl = [NSURL URLWithString:[NSString stringWithFormat:@"?error=%@", [loadErr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]] relativeToURL:errorUrl]; NSLog(@"%@", [errorUrl absoluteString]); [self.webViewEngine loadRequest:[NSURLRequest requestWithURL:errorUrl]]; } else { NSString* html = [NSString stringWithFormat:@" %@ ", loadErr]; [self.webViewEngine loadHTMLString:html baseURL:nil]; } } }]; }
下面一步步分析具体都是怎么实现的:
2.首先加载配置文件,还是看代码:
- (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]; }
3.配置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
4.我们再详细说下webViewEngine对象具体做了啥。还是看代码。
- (instancetype)initWithFrame:(CGRect)frame { 1.首先初始化 self = [super init]; if (self) { 2.这里就是刚才说的抽离具体的WebView,所以说框架不需要关系具体使用的是哪一个webView,比如说DLPanableWebView就是我们自定义的webView,那么我们完全可以将web的工作拿到DLPanableWebView里面去做,完全不会影响框架功能。 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; }
这里就是抽象类初始化的实现了。 - (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)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]; }
5.到这里为止,我们插件配置与加载完成了,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 if ([[url scheme] isEqualToString:@"gap"]) { 4.如果是就进行拦截,具体拦截后干了啥下面说。 [vc.commandQueue fetchCommandsFromJs]; [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; }
到这里着重分析两个方法,[vc.commandQueue fetchCommandsFromJs];和[vc.commandQueue 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]; } }]; } - (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]; }); } } }
6.到这里为止我们拿到了配置好的插件,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; } } - (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对象上执行对应的方法。