React Native浅析

基本概念
  • 啥是ReacNative
    React Native:(简称RN)基于React开发的App,它的有点是可以跨平台、跳过App Store审核,远程更新代码,提高迭代频率和效率,既有Native的体验,又保留React的开发效率。相对的,缺点就是对于不熟悉前端开发的人员上手比较慢,不能真正意义上做到跨平台,使用后,对app体积增加。
  • React Native原理
    RN的底层简单的说是把React转换为原生API,那么在iOS和安卓平台是有区别的,毕竟苹果和安卓是2个完全不一的系统,所以React Native需要iOS,安卓都写,React Native底层解析原生API是分开实现的,iOS一套,安卓一套。

如上所述RN在实现上iOS和安卓不太一样,下面的解析都是基于在iOS平台上的。毕竟我也不懂Android。

React Native是如何做到JS和OC交互

上面说到RN是把React的js代码转换为原生的api。也就是说要做到JS语言和OC能交互,要做到JS=>OC且OC=>JS。一门动态或者脚本语言要跟本地语言互通要具备如下几点:

  1. 本地语言有一个runtime机制来对对象的方法调用进行动态解析。
  2. 本地语言一定有一个脚本的解析引擎
  3. 建立一个脚本语言到本地语言的映射表,KEY是脚本语言认识的符号,VALUE是本地语言认识的符号。通过这个映射表来构建从脚本到本地的调用。

那么以上的条件满足了么?刚好OC有runtime,也有JavaScriptCore.frame,完全能满足这些条件。

ReactNative 结构图

在原生的OC代码和React代码之间,有座桥梁链接在了一起(OCBridge和JSBridge)。
这样的设计,原生的模块和JS模块就充分的解耦了,想要扩展就非常的方便。
假如你发现现有RN框架有些功能做不到了?扩展写个原生代码模块,接入这个桥梁就行了。也就是说只要遵循RN的协议RCTBridgeModule去写的OC Module对象,使用RCT_EXPORT_MODULE()宏注册类,使用RCT_EXPORT_METHOD()宏注册方法,那么这个OC Module以及他的OC Method都会被JS与OC的ModuleConfig进行统一控制管理。

一、RN 项目启动

  • 创建RCTBridge *bridge的时候,有不同的情况:
    1. 通过遵守RCTBridgeDelegate协议,实现协议方法sourceURLForBridge:来创建bridge。


      创建Bridge

      Bridge代理
    2. 在创建CTRootView的时候,指定URL的路径,通过创建RootView来创建bridge。


      image.png
    3. 指定URL路径的也分不同的情况。
      a、如上图,我们可以开启本地的服务,指定index路径。
      b、[[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; 在指定已经编译好的放项目里的main.jsbundle。
      c、NSURL *jsCodeLocation = [CodePush bundleURL]; 还可以通过远程来动态下发jsbundle。

注:1、当我们设置了通过codePush去远程下发,还没拿到新代码时,也会优先去读main.jsbundle的内容。
2、当我们是通过指定访问index但是本地也没有开启服务时,也会自动去读main.jsbundle的内容。

代码流程

从上面可以看出,我们在创建rootView的时候,可以通过传入创建的不能动了URL来创建,此时,在rootView内部会自动创建一个bridge。如果项目里的rootView有多个的话,那么就会出现多个bridge的情况。当重复进入react-native页面、退出react-native页面的操作,RCTBridge对象会被重复创建、销毁。这样会照成很大的内存开销。所以原生中会访问多个RN页面,建议把bridge的创建单例化,多个RCTRootView可共用一个RCTBridge。

@interface  HJBridgeManager : NSObject
// 全局唯一的bridge
@property (nonatomic, readonly, strong) RCTBridge *bridge;
+ (instancetype)shareInstance;
@end

@implementation HJBridgeManager

static HJBridgeManager *_instance = nil;
+ (instancetype)shareInstance{
    if (_instance == nil) {
        _instance = [[self alloc] init];
    }
    return _instance;
}

+ (instancetype)allocWithZone:(struct _NSZone *)zone{
    if (_instance == nil) {
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            _instance = [super allocWithZone:zone];
        });
    }
    return _instance;
}

