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 <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不是同一个。

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,313评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,369评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,916评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,333评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,425评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,481评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,491评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,268评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,719评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,004评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,179评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,832评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,510评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,153评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,402评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,045评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,071评论 2 352