React Native性能优化与异常处理全解析
1. FlatList性能优化
1.1 基础优化
-
预计算列表项高度
:使用
getItemLayout来预计算列表项的高度,这样列表就无需在运行时进行计算,提高渲染效率。 -
使用图像缓存
:例如
react-native-fast-image,可以缓存图像,减少重复下载和加载,提升用户体验。
1.2 高级优化
| 参数 | 作用 | 优化策略 |
|---|---|---|
initialNumToRender
|
定义初始渲染的列表项数量,这些项在
FlatList
的生命周期内不会被卸载。
| 根据实际情况设置合适的初始渲染数量,避免过多或过少。 |
windowSize
| 定义需要渲染列表项的窗口区域。 | 调整窗口大小,平衡渲染性能和用户体验。 |
maxToRenderPerBatch
和
updateCellsBatchingPeriod
| 控制渲染批次的大小和频率。小批次和较长的批处理周期可以提供更好的TTI(首次交互时间),而大批次和较短的批处理周期可以避免空白区域。 | 根据具体场景进行权衡,找到最佳的参数组合。 |
1.3 案例研究:应用基本启发式优化
1.3.1 添加键
为列表项添加唯一的键可以避免React发出警告。可以使用
FlatList
的
keyExtractor
属性,将
feed ID
作为键。示例代码如下:
<FlatList
...
keyExtractor={(item) => item.feed.id}
...
/>
这样可以消除警告信息:
ManyFaces[19949:1694125] [javascript] Warning: Each child in a list should have a unique "key" prop.
Check the render method of `VirtualizedList`. See https://fb.me/react-warning-keys for more information.
1.3.2 实现
shouldComponentUpdate
为所有
Feeds
组件实现
shouldComponentUpdate
方法,避免不必要的差异算法运行。由于
Feeds
组件被高阶组件(HOC)封装,无法使用纯组件。可以通过比较
item
属性来确定是否需要更新组件。示例代码如下:
export default function withMetaAndControls(Feed) {
class ElemComponent extends React.Component {
shouldComponentUpdate(nextProps, nextState) {
if (nextProps.item === this.props.item) {
return false;
}
return true;
}
render() {
return (
...
);
}
like = () => {
this.props.like(this.props.feedIndex);
}
...
}
}
通过这种方式,只有当传入的
item
属性发生变化时,才会更新
Feeds
组件,提高性能。
2. 零崩溃:设计异常处理流程
2.1 异常处理的重要性
异常处理是系统设计中至关重要的一部分。“快速失败”原则要求我们尽早发现并处理潜在的异常,避免应用在用户面前崩溃。我们需要明确异常处理的目标:
- 所有异常都应被正确导向预期的位置,避免异常被抛到调用栈的任意上层或未被捕获而导致应用崩溃。
- 可恢复的异常应被静默处理,对用户透明。
- 不可恢复的异常应定义明确的行为和UI展示,向用户表达歉意。
- 记录所有意外的逻辑流程,包括可恢复和不可恢复的异常。
- 了解应用在实际环境中崩溃时的具体情况,以便高效修复并准确报告问题。
2.2 异常的分类及处理原则
| 分类方式 | 类别 | 处理原则 |
|---|---|---|
| 严重程度 | 可恢复异常 | 应用“静默日志”技术,记录异常信息,同时保持用户体验的连续性。 |
| 不可恢复异常 | 将异常一直抛到UI层,向用户提供反馈,如显示“出错了”页面、弹出提示框或重置状态并返回登录界面。 | |
| 可控性 | 可控异常 | 记录所有必要的信息。 |
| 不可控异常 | 尽最大努力将其转换为可控异常,通过细致的日志记录来实现。 | |
| 来源 | 外部异常 | 由于不完全受客户端逻辑控制,可引入“重试”逻辑来缓解问题。 |
| 内部异常 | 通常不进行重试。 | |
| 范围 | 全局异常 | 只能由全局处理程序处理,如BAD_ACCESS和段错误。 |
| 局部异常 |
可以在局部的
catch
块或错误边界中捕获。
|
2.3 JavaScript的优势
与大多数其他编程语言(如C++和Objective-C)不同,JavaScript可以将所有异常局部化。这意味着所有异常,包括空指针异常,都可以使用
catch
块或错误边界以异常的形式捕获。为了使应用保持零崩溃的最低标准,可以采取以下措施:
- 始终应用顶级错误边界,捕获内部抛出的异常。
- 始终在方法调用的入口点应用顶级
catch
块,并将异常重定向到错误边界。
2.4 软件架构中的健壮性设计
2.4.1 入口点
在React Native中,有两个接收和处理外部数据的入口点:从服务器端接收数据和从原生层到JavaScript层的数据传递。在处理这些数据时,需要进行空值检查、优雅地回退并在必要时抛出异常。具体处理方式如下:
-
可选字段为空
:这是一种逻辑流程,而非异常流程,因为没有违反协议。可以为字段提供默认值作为回退。
-
必填字段为空
:协议被破坏,设置默认值并记录日志,秘密恢复以不打扰用户。
-
关键字段为空
:不仅协议被破坏,而且缺少的字段属于关键路径,用户体验无法继续。在这种情况下,应立即抛出异常,由UI层处理。
2.4.2 崩溃点
需要特别注意空指针异常和越界异常。这些异常可能发生在组件、全局函数和原生层中。可以使用错误边界来处理组件中的异常,将异步异常引导到错误边界,以及将原生异常引导到错误边界。对于全局函数,应避免将关键路径放在其中,所有异常应在内部被捕获和处理。
2.5 最后的手段:全局错误处理程序
即使有完善的防御代码,异常仍可能发生。这些异常应被视为失败,只能由全局错误处理程序处理。主要有以下三种全局错误处理方式:
- 包裹应用根组件的错误边界,显示“出错了”页面。
- 安装
RCTExceptionManager
并绑定委托。
- 使用
react-native-exception-handler
。
这些全局处理程序可以用于进行崩溃报告,提供来自实际环境的宝贵信息。但最好在开发和测试阶段尽早发现并解决这些问题。
2.6 总结
开发一个零崩溃的应用是一个重要的目标。优秀的开发者应具备深入研究生产阶段问题的意愿和能力,以及提前设计系统以提高其弹性和在灾难发生时提供尽可能多信息的能力。异常处理体验是用户体验的重要组成部分,需要与产品团队密切合作,制定出良好的计划。
3. 深入了解原生模块
3.1 原生模块的目的
原生模块的目的是导出原生功能。通过JavaScriptCore等引擎,可以将原生编写的函数注入到JavaScript运行时,供JavaScript代码直接使用。原生方法的导出基于这种能力,并进行了一些调整。
3.2 调用方式和性能优化
过去,调用原生模块时,参数在跨越桥接时需要进行序列化和反序列化,这会带来性能损失。基于JSI的优化消除了这种序列化和反序列化的需求,提高了性能。
3.3 初始化流程
原生模块的初始化流程大致分为三个阶段:预启动、启动和JavaScript层的初始化。以下是初始化流程的mermaid流程图:
graph LR
classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px;
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
classDef decision fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px;
A([Phase 0: Prior Bootstrap]):::startend --> B(RCT_EXTERN_MODULE):::process
B --> C(RCT_EXTERN_REMAP_MODULE):::process
C --> D(RCT_EXPORT_MODULE_NO_LOAD):::process
D --> E(RCTRegisterModule):::process
E --> F([Phase 1: Bootstrap]):::startend
F --> G(@RCTBridge):::process
G --> H(@RCTBridgeModule):::process
H --> I(setUp):::process
I --> J(@RCTBridge):::process
J --> K(application(_:didFinishLaunchingWithOptions:)):::process
K --> L(@AppDelegate):::process
L --> M(initWithDelegate:bundleURL:moduleProvider:launchOptions:):::process
M --> N(RCTBridge Delegate):::process
N --> O(sourceURLForBridge):::process
O --> P(start):::process
P --> Q(@RCTCxxBridge):::process
Q --> R(registerExtraModules):::process
R --> S(extraModulesForBridge):::process
S --> T(RCTGetModuleClasses):::process
T --> U(JSCExecutorFactory()):::process
U --> V(loadSource):::process
V --> W(prepareBridge):::process
W --> X([Phase 2: Initialization on JavaScript layer]):::startend
3.4 阶段0:预启动
原生模块的初始化从
RCT_EXTERN_MODULE
宏开始,该宏依次应用
RCT_EXTERN_REMAP_MODULE
和
RCT_EXPORT_MODULE_NO_LOAD
宏。这些宏最终会在早期阶段将模块类填充到
RCTModuleClasses
中,用于后续实例化原生模块实例。具体代码如下:
#define RCT_EXTERN_MODULE(objc_name, objc_supername)
RCT_EXTERN_REMAP_MODULE(, objc_name, objc_supername)
#define RCT_EXTERN_REMAP_MODULE(
js_name, objc_name, objc_supername)
objc_name:
objc_supername @
end @interface objc_name(RCTExternModule)<RCTBridgeModule>
@end
@implementation objc_name (RCTExternModule)
RCT_EXPORT_MODULE_NO_LOAD(js_name, objc_name)
#define RCT_EXPORT_MODULE_NO_LOAD(js_name, objc_name)
RCT_EXTERN void RCTRegisterModule(Class);
+(NSString *)moduleName
{
return @ #js_name;
}
__attribute__((constructor))
static void RCT_CONCAT(initialize_,
objc_name)()
{
RCTRegisterModule([objc_name class]);
}
-
定义
@interface使用传入的模块名称。 -
紧接着定义
@implementation。 -
实现
moduleName方法,这与Android原生模块实现中的getName()方法等效。 -
使用
__attribute__((constructor))在其他操作之前进行模块注册。 -
调用
RCTRegisterModule注册模块。
RCT_EXTERN_MODULE
主要用于基于Swift的原生模块,因为Swift中不允许使用
load()
方法。对于普通的Objective-C,可以使用更简单的
RCT_EXPORT_MODULE
来实现相同的功能。
3.5
RCTRegisterModule
的实现
void RCTRegisterModule(Class moduleClass)
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 具体实现
});
}
通过
dispatch_once
确保模块只注册一次。
综上所述,通过对FlatList的性能优化、异常处理流程的设计以及对原生模块的深入了解,可以提高React Native应用的性能和稳定性,为用户提供更好的体验。
4. 原生模块初始化流程详解
4.1 阶段0具体步骤分析
在阶段0(Prior Bootstrap)中,各个宏的使用和模块注册过程有着明确的逻辑。以下是对每个步骤的详细分析:
1.
RCT_EXTERN_MODULE
:作为起始宏,它调用了
RCT_EXTERN_REMAP_MODULE
,为后续的模块定义和注册做准备。
2.
RCT_EXTERN_REMAP_MODULE
:此宏定义了
@interface
,使用传入的模块名称,同时紧接着定义了
@implementation
,为模块的实现提供了基础。
3.
RCT_EXPORT_MODULE_NO_LOAD
:该宏实现了
moduleName
方法,这与Android原生模块实现中的
getName()
方法等效。并且使用
__attribute__((constructor))
确保模块在其他操作之前进行注册,然后调用
RCTRegisterModule
完成模块的注册。
4.2 阶段1:启动阶段
在阶段1(Bootstrap)中,涉及到多个类和方法的调用,具体流程如下:
| 步骤 | 操作 | 说明 |
| ---- | ---- | ---- |
| 1 |
@RCTBridge
| 开始桥接相关操作 |
| 2 |
@RCTBridgeModule
| 模块相关设置 |
| 3 |
setUp
| 进行初始化设置 |
| 4 |
application(_:didFinishLaunchingWithOptions:)
| 应用启动相关处理 |
| 5 |
@AppDelegate
| 应用委托处理 |
| 6 |
initWithDelegate:bundleURL:moduleProvider:launchOptions:
| 初始化桥接委托等参数 |
| 7 |
RCTBridge Delegate
| 桥接委托相关操作 |
| 8 |
sourceURLForBridge
| 获取桥接的源URL |
| 9 |
start
| 启动桥接 |
| 10 |
@RCTCxxBridge
| C++桥接相关操作 |
| 11 |
registerExtraModules
| 注册额外模块 |
| 12 |
extraModulesForBridge
| 获取桥接的额外模块 |
| 13 |
RCTGetModuleClasses
| 获取模块类 |
| 14 |
JSCExecutorFactory()
| 创建JavaScriptCore执行器工厂 |
| 15 |
loadSource
| 加载源文件 |
| 16 |
prepareBridge
| 准备桥接 |
4.3 阶段2:JavaScript层初始化
阶段2(Initialization on JavaScript layer)是原生模块初始化流程的最后一个阶段,在这个阶段,JavaScript层开始进行初始化操作,与前面的阶段共同构成了完整的原生模块初始化过程。
5. 总结与实践建议
5.1 性能优化总结
通过对FlatList的性能优化,我们可以从基础优化和高级优化两个方面入手。基础优化包括使用
getItemLayout
预计算列表项高度和使用图像缓存;高级优化则通过调整
initialNumToRender
、
windowSize
、
maxToRenderPerBatch
和
updateCellsBatchingPeriod
等参数来平衡渲染性能和用户体验。同时,为列表项添加键和实现
shouldComponentUpdate
方法也能有效提高性能。
5.2 异常处理总结
异常处理是保证应用稳定性的关键。我们需要明确异常处理的目标,对异常进行分类并遵循相应的处理原则。在软件架构中,要关注入口点和崩溃点的处理,同时使用全局错误处理程序作为最后的防线。JavaScript的优势使得我们可以将所有异常局部化,进一步提高应用的健壮性。
5.3 原生模块总结
原生模块的初始化流程分为预启动、启动和JavaScript层初始化三个阶段。通过宏的使用和模块注册,将原生功能导出供JavaScript代码使用。基于JSI的优化消除了参数序列化和反序列化的性能损失,提高了调用效率。
5.4 实践建议
为了更好地应用这些技术,以下是一些实践建议:
1.
性能优化实践
:在实际开发中,根据列表的实际情况调整高级优化参数,通过测试找到最佳的参数组合。同时,合理使用图像缓存,避免重复下载和加载。
2.
异常处理实践
:在入口点进行严格的空值检查和错误处理,对于不同类型的异常,按照处理原则进行处理。在开发和测试阶段,使用全局错误处理程序进行崩溃报告,及时发现并解决问题。
3.
原生模块实践
:理解原生模块的初始化流程,根据不同的开发语言选择合适的宏进行模块定义和注册。在调用原生模块时,利用JSI优化提高性能。
总之,通过对FlatList性能优化、异常处理和原生模块的深入了解和实践,可以提高React Native应用的性能和稳定性,为用户提供更加流畅和可靠的体验。
超级会员免费看
1501

被折叠的 条评论
为什么被折叠?



