你好,我是众文,这一讲还是由我和惠姝来讲解。
上一讲,我们讲了如何构建混合应用。当环境配置、载体页、调试打包都 OK 后,我们就要开始复杂业务的开发了。在实际开发中,除了负责 React Native 框架本身的维护迭代外,另一个重要的工作就是配合前端业务,开发对应的 Native 组件。
那么什么时候用这些自定义的 Native 组件呢?
比如,有时候 App 需要访问平台 API,但 React Native 可能还没有相应的模块包装;或者你需要复用公司内的一些用 Java/OC 写的通用组件,而不是用 JavaScript 重新实现一遍;又或者你需要实现某些高性能的、多线程的代码,譬如图片处理、数据库,或者各种高级扩展等。
当然,你可以通过官方文档(Android /iOS ),快速访问你的原生模块。但官方文档提供的主要是简单的 Demo 和步骤,在实际开发中,你可能还需要自定义组件的方方面面,包括新架构定义组件的全流程,以及实际业务中的踩坑指南等。
今天这一讲,我们会先带你补齐组件的相关基础知识,包括组件的生命周期、组件传输数据类型,并以新架构的TurboModule 和 Fabric 为案例,带你了解自定义组件的方方面面。你也能借此对React Native新架构建立起初步认识。接下来让我们先了解下期待已久的 React Native 新架构。
新架构介绍 我们现在先通过下面这张图简单对比下新老架构:
新架构变更点主要在下面这几个方面:
前面也说了,我们一讲将以 TurboModule 和 Fabric 为案例对自定义组件进行讲解。所以你现在需要对新架构有一个初步的认识,特别是要注意,TurboModule 和 Fabric 对比旧版的 Native Module 和 UIManager 有哪些差异和优势。
如果你还想了解新架构的更多信息,你可以参考官方文档 。这里包括了新架构介绍、如何在 Android、iOS 上开启新架构、如何在 Android、iOS 上开启使用 TurboModule 和 Fabric等。你也可以根据官方文档试着编译运行新架构。
而且,react-native-screens 、react-native-gesture-handler 等知名 React Native 库的新版都已适配了新架构,感兴趣的话,你可以去了解下。
好的,现在我们进入这一讲的正题,先来了解一下组件的生命周期。
组件的生命周期 组件的生命周期,指的是在组件创建、更新、销毁的过程中伴随的各种各样的函数执行。这些在组件特定的时期被触发的函数,统称为组件的生命周期。
让组件拥有生命周期,我们就可以更好地管理组件的状态、内存,跟随载体页的生命周期做相应的处理。
Android 在Android端,一个 Module 组件的生命周期包括:
1 构造 -> 初始化 -> onHostResume() -> onHostPause() -> onHostDestroy()
这几个生命周期的意思是:
如果你不熟悉 Android Activity/Fragment 生命周期,那你可以看下这篇文章 加深下理解。
那么如何让 Native Module 具备生命周期呢?
React Native 给我们提供了一个接口:com.facebook.react.bridge.LifecycleEventListener。我们只需要在组件中添加这个接口的注册和取消注册,就可以让组件具备生命周期了。这里要注意,不要忘记在 onHostDestroy() 中移除注册,否则会造成内存泄漏。示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public class TestJavaModule extends ReactContextBaseJavaModule implements LifecycleEventListener, ReactModuleWithSpec, TurboModule { public TestJavaModule(ReactApplicationContext reactContext) { super(reactContext.real()); reactContext.addLifecycleEventListener(this); } @Override public String getName() { return getClass().getSimpleName(); } @Override public void onHostResume() { } @Override public void onHostPause() { } @Override public void onHostDestroy() { getReactApplicationContext().removeLifecycleEventListener(this); } }
其实组件的生命周期原理很简单,就是观察者模式。当载体页触发自身的生命周期回调时,调用 ReactInstanceManager 的 onHostXXX() 方法,ReactInstanceManager 进而调用 ReactContext 的对应回调。
比如载体页调用 onResume() 时,最终会调用 ReactContext 的 onHostResume(),内部会遍历注册的事件进行回调:
1 2 3 4 5 6 7 8 public void onHostResume(@Nullable Activity activity) { Iterator iterator = this.mLifecycleEventListeners.iterator(); while(iterator.hasNext()) { LifecycleEventListener listener = (LifecycleEventListener) iterator.next(); // 观察者模式,载体页时调用已注册组件的生命周期回调 listener.onHostResume(); } }
以上就是 Android 端组件生命周期的讲解,我们再来看看 iOS 端。
iOS iOS中 NativeModules 组件的创建销毁时机,与bridge的创建销毁时机完全一致:
alloc:创建当前组件; dealloc:销毁当前组件。 创建一个组件TestNativeModule,通过RCT_EXPORT_MODULE() 声明组件,默认会根据类名声明组件名,当然也可以通过在参数中传入其他字符串作为组件的名。
1 2 3 4 5 6 7 8 9 10 @implementation TestNativeModule RCT_EXPORT_MODULE() - (instancetype)init{ self = [super init]; return self; } - (void)dealloc{ NSLog(@"dealloc"); }
而 TurboModule 组件的生命周期却与 NativeModule 不同。TurboModule 采用懒加载模式,在 Bridge 创建后页面中第一次 import 当前TurboModule ,也就是 JavaScript 端通过 TurboModuleRegistry.getEnforcing 方法加载组件时, Native 会创建对应的 TurboModule 并进行缓存。如果 JS 端没有加载当前自定义组件,该组件就不会进行初始化。
JS 端加载组件方式如下:
1 2 3 export default (TurboModuleRegistry.getEnforcing<Spec>( 'TestTurboModule') : Spec);
而TurboModule 的销毁时机与 Bridge 的销毁时机一致。 Bridge 进行销毁时会发送一个 RCTBridgeDidInvalidateModulesNotification 通知,TurboModuleManager会监听该事件,依次对所有已创建的 TurboModule 进行销毁。示例代码如下:
1 2 3 4 5 6 7 8 - (void)bridgeDidInvalidateModules:(NSNotification *)notification { RCTBridge *bridge = notification.userInfo[@"bridge"]; if (bridge != _bridge) { return; } [self _invalidateModules];//销毁所有TurboModules }
了解完了组件的生命周期,我们再来看下组件的传输数据类型。在组件运行过程中,Native 与 JavaScript 不可避免地需要进行数据交互,如 JavaScript 调用组件方法传入数据,Native 向 JavaScript 回传结果,而 React Native 也帮我们封装好了对应的数据类型。
组件传输数据类型 在 Native 与 JavaScript 通信的过程中,组件需要获取输入参数、回传结果,对此 React Native 给我们包装了相应的数据类型,方便快速操作,我们通过一个Demo来简单了解下。
现在,我们让JavaScript端调用 TestModule 的 testMethod 方法,传入参数 type 和 message,接收 native 回传数据:
1 2 3 4 NativeModules.TestModule.testMethod({type: 1, message: "fromJS"}, (result)=>{ console.info(result); } );
然后我们来看TestModule 在 Android、iOS 侧的实现。先来看 Android 端是怎么做的。
不过,在实现 TestModule 之前,我们需要先了解下 Android 端的组件传输数据类型:
这里你要注意,数字类型有点特殊。因为 JavaScript 不支持 long 64 位长类型,只支持 int (32)和double,所以对于长数字,JavaScript端统一用 double 表示。那么 Android 端如何转换成自己需要的数据类型呢?
我们以 long 为例,可以这样参考官方issue 这样处理:
1 2 3 4 5 6 7 8 9 10 double value = readableMap.getDouble(key); try { // 判断是否为 long 的范围: 超过了 int 的最大值且为整数 if (value > Integer.MAX_VALUE && value % 1 == 0) { long cv = (long) value; // 转换成 long 型返回 } } catch (Exception e) { // 异常时,仍使用 double }
这段代码中,我们先将 JavaScript 传入的数值统一以双精度浮点数 double 来获取。获取完后,判断这个值是否超出了整数的最大值且不为小数,条件符合就将它转换成长整数 long,否则还是以 double 来返回。
了解完 Android 端的组件数据传输类型后,我们就可以来实现上文中的 TestModule了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class TestModule extends ReactContextBaseJavaModule implements ReactModuleWithSpec, TurboModule { public TestModule(ReactApplicationContext reactContext) { super(reactContext.real()); } @Override public String getName() { return getClass().getSimpleName(); } @ReactMethod public void testMethod(ReadableMap data, Callback callback) { // 获取 JS 的调用输入参数 int type = data.getInt("type"); String message = data.getString("message"); // 回传数据给 JS WritableMap resultMap = new WritableNativeMap(); map.putInt("code", 1); map.putString("message", "success"); callback.invoke(resultMap); } }
上面代码中,我们定义了 Native 组件 TestModule,内部实现了 JavaScript 需要调用的 testMethod 方法。此方法包含两个参数:ReadableMap 和 Callback。ReadableMap 为 JavaScript 传入参数的字典,我们可以通过对应的 key 获取到 JavaScript 的入参值,而 Callback 是在 Native 回传数据时需要使用的,后面我们还有对通信方式这部分的讲解,这里我们只需要了解一下就好。
具体实现上,我们是首先获取 JavaScript 调用传入的 type 和 message,然后再通过 WritableMap 写入数据,最后通过 callback 回传给 JavaScript。
以上就是 Android 端的 React Native 读写数据类型,我们再来看下 iOS 端。
iOS端也是一样,在实现前面这个 demo 前,我们需要先看下 iOS 端支持的传入数据类型:
那么,iOS端中是如何实现上文中的TestModule的呢?我们可以在Module中进行callback,然后通过NSArray来返回,如下:
1 2 3 4 5 6 7 RCT_EXPORT_METHOD(getValueWithCallback : (RCTResponseSenderBlock)callback){ if (!callback) { return; } callback(@[ @"value from callback!" ]); }
不过,我们这个组件案例,只是演示了 Native 可以通过 callback 向 JavaScript 回传数据。那么除了 callback,React Native 与原生还有什么通信方式呢?
React Native 与原生的通信方式 总体来说,native 向 JavaScript 传递数据的方式分成以下三种:
Callback:由 JavaScript 主导触发,Native 进行回传,一次触发只能传递一次; Promise:由 JavaScript 主导触发,Native 进行回传,一次触发只能传递一次。Promise 是 ES6 的新特性,类似 RXJava 的链式调用。Promise 有三种状态,分别是pending (进行时)、resolve (已完成)、reject (已失败); 发送事件:由 Native 主导触发,可传递多次,类似 Android 的广播和 iOS 的通知中心。 Callback 在上面的例子中已经出现过了,我们通过 callback.invoke(xx) 就可以将数据回传给 JavaScript,使用起来比较简单,这边我们就不再赘述了。现在我们主要来看下 Promise 和发送事件的示例,以便更好地了解 React Native 和原生之间是如何进行通信的。
首先我们来看下Promise 示例 ,我们从 JavaScript 如何触发、Native 如何回传数据两方面来进行讲解。
首先,JavaScript 端调用客户端定义的 SystemPropsModule 的 getSystemModel 来获取手机的设备类型,获取结果的方式使用 Promise 方式 (then… catch…):
1 2 3 4 5 NativeModules.SystemPropsModule.getSystemModel().then(result=> { console.log(result); }).catch(error=> { console.log(error); });
然后,Native 端定义 SystemPropsModule,实现 getSystemModel 方法,内部使用 promise 获取手机的 model 数据。使用 promise.reolve(xx) 为成功,promise.reject(xx) 为失败:
1 2 3 4 5 6 7 8 SystemPropsModule: ... @ReactMethod public void getSystemModel(Promise promise) { // 回传成功,使用 resolve promise.resolve(Build.MODEL); } ...
接下来看发送事件示例 ,我们从 JavaScript 如何监听 Native 事件、Native 如何发送事件这两方面来进行讲解。
首先,JavaScript 端使用 EventEmitterManager 来注册 Native 的事件监听。通过 NativeModules 获取 EventEmitterManager,随后使用它构建出 NativeEventEmitter,最后通过 NativeEventEmitter 注册监听:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 componentWillMount(){ // 拿到原生模块 var eventEmitterManager = NativeModules.EventEmitterManager; const nativeEventEmitter = new NativeEventEmitter(eventEmitterManager); const eventEmitterManagerEvent = EventEmitterManager.EventEmitterManagerEvent; // 监听 Native 发送的通知 this.listener = nativeEventEmitter.addListener(eventEmitterManagerEvent, (data) => console.log("Receive native event: " + data); ); } componentWillUnmount(){ // 移除监听 this.listener.remove(); }
在 Native 端的使用则很简单。我们获取 RCTDeviceEventEmitter 这个 JSModule,使用 emit 方法就可以向 JavaScript 发送事件了:
1 2 reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) .emit("msg", "say hello");
自定义组件相关的知识点,我们先介绍到这里。接下来进入实战阶段,我们将分别以一个数据存取 TurboModule 和视频播放 Fabric component 的案例,加深你对自定义组件的理解。我们先来看TurboModule。
TurboModule:数据存取 TurboModule 采用懒加载模式,在运行时第一次 import 该 TurboModule 时, Native 会创建对应的 TurboModule 并进行缓存。而旧版本的 NativeModule 都是在创建环境时统一进行构造的,会对 React Native 的启动性能有比较大的影响。
接下来我们以一个实际的案例来带你了解 TurboModule。当你需要使用 native 数据存取相关能力,如跨进程存取、偏好存取、加密存取等,而 React Native 自带的数据存储 module 满足不了你的需求,你可以通过自定义数据存储的 TurboMoudle 来实现。我们先来看下 JavaScript 侧。
JavaScript 在 Spec 中定义方法,定义好存和取的方法后,再导出 StorageModule:
1 2 3 4 5 6 7 8 export interface Spec extends TurboModule { +save: (key: string, value: string, callback: (value: Object) => void) => void; +get: (key: string, callback: (value: Object) => void) => void; } // 导出 StorageModule export default (TurboModuleRegistry.getEnforcing<Spec>( 'StorageModule', ): Spec);
调用:
1 2 3 4 5 6 7 8 NativeModules.StorageModule.save("testKey", "testValue", (result)=>{ console.info(result); } ); NativeModules.StorageModule.get("testKey", (result)=>{ console.info(result); } );
接下来我们再看看 Android 端和 iOS 端的实现。
Android 在实现 StorageModule 之前,我们需要在混合工程中,将 React Native 新架构的运行配置搭建好,这套配置可以运行 TurboModule、Fabric,后面的 Fabric 案例也是基于此配置来运行的。
而且,上一讲在我们已经讲解了如何基于 React Native 最新版本(0.68.0)搭建混合应用,我们再这个基础上开启新架构配置就好了。
第一步,我们要做些准备工作,也就是获取 newarchitecture 的模版代码。
我们以 0.68.0 版本创建一个 React Native 工程,来获取新架构的模版代码,我们把这个工程名叫做ReactNativeNewArch:
1 npx react-native init ReactNativeNewArch --version 0.68.0
创建好后,你将看到如下工程代码,包含 Java 和 C++:
这些工程代码主要是新架构的 JSI、Fabric、TurboModule 的注册和加载代码,内部逻辑非常复杂,这一讲我们就不做过多分析了,先专注于实操部分。
如果我们想基于这个 Demo 去运行新架构,可以做如下操作:
1 2 3 4 5 6 7 8 1. 修改 android 目录下的 gradle.properties: # 开启新架构 newArchEnabled=true # 配置 java home 为 JDK 11 org.gradle.java.home=/Library/Java/JavaVirtualMachines/jdk-11.0.2.jdk/Contents/Home 2. 运行 yarn react-native run-android
第二步,拷贝 newarchitecture 的模版代码到我们之前的混合工程。
这一步中,我们需要将 Java 层和 C++ 层代码拷贝到混合工程中,需要拷贝的相关代码如下:
1 2 3 4 5 6 Java 层: - MainComponentsRegistry.java - MainApplicationTurboModuleManagerDelegate.java C++ 层: - jni 目录
拷贝完的效果是这样的:
第三步是修改拷贝的代码,主要是下面这四点。
1.MainApplicationTurboModuleManagerDelegate.java:
修改 so 库名称为我们自定义名称 geektime_new_arch。
1 2 3 4 5 6 7 @Override protected synchronized void maybeLoadOtherSoLibraries() { if (!sIsSoLibraryLoaded) { SoLoader.loadLibrary("geektime_new_arch"); sIsSoLibraryLoaded = true; } }
**2.jni/Android.mk:**修改 Android.mk 中的 so 库名称为上面的 geektime_new_arch。
1 2 # You can customize the name of your application .so file here. LOCAL_MODULE := geektime_new_arch
**3.jni/MainApplicationTurboModuleManagerDelegate.h:**修改 MainApplicationTurboModuleManagerDelegate 对应的 Java 类路径,截图中拷贝好的 MainApplicationTurboModuleManagerDelegate.java 路径为 com/reactnativenewarch/newarchitecture/modules。
1 2 static constexpr auto kJavaDescriptor = "Lcom/reactnativenewarch/newarchitecture/modules/MainApplicationTurboModuleManagerDelegate;";
**4.jni/MainComponentsRegistry.h:**修改 MainComponentsRegistry 对应的 Java 类路径,截图中拷贝好的 MainComponentsRegistry.java 路径为 com/reactnativenewarch/newarchitecture/components。
1 2 constexpr static auto kJavaDescriptor = "Lcom/reactnativenewarch/newarchitecture/components/MainComponentsRegistry;";
做完后,最后一步就是修改 React Native 初始化代码了。
这里有两处要修改。第一处是设置 ReactPackageTurboModuleManagerDelegateBuilder 为上面的 MainApplicationTurboModuleManagerDelegate(TurboModule 用);第二处是设置 setJSIModulesPackage(Fabric 用,JSI 实现):
1 2 3 4 5 6 7 8 9 10 11 public void initRN() { ReactInstanceManagerBuilder builder = ReactInstanceManager.builder() .setApplication((Application) getApplicationContext()) .addPackage(new MainReactPackage()) .setJSMainModulePath("index.android") .setInitialLifecycleState(LifecycleState.BEFORE_CREATE) .setReactPackageTurboModuleManagerDelegateBuilder(new MainApplicationTurboModuleManagerDelegate.Builder()) .setJSIModulesPackage(getJSIModulePackage()); ReactInstanceManager reactInstanceManager = builder.build(); }
其中 getJSIModulePackage() 我特意摘出来放在了下面。这段代码实现需要从模版代码的 MainApplicationReactNativeHost 的 getJSIModulePackage 拷贝,内部调用了上面的 MainComponentsRegistry 进行注册:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 static JSIModulePackage getJSIModulePackage() { return new JSIModulePackage() { @Override public List<JSIModuleSpec> getJSIModules( final ReactApplicationContext reactApplicationContext, final JavaScriptContextHolder jsContext) { final List<JSIModuleSpec> specs = new ArrayList<>(); specs.add( new JSIModuleSpec() { @Override public JSIModuleType getJSIModuleType() { return JSIModuleType.UIManager; } @Override public JSIModuleProvider<UIManager> getJSIModuleProvider() { final ComponentFactory componentFactory = new ComponentFactory(); CoreComponentsRegistry.register(componentFactory); MainComponentsRegistry.register(componentFactory); List<ViewManager> viewManagers = new ArrayList<>(); ViewManagerRegistry viewManagerRegistry = new ViewManagerRegistry(viewManagers); return new FabricJSIModuleProvider( reactApplicationContext, componentFactory, new EmptyReactNativeConfig(), viewManagerRegistry); } }); return specs; } }; }
至此,我们新架构的运行环境就配置好了。由于目前新架构文章非常少,几乎没有混合工程运行新架构的方案,上面这些主要是我们用了大量的时间去调研和测试的结果,你可以参考一下。
好了,回到正题。在混合工程中,新架构的运行环境搭建好后,我们就可以简单快速地来写 TurboModule 和 Fabric 了。
我们继续来进行数据存储的 Demo 在 Java 层定义并实现 StorageModule。Android 端我们使用 SharedPreferences 来实现轻量级偏好存取。这里要注意,你需要继承 ReactModuleWithSpec和TurboModule,具体代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 // 1. 定义 StorageModule,继承 ReactContextBaseJavaModule 类 // 实现 ReactModuleWithSpec & TurboModule 接口 public class StorageModule extends ReactContextBaseJavaModule implements ReactModuleWithSpec, TurboModule { // native 存储的 sp 文件名称 private static final String SP_NAME = "rn_storage"; // 返回给 JS 的结果码 private static final int CODE_SUCCESS = 1; private static final int CODE_ERROR = 2; public StorageModule(ReactApplicationContextWrapper reactContext) { super(reactContext); } // 返回 module 名称,一般以类名作为 module 名称 @Override public String getName() { return StorageModule.class.getSimpleName(); } // 定义供 JS 调用的存储数据方法,isBlockingSynchronousMethod 表示是否同步执行 @ReactMethod(isBlockingSynchronousMethod = true) public void save(String key, String value, Callback callback) { WritableMap result = new WritableNativeMap(); // 如果 js 传入的 key 为空,则回传失败码和信息 if (TextUtils.isEmpty(key)) { result.putInt("code", CODE_ERROR); result.putString("msg", "key is empty or null"); callback.invoke(result); return; } // 调用 native 的 sp 进行数据存储 SharedPreferences sp = getReactApplicationContext().getSharedPreferences(SP_NAME, Context.MODE_PRIVATE); sp.edit().putString(key, value).apply(); result.putInt("code", CODE_SUCCESS); result.putString("msg", "save success"); // 回传 js 告知存储成功 callback.invoke(result); } // 定义供 JS 调用的获取数据方法,isBlockingSynchronousMethod 表示是否同步执行 @ReactMethod(isBlockingSynchronousMethod = true) public void get(String key, Callback callback) { // 如果 js 传入的 key 为空,则回传失败码和信息 WritableMap result = new WritableNativeMap(); if (TextUtils.isEmpty(key)) { result.putInt("code", CODE_ERROR); result.putString("msg", "key is empty or null"); callback.invoke(result); return; } // 调用 native 的 sp 获取 key 对应的 value 值 SharedPreferences sp = getReactApplicationContext().getSharedPreferences(SP_NAME, Context.MODE_PRIVATE); String value = sp.getString(key, ""); result.putInt("code", CODE_SUCCESS); result.putString("data", value); // 将结果回传给 js callback.invoke(result); } }
然后是注册组件,注意 Package 需要继承 TurboReactPackage:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class MyTurboModulePackage extends TurboReactPackage { @Override public NativeModule getModule(String name, ReactApplicationContext reactContext) { switch(name) { case "StorageModule": return new StorageModule(reactContext); break; default: return null; } } @Override public ReactModuleInfoProvider getReactModuleInfoProvider() { //... } }
接下来注册到 ReactInstanceManager (使用 reactInstanceManagerBuilder注册):
1 2 3 4 ReactInstanceManagerBuilder builder = ReactInstanceManager.builder() .addPackage(new MyTurboModulePackage()); ... // 其他 RN 初始化配置 ReactInstanceManager reactInstanceManager = builder.build();
然后我们利用新架构提供的 Codegen,生成新架构需要的 native 代码。不过,在使用 codegen 之前,我们需要在项目中应用相关的插件:
(1) 在工程根目录安装 react-native-gradle-plugin。
1 yarn add react-native-gradle-plugin
(2) 在工程根目路的 settings.gradle 中配置 react-native-gradle-plugin,使用复合构建引入。
1 2 3 include ':app' rootProject.name = "GeekTimeRNAndroid" includeBuild('./node_modules/react-native-gradle-plugin')
(3) 在 app/build.gradle 中应用插件。
1 apply plugin: "com.facebook.react"
这样做后,工程的 gradle 任务中就会出现 generateCodegenArtifactsFromSchema task。
1 2 配置好后,我们以后就可以使用 codegen 能力了,然后执行 generateCodegenArtifactsFromSchema,最后运行App就可以了。 ../gradlew generateCodegenArtifactsFromSchema
Android端的就是这样,现在我们看iOS端需要怎么做。
iOS 在iOS端中,首先我们要创建一个类并遵循一个协议 spec ,协议中包含注册的API声明。而且,该协议需要遵循 RCTBridgeModule 协议和 RCTTurboModule 协议,并且创建一个JSI。示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 //定义一个Spec协议 @protocol DataStorageTurboModuleSpec <RCTBridgeModule, RCTTurboModule> - (NSString *)getString:(NSString *)string; @end //JSI实现 namespace facebook { namespace react { class JSI_EXPORT DataStorageTurboModuleSpecJSI : public ObjCTurboModule { public: DataStorageTurboModuleSpecJSI(const ObjCTurboModule::InitParams ¶ms); }; } // namespace react } // namespace facebook //定义一些方法 namespace facebook { namespace react { static facebook::jsi::Value __hostFunction_DataStorageTurboModuleSpecJSI_getString( facebook::jsi::Runtime &rt, TurboModule &turboModule, const facebook::jsi::Value *args, size_t count){ return static_cast<ObjCTurboModule &>(turboModule) .invokeObjCMethod(rt, VoidKind, "getString", @selector(getString:), args, count); } DataStorageTurboModuleSpecJSI::NativeSampleTurboModuleSpecJSI(const ObjCTurboModule::InitParams ¶ms) : ObjCTurboModule(params) { //MethodMetadata 第一个参数为0代表该方法有一个参数 methodMap_["getString"] = MethodMetadata{1, __hostFunction_DataStorageTurboModuleSpecJSI_getString}; } } } //TurboModule遵循Spec协议 @interface DataStorageTurboModule : NSObject <DataStorageTurboModuleSpec> @end
之后,我们要在该类中注册组件名和API:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 //DataStorageTurboModule.mm @implementation DataStorageTurboModule RCT_EXPORT_MODULE() - (std::shared_ptr<facebook::react::TurboModule>)getTurboModule: (const facebook::react::ObjCTurboModule::InitParams &)params{ //指定JSI return std::make_shared<DataStorageTurboModuleSpecJSI>(params); } RCT_EXPORT_METHOD(getString:(NSString *)string){ NSLog(@""); } @end
以上便是自定义一个 TurboModule的流程。其实定义 TurboModule 并不复杂,而且 Facebook 也提供了代码生成工具 codegen,比较复杂的是在混合工程中搭建新架构的运行环境。前面我们花了不少内容讲述如何在客户端开启新架构,接下来的 Fabric 组件介绍也将在新架构环境基础上进行讲解,接下来我们继续来看Fabric 自定义组件。
Fabric:视频播放 Fabric 对标旧框架的 UIManager。FabricUIManager 可以和 C++ 层直接进行通讯,解除了原有的 UIManager 依赖单个 bridge 的问题。有了 JSI 后,以前批量依赖 bridge 的 UI 操作,都可以同步执行到 C++ 层,性能得到大幅提升,特别是在列表快速滑动、复杂动画交互方面提升更加明显。
现在,我们以一个视频播放组件为例,讲讲如何定义 Fabric 组件。我们先来看下 JavaScript 端的实现。
JavaScipt JavaScript需要定义属性以及 API,并 export 组件。示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 type NativeProps = $ReadOnly<{| ...ViewProps, url?: string |}>; // 定义视频播放的属性,url 为视频地址 export type VideoViewType = HostComponent<NativeProps>; // 定义视频播放的方法,包括开始播放、停止播放、暂停播放 interface NativeCommands { +callNativeMethodToPlayVideo: ( ) => void; +callNativeMethodToStopVideo: ( ) => void; +callNativeMethodToPauseVideo: ( ) => void; } //导出外部调用的命令,包括开始播放、停止播放、暂停播放 export const Commands: NativeCommands = codegenNativeCommands<NativeCommands>({ supportedCommands: ['callNativeMethodToPlayVideo'], supportedCommands: ['callNativeMethodToStopVideo'], supportedCommands: ['callNativeMethodToPauseVideo'], }); // 导出包装好的组件,其中 VideoView 为引入 Native 的组件 export default (codegenNativeComponent<NativeProps>( 'VideoView', ): VideoViewType);
JavaScript 端使用该组件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 // 导入 ViewView 组件和工具 import VideoView, { Commands as VideoViewCommands, } from './VideoNativeComponent'; // 外部调用此方法即可调用 ViewView 视频播放能力 export default function MyView(props: {}): React.Node { return ( <View> <VideoView url={"url"} style={{flex: 1}} /> <Button title="play" onPress={()=>{ VideoViewCommands.callNativeMethodToPlayVideo(); } } </View> ) }
接下来我们再看看 Android 端和 iOS 端的实现。
Android 由于在前面 TurboModule 的部分,我们已经讲解了如何在混合工程中开启新架构运行模式,我么这里就不再重复了。前面的方法同样适用于 Fabric,我们只需要搭建一次就好了。所以现在要在 Android 端实现 Fabric 组件也非常简单,我们来看下具体实现。
第一步,定义视频播放接口。
这里我们要定义 VideoViewManagerInterface,其中包含三个方法:播放视频、停止播放、暂停播放:
1 2 3 4 5 public interface VideoViewManagerInterface<T extends View> { void playVideo(T view, String url); void stopVideo(T view); void pauseVideo(T view); }
第二步,定义视频播放 View。
这一步中,我们要实现视频播放的 View。在 View 中,我们需要实现视频的播放、停止和暂停功能。但播放能力的实现并不是我们讲解的重点,我们这一讲侧重于新架构中 Frabic 组件的实现流程,所以我们这边使用伪代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class MyVideoView extends View { // ... public void playVideo(String url) { // 播放视频实现 } public void stopVideo() { // 停止视频播放实现 } public void pauseVideo() { // 暂停视频播放实现 } }
第三步,定义 ViewManager。
在这一步中,我们要实现暴露给 React Native 调用的能力,包括视频播放、停止,以及暂停,内部会转发到上面我们定义的视频播放 View 的实现中。示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 @ReactModule(name = VideoViewManager.REACT_CLASS) public class VideoViewManager: ViewGroupManager<VideoView>(), VideoViewManagerInterface<VideoView> { private static final String REACT_CLASS = "VideoView"; public VideoViewManager() { } override public String getName() { return REACT_CLASS; } override public VideoView createViewInstance(ThemedReactContext reactContext) { return new MyVideoView(reactContext); } @ReactProp(name = "url") override public void playVideo(VideoView view, String url) { view.playVideo(url); } override public void stopVideo(VideoView view) { view.stopVideo(); } override public void pauseVideo(VideoView view) { view.pauseVideo(); } }
**最后一步就是注册 ViewManager。**我们在 ReactInstanceManager 的 JSIModulesPackage 中注册 VideoViewManager:
1 2 3 4 5 6 7 8 9 List<ViewManager> viewManagers = new ArrayList<>(); viewManagers.add(new VideoViewManager()) ViewManagerRegistry viewManagerRegistry = new ViewManagerRegistry(viewManagers); return new FabricJSIModuleProvider( reactApplicationContext, componentFactory, new EmptyReactNativeConfig(), viewManagerRegistry);
然后我们利用新架构提供的 Codegen,调用 gradlew generateCodegenArtifactsFromSchema 生成代码 Native 代码:
1 ../gradlew generateCodegenArtifactsFromSchema
最后,运行即可。Android 端的实现就是这样,接下来我们再看下 iOS 端。
iOS 在iOS端汇总,首先我们要创建一个继承于RCTViewComponentView 的一个类作为视频组件,如下:
1 2 3 4 5 6 7 8 9 @interface VideoComponentView : RCTViewComponentView //声明播放器组件的一些方法 //播放视频 - (void)playVideo; //停止视频 - (void)stopVideo; //暂停视频 - (void)pauseVideo; @end
之后,该类需要遵循一个协议,协议中需要声明执行Command的方法名,示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 @protocol VideoComponentViewProtocol <NSObject> - (void)callNativeMethodToPlayVideo; - (void)callNativeMethodToStopyVideo; - (void)callNativeMethodToPauseVideo; @end RCT_EXTERN inline void VideoComponentCommand( id<VideoComponentViewProtocol> componentView, NSString const *commandName, NSArray const *args) if([commandName isEqualToString:@"callNativeMethodToPlayVideo"]){ [componentView callNativeMethodToPlayVideo]; return; } if(![commandName isEqualToString:@"callNativeMethodToStopVideo"]){ [componentView callNativeMethodToStopVideo]; return; } if(![commandName isEqualToString:@"callNativeMethodToPauseVideo"]){ [componentView callNativeMethodToPauseVideo]; return; } return; )
接下来,ComponentView需要遵循该Protocol协议,并在执行common时调用对应的方法。此外,我们还可以设置组件的属性:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 using namespace facebook::react; @interface VideoComponentView() <VideoComponentViewProtocol> @end @implementation VideoComponentView{ VideoPlayer *_videoPlayer; } #pragma mark - Native Commands - (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args{ VideoComponentCommand(self, commandName, args); } - (void)callNativeMethodToPlayVideo{ //实现视频播放功能 [_videoPlayer startPlay]; } - (void)callNativeMethodToStopVideo{ //实现视频停止功能 [_videoPlayer stopPlay]; } - (void)callNativeMethodToPauseVideo{ //实现视频暂停功能 [_videoPlayer pausePlay]; } #pragma mark - Props //遵循descriptor协议 + (ComponentDescriptorProvider)componentDescriptorProvider{ return concreteComponentDescriptorProvider<VideoComponentDescriptor>(); } - (instancetype)initWithFrame:(CGRect)frame{ if (self = [super initWithFrame:frame]) { static const auto defaultProps = std::make_shared<const ComponentViewProps>(); _props = defaultProps; _videoPlayer = [[VideoPlayer alloc] init]; self.contentView = _videoPlayer; } return self; } - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps{ [super updateProps:props oldProps:oldProps]; } - (void)onChange:(UIView *)sender{ // No-op // std::dynamic_pointer_cast<const ViewEventEmitter>(_eventEmitter) // ->onChange(ViewEventEmitter::OnChange{.value = static_cast<bool>(sender.on)}); } @end Class<RCTComponentViewProtocol> VideoViewCls(void){ return VideoComponentView.class; }
接着,注册属性遵循VideoComponentDescriptor,并且需要指定该组件的名字:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 namespace facebook { namespace react { using VideoComponentDescriptor = ConcreteComponentDescriptor<VideoViewShadowNode>; } // namespace react } // namespace facebook namespace facebook { namespace react { extern const char VideoViewComponentName[]; using VideoViewShadowNode = ConcreteViewShadowNode< VideoViewComponentName,//组件名 VideoViewProps>;//注册属性 } // namespace react } // namespace facebook namespace facebook { namespace react { extern const char VideoViewComponentName[] = "VideoView";//组件名 } // namespace react } // namespace facebook namespace facebook { namespace react { //属性定义 class VideoViewProps final : public ViewProps { public: VideoViewProps() = default; VideoViewProps(const PropsParserContext& context, const VideoViewProps &sourceProps, const RawProps &rawProps); #pragma mark - Props std::string url{""};//视频url }; } // namespace react } // namespace facebook
这样,我们就创建好了一个 React Native 的 Fabric 组件、定义属性以及 API 的方法。 以上便是如何使用 Fabric 自定义视频播放组件,在混合工程中搭建好新架构的运行环境后,只需要遵守 Fabric 的组件定义方式,进行接口定义、功能实现和组件注册即可。关于一些复杂的 Fabric 组件,可以查看 https://github.com/software-mansion/react-native-gesture-handler/tree/main/FabricExample ,https://github.com/software-mansion/react-native-reanimated/tree/main/FabricExample ,目前 react-native-gesture-handler、react-native-reanimated 都已经适配了新架构,感兴趣的同学可以去学习下。
总结 这一讲,我们系统讲解了个性化组件的使用场景、生命周期、传输类型,以及通信方式,并通过两个实际案例讲解了如何在新架构下定制个性化的 TurboModules 与 Fabric。而且,我们也简单介绍了一下React Native新架构,你可以通过官方文档进行新架构的体验。
这一讲是我们 Native 相关的三讲中花的时间最长的,也是最“伤肝”的,我们前前后后加调研花了两个月的时间。但我们相信,新架构在未来会有很好的发展,这是可以预见的。因为它解决了 React Native 几个最痛的点,包括启动速度、运行时性能等。如果新架构还能在易用性上继续优化,将会大大拓展 React Native 的用户群体。
因为目前新架构还处于未发布的状态,网上相关的文章大都是对官方纯 React Native 模式 Demo 和介绍,少数几篇会深挖原理,但讲混合模式的新架构运行文章几乎没有。我们这一讲中对 TurboModule 和 Fabric 的讲解,更侧重于如何在混合工程中开启并运行。如果有对 TurboModule、Fabric、JSI 的原理感兴趣的同学,后面有机会我们再来分享。
作业 设计一个打印 Native 日志的 TurboModule,以及一个 Native 加载进度条的 Fabric 组件。 欢迎在评论区写下你的想法和经验,和我们多多交流。我们下一讲见。