前言:
这两年一直在做 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 (!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];
- }
五, 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 对象上执行对应的方法.
注: 图片资源来源互联网.
来源: https://juejin.im/post/5bf35c6a5188257c5237a66d