Cordova源碼解析

本文設(shè)計(jì)到的源碼是基于Cordova 4.2.1版本,Cordova官網(wǎng)吟吝。

CDVViewController

CDVViewController是Cordova最主要的類(lèi)指蚁,它把所有模塊整合在一起菩佑,直接初始化一個(gè)它的實(shí)例就可以使用。例如下面的代碼:

CDVViewController *vc = [[CDVViewController alloc] init];
vc.startPage = @"www.baidu.com";
[self presentViewController:vc animated:YES completion:nil];

CDVViewController主要實(shí)現(xiàn)的功能:

  • 注冊(cè)凝化、初始化插件
  • 讀取稍坯、應(yīng)用配置文件
  • 初始化并配置WebView,設(shè)置其代理
  • 管理js與原生的方法調(diào)用
  • 管理應(yīng)用與網(wǎng)頁(yè)的生命周期
    搓劫。瞧哟。。

它主要的屬性有:

  • CDVWebViewEngineProtocol:webview相關(guān)的回調(diào)
  • CDVCommandDelegate:js與原生插件交互方法糟把,插件初始化
  • CDVCommandQueue:命令執(zhí)行隊(duì)列

CDVCommandDelegateCDVCommandQueue會(huì)在js調(diào)用原生插件與插件初始化提到绢涡,這里先不細(xì)說(shuō)。
CDVWebViewEngineProtocol定義了WebView引擎的抽象類(lèi)遣疯,具體實(shí)現(xiàn)由插件提供雄可,例如CDVUIWebViewEngine實(shí)現(xiàn)UIWebView的引擎。

CDVWebViewEngineProtocol協(xié)議定義

CDVWebViewEngineProtocol協(xié)議其實(shí)是對(duì)于WebView的一層封裝缠犀,屏蔽了不同WebView接口的差異数苫,現(xiàn)在iOS有UIWebViewWKWebView

@protocol CDVWebViewEngineProtocol <NSObject>
@property (nonatomic, strong, readonly) UIView* engineWebView;

- (id)loadRequest:(NSURLRequest*)request;
- (id)loadHTMLString:(NSString*)string baseURL:(NSURL*)baseURL;
- (void)evaluateJavaScript:(NSString*)javaScriptString completionHandler:(void (^)(id, NSError*))completionHandler;
- (NSURL*)URL;
- (BOOL)canLoadRequest:(NSURLRequest*)request;
- (instancetype)initWithFrame:(CGRect)frame;
- (void)updateWithInfo:(NSDictionary*)info;
@end

engineWebView屬性對(duì)外直接暴露了內(nèi)部封裝的WebView辨液,其它方法都是對(duì)WebView方法的一層簡(jiǎn)單封裝虐急。

UIWebView引擎CDVUIWebViewEngine

我們以UIWebView的實(shí)現(xiàn)CDVUIWebViewEngine為例說(shuō)明,它是以插件的形式實(shí)現(xiàn)的滔迈,主要作用是初始化UIWebView的配置止吁,對(duì)UIWebView的方法和代理進(jìn)行了一層封裝。它實(shí)現(xiàn)了協(xié)議CDVWebViewEngineProtocol燎悍,主要有以下幾個(gè)屬性敬惦。

// CDVUIWebViewEngine
// UIWebview
@property (nonatomic, strong, readwrite) UIView* engineWebView;
// UIWebView的代理
@property (nonatomic, strong, readwrite) id <UIWebViewDelegate> uiWebViewDelegate;
@property (nonatomic, strong, readwrite) CDVUIWebViewNavigationDelegate* navWebViewDelegate;

初始化從initWithFrame:方法開(kāi)始,它創(chuàng)建了一個(gè)UIWebView谈山,并賦值給了engineWebView俄删,然后在插件初始化方法pluginInitialize中初始化UIWebView的代理和配置。

