cordova-ios源码解析

3,517 阅读10分钟

前言:

这两年一直在做Cordova工程的项目,目前我们基于Cordova的jsBridge进行两端的交互,通过加载本地JS优化渲染时间和白屏问题,Cordova给我们带来了交互的插件化,可配置等优点,可以说Cordova为我们进行Hybrid应用的构建提供了一个优秀的平台,总结一下Cordova实现,下面主要基于native端部分的源代码进行一下分析和学习,本篇不具体分析Cordova的源代码,旨在总结Cordova的编码思想。

目录

  • 1.viewDidLoad
  • 2.加载配置文件
  • 3.配置webview
  • 4.webViewEngine实现分析
  • 5.js与native交互以及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];
    }
    
    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里面究竟具体做了哪些工作。

二、加载配置文件

首先加载配置文件,还是看代码:

- (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 (!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实现分析

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

五、js与native交互以及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,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];
            });
        }
    }
}

六、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;
    }
}

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对象上执行对应的方法,到这里整个调用过程全部结束了。

注:本文属于原创,转载注明出处。图片资源来源互联网。