淺析Cordova-iOS
這兩年一直在做Cordova工程的專案,目前我們基於Cordova的jsBridge進行兩端的互動,通過載入本地JS優化渲染時間和白屏問題,Cordova給我們帶來了互動的外掛化,可配置等優點,總結一下Cordova實現,下面主要基於native端部分的原始碼進行一下分析和學習。
目錄
- 1.viewDidLoad
- 2.載入配置檔案
- 3.配置webview
- 4.webViewEngine實現分析
- 5.native與js互動
- 6.native外掛具體呼叫過程
一、viewDidLoad
cordova入口
- (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:@"<html><body> %@ </body></html>", loadErr]; [self.webViewEngine loadHTMLString:html baseURL:nil]; } } }]; } 複製程式碼
viewDidload裡面已經將整個呼叫過程走完了,所以我們在使用的時候可以直接繼承自CDVViewController來實現我們自己的邏輯。
二、載入配置檔案
首先載入配置檔案,還是看程式碼:
- (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配置的類名、外掛提前載入還是使用的時候再建立等資訊。
三、配置webview
配置Cordova的webview,這一塊比較重要著重分析。
- (UIView*)newCordovaViewWithFrame:(CGRect)bounds { 1.預設的webView抽象類,實際上CDVViewController中是沒有webView的具體實現等程式碼的,他們的實現都是在這個抽象類裡面。當然這個抽象類也可以我們自己去配置,然後在我們自己的抽象類裡面去做具體實現,比如說我們現在專案使用的是UIWebView那麼就完全可以使用框架內不提供的預設實現,如果我們升級WKWebView,就可以直接修改了。 NSString* defaultWebViewEngineClass = @"CDVUIWebViewEngine"; NSString* webViewEngineClass = [self.settings cordovaSettingForKey:@"CordovaWebViewEngine"]; if (ViewEngineClass) { 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實現分析
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初始化配置
- (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]; } 複製程式碼
五、native與js互動
到這裡為止,我們外掛配置與載入完成了,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]; }); } } } 複製程式碼
六、native外掛具體呼叫過程
到這裡為止我們拿到了配置好的外掛,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物件上執行對應的方法。
注:圖片資源來源網際網路。