react-native从入门到放弃(二)

两年前建立了这个文件夹,希望能入手rn多更新一点的,但是只更新了两篇就断了,因为当前公司规定,年底之前我必须要做一次技术分享,于是乎,想重新拾起React-native入门

一、必须条件
node、watchman、xcode、cocoapod。这些都可以通过brew来装。

npx react-native init AwesomeProject
cd AwesomeProject
npx react-native run-ios

中间报了一次错Could not connect to development server


react-native从入门到放弃(二)_第1张图片
image.png

最后解决办法
配置localhost 127.0.0.1 解决。

一、简单的聊一下RN和oc的交互

iOS原生API有个JavaScriptCore框架,通过它就能实现JS和OC交互

  1. 首先写好前端jsx代码
    2.把jsx代码解析成JavaScript代码
    3.OC读取JS文件
    4.把JavaScript读取出来,利用JavaScriptCore执行
    5.javaScript代码返回一个数组,数组中会描述OC对象,OC对象的属性,OC对象所需要执行的方法,这样就能让这个对象设置属性,并且调用方法


    react-native从入门到放弃(二)_第2张图片
    image.png

二、聊聊Properties

React Native用iOS自带的JavaScriptCore作为JS的解析引擎,但并没有用到JavaScriptCore提供的一些可以让JS与OC互调的特性,而是自己实现了一套机制,这套机制可以通用于所有JS引擎上,在没有JavaScriptCore的情况下也可以用webview代替,实际上项目里就已经有了用webview作为解析引擎的实现,应该是用于兼容iOS7以下没有JavascriptCore的版本。
在appdelegate的RN声明中。
通过RCTRootView的初始化函数你可以将任意属性传递给React Native应用。参数initialProperties必须是NSDictionary的一个实例。这一字典参数会在内部被转化为一个可供JS组件调用的JSON对象
appProperties被设置之后,如果和之前的值有变化,重新渲染

NSArray *imageList = @[@"http://foo.com/bar3.png",
                   @"http://foo.com/bar4.png"];
rootView.appProperties = @{@"images" : imageList}; 
react-native从入门到放弃(二)_第3张图片
image.png
1、创建RCTRootView

设置窗口根控制器的View,把RN的View添加到窗口上显示
我们使用RCTRootView将React Natvie视图封装到原生组件中。RCTRootView是一个UIView容器,承载着React Native应用。同时它也提供了一个联通原生端和被托管端的接口

2、创建RCTBridge

桥接对象,管理JS和OC交互,做中转左右

3、创建loadSource

加载JS资源