- (void)pluginInitialize
{
    UIWebView* uiWebView = (UIWebView*)_engineWebView;

    // 判斷當(dāng)前controller是否實(shí)現(xiàn)了UIWebViewDelegate
    // 如果實(shí)現(xiàn)了就把當(dāng)前controller設(shè)置為CDVUIWebViewDelegate的代理實(shí)現(xiàn)
    if ([self.viewController conformsToProtocol:@protocol(UIWebViewDelegate)]) {
        self.uiWebViewDelegate = [[CDVUIWebViewDelegate alloc] initWithDelegate:(id <UIWebViewDelegate>)self.viewController];
        uiWebView.delegate = self.uiWebViewDelegate;
    } 
    // 如果沒(méi)有實(shí)現(xiàn),創(chuàng)建一個(gè)CDVUIWebViewNavigationDelegate畴椰,作為CDVUIWebViewDelegate的代理實(shí)現(xiàn)
    else {
        self.navWebViewDelegate = [[CDVUIWebViewNavigationDelegate alloc] initWithEnginePlugin:self];
        self.uiWebViewDelegate = [[CDVUIWebViewDelegate alloc] initWithDelegate:self.navWebViewDelegate];
        uiWebView.delegate = self.uiWebViewDelegate;
    }

    // 初始化配置信息
    // self.commandDelegate.settings是CDVViewController的配置信息臊诊,定義在config.xml
    [self updateSettings:self.commandDelegate.settings];
}

js調(diào)用原生插件解析

插件調(diào)用流程:

  1. js發(fā)起請(qǐng)求gap://
  2. 實(shí)現(xiàn)WebView的代理webView:shouldStartLoadWithRequest:navigationType:,攔截schemegap的請(qǐng)求
  3. 執(zhí)行js方法cordova.require('cordova/exec').nativeFetchMessages()獲取需要執(zhí)行的原生插件的信息(插件名斜脂,插件方法抓艳,回調(diào)ID,參數(shù))
  4. 將需要執(zhí)行的原生插件信息放入命令隊(duì)列等待執(zhí)行
  5. 執(zhí)行原生插件帚戳,并把結(jié)果回調(diào)給js

插件調(diào)用堆棧如圖所示:

15256629038423.jpg

js請(qǐng)求攔截

CDVUIWebViewDelegate實(shí)現(xiàn)了UIWebView的webView:shouldStartLoadWithRequest:navigationType:代理壶硅,在頁(yè)面加載前做一些處理。

- (BOOL)webView:(UIWebView*)webView shouldStartLoadWithRequest:(NSURLRequest*)request navigationType:(UIWebViewNavigationType)navigationType
{
    BOOL shouldLoad = YES;

    // 1. 判斷如果有代理销斟,先調(diào)用代理方法
    // 這里的_delegate是CDVUIWebViewNavigationDelegate
    if ([_delegate respondsToSelector:@selector(webView:shouldStartLoadWithRequest:navigationType:)]) {
        shouldLoad = [_delegate webView:webView shouldStartLoadWithRequest:request navigationType:navigationType];
    }

    if (shouldLoad) {
        // 是否是調(diào)試工具refresh
        BOOL isDevToolsRefresh = (request == webView.request);
        // 是否是頂層頁(yè)面
        BOOL isTopLevelNavigation = isDevToolsRefresh || [request.URL isEqual:[request mainDocumentURL]];
        
        if (isTopLevelNavigation) {
            if ([self request:request isEqualToRequestAfterStrippingFragments:webView.request]) {
                NSString* prevURL = [self evalForCurrentURL:webView];
                if ([prevURL isEqualToString:[request.URL absoluteString]]) {
                    VerboseLog(@"Page reload detected.");
                } else {
                    VerboseLog(@"Detected hash change shouldLoad");
                    return shouldLoad;
                }
            }

            switch (_state) {
                case STATE_WAITING_FOR_LOAD_FINISH:
                    // 重定向情況庐椒,判斷l(xiāng)oadCount是否是1
                    if (_loadCount != 1) {
                        NSLog(@"CDVWebViewDelegate: Detected redirect when loadCount=%ld", (long)_loadCount);
                    }
                    break;

                case STATE_IDLE:
                case STATE_IOS5_POLLING_FOR_LOAD_START:
                case STATE_CANCELLED:
                    // 頁(yè)面導(dǎo)航開(kāi)始
                    _loadCount = 0;
                    _state = STATE_WAITING_FOR_LOAD_START;
                    break;

                default:
                    {
                        // 其它情況,回調(diào)webView:didFailLoadWithError:
                        NSString* description = [NSString stringWithFormat:@"CDVWebViewDelegate: Navigation started when state=%ld", (long)_state];
                        NSLog(@"%@", description);
                        _loadCount = 0;
                        _state = STATE_WAITING_FOR_LOAD_START;
                        if ([_delegate respondsToSelector:@selector(webView:didFailLoadWithError:)]) {
                            NSDictionary* errorDictionary = @{NSLocalizedDescriptionKey : description};
                            NSError* error = [[NSError alloc] initWithDomain:@"CDVUIWebViewDelegate" code:1 userInfo:errorDictionary];
                            [_delegate webView:webView didFailLoadWithError:error];
                        }
                    }
            }
        } else {
            // 屏蔽一些無(wú)效網(wǎng)站的訪問(wèn)
            shouldLoad = shouldLoad && [self shouldLoadRequest:request];
        }
    }
    return shouldLoad;
}

