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

constantsToExport

- (NSDictionary *)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 代码逻辑

你可能感兴趣的:(React-Native iOS 中 Native 与 JS 通信)