4、执行[RCTBatchedBridge initModulesWithDispatchGroup

创建OC模块表

5、往JS中插入OC模块表

6、执行完JS代码,回调OC,调用OC中的组件

7、完成UI渲染

三、RN UI控件的渲染流程

react-native从入门到放弃(二)_第4张图片
image.png

1、RCTRootView runApplication:bridge

通知JS运行App

2、RCTBatchedBridge _processResponse:json error:error

处理执行完JS代码(runApplication)返回的相应,包含需要添加多少子控件的信息。

3、RCTBatchedBridge batchDidComplete

RCTUIManager调用处理完成的方法,就会开始去加载rootView的子控件。

4、RCTUIManager createView:viewName:rootTag:props

通过JS执行OC代码,让UI管理者创建子控件View

通过RCT_EXPORT_METHOD宏定义createView这个方法

RCT_EXPORT_METHOD(createView:(nonnull NSNumber *)reactTag
                  viewName:(NSString *)viewName
                  rootTag:(nonnull NSNumber *)rootTag
                  props:(NSDictionary *)props)

RCT_EXPORT_METHOD宏:会在JS中生成对应的OC方法,这样JS就能直接调用

注意每创建一个UIView,就会创建一个RCTShadowView,与UIView一一对应

RCTShadowView:保存对应UIView的布局和子控件,管理UIView的加载

5、[RCTUIManager _layoutAndMount]

布局RCTRootView和增加子控件

6、[RCTUIManager setChildren:reactTags:]

给RCTRootView对应的RCTRootShadowView设置子控件

注意:此方法也是JS调用OC方法

7、[RCTRootShadowView insertReactSubview:view atIndex:index++]

遍历子控件数组,给RCTRootShadowView插入所有子控件

8、[RCTShadowView processUpdatedProperties:parentProperties:]

处理保存在RCTShadowView中属性,就会去布局RCTShadowView对应UIView的所有子控件

9、[RCTView didUpdateReactSubviews]

给原生View添加子控件

10、完成UI渲染

四、RCTBridgeModule

1、RCTBridgeModule
在React Native中,如果实现一个原生模块,需要实现RCTBridgeModule”协议

2、RCT_EXPORT_MODULE()
如果我们实现了RCTBridgeModule协议,我们的类需要包含RCT_EXPORT_MODULE()宏。这个宏也可以添加一个参数用来指定在Javascript中访问这个模块的名字。如果你不指定,默认就会使用这个Objective-C类的名字

3、RCT_EXPORT_METHOD()
与此同时我们需要声明RCT_EXPORT_METHOD()宏来实现要给Javascript导出的方法,否则React Native不会导出任何方法。

举个例子,OC定义了一个模块RCTSQLManager,里面有个方法-query:successCallback:,JS可以直接调用RCTSQLManager.query并通过回调获取执行结果。

//OC:
@implement RCTSQLManager
- (void)query:(NSString *)queryData successCallback:(RCTResponseSenderBlOCk)responseSender
{
     RCT_EXPORT();
     NSString *ret = @"ret"
     responseSender(ret);
}
@end

//JS:
RCTSQLManager.query("SELECT * FROM table", function(result) {
     //result == "ret";
});

接下来看看它是怎样实现的。

模块配置表

首先OC要告诉JS它有什么模块,模块里有什么方法,JS才知道有这些方法后才有可能去调用这些方法。这里的实现是OC生成一份模块配置表传给JS,配置表里包括了所有模块和模块里方法的信息。例:

{
    "remoteModuleConfig": {
        "RCTSQLManager": {
            "methods": {
                "query": {
                    "type": "remote",
                    "methodID": 0
                }
            },
            "moduleID": 4
        },
        ...
     },
}

里面标记了所有 Objective-C 暴露给 JavaScript 的模块和方法。这样,无论是哪一方调用另一方的方法,实际上传递的数据只有 ModuleId、MethodId 和 Arguments
这三个元素,它们分别表示类、方法和方法参数,当 Objective-C 接收到这三个值后,就可以通过 runtime 唯一确定要调用的是哪个函数,然后调用这个函数

OC端和JS端分别各有一个bridge,两个bridge都保存了同样一份模块配置表,JS调用OC模块方法时,通过bridge里的配置表把模块方法转为模块ID和方法ID传给OC,OC通过bridge的模块配置表找到对应的方法执行之,以上述代码为例,流程大概是这样(先不考虑callback):


react-native从入门到放弃(二)_第5张图片
image.png

在了解这个调用流程之前,我们先来看看OC的模块配置表式怎么来的。我们在新建一个OC模块时,JS和OC都不需要为新的模块手动去某个地方添加一些配置,模块配置表是自动生成的,只要项目里有一个模块,就会把这个模块加到配置表上,那这个模块配置表是怎样自动生成的呢?分两个步骤:
1.取所有模块类

每个模块类都实现了RCTBridgeModule接口,可以通过runtime接口objc_getClassList或objc_copyClassList取出项目里所有类,然后逐个判断是否实现了RCTBridgeModule接口,就可以找到所有模块类,实现在RCTBridgeModuleClassesByModuleID()方法里。

2.取模块里暴露给JS的方法

一个模块里可以有很多方法,一些是可以暴露给JS直接调用的,一些是私有的不想暴露给JS,怎样做到提取这些暴露的方法呢?我能想到的方法是对要暴露的方法名制定一些规则,比如用RCTExport_作为前缀,然后用runtime方法class_getInstanceMethod取出所有方法名字,提取以RCTExport_为前缀的方法,但这样做恶心的地方是每个方法必须加前缀。React Native用了另一种黑魔法似的方法解决这个问题:编译属性attribute

在上述例子中我们看到模块方法里有句代码:RCT_EXPORT(),模块里的方法加上这个宏就可以实现暴露给JS,无需其他规则,那这个宏做了什么呢?来看看它的定义:

`#define RCT_EXPORT(JS_name) __attribute__((used, section("__DATA,RCTExport" \`

`)))` `static` `const` `char` `*__rct_export_entry__[] = { __func__, #JS_name }`

这个宏的作用是用编译属性attribute给二进制文件新建一个section,属于__DATA数据段,名字为RCTExport,并在这个段里加入当前方法名。编译器在编译时会找到attribute进行处理,为生成的可执行文件加入相应的内容。效果可以从linkmap看出来:

`# Sections:`

`# Address Size Segment Section`

`0x100001670 0x000C0180 __TEXT __text`

`...`

`0x10011EFA0 0x00000330 __DATA RCTExport`

`0x10011F2D0 0x00000010 __DATA __common`

`0x10011F2E0 0x000003B8 __DATA __bss`

`...`

`0x10011EFA0 0x00000010 [ 4] -[RCTStatusBarManager setStyle:animated:].__rct_export_entry__`

`0x10011EFB0 0x00000010 [ 4] -[RCTStatusBarManager setHidden:withAnimation:].__rct_export_entry__`

`0x10011EFC0 0x00000010 [ 5] -[RCTSourceCode getScriptText:failureCallback:].__rct_export_entry__`

`0x10011EFD0 0x00000010 [ 7] -[RCTAlertManager alertWithArgs:callback:].__rct_export_entry__`

`...`

可以看到可执行文件数据段多了个RCTExport段,内容就是各个要暴露给JS的方法。这些内容是可以在运行时获取到的,在RCTBridge.m的RCTExportedMethodsByModuleID()方法里获取这些内容,提取每个方法的类名和方法名,就完成了提取模块里暴露给JS方法的工作。

整体的模块类/方法提取实现在RCTRemoteModulesConfig()方法里。

五、js调用OC

Module一定实现RCTBridgeModule协议。
首先我们要在iOSExport类的实现中添加这句宏定义:RCT_EXPORT_MODULE()
RCT_EXPORT_MODULE()如果你不传入参数,那么你在iOS中导出的模块名就是类名,你也可以插入参数作为自定义模块名。

@implementation iOSExport
//定义导出的模块名
RCT_EXPORT_MODULE()
@end

协议方法的实现需要在RCT_EXPORT_METHOD,这个宏里面。
我们先写一个有两个参数的方法给js调用:

@implementation iOSExport
//定义导出的模块名
RCT_EXPORT_MODULE()

//定义导出的方法名
RCT_EXPORT_METHOD(rnToiOS:(NSString *)name :(NSInteger)age) {
  NSString *st = [NSString stringWithFormat:@"name:%@,age:%ld",name,age];
    NSLog(@"test:%@",st);
    [self alter:st];
}
@end

这样OC端的工作就OK了,下面我们继续看看js端怎么调用:
首先我们要在js文件里面 import NativeModules
然后在我们需要使用的时候获取导出的模块,我们再用模块调用iOS的导出的函数名就可以了,看代码
//创建一个可以点击的按钮,点击按钮后调用iOS的rnToiOS方法

 this._nameAndAge()}
    >
    简单数据传递