攔截js調(diào)用原生插件請(qǐng)求的關(guān)鍵代碼在CDVUIWebViewNavigationDelegate蚂踊,它實(shí)現(xiàn)了CDVUIWebViewDelegate的代理约谈,在CDVUIWebViewDelegate會(huì)把請(qǐng)求轉(zhuǎn)發(fā)給它。

- (BOOL)webView:(UIWebView*)theWebView shouldStartLoadWithRequest:(NSURLRequest*)request navigationType:(UIWebViewNavigationType)navigationType
{
    NSURL* url = [request URL];
    CDVViewController* vc = (CDVViewController*)self.enginePlugin.viewController;
    
    // H5調(diào)用原生插件犁钟,后面分析
    if ([[url scheme] isEqualToString:@"gap"]) {
        [vc.commandQueue fetchCommandsFromJs];
        [vc.commandQueue executePending];
        return NO;
    }

    // 給插件預(yù)留了一個(gè)處理URL的方法棱诱,調(diào)用插件的方法`shouldOverrideLoadWithRequest:navigationType:`,獲取返回值
    // 應(yīng)用:系統(tǒng)默認(rèn)插件CDVIntentAndNavigationFilter中涝动,實(shí)現(xiàn)了Intent與Navigation的白名單機(jī)制迈勋。
    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;
    }

     // 處理其它類(lèi)型的url,file:類(lèi)型直接返回YES
    BOOL shouldAllowNavigation = [self defaultResourcePolicyForURL:url];
    if (shouldAllowNavigation) {
        return YES;
    } else {
        [[NSNotificationCenter defaultCenter] postNotification:[NSNotification notificationWithName:CDVPluginHandleOpenURLNotification object:url]];
    }
    
    return NO;
}

H5調(diào)用原生插件

Cordova調(diào)用原生插件的方式是通過(guò)攔截gap://的URL,然后執(zhí)行js代碼cordova.require('cordova/exec').nativeFetchMessages()獲取參數(shù)醋粟,來(lái)實(shí)現(xiàn)調(diào)用靡菇。

我們來(lái)看關(guān)鍵代碼:

// CDVUIWebViewNavigationDelegate.m
- (BOOL)webView:(UIWebView*)theWebView shouldStartLoadWithRequest:(NSURLRequest*)request navigationType:(UIWebViewNavigationType)navigationType
{
    CDVViewController* vc = (CDVViewController*)self.enginePlugin.viewController;
    
    if ([[url scheme] isEqualToString:@"gap"]) {
        [vc.commandQueue fetchCommandsFromJs];
        [vc.commandQueue executePending];
        return NO;
    }
}
- (void)fetchCommandsFromJs
{
    __weak CDVCommandQueue* weakSelf = self;
    NSString* js = @"cordova.require('cordova/exec').nativeFetchMessages()";

    [_viewController.webViewEngine evaluateJavaScript:js
                                    completionHandler:^(id obj, NSError* error) {
        if ((error == nil) && [obj isKindOfClass:[NSString class]]) {
            NSString* queuedCommandsJSON = (NSString*)obj;
            // 調(diào)用的插件信息加入到queue中
            [weakSelf enqueueCommandBatch:queuedCommandsJSON];
            // 調(diào)用執(zhí)行方法
            [self executePending];
        }
    }];
}

調(diào)用js方法cordova.require('cordova/exec').nativeFetchMessages(),獲取調(diào)用的插件信息米愿。

// 插件信息示例
[["DevicePlugin1678563772","DevicePlugin","getDeviceInfo",[]]]

命令隊(duì)列CDVCommandQueue

js的每次調(diào)用信息會(huì)封裝被封裝為一個(gè)命令CDVInvokedUrlCommand厦凤,CDVInvokedUrlCommand繼承自NSObject,主要存儲(chǔ)了下面的信息:

