在工作中我是负责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的支持。
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插件。
一下只对一些比较重要的类做分析
<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的桥梁,相信大家都知道。
(BOOL)webView:(UIWebView)webView shouldStartLoadWithRequest:(NSURLRequest)request navigationType:(UIWebViewNavigationType)navigationType; ```
CDVCommandDelegate CDVCommandDelegate是一个协议,在Cordova中实现协议的其实是CDVCommandDelegateImpl对象。遵循CDVCommandDelegate协议的对象负责回调处理完成的CDVInvokedUrlCommandResult对象给js。以下是关键方法
- (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实例,
@interface CDVInvokedUrlCommand : NSObject {
// 回调js id
NSString* _callbackId;
// 插件名(类名)
NSString* _className;
// 方法名
NSString* _methodName;
// 参数
NSArray* _arguments;
}
@end
@interface CDVPluginResult : NSObject {}
// 执行状态
@property (nonatomic, strong, readonly) NSNumber* status;
// 回调内容
@property (nonatomic, strong, readonly) id message;
// 回调给js后是否保留回调函数
@property (nonatomic, strong) NSNumber* keepCallback;
@end
和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类型属性,规范的框架确实应该多用用,保证数据安全。我也应该提高这个意识。