_nameAndAge() { //多参数的传递
        var iOSExport = NativeModules.iOSExport //获取到模块
        iOSExport.rnToiOS('帝君',200) //直接调用函数
        this.setState({
            text:'rnToiOS'
        })
}

下面我们再看如何在js端调用iOS的含有字典参数和回调函数的方法。iOS提供给js的回调函数是使用block实现的,看下回调函数的说明:

/**
 * The type of a block that is capable of sending a response to a bridged
 * operation. Use this for returning callback methods to JS.
 */
typedef void (^RCTResponseSenderBlock)(NSArray *response);

下面我们就可以用回调函数做参数,写一个我们需要的方法:

RCT_EXPORT_METHOD(rnToiOSwithDic:(NSDictionary*)dic andCallback:(RCTResponseSenderBlock)callback) {
  NSMutableString *st = [NSMutableString string];
  for (NSObject *key in dic.allKeys) {
    NSString *string = [NSString stringWithFormat:@"%@:%@;",key,[dic objectForKey:key]];
     [st appendString:string];
  }
  callback(@[@"error",st]);
  [self alter:st];
}

在js中调用如下

//字典的传递和返回值
    _dic() { 
        var iOSExport = NativeModules.iOSExport //获取导出的模块
        iOSExport.rnToiOSwithDic({ //调用iOS的方法,第一个参数是字典
            '姓名':'幽冥',
            '年龄':20,
            '法力':'200'
        },(error,strings) =>{ //第二个参数是函数,做为回调函数给iOS将由iOS调用

            this.setState({
                text:strings
            })
        })
        this.setState({
            text:'rnToiOSwithDic'
        })
    }
