React-Native iOS 中 Native 与 JS 通信

此文写于2016/03/14,所以比较老了

React-Native 中的 宏

RCT_CONACT

/**
 * Concat two literals. Supports macro expansions,
 * e.g. RCT_CONCAT(foo, __FILE__).
 */
#define RCT_CONCAT2(A, B) A ## B
#define RCT_CONCAT(A, B) RCT_CONCAT2(A, B)

char* RCT_CONCAT(name, __FUNCTION__) = "logtag"; 展开之后:
char* name__FUNCTION__ = "logtag";

RCT_EXTERN_REMAP_METHOD

定义在 (React/Base/RCTBridgeModule.h)

#define RCT_EXTERN_REMAP_METHOD(js_name, method) \
+ (NSArray<NSString *> *)RCT_CONCAT(__rct_export__, \
RCT_CONCAT(js_name, RCT_CONCAT(__LINE__, __COUNTER__))) { \
return @[@#js_name, @#method]; \
}

用一放在类实现中:

@interface MyMacroDecl : NSObject
@end

@implementation MyMacroDecl
RCT_EXTERN_REMAP_METHOD(itemAtIndex,itemAtIndexPath:(NSIndexPath*)indexPath)
@end

上面的宏展开之后:

+ (NSArray<NSString *> *)__rct_export__itemAtIndex600 { 
return @[@"itemAtIndex", @"itemAtIndexPath:(NSIndexPath*)indexPath"];
 }

得到一个将要公司的方法的配置元组. 元组第一个是在JS 中调用所指定的名称,右边是对应的 ObjC 对应方法的签名.
我有种感觉就是, React 通过宏,将软件语义中需要通过协议来实现的方法使用 宏来帮且实现.

在 React 的库中,并不会直接使用 RCT_EXTERN_REMAP_METHOD
因为上面只是生成了 JS 中的方法名对 OC 中的方法名的映射的一个元组.

实际上使用的是另外的包装了 OC 方法签名的宏

PS: 上面的宏中 __COUNTER__ 宏的使用也很关键. 参考 https://gcc.gnu.org/onlinedocs/cpp/Common-Predefined-Macros.html
它保证了生成的方法签名的唯一性.因为 __COUNTER__ 是由编译器提供的一个计数器.

RCT_REMAP_METHOD

它在 RCT_EXTERN_REMARP_METHOD 基础之上加上了,OC 中的方法签名声明.

#define RCT_REMAP_METHOD(js_name, method) \
  RCT_EXTERN_REMAP_METHOD(js_name, method) \
  - (void)method

这样使用:

@implementation MyMacroDecl
RCT_REMAP_METHOD(itemAtIndex,itemAtIndexPath:(NSIndexPath*)indexPath){
  
}
@end

展开之后得到:

@implementation MyMacroDecl
+ (NSArray<NSString *> *)__rct_export__itemAtIndex600 { 
        return @[@"itemAtIndex", @"itemAtIndexPath:(NSIndexPath*)indexPath"];
 }
- (void)itemAtIndexPath:(NSIndexPath*)indexPath{

}
@end

这样就实现了通过使用宏包装,自动为 Objc 的方法itemAtIndexPath:(NSIndexPath*)indexPath 添加到发布到 JS 环境的映射.

React 还提供了一个更方便的,它甚至可以自动确定发布到 JS 环境中的方法名.

以前的 ReactNative 按

RCT_EXPORT_METHOD

它是通过对 RCT_REMAP_METHOD 进一步包装,默认将 js_name 这一宏参数设置为空来实现的.
```objc
#define RCT_EXPORT_METHOD(method)
RCT_REMAP_METHOD(, method)


官方示例文档说明:

```objc
/*
* For example, in ModuleName.m:
*
* - (void)doSomething:(NSString *)aString withA:(NSInteger)a andB:(NSInteger)b
* { ... }
*
* becomes
*
* RCT_EXPORT_METHOD(doSomething:(NSString *)aString
*                   withA:(NSInteger)a
*                   andB:(NSInteger)b)
* { ... }
*
* and is exposed to JavaScript as `NativeModules.ModuleName.doSomething`.
*/

对于如何实现一个带有 Promise 的方法,其文档说明如下:

 /*
 * ## Promises
 *
 * Bridge modules can also define methods that are exported to JavaScript as
 * methods that return a Promise, and are compatible with JS async functions.
 *
 * Declare the last two parameters of your native method to be a resolver block
 * and a rejecter block. The resolver block must precede the rejecter block.
 *
 * For example:
 *
 * RCT_EXPORT_METHOD(doSomethingAsync:(NSString *)aString
 *                           resolver:(RCTPromiseResolveBlock)resolve
 *                           rejecter:(RCTPromiseRejectBlock)reject
 * { ... }
 *
 * Calling `NativeModules.ModuleName.doSomethingAsync(aString)` from
 * JavaScript will return a promise that is resolved or rejected when your
 * native method implementation calls the respective block.
 */

刚才忘记说一点了 就是我们公开的方法是无法直接返回值的,
必须得通过回调机制来返回值.

PS:我曾经实现过从公司实现接口直接给 JS 环境可以直接有返回值的方法,
实现方法就是: 直接用纯JS 注入一个同步方法. 这样的方法其实只能返回固定的值了.

RCT_EXPORT_MODULE(js_name)

其定义如下: (在 React/Base/RCTBridgeModule.h) 中

#define RCT_EXTERN extern __attribute__((visibility("default")))

#define RCT_EXPORT_MODULE(js_name) \
RCT_EXTERN void RCTRegisterModule(Class); \
+ (NSString *)moduleName { return @#js_name; } \
+ (void)load { RCTRegisterModule(self); }

展开之后得到:

     extern __attribute__((visibility("default"))) void RCTRegisterModule(Class); 
    + (NSString *)moduleName { return @""; } 
    + (void)load { RCTRegisterModule(self); }

需要参与到交到的模块中,在其实现块中,使用 上面的宏.
上面的宏 实现的模型的基本要求.

+(void)load 在类被加载后调用以便自行注册到模块表中.

其代码注释:

/**
 * Place this macro in your class implementation to automatically register
 * your module with the bridge when it loads. The optional js_name argument
 * will be used as the JS module name. If omitted, the JS module name will
 * match the Objective-C class name.
 */
#define RCT_EXPORT_MODULE(js_name) \
RCT_EXTERN void RCTRegisterModule(Class); \
+ (NSString *)moduleName { return @#js_name; } \
+ (void)load { RCTRegisterModule(self); }

// Implemented by RCT_EXPORT_MODULE
+ (NSString *)moduleName;

�### RCT_EXTERN_REMAP_MODULE,RCT_EXTERN_MODULERCT_EXTERN_METHOD
这几个宏的定义如下, 主要是用来将 Swift 的类,或者 私有类的方法公开出去.

#define RCT_EXTERN_METHOD(method) \
RCT_EXTERN_REMAP_METHOD(, method)

#define RCT_EXTERN_REMAP_MODULE(js_name, objc_name, objc_supername) \
objc_name : objc_supername \
@end \
@interface objc_name (RCTExternModule) <RCTBridgeModule> \
@end \
@implementation objc_name (RCTExternModule) \
RCT_EXPORT_MODULE(js_name)

#define RCT_EXTERN_MODULE(objc_name, objc_supername) \
RCT_EXTERN_REMAP_MODULE(, objc_name, objc_supername)

上面是通过宏创建一个空的类声明:

@interface objc_name : objc_supername \
@end \

一个实现了 RCTBridgeModule 协议,名为 RCTExternModule 的空类别声明:

@interface objc_name (RCTExternModule) <RCTBridgeModule> \
@end \

和一个类及类别的实现:

@implementation objc_name (RCTExternModule) \
RCT_EXPORT_MODULE(js_name)
@end

还是通过 RCT_EXPORT_MODULE(js_name) 实现了 moduleName 这一 Getter

比如向 JS 环境发布 Swift 环境下的方法的方式就是在 OC 环境下创建一个用于发布方法的包装类.
比如下面就是发布 Swift 环境下的方法的官方文档 .

/**
 * Use this macro in a private Objective-C implementation file to automatically
 * register an external module with the bridge when it loads. This allows you to
 * register Swift or private Objective-C classes with the bridge.
 *
 * For example if one wanted to export a Swift class to the bridge:
 *
 * MyModule.swift:
 *
 *   @objc(MyModule) class MyModule: NSObject {
 *
 *     @objc func doSomething(string: String! withFoo a: Int, bar b: Int) { ... }
 *
 *   }
 *
 * MyModuleExport.m:
 *
 *   #import "RCTBridgeModule.h"
 *
 *   @interface RCT_EXTERN_MODULE(MyModule, NSObject)
 *
 *   RCT_EXTERN_METHOD(doSomething:(NSString *)string withFoo:(NSInteger)a bar:(NSInteger)b)
 *
 *   @end
 *
 * This will now expose MyModule and the method to JavaScript via
 * `NativeModules.MyModule.doSomething`
 */

RCTBridgeModule 原生模块的协议.

RCTBridgeModule 协议
有1个必须的方法moduleName,
有2个可选的属性 bridge,methodQueue
有4个可选的方法 methodsToExport,constantsToExport,batchDidComplete,partialBatchDidFlush

通过使用 RCTBridgeModule 头文件提供的宏,第一个必须的方法,都通过宏帮我们实现了.
其实则按使用场景实现.

下面对其协议属性及方法进行说明.

moduleName 模块名

默认是以类名为模块名. 可以通过指定的宏来实现指定模块名.

methodQueue

@property (nonatomic, strong, readonly) dispatch_queue_t methodQueue;
在 iOS 开发中通常我们都要求 UI 操作需要在 UI 线程中执行,一些 IO 操作或者网络操作在 工作线程中执行.
为了避免阻塞 UI 线程, 模块的操作默认都是在 默认的后台线程中执行的.
如果需要在 UI 线程中执行,
可以实现此可选的协议方法,如下:

- (dispatch_queue_t)methodQueue
{
  return dispatch_get_main_queue();
}

这里是作为一个 Getter 方法来实现此属性协议,如果你只是需要用到此模块的后台线程.
那可以在类实现中,添加一个属性合成语句即可,模块初始化时会被自动设置属性的.
@synthesize methodQueue = _methodQueue;

bridge

@property (nonatomic, weak, readonly) RCTBridge *bridge;
当需要在模块中调用 JS 环境中代码时,就需要这个 bridge 了.
为了在模块中可以使用 bridge 实现,只要在类实现中添加一个相应的属性合成声明即可,
模型初始化时会自动设置此变量.
@synthesize bridge = _bridge;

关于 RCTBridge 后面会有更详细的研究.

methodsToExport

- (NSArray<id<RCTBridgeMethod>> *)methodsToExport;
通过实现此协议方法,可以提供一个批量公开模型方法的接口. 因此也可以通过此协议来返回要公开的接口.
使用宏的方法也是最终解析 方法映射表然后得到 实现了 RCTBridgeMethod 协议的类 RCTModuleMethod
值得注意的是,此协议方法只会在注册时被调用一次.

constantsToExport

- (NSDictionary<NSString *, id> *)constantsToExport;
通过此协议方法可以实现公开属性对对应的 JS 模块中.
比如公开一个名为 MyName 的属性.
那 JS 环境下就可以通过 NativeModules.ModuleName.MyName 的方式来使用此模块的属性.
值得注意的是,此协议方法只会在注册时被调用一次. 无法用来传递动态的值.

batchDidComplete

- (void)batchDidComplete;
通知此模块有一批JS 方法的调用已经完成了.

partialBatchDidFlush

- (void)partialBatchDidFlush;
通知此模块一些 JS 方法调用已经开始了. 它比 batchDidComplete 先调用,也会被更频繁的调用.

通信桥梁

/**
 * 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;

enqueueJSCall: args:

从通信上来说, RCTBridge 提供了如下方法:

- (void)enqueueJSCall:(NSString *)moduleDotMethod args:(NSArray *)args;

用来调用当前 RCTBridge 连接的 JS 环境中的 JS 函数.
moduleDotMethod 正如其名,就是类似 React.getCompById 这样的调用方法名,
args 是调用的参数.
此方法在任何线程中调用都安全,因为 RCTBridge 总是会在 JS 线程中调用它的.

RCTBridgeDelegate

此协议为我们实现自己的加载JS bundle 逻辑提供了可能.

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

推荐阅读更多精彩内容