-(instancetype)init{
    if (self = [super init]) {
       NSURL *jsCodeLocation = [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil];
       //NSURL *jsCodeLocation = [CodePush bundleURL];
       //NSURL* jsCodeLocation = [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
        
        _bridge = [[RCTBridge alloc]initWithBundleURL:jsCodeLocation moduleProvider:nil launchOptions:nil];
    }
    return self;
}
@end

二、创建Bridge
1、bridge的初始化函数中,会调用setUp 方法,setUp方法最重要的就是去初始化batchedBridge。batchedBridge是RCTCxxBridge的实例,而RCTCxxBridge又是RCTBridge的子类。RCTBridge负责对外暴露接口,而正在的实现都是在RCTCxxBridge中。

// 这是setUp方法中最重要的代码,去初始化batchedBridge,调用它的start方法。
self.batchedBridge = [[bridgeClass alloc] initWithParentBridge:self];
[self.batchedBridge start];

2、RCTCxxBridge的start方法

  [[NSNotificationCenter defaultCenter]
    postNotificationName:RCTJavaScriptWillStartLoadingNotification
    object:_parentBridge userInfo:@{@"bridge": self}];

  // Set up the JS thread early
  _jsThread = [[NSThread alloc] initWithTarget:[self class]
                                      selector:@selector(runRunLoop)
                                        object:nil];
  _jsThread.name = RCTJSThreadName;
  _jsThread.qualityOfService = NSOperationQualityOfServiceUserInteractive;
#if RCT_DEBUG
  _jsThread.stackSize *= 2;
#endif
  [_jsThread start];

首先会触发一个RCTJavaScriptWillStartLoadingNotification通知,从通知命名一看便知这里是告诉我们即将要加载JavaScript代码。然后去创建了一个名称叫“com.facebook.react.JavaScript”的线程并启动线程的runloop(线程保活)。通过线程的名称很容易明白它是用来干啥的。这个线程是一直存在的。

  [_performanceLogger markStartForTag:RCTPLNativeModuleInit];
  [self registerExtraModules];
  // Initialize all native modules that cannot be loaded lazily
  (void)[self _initializeModules:RCTGetModuleClasses() withDispatchGroup:prepareBridge lazilyDiscovered:NO];
  [self registerExtraLazyModules];
  [_performanceLogger markStopForTag:RCTPLNativeModuleInit];

这里主要是操作我们的ExtraModules。把我们初试话bridge时传入的model数组里的modules包装成RCTModuleData的对象,并添加到bride的_moduleDataByName、_moduleClassesByID和_moduleDataByID中。

// 在RCTModuleData类中去收集modules类中通过RCT_EXPORT_METHOD这个宏定义的的方法
- (void) calculateMethods
{
    if (_methods && _methodsByName) {
      return;
    }
    NSMutableArray<id<RCTBridgeMethod>> *moduleMethods = [NSMutableArray new];
    NSMutableDictionary<NSString *, id<RCTBridgeMethod>> *moduleMethodsByName = [NSMutableDictionary new];
    if ([_moduleClass instancesRespondToSelector:@selector(methodsToExport)]) {
        [moduleMethods addObjectsFromArray:[self.instance methodsToExport]];
    }
    unsigned int methodCount;
    Class cls = _moduleClass;
    while (cls && cls != [NSObject class] && cls != [NSProxy class]) {
        Method *methods = class_copyMethodList(object_getClass(cls), &methodCount);
        for (unsigned int i = 0; i < methodCount; i++) {
            Method method = methods[i];
            SEL selector = method_getName(method);
// 通过宏导出的方法的方法名会被加了__rct_export__前缀,这里通过这个前缀来过滤那些导出的方法。
            if ([NSStringFromSelector(selector) hasPrefix:@"__rct_export__"]) {
                IMP imp = method_getImplementation(method);
                auto exportedMethod = ((const RCTMethodInfo *(*)(id, SEL))imp)(_moduleClass, selector);
                id<RCTBridgeMethod> moduleMethod = [[RCTModuleMethod alloc] initWithExportedMethod:exportedMethod moduleClass:_moduleClass];
                NSString *str = [NSString stringWithUTF8String:moduleMethod.JSMethodName];
                [moduleMethodsByName setValue:moduleMethod forKey:str];
                [moduleMethods addObject:moduleMethod];
            }
        }
        free(methods);
        cls = class_getSuperclass(cls);
    }
    _methods = [moduleMethods copy];
    _methodsByName = [moduleMethodsByName copy];
}

