Cordova-iOS框架源碼解析

這兩年一直在作Cordova工程的項目,目前咱們基於Cordova的jsBridge進行兩端的交互,經過加載本地JS優化渲染時間和白屏問題,Cordova給咱們帶來了交互的插件化,可配置等優勢,總結一下Cordova實現,下面主要基於主要部分的源代碼進行一下分析和學習。html

1.先看一下viewDidLoad裏面作了什麼: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];
    }

    // /////////////////

    /*
     * 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];
            }
        }
    }];
}

下面一步步分析具體都是怎麼實現的:編程

2.首先加載配置文件,仍是看代碼:json

- (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 <CDVWebViewEngineProtocol> webViewEngine,實際上它指向的是一個id類型而且遵循了CDVWebViewEngineProtocol協議的對象,也就是說它能夠實現CDVWebViewEngineProtocol報漏出來的接口,這樣咱們只要讓抽象類遵循了這個協議,那麼就能夠實現協議裏面定義的方法和屬性,從而實現接口分離。同理webViewEngine抽象類表面上看是個webview其實是將webView抽離出來,實現試圖分離,達到解耦合。數組

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 <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];
}

5.到這裏爲止,咱們插件配置與加載完成了,webView的具體實現與代理的設置也完成了,那麼接下來講一下native與js的具體交互吧,主要說一下native端都作了什麼。這是在CDVUIWebViewNavigationDelegate類中對web代理的實現,也是在上面配置webView的時候將它配置爲代理的。這裏的實現就是交互的重中之重了,那麼咱們在詳細的說一下。app

- (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對象上執行對應的方法。

相關文章
相關標籤/搜索