rn-relates
rn-relates copied to clipboard
混编下的RCTRootView加载方式
算下来转正也接近一个月了,在这里参与了两个混编项目,遇到一些坑,说实在的,自己差点就忘了做纯RN开发是什么感觉了。Σ( ° △ °|||)︴
目前正开发中的项目,是一个面向销售人员的汽车营销APP,基于公司原生技术的积累,该项目也是在原生的骨架上来开发。按目前项目分工来看,原生与RN的业务比例呈1:1分布,仍然没达到团队老大的理想效果,他期望的是2:3。RN表示我也很绝望啊,但是有些东西给原生这大佬去做,肯定是要好的嘛~
在目前的APP架构中,底部分4个Tab,其中有两个界面是由原生ViewController加载的RN界面。在实践过程中可以发现,如果在点击切换界面时才加载RN的rootView,那么在第一次切换到RN界面时,就会出现短暂的白屏,非第一次则没有该问题。在与小伙伴的一番讨论之后,决定是在APP启动之后,采用预加载的形式提前渲染这两个RN界面。
其实需求比较简单,就是提前把rootView给加载出来,然后在相应ViewController的viewDidLoad中去把保存好的rootView取出来,赋值给self.view就可以了。这里用单例形式来管理:
#import <React/RCTRootView.h>
#import <React/RCTBundleURLProvider.h>
@interface PCReactRootViewManager : NSObject
/*
* 获取单例
*/
+ (instancetype)manager;
/*
* 根据viewName预加载bundle文件
* param:
* viewName RN界面名称
* initialProperty: 初始化参数
*/
- (void)preLoadRootViewWithName:(NSString *)viewName;
- (void)preLoadRootViewWithName:(NSString *)viewName initialProperty:(NSDictionary *)initialProperty;
/*
* 根据viewName获取rootView
* param:
* viewName RN界面名称
*
* return: 返回匹配的rootView
*/
- (RCTRootView *)rootViewWithName:(NSString *)viewName;
@end
在混编模式中,渲染不同的RN界面有两种方式:
- 统一由一个
ViewController加载rootView,在initialProperties中添加区分不同界面的标识位,然后在JS中根据该标识位,渲染不同的Component- 在
AppRegistry的registerComponent方法中,第一个参数需要跟加载的moduleName一一对应。通过加载对应的moduleName,然后在JS中注册对应组件,也可以达到渲染不同界面的目的
我司在实践中,采用的是第二种方式。在实践中,第二种方式依然可以统一由一个ViewController来作为加载rootView的入口,也可以根据业务情况,来新建一些独立的ViewController,方便业务逻辑分离。
抛开两种方式的不同点,我们会发现,不管用哪种方式,都需要有一个新建RCTRootView实例的过程,而查看源码可知,在这之前是需要新建一个RCTBridge实例的:
// RCTRootView.m
- (instancetype)initWithBundleURL:(NSURL *)bundleURL
moduleName:(NSString *)moduleName
initialProperties:(NSDictionary *)initialProperties
launchOptions:(NSDictionary *)launchOptions
{
RCTBridge *bridge = [[RCTBridge alloc] initWithBundleURL:bundleURL
moduleProvider:nil
launchOptions:launchOptions];
return [self initWithBridge:bridge moduleName:moduleName initialProperties:initialProperties];
}
关于原生与RN的通信原理,网上已经有不少资料,有兴趣的伙伴戳:
所以,除了可做预加载之外,我们还可以在RCTBridge实例这里做下处理。首先让该类实现RCTBridgeDelegate协议,再添加一个bridge属性,以便不同的ViewController都可以访问到这个属性:
#import <React/RCTRootView.h>
#import <React/RCTBundleURLProvider.h>
#import <React/RCTBridge.h>
@interface PCReactRootViewManager : NSObject<RCTBridgeDelegate>
// 全局唯一的bridge
@property (nonatomic, strong, readonly) RCTBridge * bridge;
/*
* 获取单例
*/
+ (instancetype)manager;
/*
* 根据viewName预加载bundle文件
* param:
* viewName RN界面名称
* initialProperty: 初始化参数
*/
- (void)preLoadRootViewWithName:(NSString *)viewName;
- (void)preLoadRootViewWithName:(NSString *)viewName initialProperty:(NSDictionary *)initialProperty;
/*
* 根据viewName获取rootView
* param:
* viewName RN界面名称
*
* return: 返回匹配的rootView
*/
- (RCTRootView *)rootViewWithName:(NSString *)viewName;
@end
具体的.m文件实现:
#import "PCReactRootViewManager.h"
@interface PCReactRootViewManager ()
// 以 viewName-rootView 的形式保存需预加载的RN界面
@property (nonatomic, strong) NSMutableDictionary<NSString *, RCTRootView*> * rootViewMap;
@end
@implementation PCReactRootViewManager
- (void)dealloc {
_rootViewMap = nil;
[_bridge invalidate];
_bridge = nil;
}
+ (instancetype)manager {
static PCReactRootViewManager * _rootViewManager = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_rootViewManager = [[PCReactRootViewManager alloc] init];
});
return _rootViewManager;
}
- (instancetype)init {
if (self = [super init]) {
_rootViewMap = [NSMutableDictionary dictionaryWithCapacity:0];
_bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:nil];
}
return self;
}
- (void)preLoadRootViewWithName:(NSString *)viewName {
[self preLoadRootViewWithName:viewName initialProperty:nil];
}
- (void)preLoadRootViewWithName:(NSString *)viewName initialProperty:(NSDictionary *)initialProperty {
if (!viewName && [_rootViewMap objectForKey:viewName]) {
return;
}
NSMutableDictionary *tmpInitialProperty = [NSMutableDictionary dictionaryWithDictionary:initialProperty];
[tmpInitialProperty setObject:viewName forKey:@"viewName"];
// 由bridge创建rootView
RCTRootView * rnView = [[RCTRootView alloc] initWithBridge:self.bridge
moduleName:@"YourAppName"
initialProperties:initialProperty];
[_rootViewMap setObject:rnView forKey:viewName];
}
- (RCTRootView *)rootViewWithName:(NSString *)viewName {
if (!viewName) {
return nil;
}
return [self.rootViewMap objectForKey:viewName];
}
#pragma mark - RCTBridgeDelegate
- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge {
return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil];
}
以下是预加载与读取rootView的简易方式:
// APP启动后,在合适时机加载
[[PCReactRootViewManager manager] preLoadRootViewWithName:@"discovery"];
[[PCReactRootViewManager manager] preLoadRootViewWithName:@"mine"];
// 渲染界面时,获取rootView
self.view = [[PCReactRootViewManager manager] rootViewWithName:@"discovery"];
普通的RN界面渲染方式:
- (void)setupRootView {
NSString * moduleName = [self.initialProperty objectForKey:@"viewName"];
if (!moduleName || ![moduleName isKindOfClass:[NSString class]]) {
NSLog(@"viewName为空,无法加载rootView");
return;
}
// 设置初始属性
[self configInitialProperties];
// 根据唯一的bridge来创建rootView,减少新建bridge的时间和资源开销
RCTRootView * rnView = [[RCTRootView alloc] initWithBridge:[PCReactRootViewManager manager].bridge
moduleName:@"YourAppName"
initialProperties:self.initialProperty];
self.view = rnView;
}
在 JavaScript 端:
// index.js
...
AppRegistry.registerComponent('YourAppName', () => Root);
// Root.js
class Root extends React.Component {
render() {
const { viewName } = this.props;
if (viewName === 'Page1') {
return <Page1 />;
} else if (viewName === 'Page2') {
return <Page2 />;
}
}
}
如果项目中还集成了 react-navigation 的话,入口文件需要修改为:
import { createStackNavigator } from 'react-navigation';
const createNavigatorWithInitialRouteName = (initialRouteName = 'Splash') => createStackNavigator(
{
PageA: {},
...
}, {
initialRouteName,
...
});
export default class NavigatorContainer extends React.Component {
constructor(props) {
super(props);
this.Navigator = createNavigatorWithInitialRouteName(props.viewName);
}
render() {
const { Navigator } = this;
return <Navigator />;
}
}
现在有个问题请教下,项目刚接触rn,现在是每个新的rn都会去新建一个vc,但是在返回上一页vc释放了,但是内存并没有被释放,切每个页面都需要重新加载bundle 采用您这种写法,全局唯一的bridge 可以解决这种问题么
@lh9403 嗯是的,每次新建VC不会重新加载bundle,不过今天试了下,发现pop之后,内存貌似没怎么降,欢迎一起探讨是啥原因导致😆
greaaaat job!!! 总结的真的好,学到了很多,3q,3q啦
@sheepmiee 有帮助就好,本来当做自己的实践记录而已~
请教 一个问题 我在 加载 [[ReactRootViewManager manager] preLoadRootViewWithName:@"tastRN"];// [[ReactRootViewManager manager] preLoadRootViewWithName:@"mainPage"]; 这个的时候mainpage 就报错 是为什么?
@loss99 请问是报什么错来的
请问一下 内存没有降低这个应该怎么解决
Application mainPage has not been registered.
Hint: This error often happens when you're running the packager (local dev server) from a wrong folder. For example you have multiple apps and the packager is still running for the app you were working on before. If this is the case, simply kill the old packager instance (e.g. close the packager terminal window) and start the packager in the correct app folder (e.g. cd into app folder and run 'npm start').
This error can also happen due to a require() error during initialization or failure to call AppRegistry.registerComponent.
我 现在 正在做 混合的这样一个 项目 非常希望您能指导一下
@loss99 如果是这种方式的话,每添加一个页面,都需要在 JavaScript 这边进行注册:
import MainPage from './pages/main/MainPage';
AppRegistry.registerComponent('mainPage', () => MainPage);
附:这里推荐另外一种方式,源码需要有所修改:
- (void)preLoadRootViewWithName:(NSString *)viewName initialProperty:(NSDictionary *)initialProperty {
if (!viewName && [_rootViewMap objectForKey:viewName]) {
return;
}
NSMutableDictionary *newDict = [NSMutableDictionary dictionaryWithDictionary:initialProperty];
[newDict setObject:viewName forKey:@"viewName"];
// 由bridge创建rootView
RCTRootView * rnView = [[RCTRootView alloc] initWithBridge:self.bridge
moduleName:@"YourAppRegistryComponentName"
initialProperties:newDict];
[_rootViewMap setObject:rnView forKey:viewName];
}
在 JavaScript 端只要注册一个组件即可,具体的页面区分,依然可以通过 this.props.viewName 来获取:
import App from './pages';
AppRegistry.registerComponent('YourAppRegistryComponentName', () => App);
@SanlinBlackball 囧,项目现在没有在跟了,内存这块没怎么跟进,之前印象是增加的不多,所以没怎么管
嗯 ,感谢您的回答, 如果我用第二个方法只注册 一个组件 "应用"这个 VC 可以对应的使用这个注册的 RN, 但是"我"这个 VC 怎么使用 mainPage 这个 RN 页面呢? this.props.viewName 没看懂在哪里用... 初入 RN坑 才疏学浅 请见谅 ... 能具体一点吗?
1、首先每新建一个 VC 实例后,都传入了 initialProperties 的初始参数,类型为字典,该字典里面保存了当前的 RN 页面名称,比如你可以用 viewName 来命名;
2、回到 JS 端,注册的唯一组件对应到 App.js,initialProperties 将作为 App 页面的 props,所以就可通过 this.props.viewName 获取到在原生设置的 RN 页面名称,剩下的就是根据这个名称,去匹配返回不同的 RN 页面组件了,比如 if ,或者是 switch,又或者配置一个路由表(可参考我这个 仓库 的做法:入口文件 - App,以及相关路由配置 - routers)
非常感谢您的回答,我现在已经完成了混合开发的基础工作,希望能加您的微信或者 QQ 方便请教您一些问题
@loss99 那你加我QQ好了 824771861
@ljunb 这个会不会是因为创建了一个单例,即使退出界面但是渲染的view并没有被销毁
@SanlinBlackball 刚才调试了下,界面 pop 掉的时候,会走 PayloadVC 的 dealloc 方法,所以是释放掉的了。没有销毁的情况,猜测是预加载那种(单例类中的数组持有了预加载的 VC),不过也提供了移除 VC 的相应方法。暂时还不知道,是什么原因导致这个内存不减的问题
RCTBridge * bridge = _bridge; [bridge invalidate]; bridge = nil;
@ZhangRuixiang thx
更详细的应用例子 RNProjectPlayground,可看看 iOS 端的 native 相关代码
RCTBridge * bridge = _bridge; [bridge invalidate]; bridge = nil;
想问下,[bridge invalidate]; 与 bridge = nil; 是在什么时机执行的,如果在页面退出前(dismissViewController 或 popViewControllerAnimated: )执行会导致 RN 视图立即被卸载,出现闪白屏的情况。
混合编程的时候,iosapp每次上传appStore的时候,ios工程中存放的main.bundle需要更新成最新的,还是工程中的main.bundle一直不变?@ljunb
@xieliying 这个属于 PCReactRootViewManager 单例的生命周期,App 退出才执行 dealloc
@qingluanchou xcode里面有配置打包脚本的命令,每次 archive 都会生成新的 bundle
楼主, 找到内存增长的原因了吗,目前也遇到这个问题了
楼主 通过 initWithBridge 的方法初始化RCTRootView确实是不行,内存持续增大
@mybadge @jf4444 试试:
- 1、在
PCReactRootViewManager中添加一个移除内存中已经 pop 掉的页面方法,比如- (void)removeRootViewWithName(String viewName);,根据viewName移除内存中对应的RCTRootView实例 - 2、然后在加载
RCTRootView的 VC 的dealloc方法中,去手动调用这个移除的方法
推荐下公司的跨平台开发框架 CRN