// CDVInvokedUrlCommand
// 參數(shù)
@property (nonatomic, readonly) NSArray* arguments;
// 回調(diào)ID
@property (nonatomic, readonly) NSString* callbackId;
// 類(lèi)名
@property (nonatomic, readonly) NSString* className;
// 方法名
@property (nonatomic, readonly) NSString* methodName;

CDVCommandQueue管理著所有的命令育苟,實(shí)現(xiàn)了一個(gè)命令的隊(duì)列较鼓。在js調(diào)用原生插件時(shí),會(huì)調(diào)用CDVCommandQueueenqueueCommandBatch:方法违柏,將插件調(diào)用信息加到commandBatchHolder數(shù)組中博烂,最后commandBatchHolder數(shù)組添加到CDVCommandQueuequeue

- (void)enqueueCommandBatch:(NSString*)batchJSON
{
    if ([batchJSON length] > 0) {
        NSMutableArray* commandBatchHolder = [[NSMutableArray alloc] init];
        [_queue addObject:commandBatchHolder];
        if ([batchJSON length] < JSON_SIZE_FOR_MAIN_THREAD) {
            [commandBatchHolder addObject:[batchJSON cdv_JSONObject]];
        } else {
            dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^() {
                NSMutableArray* result = [batchJSON cdv_JSONObject];
                @synchronized(commandBatchHolder) {
                    [commandBatchHolder addObject:result];
                }
                [self performSelectorOnMainThread:@selector(executePending) withObject:nil waitUntilDone:NO];
            });
        }
    }
}

插件的執(zhí)行由CDVCommandQueue管理漱竖,每個(gè)CDVViewController有自己的隊(duì)列禽篱,有兩個(gè)重要的成員變量。

    /* 二維數(shù)組闲孤,存儲(chǔ)著所有插件調(diào)用的json */
    NSMutableArray* _queue;
    /* 記錄開(kāi)始調(diào)用的時(shí)間 */
    NSTimeInterval _startExecutionTime;

executePending負(fù)責(zé)執(zhí)行命令隊(duì)列中待執(zhí)行的插件谆级,具體實(shí)現(xiàn)就是遍歷執(zhí)行二維數(shù)組queue

- (void)executePending
{
    // 如果已經(jīng)開(kāi)始執(zhí)行了讼积,返回
    if (_startExecutionTime > 0) {
        return;
    }
    @try {
        // 記錄開(kāi)始執(zhí)行的時(shí)間
        _startExecutionTime = [NSDate timeIntervalSinceReferenceDate];

        // 遍歷_queue
        while ([_queue count] > 0) {
            NSMutableArray* commandBatchHolder = _queue[0];
            NSMutableArray* commandBatch = nil;
            @synchronized(commandBatchHolder) {
                if ([commandBatchHolder count] == 0) {
                    break;
                }
                commandBatch = commandBatchHolder[0];
            }
            
            // 遍歷commandBatch
            while ([commandBatch count] > 0) {
                @autoreleasepool {
                    // 取出commandBatch的第一條數(shù)據(jù)肥照,并移除
                    NSArray* jsonEntry = [commandBatch cdv_dequeue];
                    if ([commandBatch count] == 0) {
                        [_queue removeObjectAtIndex:0];
                    }
                    
                    // 用插件調(diào)用json信息,創(chuàng)建CDVInvokedUrlCommand
                    CDVInvokedUrlCommand* command = [CDVInvokedUrlCommand commandFromJson:jsonEntry];

                    // 調(diào)用插件
                    [self execute:command]);
                }

                // 對(duì)于性能的一個(gè)優(yōu)化勤众,后面會(huì)詳細(xì)說(shuō)
                if (([_queue count] > 0) && ([NSDate timeIntervalSinceReferenceDate] - _startExecutionTime > MAX_EXECUTION_TIME)) {
                    [self performSelector:@selector(executePending) withObject:nil afterDelay:0];
                    return;
                }
            }
        }
    } @finally
    {
        _startExecutionTime = 0;
    }
}

用Runloop優(yōu)化性能

Cordova對(duì)于插件的執(zhí)行進(jìn)行了優(yōu)化舆绎,保證頁(yè)面的流程度,運(yùn)用了RunLoop们颜,巧妙的將代碼分割為多塊分次執(zhí)行吕朵,避免由于插件執(zhí)行導(dǎo)致主線程阻塞,影響頁(yè)面繪制窥突,導(dǎo)致掉幀努溃。具體代碼如下:

