笔者前不久刚刚从零开始用ReactNative(以下简称RN)开发并上架一款小型App,项目虽小但涉及到原生的真不少,RN的本质上还是调的原生亦或说是原生加载JS,总而言之,RN跟原生代码的交互通信是必须掌握的基本操作了,本文主要针对记录一下iOS原生主动调RN的填坑之旅。
RN与原生iOS的交互,我这里找了这篇江清清老师的这篇文章【React Native开发】React Native 进阶之原生混合与数据通信开发详解-适配iOS开发(61)总结得非常全面了。对于RN调iOS原生,一目了然,这里不再赘述,至于原生调iOS:显然我们可以通过RN主动调原生继而通过Callback或者Promise回调从而拿到原生传过来的数据;然而这样只是原生被动向RN传数据,很多时候我们需要原生代码主动向RN发送消息,这种场景其实很常见,比如集成第三方的服务,通过代理回调获取结果发送给RN。。。具体来说比如我项目里,接入了第三方IM,我需要在RN的代码里监听IM账号被其它设备踢出登录,第三方SDK已经提供了账号被踢出的监听回调。
对于上面所述情况,需要指出的是江清清老师文章中所用的RCTEventDispatcher的sendAppEventWithName
方法已经提示过时了,取而代之的就是我们所要说RCTEventEmitter。首先,我们来看看官方文档怎么说:
给 JavaScript 端发送事件
即使没有被 JavaScript 调用,原生模块也可以给 JavaScript 发送事件通知。最好的方法是继承RCTEventEmitter,实现suppportEvents方法并调用self sendEventWithName:。
按照官方文档的描述,你自定义了CommonEventEmitter继承RCTEventEmitter实现RCTBridgeModule协议
#import <React/RCTBridgeModule.h>
#import <React/RCTEventEmitter.h>
@interface CommonEventEmitter : RCTEventEmitter<RCTBridgeModule>
@end
并在 .m 文件中,使用RCT_EXPORT_MODULE();
宏导出了模块,也实现了supportedEvents
方法返回了所有需要传递的Event的名字
(NSArray<NSString *> *)supportedEvents{
return @[@"LogoutEventReminder"];
}
RN(JS)端,也完全依照官方文档创建了一个包含你的模块的NativeEventEmitter实例来订阅这些事件。
import { NativeEventEmitter, NativeModules } from 'react-native';
const { CommonEventEmitter } = NativeModules;
const managerEmitter = new NativeEventEmitter(CommonEventEmitter);
const subscription = managerEmitter.addListener(
'LogoutEventReminder',
(reminder) => console.log(reminder.name)
);
...
// 别忘了取消订阅,通常在componentWillUnmount生命周期方法中实现。
subscription.remove();
一切准备就绪,一运行却并没有如愿顺利执行,出大问题了搬好小板凳记下来_
在Xcode上真机运行,就会发现错误断在了这里抛出了一个异常:
异常.png
说_bridage
为空
此时你也可能会在网上搜到,.m文件里少写了@synthesize bridge = _bridge;
一开始我也不觉得不是这个问题,但鉴于看到RCTBridgeModule
协议里这样一段注释
@optional
/**
* A reference to the RCTBridge. Useful for modules that require access
* to bridge features, such as sending events or making JS calls. This
* will be set automatically by the bridge when it initializes the module.
* To implement this in your module, just add `@synthesize bridge = _bridge;`
*/
@property (nonatomic, weak, readonly) RCTBridge *bridge;
便试了一下,果不其然,这里并不是这个原因
到此,你可能想到给_bridge
赋值,(这里也留意到上面代码中bridge是只读属性。。。)或许你也看到有人提示把Appdelegate
的rootView
的bridage
赋给这个你创建的对象,同样你也抱着试一试的心态试了:
CommonEventEmitter *emitter = [[CommonEventEmitter alloc] init];
AppDelegate *app = (AppDelegate *)[[UIApplication sharedApplication] delegate];
[emitter setBridge:app.rootV.bridge];
[emitter sendEventWithName:@"LogoutEventReminder" body:@{@"name": @"fuck"}] ;
结果发现发现不报这个错消息发出去了,然而你却哈啤不起来,你会发现RN代码里面明明写了监听,无论你怎么改都监听不到,同时页面也给了你一个警告:Sending
xxxx(你发送的消息事件名)with no listeners registered.
看来不究其所以然,问题没法解决了。o(╯□╰)o,最后在Stack Overflow上找到这样一个回答
When you used the macro RCT_EXPORT_MODULE() React-Native will instantiate the class for you, and any subsequent alloc/inits will create new instances, unrelated the original. The bridge will not be instantiated in these new instances.
You can solve your problem by using NSNotifications.
意思一旦你使用宏声明该类是EXPORT_MODULE
,React Native将会为你初始化该类的实例.之后你在其他任何地方创建这个类的实例(alloc 、new或 init),都将会创建新的实例,与原始的那个无关,bridge
也不会在这些新的实例中被初始化,同时也给出了提示可以用通知NSNotifications
来曲线救国。
Helper.h:
#import "RCTEventEmitter.h"
@interface Helper : RCTEventEmitter
+ (void)emitEventWithName:(NSString *)name andPayload:(NSDictionary *)payload;
@end
Helper.m:
#import "Helper.h"
/*
优化无监听处理的事件
如果你发送了一个事件却没有任何监听处理,则会因此收到一个资源警告。要优化因此带来的额外开销,你可以在你的RCTEventEmitter子类中覆盖startObserving和stopObserving方法。
*/
@implementation Helper
{
bool hasListeners;
}
RCT_EXPORT_MODULE();
- (NSArray<NSString *> *)supportedEvents {
return @[@"SpotifyHelper"];
}
// 在添加第一个监听函数时触发
- (void)startObserving
{
hasListeners = YES;
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(emitEventInternal:)
name:@"event-emitted"
object:nil];
}
- (void)stopObserving
{
hasListeners = NO;
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)emitEventInternal:(NSNotification *)notification
{
if (hasListeners) { // Only send events if anyone is listening
[self sendEventWithName:@"SpotifyHelper"
body:notification.userInfo];
}
}
+ (void)emitEventWithName:(NSString *)name andPayload:(NSDictionary *)payload
{
[[NSNotificationCenter defaultCenter] postNotificationName:@"event-emitted"
object:self
userInfo:payload];
}
// Remaining methods
@end
答案里的代码很清晰明了,我就补贴自己的代码了,要说的是结合报错的原因还有一种解决方案就是用单例:
+(id)allocWithZone:(NSZone *)zone {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [super allocWithZone:zone];
});
return sharedInstance;
}
RCT_EXPORT_MODULE();
在自定义的EventEmitter
类中复写上述方法,这样我们使用宏导出前后创建的都是一个实例了。
最后,问题的答案,也可以通过在你的EventEmitter
类中分别打印startObserving方法和你发送消息的方法里self.bridge
,发现开始监听的bridge
有值,而发送方法里的bridge
却为空,进一步再打印两者中的self
,发现值不同得到印证。当然,文中提到的把Appdelegate
的rootView
的bridage
设给你new的bridge
之所以会报警告没有监听者也是如此,因为你addListener
监听的bridge
,跟发送消息的bridge
不是同一个。
回溯思路,记下一步一步脱坑之旅并写下来真的费神,如果鄙文对你有用,手抖点个👍呗!
网友评论