简单的可以理解为我们定义的modules类被包装成了RCTModuleData。类的信息都放在了RCTModuleData中,包括所有导出到js的方法。(methods属性存放所有导出方法的数组,每个方法包装成一个RCTModuleMethod对象)

// Dispatch the instance initialization as soon as the initial module metadata has
  // been collected (see initModules)
  dispatch_group_enter(prepareBridge);
  [self ensureOnJavaScriptThread:^{
    [weakSelf _initializeBridge:executorFactory];
    dispatch_group_leave(prepareBridge);
  }];

  // Load the source asynchronously, then store it for later execution.
  dispatch_group_enter(prepareBridge);
  __block NSData *sourceCode;
  [self loadSource:^(NSError *error, RCTSource *source) {
    if (error) {
      [weakSelf handleError:error];
    }

    sourceCode = source.data;
    dispatch_group_leave(prepareBridge);
  } onProgress:^(RCTLoadingProgress *progressData) {
#if RCT_DEV && __has_include(<React/RCTDevLoadingView.h>)
    // Note: RCTDevLoadingView should have been loaded at this point, so no need to allow lazy loading.
    RCTDevLoadingView *loadingView = [weakSelf moduleForName:RCTBridgeModuleNameForClass([RCTDevLoadingView class])
                                       lazilyLoadIfNecessary:NO];
    [loadingView updateProgress:progressData];
#endif
  }];

  // Wait for both the modules and source code to have finished loading
  dispatch_group_notify(prepareBridge, dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
    RCTCxxBridge *strongSelf = weakSelf;
    if (sourceCode && strongSelf.loading) {
      [strongSelf executeSourceCode:sourceCode sync:NO];
    }
  });

这里通过dispatch_group_t来异步的去子线程操作,主要有2个。一个是去上面创建的_jsThread子线程里执行block的内容,主要是_reactInstance的初始化。
另一个线程主要是loadSource方法来加载JavaScript代码。在loadSource里会先去判断是否有自己实现了代理方法,如果有则调用开发者实现的代理方法,如果没有则会通过RCTJavaScriptLoader 类去调用loadBundleAtURL 方法来加载js代码。在loadBundleAtURL 方法中会先用同步的方法去加载,如果失败,会调用异步加载的方法,最后加载得到的是一个NSData对象。

加载JavaScript代码流程

 // Wait for both the modules and source code to have finished loading
  dispatch_group_notify(prepareBridge, dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
    RCTCxxBridge *strongSelf = weakSelf;
    if (sourceCode && strongSelf.loading) {
      [strongSelf executeSourceCode:sourceCode sync:NO];
    }
  });

当上面的2个线程都结束了,就会通过dispatch_group_notify回调到下面去执行[strongSelf executeSourceCode:sourceCode sync:NO]; 就是去执行JS代码。
至此,RCTBride的初始化就结束了。

三、创建RCTRootView

- (instancetype)initWithBridge:(RCTBridge *)bridge
                    moduleName:(NSString *)moduleName
             initialProperties:(NSDictionary *)initialProperties
{
if (self = [super initWithFrame:CGRectZero]) {
    self.backgroundColor = [UIColor whiteColor];
    _bridge = bridge;
    _moduleName = moduleName;
    _appProperties = [initialProperties copy];
    _loadingViewFadeDelay = 0.25;
    _loadingViewFadeDuration = 0.25;
    _sizeFlexibility = RCTRootViewSizeFlexibilityNone;
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(bridgeDidReload)
name:RCTJavaScriptWillStartLoadingNotification object:_bridge];
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(javaScriptDidLoad:)
                                                 name:RCTJavaScriptDidLoadNotification
                                               object:_bridge];
     [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(hideLoadingView)
                                                 name:RCTContentDidAppearNotification
                                             object:self];

    ......

      // Immediately schedule the application to be started.
      // (Sometimes actual `_bridge` is already batched bridge here.)
    [self bundleFinishedLoading:([_bridge batchedBridge] ?: _bridge)];
  }  
    return self;
}