// CDVCommandQueue.m

// MAX_EXECUTION_TIME ≈ 1s / 60 / 2
// 計(jì)算出繪制一幀時(shí)間的一半
static const double MAX_EXECUTION_TIME = .008;

// 判斷本次執(zhí)行時(shí)間,如果大于MAX_EXECUTION_TIME阻问,調(diào)用performSelector:withObject:afterDelay梧税,結(jié)束本次調(diào)用
if (([_queue count] > 0) && ([NSDate timeIntervalSinceReferenceDate] - _startExecutionTime > MAX_EXECUTION_TIME)) {
    [self performSelector:@selector(executePending) withObject:nil afterDelay:0];
    return;
}

優(yōu)化策略分析:

  • 將隊(duì)列中的插件分割為很多小塊來(lái)執(zhí)行
  • 開(kāi)始執(zhí)行executePending方法時(shí),記錄開(kāi)始時(shí)間称近,每次執(zhí)行完一個(gè)插件方法后第队,判斷本次執(zhí)行時(shí)間是否超過(guò)MAX_EXECUTION_TIME,如果沒(méi)有超過(guò)刨秆,繼續(xù)執(zhí)行凳谦,如果超過(guò)了MAX_EXECUTION_TIME,調(diào)用performSelector:withObject:afterDelay衡未,結(jié)束本次調(diào)用
  • 如果要保證UI流暢尸执,需要滿(mǎn)足條件CPU時(shí)間 + GPU時(shí)間 <= 1s/60, 為了給GPU留下足夠的時(shí)間渲染缓醋,要盡量讓CPU占用時(shí)間小于1s/60/2
  • Runloop執(zhí)行的流程如下圖所示剔交,系統(tǒng)在收到kCFRunLoopBeforeWaiting(線程即將休眠)通知時(shí),會(huì)觸發(fā)一次界面的渲染改衩,也就是在完成source0的處理后
  • source0在這里就是插件的執(zhí)行代碼岖常,在kCFRunLoopBeforeWaiting通知之前,如果source0執(zhí)行時(shí)間過(guò)長(zhǎng)就會(huì)導(dǎo)致界面沒(méi)有得到及時(shí)的刷新葫督。
  • 函數(shù)performSelector:withObject:afterDelay竭鞍,會(huì)將方法注冊(cè)到Timer,結(jié)束source0調(diào)用橄镜,開(kāi)始渲染界面偎快。界面渲染完成后,Runloop開(kāi)始sleep洽胶,然后被timer喚醒又開(kāi)始繼續(xù)處理source0晒夹。
15256833278020.png

插件方法執(zhí)行

方法最終的執(zhí)行在方法execute:中,從command中取出要執(zhí)行的插件類(lèi)、方法丐怯、參數(shù)喷好,然后執(zhí)行方法。

