iOS原生主动调RN之RCTEventEmitter

笔者前不久刚刚从零开始用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 
#import 

@interface CommonEventEmitter : RCTEventEmitter

@end

并在 .m 文件中,使用RCT_EXPORT_MODULE();宏导出了模块,也实现了supportedEvents方法返回了所有需要传递的Event的名字

  (NSArray *)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();

一切准备就绪,一运行却并没有如愿顺利执行,出大问题了搬好小板凳记下来_

iOS原生主动调RN之RCTEventEmitter_第1张图片
记笔记.jpg

iOS原生主动调RN之RCTEventEmitter_第2张图片
忘了截图了借用.png

在Xcode上真机运行,就会发现错误断在了这里抛出了一个异常:


iOS原生主动调RN之RCTEventEmitter_第3张图片
异常.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是只读属性。。。)或许你也看到有人提示把AppdelegaterootViewbridage赋给这个你创建的对象,同样你也抱着试一试的心态试了:

  CommonEventEmitter *emitter = [[CommonEventEmitter alloc] init];
   AppDelegate *app = (AppDelegate *)[[UIApplication sharedApplication] delegate];
  [emitter setBridge:app.rootV.bridge];
  [emitter sendEventWithName:@"LogoutEventReminder" body:@{@"name": @"fuck"}] ;

结果发现发现不报这个错消息发出去了,然而你却哈啤不起来,你会发现RN代码里面明明写了监听,无论你怎么改都监听不到,同时页面也给了你一个警告:Sendingxxxx(你发送的消息事件名)with no listeners registered.

iOS原生主动调RN之RCTEventEmitter_第4张图片
warning.jpg

看来不究其所以然,问题没法解决了。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 *)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,发现值不同得到印证。当然,文中提到的把AppdelegaterootViewbridage设给你new的bridge之所以会报警告没有监听者也是如此,因为你addListener监听的bridge,跟发送消息的bridge不是同一个。

回溯思路,记下一步一步脱坑之旅并写下来真的费神,如果鄙文对你有用,手抖点个呗!

你可能感兴趣的:(iOS原生主动调RN之RCTEventEmitter)