notes

前言

在工作中我是负责iOS中h5和native交互的模块,然后再提供一个JS的SDK,SDK负责和native通信,并且提供一些硬件功能的接口。如蓝牙,定位等功能。\n\n最近由于业务发生变化,需要把js-native交互和蓝牙,定位这些做成基础模块,给公司其他部门的APP开发者去用。和Cordova比较类似,Cordova是使用h5制作app的一种解决方案。也是由JS和Native两部分组成的,开发者只要面向h5开发就可以了,所以就研究了下它的源码。\n\n和Cordova不同的是我们的模块既要支持js调用又要支持native调用,所以也不能照搬Cordova的架构来用。\n\n很久以前,Cordova是用UIWebView来作为载体的,但是UIWebView本身就有内存泄露的问题。由于iOS8中WKWebView的出现,解决了UIWebView中很多的问题,Cordova也有一个插件叫做cordova-plugin-wkwebview-engine,新增了对WKWebView的支持。

Xcode中导入Cordova

Podfile

target 'TargetName' do    
pod 'Cordova'    
pod 'CordovaPlugin-console'   
pod 'cordova-plugin-camera'
pod 'cordova-plugin-contacts'
pod 'cordova-plugin-device'
pod 'cordova-plugin-device-orientation'
pod 'cordova-plugin-device-motion'
pod 'cordova-plugin-globalization'
pod 'cordova-plugin-geolocation'
pod 'cordova-plugin-file'
pod 'cordova-plugin-media-capture'
pod 'cordova-plugin-network-information'
pod 'cordova-plugin-splashscreen'
pod 'cordova-plugin-inappbrowser'
pod 'cordova-plugin-file-transfer'
pod 'cordova-plugin-statusbar'
pod 'cordova-plugin-vibration'
pod 'cordova-plugin-wkwebview-engine'
# The following includes the PhoneGap iOS Platform Project Template for a quick start
pod 'phonegap-ios-template'
end

其中phonegap-ios-template是h5的一个demo,可以参考html的写法和结构。里面有一个config.xml文件,描述了当前h5应用中用到的native插件。

Native源码分析

一下只对一些比较重要的类做分析

<widget id="io.cordova.hellocordova" version="0.0.1" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">\n <preference name="AllowInlineMediaPlayback" value="false" />\n <preference name="BackupWebStorage" value="cloud" />\n <preference name="DisallowOverscroll" value="true" />\n <preference name="EnableViewportScale" value="false" />\n …\n <feature name="SplashScreen">\n <param name="ios-package" value="CDVSplashScreen" />\n </feature>\n <feature name="StatusBar">\n <param name="ios-package" value="CDVStatusBar" />\n <param name="onload" value="true" />\n </feature>\n …\n</widget>


里面有两类标签`preference`主要用于h5的偏好设置,如把UIWebView的引擎换成WKWebView。`feature`描述了当前app依赖哪些native功能。

CDVConfigParser就是用来解析config.xml文件的,实现了NSXMLParserDelegate代理方法。

… CDVConfigParser* delegate = [[CDVConfigParser alloc] init]; … [self.configParser setDelegate:((id < NSXMLParserDelegate >)delegate)]; [self.configParser parse];

CDVConfigParser的pluginsDict属性,里面存放着解析后的feature。
CDVConfigParser的settings属性,里面存放着解析后的preference。


* **CDVViewController**
展示webView的控制器。一切Cordova的代码从viewDidLoad开始执行。\n还包括捕获一些系统状态,如app退到后台

* **CDVUIWebViewEngine**
Cordova默认的webView引擎,里面用的是UIWebView

return self; }

如果装了WKWebView插件并且在config.xml中配置过的话,就会创建CDVWKWebViewEngine实例,里面用的是WKWebView。

self.engineWebView = [[WKWebView alloc] initWithFrame:frame]; }

return self; }


* **CDVTimer**
负责记录和打印执行一些命令所花费的时间