- (BOOL)execute:(CDVInvokedUrlCommand*)command
{
    // 獲取插件實(shí)例
    CDVPlugin* obj = [_viewController.commandDelegate getCommandInstance:command.className];

    BOOL retVal = YES;
    double started = [[NSDate date] timeIntervalSince1970] * 1000.0;
    
    NSString* methodName = [NSString stringWithFormat:@"%@:", command.methodName];
    SEL normalSelector = NSSelectorFromString(methodName);
    if ([obj respondsToSelector:normalSelector]) {
        ((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;
    // 監(jiān)控插件方法執(zhí)行時(shí)間读跷,打印出大于10ms的方法
    if (elapsed > 10) {
        NSLog(@"THREAD WARNING: ['%@'] took '%f' ms. Plugin should use a background thread.", command.className, elapsed);
    }
    return retVal;
}

原生回調(diào)js

原生方法執(zhí)行完成后梗搅,會(huì)把結(jié)果返回給js,調(diào)用方法sendPluginResult:callbackId:效览,用CDVPluginResult來(lái)傳遞回調(diào)參數(shù)无切,用callbackId來(lái)區(qū)分是哪次調(diào)用(callbackId由js產(chǎn)生)。

// CDVCommandDelegateImpl.m
- (void)sendPluginResult:(CDVPluginResult*)result callbackId:(NSString*)callbackId
{
    // 判斷callbackId長(zhǎng)度是否小于100
    // 用正則表達(dá)式"[^A-Za-z0-9._-]"來(lái)驗(yàn)證callbackId
    if (![self isValidCallbackId:callbackId]) {
        return;
    }
    // 狀態(tài)碼
    int status = [result.status intValue];
    // 是否持續(xù)回調(diào)
    BOOL keepCallback = [result.keepCallback boolValue];
    // 會(huì)帶哦參數(shù)
    NSString* argumentsAsJSON = [result argumentsAsJSON];
    
    // 執(zhí)行js方法丐枉,回調(diào)
    NSString* js = [NSString stringWithFormat:@"cordova.require('cordova/exec').nativeCallback('%@',%d,%@,%d, %d)", callbackId, status, argumentsAsJSON, keepCallback, debug];
    [self evalJsHelper:js];
}

CDVPlugin注冊(cè)與初始化

我們先看看配置文件中插件的定義:

<!-- 定義插件名為HandleOpenUrl的插件 -->
<feature name="HandleOpenUrl">
    <!-- 對(duì)應(yīng)的iOS類(lèi)名是CDVHandleOpenURL -->
    <param name="ios-package" value="CDVHandleOpenURL" />
    <!-- 需要默認(rèn)加載的插件 -->
    <param name="onload" value="true" />
</feature>

加載默認(rèn)插件

CDVViewControllerviewDidLoad時(shí)哆键,從Cordova的配置文件config.xml中,讀取出需要默認(rèn)加載的插件瘦锹,遍歷初始化洼哎。

CDVViewController中初始化默認(rèn)插件代碼。

- (void)viewDidLoad {
    // Load settings
    [self loadSettings];
    
    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"];
    }
}

插件初始化

插件初始化的過(guò)程:

  1. 加載配置文件config.xml
  2. 根據(jù)插件名獲取對(duì)應(yīng)類(lèi)名
  3. 根據(jù)類(lèi)名從緩存中查找沼本,如果命中直接返回
  4. 沒(méi)有緩存重新創(chuàng)建一個(gè)實(shí)例噩峦,并寫(xiě)入緩存

插件初始化的入口是getCommandInstance,傳入?yún)?shù)是插件名稱(chēng)抽兆,返回一個(gè)插件的實(shí)例對(duì)象识补。

- (id)getCommandInstance:(NSString*)pluginName
{
    // 在pluginsMap中用插件名稱(chēng)獲取類(lèi)名(插件名不區(qū)分大小寫(xiě))
    NSString* className = [self.pluginsMap objectForKey:[pluginName lowercaseString]];
    
    // 沒(méi)有配置插件,初始化失敗
    if (className == nil) {
        return nil;
    }
    
    // 從緩存中獲取辫红,如果命中直接返回緩存
    id obj = [self.pluginObjects objectForKey:className];
    if (!obj) {
        // 沒(méi)有緩存凭涂,創(chuàng)建一個(gè)新的實(shí)例
        obj = [[NSClassFromString(className)alloc] initWithWebViewEngine:_webViewEngine];
        
        if (obj != nil) {
            // 實(shí)例創(chuàng)建成功,注冊(cè)插件
            [self registerPlugin:obj withClassName:className];
        } else {
            NSLog(@"CDVPlugin class %@ (pluginName: %@) does not exist.", className, pluginName);
        }
    }
    return obj;
}

注冊(cè)插件的關(guān)鍵方法registerPlugin:withClassName:

- (void)registerPlugin:(CDVPlugin*)plugin withClassName:(NSString*)className
{
    if ([plugin respondsToSelector:@selector(setViewController:)]) {
        [plugin setViewController:self];
    }
    
    if ([plugin respondsToSelector:@selector(setCommandDelegate:)]) {
        [plugin setCommandDelegate:_commandDelegate];
    }
    
    // 寫(xiě)入緩存
    [self.pluginObjects setObject:plugin forKey:className];
    [plugin pluginInitialize];
}

Cordova還提供了插件名注冊(cè)插件的方式贴妻,使用函數(shù)registerPlugin:withPluginName:切油,實(shí)現(xiàn)方式差不多,就不贅述了名惩。

注冊(cè)插件步驟

  1. 設(shè)置插件的viewControllerdelegate
  2. 將插件以className為key放入pluginObjects中澎胡,pluginObjects是一個(gè)插件的緩存
  3. 調(diào)用插件的pluginInitialize

