美文网首页
react-native从入门到放弃(二)

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

作者: RunningTeemo | 来源:发表于2019-12-16 22:38 被阅读0次

    两年前建立了这个文件夹,希望能入手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


    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对象所需要执行的方法,这样就能让这个对象设置属性,并且调用方法


      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}; 
    
    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控件的渲染流程

    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):


    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方法

    <TouchableHighlight 
        style={[styles.highLight,{marginTop:50}]} 
        underlayColor='#deb887' 
        activeOpacity={0.8}
        onPress={() => this._nameAndAge()}
        >
        <Text>简单数据传递</Text>
    </TouchableHighlight>
    
    _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'
            })
        }
    
    image.png 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从入门到放弃(二)

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