React Native新架構:那些棘手的部分(上篇)
终于等到了——随着React Native 0.76版本的发布,新的架构将成为创建应用的默认方式。如果你还没有听说过这个,可以查看这个页面以了解React Native文档中的相关信息。对于用户来说,这种变化应该是透明的,但库的开发者可能需要做一些修改。随着API在过去的几年里逐渐稳定,现在是让库适应新架构的最佳时机。新的架构通过一系列底层的改动,现在可以实现以前无法实现的功能。
在这篇文章中,我们将介绍一些新的概念和可能性,并说明你如何可以利用它们。在继续阅读之前,请先熟悉我们在本文中将用到的术语,并了解渲染管道。
平面视图视图扁平化技术是一种渲染优化,允许跳过创建那些只会对布局产生影响的原生视图。这大大减少了应用程序中创建的原生视图的数量,在使用 React Native 的情况下。决定是否扁平化视图的算法做得非常出色。除非你查看原生视图的层次结构,否则很难看出它实际上是否在起作用。
遗憾的是,它可能会引发一些问题。当 TextInput
组件在原生视图层次结构中被移动时,它们将失去焦点。这就是视图扁平化起作用时发生的情况。让我们考虑以下场景:
- 有一个
TextInput
组件被渲染在一个只有内边距样式的View
中,让它仅仅作为一个布局元素存在。 - 点击
TextInput
,它就获得了焦点。 - 输入了一些文本,但并不是有效的电子邮件地址。
- 父
View
的背景变红,表示有错误。 View
不再只是布局元素,还需要在屏幕上显示。TextInput
被移动到了红色视图之下,从而失去了焦点。
库也可能受到视图扁平化的影响,因为这种情况只影响到渲染管道的最后一步——挂载。React Native Gesture Handler 就是这样的一个库。它提供 原生手势识别器,因此需要将其附加到实际的原生视图上。在这种情况下,首个将被挂载在 GestureDetector
下的组件可能会被扁平化。这时,检测器将尝试将手势附加到一个实际上不存在的原生视图上,导致手势无法工作。幸运的是,Gesture Handler 能够检测并报告这种情况,同时提示你需要为此特定视图禁用扁平化。
你可能想知道如何禁用它。只需在不想折叠的视图上设置一个属性 collapsable
。
<View collapsable={false}>
{/* 此视图不可折叠 */}
</View>
查看回收站
视图再利用是新架构引入的另一种优化。当视图被卸载时,React Native 不再销毁它们,而是将它们放入回收池中等待重用,不再在需要时重新创建视图。当内存使用量高时,该池会被清空以防止应用被操作系统终止。默认在 iOS 上启用,而在 Android 上则需要通过[功能标志]启用。
在 iOS 中,每个视图默认都会被回收 — 当它被移除或不再使用时,会调用其 prepareForRecycle
方法,你可以在这里重置任何自定义属性,之后视图会被放入回收池中,直到需要相同类型的视图时再被使用。
- (void)准备重新使用
{
[super 准备重新使用];
// Reset any custom properties here
[_customView setBackgroundColor:[UIColor black]];
}
如果你想让你的自定义视图不被回收,你可以在 ComponentView
上添加一个静态的 shouldBeRecycled
方法,使其返回 NO
。
// 是否应该回收
+ (BOOL)shouldBeRecycled
{
// 返回 不应该回收;
return NO;
}
在 Android 上,首先将 enableViewRecycling
功能标志设置为 true
以启用视图回收,然后通过调用你的 ViewManager
的 _setupViewRecycling
方法来回收。为了将任何可能已更改的属性重置为其默认值,你可以实现一个 prepareToRecycleView
方法。
init {
setupViewRecycling()
}
override fun prepareToRecycleView(reactContext: ThemedReactContext, view: View): View? {
// 在这里重置任何自定义属性
view.setBackgroundColor(Color.BLACK)
return super.prepareToRecycleView(reactContext, view)
}
你可以看看this分支以获取完整例子。
支持新版本架构在你的库中随着新架构变得越来越完善和稳定,相关的文档也变得更加丰富和详细。几乎所有内容都由 Meta 新架构工作组提供的优秀指南涵盖,可在 GitHub 查看。不论是使用 Kotlin 或 Objective-C 进行平台特定开发,还是使用 C++ 实现跨平台开发,这些指南都会教你如何创建 Fabric 组件或 Turbo 模块。如果你想找一个简短的总结,可以查看 这个提交,它介绍了如何迁移视图和模块。还有一些更复杂的话题未在这些指南中详述,但在特定情况下可能非常有用。接下来我们将深入探讨新架构可以实现的高级功能。
自定义阴影结点自定义阴影节点是一个非常强大的工具,可让您在渲染管道的每个阶段影响组件的行为。当你为 fabric 组件实现自定义阴影节点时,最好的开始方式是复制 codegen 生成且已知可以正常工作的相关代码。记得那里会有一些内容混在一起,但你只需要关注与特定组件相关的部分。
可以在您的应用中找到生成的文件,位于 ios/build/generated/ios/react/renderer/components/<CODEGEN NAME>/
或 android/build/generated/source/codegen/jni/react/renderer/components/<CODEGEN NAME>/
。 <CODEGEN NAME>
指的是在 package.json
文件中的 codegenConfig
字段设置的名称。
你需要创建实际的 ShadowNode 文件及其状态和组件描述符的头文件。你可以从生成的 ShadowNodes.h
、ShadowNodes.cpp
、States.h
和 ComponentDescriptors.h
中复制内容。此时的一个有用改变是将 ShadowNode
及其描述符的定义改为继承具体的类,而不是使用别名——你可能需要自定义其行为。将它们放在 shared
目录下,这个目录会在 Android 和 iOS 上都使用。
// NewArchTricksViewComponentDescriptor.h
#pragma once // 该指令用于防止头文件被多次包含
#include <react/debug/react_native_assert.h> // 引入react_native的断言宏
#include <react/renderer/components/NewArchTricksSpec/Props.h>
#include <react/renderer/core/ConcreteComponentDescriptor.h>
#include <react/renderer/components/NewArchTricksSpec/NewArchTricksViewShadowNode.h>
namespace facebook {
namespace react {
// NewArchTricksViewComponentDescriptor类,继承自ConcreteComponentDescriptor<NewArchTricksViewShadowNode>
class NewArchTricksViewComponentDescriptor final
: public ConcreteComponentDescriptor<NewArchTricksViewShadowNode> {
public:
using ConcreteComponentDescriptor::ConcreteComponentDescriptor; // 使用基类的构造函数
// adopt方法用于适配ShadowNode,它会断言传入的ShadowNode是NewArchTricksViewShadowNode类型,并调用基类的adopt方法
void adopt(ShadowNode &shadowNode) const override {
react_native_assert(dynamic_cast<NewArchTricksViewShadowNode *>(&shadowNode)); // 断言shadowNode是NewArchTricksViewShadowNode类型
ConcreteComponentDescriptor::adopt(shadowNode); // 调用基类的adopt方法
}
};
} // namespace react
} // namespace facebook
// NewArchTricksViewShadowNode.cpp 文件中的代码
// 包含 react 渲染器中的 NewArchTricksView 组件的阴影节点实现。
// 下面的命名空间定义了一个外部的字符数组,用于存储组件名 "NewArchTricksView"。
// 保留原代码,不做翻译
namespace facebook::react {
extern const char NewArchTricksViewComponentName[] = "NewArchTricksView";
} // namespace facebook::react
// NewArchTricksViewShadowNode.h
#pragma once
#include <react/renderer/components/NewArchTricksSpec/EventEmitters.h>
#include <react/renderer/components/NewArchTricksSpec/Props.h>
#include <react/renderer/components/view/ConcreteViewShadowNode.h>
#include <react/renderer/components/NewArchTricksSpec/NewArchTricksViewState.h>
#include <jsi/jsi.h>
namespace facebook::react {
JSI_EXPORT extern const char NewArchTricksViewComponentName[];
/*
* `NewArchTricksViewShadowNode` 对应的 <NewArchTricksView> 组件。
*/
class NewArchTricksViewShadowNode final : public ConcreteViewShadowNode<
NewArchTricksViewComponentName,
NewArchTricksViewProps,
NewArchTricksViewEventEmitter,
NewArchTricksViewState> {
public:
using ConcreteViewShadowNode::ConcreteViewShadowNode;
};
} // namespace facebook::react
// NewArchTricksViewState.h
#pragma once
#ifdef ANDROID
#include <folly/dynamic.h>
#endif
namespace facebook::react {
class NewArchTricksViewState {
public:
NewArchTricksViewState() = default;
#ifdef ANDROID
NewArchTricksViewState(NewArchTricksViewState const &previousState, folly::dynamic data) {};
folly::dynamic getDynamic() const {
return {};
}
#endif
};
} // } 命名空间 facebook::react
下一步是更新代码生成配置文件,让它不再生成你刚刚复制的部分,这样就可以避免重复的元素。
// 新架构技巧原生组件
// NewArchTricksNativeComponent.ts
export default codegenNativeComponent<NativeProps>('NewArchTricksView', {
interfaceOnly: true,
});
现在你已经准备好实现的准备工作,可以开始在项目中使用它了。我们将从iOS开始,因为相对简单——你只需要将创建的文件包含到一个子规格中,并在相关的ComponentView
文件中更改导入路径——就这样,搞定!
# react-native-new-arch-tricks.podspec
require "json"
package = JSON.parse(File.read(File.join(__dir__, "package.json")))
folly_compiler_flags = '-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -Wno-comma -Wno-shorten-64-to-32'
Pod::Spec.new do |s|
s.name = "react-native-new-arch-tricks"
s.version = package["version"]
s.summary = package["description"]
s.homepage = package["homepage"]
s.license = package["license"]
s.authors = package["author"]
s.platforms = { :ios => min_ios_version_supported }
s.source = { :git => "https://github.com/j-piasecki/react-native-new-arch-tricks.git", :tag => "#{s.version}" }
s.source_files = "ios/**/*.{h,m,mm}"
install_modules_dependencies(s)
+ s.subspec "shared" do |ss|
+ ss.source_files = ["shared/**/*.{cpp,h}"]
+ ss.header_dir = "NewArchTricks"
+ ss.pod_target_xcconfig = { "HEADER_SEARCH_PATHS" => "\"$(PODS_TARGET_SRCROOT)/shared\"" }
+ end
end
// NewArchTricksViewComponentView.mm
#import "NewArchTricksViewComponentView.h"
// 没有这个头文件,RN将无法找到提供者
#import <React/RCTFabricComponentsPlugins.h>
-#import <react/renderer/components/NewArchTricksSpec/ComponentDescriptors.h>
+// 导入自定义头文件以便我们可以使用自己的组件描述符
+#import <NewArchTricks/NewArchTricksViewComponentDescriptor.h>
#import <react/renderer/components/NewArchTricksSpec/EventEmitters.h>
#import <react/renderer/components/NewArchTricksSpec/Props.h>
#import <react/renderer/components/NewArchTricksSpec/RCTComponentViewHelpers.h>
在Android上则更复杂一些——你需要配置React Native,使其在构建时包含你自定义的C++代码。这是通过一个名为react-native.config.js
的文件实现的,该文件会指定你定义的组件描述符,并说明如何使用自定义的CMake文件来构建。
/*
react-native.config.js
*/
module.exports = {
dependency: {
platforms: {
android: {
componentDescriptors: ['NewArchTricksViewComponentDescriptor'],
cmakeListsPath: './CMakeLists.txt',
},
},
},
};
记住你在配置文件中设置的 CMake 文件路径是相对于你的库的 android 目录的。另外,还需要注意的是,当你修改配置文件时,你需要在测试应用中删除 android/build
目录,因为它保存了自动链接的缓存信息,这些信息可能不会自动更新,因此需要手动删除。
现在,你需要实际创建这个CMake文件——和之前一样,你可以从codegen生成的那个开始。关键是重新定义react_codegen_<CODEGEN名称>
目标,并将你的自定义代码与生成的代码一起包含进去。为了确保自定义代码会被使用而不是生成的代码,包含自定义代码的目录需要首先设置在包含路径中。
# CMakeLists.txt配置文件
cmake_minimum_required(VERSION 3.13) # 设置CMake最低版本要求为3.13
project(NewArchTricks) # 项目名称为NewArchTricks
set(CMAKE_VERBOSE_MAKEFILE on) # 设置详细的构建日志
set(CMAKE_CXX_STANDARD 20) # 设置C++标准为20
file(GLOB_RECURSE new_arch_tricks_SRCS CONFIGURE_DEPENDS ../shared/*.cpp) # 新架构技巧源文件
file(GLOB_RECURSE new_arch_tricks_codegen_SRCS CONFIGURE_DEPENDS ./build/generated/source/codegen/jni/*.cpp) # 新架构代码生成源文件
add_library(
react_codegen_NewArchTricksSpec # 反编译新架构特性的库
SHARED
${new_arch_tricks_SRCS}
${new_arch_tricks_codegen_SRCS}
)
target_include_directories(react_codegen_NewArchTricksSpec PUBLIC ../shared) # 指定公共包含目录为../shared
target_include_directories(react_codegen_NewArchTricksSpec PUBLIC ./build/generated/source/codegen/jni) # 指定公共包含目录为./build/generated/source/codegen/jni
target_link_libraries(
react_codegen_NewArchTricksSpec # 将react_codegen_NewArchTricksSpec链接到以下库
fbjni
jsi
reactnative
)
最后一步是创建 ComponentDescriptors.h
和 ComponentDescriptors.cpp
文件——同样,你可以从生成的文件入手。你需要确保自定义组件描述器被包含在头文件中。头文件中定义的所有内容也将对 React Native 可用,并且 React Native 期望这些相关的描述符来自该文件。
// ComponentDescriptors.h
#pragma once
#include <react/renderer/components/NewArchTricksSpec/ShadowNodes.h>
#include <react/renderer/core/ConcreteComponentDescriptor.h>
#include <react/renderer/componentregistry/ComponentDescriptorProviderRegistry.h>
#include <react/renderer/components/NewArchTricksSpec/NewArchTricksViewComponentDescriptor.h>
namespace facebook::react {
void NewArchTricksSpec_registerComponentDescriptorsFromCodegen(
std::shared_ptr<const ComponentDescriptorProviderRegistry> registry);
} // namespace facebook::react
// ComponentDescriptors.cpp
#include <react/renderer/components/NewArchTricksSpec/ComponentDescriptors.h>
#include <react/renderer/core/ConcreteComponentDescriptor.h>
#include <react/renderer/componentregistry/ComponentDescriptorProviderRegistry.h>
namespace facebook::react {
} // 结束
就这样!从现在起,每次渲染你的组件,它将使用你刚才创建的 shadow node,但实际上并没有什么实质性的变化。
想了解更多,请点击这里阅读React Native新架构的第二部分文章。
这只是好玩部分的前奏——请查看ShadowNode.h,LayoutableShadowNode.h,以及YogaLayoutableShadowNode.h,看看你刚刚获得哪些字段和方法。
我们是Software Mansion:React Native核心开发人员、新架构专家、社区构建者、多媒体专家,以及软件开发顾问。您需要帮助完成项目吗?请联系我们,我们将很乐意帮助您:[email protected]。
共同學習,寫下你的評論
評論加載中...
作者其他優質文章