插件銷(xiāo)毀

插件銷(xiāo)毀的時(shí)機(jī)是創(chuàng)建插件的CDVViewController釋放的時(shí)候,因?yàn)椴寮?shí)例被創(chuàng)建后被緩存map引用娩鹉,對(duì)應(yīng)的銷(xiāo)毀代碼攻谁。

// CDVViewController.m
- (void)dealloc
{
    [[NSNotificationCenter defaultCenter] removeObserver:self];
    
    [CDVUserAgentUtil releaseLock:&_userAgentLockToken];
    [_commandQueue dispose];
    [[self.pluginObjects allValues] makeObjectsPerformSelector:@selector(dispose)];
}

// CDVPlugin.m
- (void)dispose
{
    viewController = nil;
    commandDelegate = nil;
}

小結(jié)

  1. config.xml文件中配置插件,聲明插件與類(lèi)的映射關(guān)系弯予,以及加載策略
  2. 插件的初始化時(shí)懶加載戚宦,除了onload配置為YES的插件會(huì)默認(rèn)加載,其它插件都是使用時(shí)加載
  3. 插件使用了兩個(gè)Map來(lái)緩存锈嫩,pluginObjects建立了類(lèi)名與插件實(shí)例對(duì)象的映射受楼,pluginsMap建立了插件名與類(lèi)名的映射垦搬。
  4. 在一個(gè)CDVViewController中,同一個(gè)插件同時(shí)只會(huì)存在一個(gè)實(shí)例

歡迎關(guān)注我的博客

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末艳汽,一起剝皮案震驚了整個(gè)濱河市猴贰,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌骚灸,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,843評(píng)論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件慌植,死亡現(xiàn)場(chǎng)離奇詭異甚牲,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)蝶柿,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,538評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén)丈钙,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人交汤,你說(shuō)我怎么就攤上這事雏赦。” “怎么了芙扎?”我有些...
    開(kāi)封第一講書(shū)人閱讀 163,187評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵星岗,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我戒洼,道長(zhǎng)俏橘,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,264評(píng)論 1 292
  • 正文 為了忘掉前任圈浇,我火速辦了婚禮寥掐,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘磷蜀。我一直安慰自己召耘,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,289評(píng)論 6 390
  • 文/花漫 我一把揭開(kāi)白布褐隆。 她就那樣靜靜地躺著污它,像睡著了一般。 火紅的嫁衣襯著肌膚如雪庶弃。 梳的紋絲不亂的頭發(fā)上轨蛤,一...
    開(kāi)封第一講書(shū)人閱讀 51,231評(píng)論 1 299
  • 那天,我揣著相機(jī)與錄音虫埂,去河邊找鬼祥山。 笑死,一個(gè)胖子當(dāng)著我的面吹牛掉伏,可吹牛的內(nèi)容都是我干的缝呕。 我是一名探鬼主播澳窑,決...
    沈念sama閱讀 40,116評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼供常!你這毒婦竟也來(lái)了摊聋?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 38,945評(píng)論 0 275
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤栈暇,失蹤者是張志新(化名)和其女友劉穎麻裁,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體源祈,經(jīng)...
    沈念sama閱讀 45,367評(píng)論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡煎源,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,581評(píng)論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了香缺。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片手销。...
    茶點(diǎn)故事閱讀 39,754評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖图张,靈堂內(nèi)的尸體忽然破棺而出锋拖,到底是詐尸還是另有隱情,我是刑警寧澤祸轮,帶...
    沈念sama閱讀 35,458評(píng)論 5 344
  • 正文 年R本政府宣布兽埃,位于F島的核電站,受9級(jí)特大地震影響适袜,放射性物質(zhì)發(fā)生泄漏讲仰。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,068評(píng)論 3 327
  • 文/蒙蒙 一痪蝇、第九天 我趴在偏房一處隱蔽的房頂上張望鄙陡。 院中可真熱鬧,春花似錦躏啰、人聲如沸趁矾。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,692評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)毫捣。三九已至,卻和暖如春帝际,著一層夾襖步出監(jiān)牢的瞬間蔓同,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,842評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工蹲诀, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留斑粱,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,797評(píng)論 2 369
  • 正文 我出身青樓脯爪,卻偏偏與公主長(zhǎng)得像则北,于是被迫代替她去往敵國(guó)和親矿微。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,654評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容