简书上的文章更新的不够及时,欢迎直接阅读的我的博客查看。
原文链接
因为我们还用React Native 0.30版本,以后肯定会换最新版本,所以着急炒炒这份冷饭,总结总结使用经验。
上一篇介绍了Android的一些React Native应用中Native部分的开发,这篇主要在这个基础上继续介绍下iOS部分。iOS坑会少一点。
该文章为系列文章,之前的文章为RN学习1——前奏,app插件化和热更新的探索,RN学习2——客户端开发者的一些准备工作,RN学习3——集成进现有原生项目,RN学习4——QDaily Android app中通信和热修复实践。
一、先说针对hot fix的支持
启动时请求JSBundle更新
直接看流程图,此类用来管理JSBundle的位置以及热更新的版本。

资源文件的指向
上一篇说过,为支持资源的热更新,我们将所有resource都使用http://qdaily.cage/
开头作为标记,在编译Android的JSBundle过程中,将其修改为file://data/*的本地文件路径。
iOS在此处的处理更为简单:在看React Native源码中可以发现,处理ImageView(即RCTImageView)的请求资源的方法在RCTConvert中,负责将传入的source json文件进行识别,如果uri以http开头,最终会调到+ (NSURL *)NSURL:(id)json
方法中,最后调用系统方法NSURL *URL = [NSURL URLWithString:path]
完成地址的绑定。知道原理就好了,我们只需要改变系统的+(NSURL*) URLWithString:(NSString*)url
方法即可:
// NSURL+QDAdditions.m
+ (void) load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self swizzleSEL:@selector(URLWithString:) withSEL:@selector(swizzled_URLWithString:)];
});
}
+ (void)swizzleSEL:(SEL)originalSEL withSEL:(SEL)swizzledSEL {
Class class = object_getClass((id)self); //不是类方法这里写 Class class = [self class]; 就好了
Method originalMethod = class_getInstanceMethod(class, originalSEL);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSEL);
BOOL didAddMethod =
class_addMethod(class,
originalSEL,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
class_replaceMethod(class,
swizzledSEL,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
}
+ (NSURL* )swizzled_URLWithString:(NSString*)strUrl {
if ([strUrl hasPrefix:RNBundleResourcePrefix]) {
strUrl = [GET_SERVICE(QDRNBundleManager) fileURLStringFromRNResourceRequest:strUrl];
}
return [self swizzled_URLWithString:strUrl];
}
原理很简单,替换NSURL的类方法,针对固定Prefix的url进行处理,下面是处理方法,针对模拟器情况下,为调试方便,会指向本地server的地址,非模拟器情况会按顺序读取沙盒和mainBundle。
//非模拟器读缓存
- (NSString *)fileURLStringFromRNResourceRequest:(NSString *)request {
#if (TARGET_IPHONE_SIMULATOR)
return [request stringByReplacingOccurrencesOfString:RNBundleResourcePrefix withString:@"http://localhost:8081/resources/"];
#else
NSString* strPath = [request stringByReplacingOccurrencesOfString:RNBundleResourcePrefix withString:@""];
//查找沙盒
if (_isDirReady) {
NSString* file = [_sandBoxBundleResourceDirectory stringByAppendingPathComponent:strPath];
if ([_fileManager fileExistsAtPath:file]) {
return [NSString stringWithFormat:@"file://%@", file];
}
}
//查找bundle
NSString* file = [[NSBundle mainBundle].resourcePath stringByAppendingPathComponent:strPath];
if ([_fileManager fileExistsAtPath:file]) {
return [NSString stringWithFormat:@"file://%@", file];
}
return request;
#endif
}
二、调起一个React Native组件
这里appdelegate中增加RCTBridge的成员变量bridge,因其初始化时会进行bundle的load操作以及大量的反射来生成module映射表,耗性能还耗内存,而我们又是混合app,很大概率不会使用,所以采用懒加载方式。为调试方便,在模拟器下指向本机server。
- (RCTBridge*)bridge {
if (_bridge == nil) {
_bridge= [[RCTBridge alloc]initWithDelegate:self launchOptions:nil];
}
return _bridge;
}
#pragma - mark RCTBridgeDelegate
- (NSURL*) sourceURLForBridge:(RCTBridge *)bridge {
#if (TARGET_IPHONE_SIMULATOR)
return [NSURL URLWithString:@"http://localhost:8081/index.ios.bundle?platform=ios&dev=true"];
#else
return [NSURL fileURLWithPath:GET_SERVICE(QDRNBundleManager).localReactNativeBundle];
#endif
}
在使用过程中,iOS没有那么多坑,很简单,初始化rootView并赋给ViewController的self.view就好了,传参可以传字典,所有基本数据类型都可以~
- (void) showImageSplash:(QDSplashResource*) source :(QDSplashEntity *)splash{
RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:[AppDelegate sharedAppDelegate].bridge
moduleName:@"adImageLaunch"
initialProperties:@{
@"imagePaths" : source.imagePathArray,
@"redirectUrl" : splash.url,
@"feedbackUrl" : splash.feedbackUrl,
@"adTagUrl" : splash.adTagUrl,
@"totalSeconds" : splash.totalSeconds,
@"extras" : source.themeDictionary? source.themeDictionary: @{}
}];
[self showAdWithRN:rootView];
}
- (void) showAdWithRN:(RCTRootView* )rootView {
if (rootView == nil) {
return;
}
QDLaunchAdViewController* adControl = [[QDLaunchAdViewController alloc] init];
rootView.frame = [UIScreen mainScreen].bounds;
UIView *adVideoHub = [[[NSBundle mainBundle] loadNibNamed:@"LaunchVideoHub" owner:nil options:nil] firstObject];
adVideoHub.frame = [UIScreen mainScreen].bounds;
rootView.loadingView = adVideoHub;
adControl.view = rootView;
_currentAdController = adControl;
adControl.delegate = self;
self.adWindow.rootViewController = [[QDNavigationController alloc] initWithRootViewController: adControl];
}
非常小的一个技巧,可以把任意一个xib文件座位RCTRootView的loadingView,作为其没有加载时候的默认图。
三、JS端向Native端的通信方式
先贴一段code,和之前Android部分贴的功能一致:
@interface LaunchBridgeModule : NSObject<RCTBridgeModule>
@end
@implementation LaunchBridgeModule
RCT_EXPORT_MODULE();
RCT_EXPORT_METHOD(dismissSplash:(RCTResponseSenderBlock)callback)
{
[[QDLaunchADTool shareLaunchTool] clickForRemoveLaunthADView];
callback(@[[NSNull null]]);
}
RCT_EXPORT_METHOD(open:(NSString*) strUrl:(RCTResponseSenderBlock)callback)
{
if (EmptyString(strUrl)) {
return;
}
QDLaunchAdViewController* launchController = [QDLaunchADTool shareLaunchTool].currentAdController;
if (launchController) {
strUrl = [strUrl stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
[QDViewControllerRouter routeWithURL:[NSURL URLWithString:strUrl] byNavigationController:launchController.navigationController ArticleDetailJumpDelegate:nil];
launchController.view.window.windowLevel = UIWindowLevelNormal;
launchController.showRedirectURL = YES;
}
callback(@[[NSNull null]]);
}
- (dispatch_queue_t)methodQueue
{
return dispatch_get_main_queue();
}
@end
所有JS能调用的Native都能力都在RCTBridgeModule
这个protocol里面做了定义。每个原生模块都以单实例模式限制了嵌入。
RCTBridgeModule
这个协议只有一个require方法:
+ (NSString *)moduleName;
// 对应Android中:
public String getName();
相对于Android将Module加进react native的映射表要先后生成Packager文件,然后addPackage两步,iOS拥有宏
和load
方法:在implement中增加RCT_EXPORT_MODULE();
一行宏就能解决之前的所有问题,极限解耦。该宏会覆写require方法,默认返回nil,并覆写load方法,该方法会在app启动时候class进入内存就会调用。具体load方法有哪些黑科技请自行google,这里不做介绍。
通过load方法的覆写,会把当前class在ReactBridge中进行注册,如果moduleName为空就直接使用当前class的name(注意,不管是否为空,都要保证最终module name和Android中定义的一致)。
第二个神奇的黑科技是RCT_EXPORT_METHOD()
这个宏(Android中一般实现AOP编程--所谓黑科技基本用注解,iOS基本是宏)。这个宏会为其包住的方法再生成一个新的方法:
+ (NSArray<NSString *> *)RCT_CONCAT(__rct_export__, \
RCT_CONCAT(js_name, RCT_CONCAT(__LINE__, __COUNTER__))) { \
return @[@#js_name, @#method]; \
}
看实现可以知道所有用这个宏新生成的方法都以__rct_export__
开头,返回值的都包含其方法名,在bridge初始化的时候,通过OC的runtime特性将其添加进入映射表,从而实现了"export"。
while (cls && cls != [NSObject class] && cls != [NSProxy class]) {
Method *methods = class_copyMethodList(object_getClass(cls), &methodCount);
for (unsigned int i = 0; i < methodCount; i++) {
Method method = methods[i];
SEL selector = method_getName(method);
if ([NSStringFromSelector(selector) hasPrefix:@"__rct_export__"]) {
IMP imp = method_getImplementation(method);
NSArray<NSString *> *entries =
((NSArray<NSString *> *(*)(id, SEL))imp)(_moduleClass, selector);
id<RCTBridgeMethod> moduleMethod =
[[RCTModuleMethod alloc] initWithMethodSignature:entries[1]
JSMethodName:entries[0]
moduleClass:_moduleClass];
[moduleMethods addObject:moduleMethod];
}
}
free(methods);
cls = class_getSuperclass(cls);
}
这些非常好的宏技巧我也在我自己的iOSBus中进行了广泛的使用,实现了非常好的解耦,还在进行开发调试中,欢迎follow、star。
RCTBridgeModule
中还定义了一些optional方法,我这里只用了一个methodQueue,规定调用在主线程队列,这个也是默认队列,可以不写,其它请自行发现。
网友评论