- (void)bundleFinishedLoading:(RCTBridge *)bridge{
  RCTAssert(bridge != nil, @"Bridge cannot be nil");
  if (!bridge.valid) {
    return;
  }
  [_contentView removeFromSuperview];
  _contentView = [[RCTRootContentView alloc] initWithFrame:self.bounds
                                                    bridge:bridge
                                                  reactTag:self.reactTag
                                            sizeFlexiblity:_sizeFlexibility];
  // 主要的执行代码
  [self runApplication:bridge];
  _contentView.passThroughTouches = _passThroughTouches;
  [self insertSubview:_contentView atIndex:0];

  if (_sizeFlexibility == RCTRootViewSizeFlexibilityNone) {
    self.intrinsicContentSize = self.bounds.size;
  }
}

- (void)runApplication:(RCTBridge *)bridge
{
  NSString *moduleName = _moduleName ?: @"";
  NSDictionary *appParameters = @{
    @"rootTag": _contentView.reactTag,
    @"initialProps": _appProperties ?: @{},
  };

  RCTLogInfo(@"Running application %@ (%@)", moduleName, appParameters);
  [bridge enqueueJSCall:@"AppRegistry"
                 method:@"runApplication"
                   args:@[moduleName, appParameters]
             completion:NULL];
}

从上面的 代码可以看出,rootView的初始化主要是去执行runApplication方法,这个方法在jsTread线程里去调用js的函数。另外就是去初始化RCTRootContentView。这个类有个touchHandler属性,继承于UIGestureRecognizer用于处理页面的手势的响应。

四、React Native事件处理流程(iOS)
1.在创建RCTRootContentView的时候,内部会创建RCTTouchHandler
RCTTouchHandler:继承UIGestureRecognizer,也就是它就是一个手势
并且它会作为RCTRootContentView的手势,这样点击RCTRootContentView,就会触发RCTTouchHandler
RCTTouchHandler:内部实现了touchBegin等触摸方法,用来处理触摸事件
2.在创建RCTTouchHandler的时候,内部会创建RCTEventDispatcher
RCTEventDispatcher:用来把事件处理传递给JS的方法处理,也就是当UI界面产生事件,就会执行JS的代码处理。
3.通过RCTRootContentView截获点击事件
产生事件就会去触犯RCTRootContentView中的RCTTouchHandler对象。
4.当产生事件的时候,会执行[RCTTouchHandler touchBegin]
5.RCTTouchHandler的touch方法,会执行[RCTTouchHandler _updateAndDispatchTouches:eventName:]
内部会创建RCTTouchEvent事件对象
6.[RCTEventDispatcher sendEvent:event] -> 让事件分发对象调用发送事件对象
内部会把事件保存到_eventQueue(事件队列中)
7.[RCTEventDispatcher flushEventsQueue] -> 让事件分发对象冲刷事件队列,就是获取事件队列中所有事件执行
8.[RCTEventDispatcher dispatchEvent:event] -> 遍历事件队列,一个一个分发事件
分发事件的本质:就是去执行JS的代码,相应事件。
9.[RCTBatchedBridge enqueueJSCall:[[event class] moduleDotMethod] args:[event arguments]]; -> 让桥架对象调用JS处理事件
本质:就是产生事件调用JS代码
10.这样就能完成把UI事件交给JS代码相应

总结图

引用文章:
ReactNative源码分析
React Native在美团外卖客户端的实践
ReactNative iOS源码解析(二)

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

推荐阅读更多精彩内容