react-native从入门到放弃(二)_第6张图片
image.png
react-native从入门到放弃(二)_第7张图片
image.png

11个步骤,详细说明下这些步骤:

1.JS端调用某个OC模块暴露出来的方法。

2.把上一步的调用分解为ModuleName,MethodName,arguments,再扔给MessageQueue处理。

在初始化时模块配置表上的每一个模块都生成了对应的remoteModule对象,对象里也生成了跟模块配置表里一一对应的方法,这些方法里可以拿到自身的模块名,方法名,并对callback进行一些处理,再移交给MessageQueue。具体实现在BatchedBridgeFactory.js的_createBridgedModule里,整个实现区区24行代码。

3.在这一步把JS的callback函数缓存在MessageQueue的一个成员变量里,用CallbackID代表callback。在通过保存在MessageQueue的模块配置表把上一步传进来的ModuleName和MethodName转为ModuleID和MethodID。

4.把上述步骤得到的ModuleID,MethodId,CallbackID和其他参数argus传给OC。至于具体是怎么传的,后面再说。

5.OC接收到消息,通过模块配置表拿到对应的模块和方法。

实际上模块配置表已经经过处理了,跟JS一样,在初始化时OC也对模块配置表上的每一个模块生成了对应的实例并缓存起来,模块上的每一个方法也都生成了对应的RCTModuleMethod对象,这里通过ModuleID和MethodID取到对应的Module实例和RCTModuleMethod实例进行调用。具体实现在_handleRequestNumber:moduleID:methodID:params:。

6.RCTModuleMethod对JS传过来的每一个参数进行处理。

RCTModuleMethod可以拿到OC要调用的目标方法的每个参数类型,处理JS类型到目标类型的转换,所有JS传过来的数字都是NSNumber,这里会转成对应的int/long/double等类型,更重要的是会为block类型参数的生成一个block。

例如-(void)select:(int)index response:(RCTResponseSenderBlock)callback 这个方法,拿到两个参数的类型为int,block,JS传过来的两个参数类型是NSNumber,NSString(CallbackID),这时会把NSNumber转为int,NSString(CallbackID)转为一个block,block的内容是把回调的值和CallbackID传回给JS。

这些参数组装完毕后,通过NSInvocation动态调用相应的OC模块方法。

7.OC模块方法调用完,执行block回调。

8.调用到第6步说明的RCTModuleMethod生成的block。

9.block里带着CallbackID和block传过来的参数去调JS里MessageQueue的方法invokeCallbackAndReturnFlushedQueue。

10.MessageQueue通过CallbackID找到相应的JS callback方法。

11.调用callback方法,并把OC带过来的参数一起传过去,完成回调。

整个流程就是这样,简单概括下,差不多就是:JS函数调用转ModuleID/MethodID -> callback转CallbackID -> OC根据ID拿到方法 -> 处理参数 -> 调用OC方法 -> 回调CallbackID -> JS通过CallbackID拿到callback执行

思考
上述第4步留下一个问题,JS是怎样把数据传给OC,让OC去调相应方法的?

通过返回值。JS不会主动传递数据给OC,在调OC方法时,会在上述第4步把ModuleID,MethodID等数据加到一个队列里,等OC过来调JS的任意方法时,再把这个队列返回给OC,此时OC再执行这个队列里要调用的方法。

一开始不明白,设计成JS无法直接调用OC,需要在OC去调JS时才通过返回值触发调用,整个程序还能跑得通吗。后来想想纯native开发里的事件响应机制,就有点理解了。native开发里,什么时候会执行代码?只在有事件触发的时候,这个事件可以是启动事件,触摸事件,timer事件,系统事件,回调事件。而在React Native里,这些事件发生时OC都会调用JS相应的模块方法去处理,处理完这些事件后再执行JS想让OC执行的方法,而没有事件发生的时候,是不会执行任何代码的,这跟native开发里事件响应机制是一致的。

你可能感兴趣的:(react-native从入门到放弃(二))