* **CDVUIWebViewDelegate**
顾名思义,就是UIWebView的delegate。实现了UIWebViewDelegate的方法。里面有个方法是作为js和native的桥梁,相信大家都知道。
- (void)sendPluginResult:(CDVPluginResult*)result callbackId:(NSString*)callbackId
{
CDV_EXEC_LOG(@"Exec(%@): Sending result. Status=%@", callbackId, result.status);
// This occurs when there is are no win/fail callbacks for the call.
if ([@"INVALID" isEqualToString:callbackId]) {
return;
}
// This occurs when the callback id is malformed.
if (![self isValidCallbackId:callbackId]) {
NSLog(@"Invalid callback id received by sendPluginResult");
return;
}
int status = [result.status intValue];
BOOL keepCallback = [result.keepCallback boolValue];
NSString* argumentsAsJSON = [result argumentsAsJSON];
BOOL debug = NO;

#ifdef DEBUG
debug = YES;
#endif

NSString* js = [NSString stringWithFormat:@"cordova.require('cordova/exec').nativeCallback('%@',%d,%@,%d, %d)", callbackId, status, argumentsAsJSON, keepCallback, debug];

[self evalJsHelper:js];\n}\n```\n方法返回给js。\n\n```\nNSString* js = [NSString stringWithFormat:@"cordova.require('cordova/exec').nativeCallback('%@',%d,%@,%d, %d)", callbackId, status, argumentsAsJSON, keepCallback, debug];

commandDelegate属性:主要功能就是回调每个plugin处理完js任务之后生成的CDVPluginResult对象给js。

pluginInitialize方法:在CDVViewController中的ViewDidLoad方法中,会调用CDVPlugin实例的pluginInitialize方法,所有插件都必须重写pluginInitialize方法。比如在CDVUIWebViewEngine的pluginInitialize方法中,设置了webView的基本设置(如是否缩放网页适配屏幕),而这些设置就是从config.xml文件中解析出来的。

负责获取js任务并执行,关键方法是fetchCommandsFromJs,以下是源码。

- (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;
CDV_EXEC_LOG(@"Exec: Flushed JS->native queue (hadCommands=%d).", [queuedCommandsJSON length] > 0);
[weakSelf enqueueCommandBatch:queuedCommandsJSON];
// this has to be called here now, because fetchCommandsFromJs is now async (previously: synchronous)
[self executePending];
}
}];
}

CDVCommandQueue是在while循环里遍历任务队列_queue的,在执行js任务的时候会将一个js任务信息(json)转化成一个CDVInvokedUrlCommand实例,


和Cordova比较类似,但是我的JSNative框架内部分模块也是通过runtime生成具体类和方法,但是最后是通过performSelector:withObject:分发到各个模块的,所以对Cordova团队弃用performSelector:withObject:转而使用objc_msgSend函数的原因很感兴趣。就google了一下,但是从StackOverflow的回答来看,两者是差不多的,我们知道,performSelector:withObject的内部还是调用objc_msgSend方法的,Cordova团队修改的目的无非就是性能上稍微提高了那么一点点,但是舍弃了编译器的类型检查。其实两者都是差不多的。

// [obj performSelector:normalSelector withObject:command];
((void (*)(id, SEL, id))objc_msgSend)(obj, normalSelector, command);

最后的模块化分案是:每个本地功能对应一个模块,如:蓝牙,定位。每个模块间通过Mediator中介者来通信,参考文章。最后决定一个模块对应两个私有库,主要是拆分了native和js的调用接口,因为这两个接口的参数和返回值都不一样。native私有库包含着模块的具体实现和对应的native调用接口。js私有库的主要作用接收js调用native的参数并且生成返回值返回给js。该类继承自JSNative中的Module类,因为Module类中提供了回调js的方法。最后还是要调用native私有库去具体实现,所以js私有库依赖native私有库。

另外Cordova中使用了大量的readOnly类型属性,规范的框架确实应该多用用,保证数据安全。我也应该提高这个意识。