美文网首页React Native
iOS原生主动调RN之RCTEventEmitter

iOS原生主动调RN之RCTEventEmitter

作者: Obsession丶执 | 来源:发表于2018-10-25 19:20 被阅读247次

    笔者前不久刚刚从零开始用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();
    

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

    记笔记.jpg 忘了截图了借用.png

    在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是只读属性。。。)或许你也看到有人提示把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.

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

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

    相关文章

      网友评论

        本文标题:iOS原生主动调RN之RCTEventEmitter

        本文链接:https://www.haomeiwen.com/subject/jghxtqtx.html