React Native 0.86.2 已经发布。由于 Maven 相关问题,0.86.1 并未正式发布,因此准备升级 0.86.x 的项目应直接采用 0.86.2,而不是在自动化脚本中继续等待 0.86.1。
这次更新规模不大,但修复点都落在容易产生偶发故障的底层路径:一处涉及 display: contents 节点的新布局状态,另一处涉及 Android Runtime 销毁 React 实例时的并发同步。
布局修复:正确维护 display: contents 节点状态
0.86.2 修复了 display: contents 节点中 hasNewLayout 设置错误的问题。
display: contents 的特殊之处在于,节点本身不生成普通布局盒,但其子节点仍参与布局。若中间节点的新布局标记不正确,上层提交阶段就可能错误判断某些布局结果是否需要继续传递或应用。实际表现未必是稳定崩溃,更可能是嵌套结构更新后偶发不刷新、尺寸变化没有及时反映,或者只有再次触发渲染后界面才恢复。
可以在升级前后运行下面这个最小页面,重点观察切换状态后文本、间距和边框是否立即更新。将它保存为项目根目录的 App.tsx:
import React, {useState} from 'react';
import {
Button,
SafeAreaView,
StyleSheet,
Text,
View,
} from 'react-native';
export default function App() {
const [expanded, setExpanded] = useState(false);
return (
<SafeAreaView style={styles.screen}>
<Button
title={expanded ? '收起内容' : '展开内容'}
onPress={() => setExpanded(value => !value)}
/>
<View style={styles.card}>
<View style={styles.contentsNode}>
<Text style={[styles.title, expanded && styles.titleExpanded]}>
display: contents 布局回归测试
</Text>
{expanded && (
<Text style={styles.description}>
这段文本出现后,父级边框、子节点位置和文本样式都应立即更新。
</Text>
)}
</View>
</View>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
screen: {
flex: 1,
padding: 24,
gap: 20,
},
card: {
padding: 16,
borderWidth: 2,
borderColor: '#2563eb',
},
contentsNode: {
display: 'contents',
},
title: {
fontSize: 18,
color: '#111827',
},
titleExpanded: {
fontSize: 24,
color: '#b91c1c',
},
description: {
marginTop: 12,
lineHeight: 22,
},
});
运行时可分别在 Android 和 iOS 上连续切换几十次:
npm start
# 另开一个终端
npm run android
这个例子只是回归测试入口。业务项目还应覆盖条件渲染、动态文本、列表项增删、动画结束后的重新布局,以及 display: contents 多层嵌套等真实场景。
Android Runtime:为销毁流程使用显式锁
另一个修复位于 Android Runtime。React Native 0.86.2 在 ReactInstanceManager 的销毁流程中使用显式的 mHasStartedDestroyingLock,让“是否已经开始销毁”的状态拥有更清晰的同步边界。
这类改动的价值通常不体现在正常启动路径,而是在生命周期快速切换或多个线程接近同时触发清理时降低竞态风险。例如:
- 开发环境连续 Reload;
- Activity 快速退出并重新启动;
- 原生容器频繁挂载和卸载 React 页面;
- 自动化测试连续执行启动、后台切换和强制停止;
- 应用销毁期间仍有异步任务准备访问 React 实例。
显式锁并不意味着应用层可以忽略资源释放。原生模块仍应正确注销监听器、停止线程,并避免在 React 上下文失效后继续回调 JavaScript。
Android 项目可以用下面的脚本做一轮简单的启动与强制停止压力测试。运行前把包名替换为自己的 applicationId:
#!/usr/bin/env bash
set -euo pipefail
APP_ID="com.example.myapp"
ROUNDS=20
for i in $(seq 1 "$ROUNDS"); do
echo "[$i/$ROUNDS] launch"
adb shell monkey -p "$APP_ID" -c android.intent.category.LAUNCHER 1 >/dev/null
sleep 1
echo "[$i/$ROUNDS] force-stop"
adb shell am force-stop "$APP_ID"
sleep 1
done
echo "Stress test finished. Inspect native errors with:"
echo "adb logcat -d | grep -E 'AndroidRuntime|ReactNative|FATAL EXCEPTION'"
将脚本保存为 scripts/android-lifecycle-stress.sh 后执行:
chmod +x scripts/android-lifecycle-stress.sh
./scripts/android-lifecycle-stress.sh
这不是完整的并发证明,但很适合放进升级验收流程,用来暴露 Activity 生命周期、原生模块清理和异步回调方面的已有问题。
从 0.86.x 升级时怎么做
由于 0.86.1 被跳过,版本检查脚本不要假设补丁版本一定连续。如果项目已在 0.86.0,可以直接将依赖更新到 0.86.2:
npm install react-native@0.86.2 --save-exact
# iOS 项目同步原生依赖
cd ios
bundle exec pod install
cd ..
# 检查最终安装版本
node -p "require('react-native/package.json').version"
如果项目没有使用 Bundler 管理 CocoaPods,可根据团队现有环境运行 pod install。不要因为补丁版本较小就跳过原生差异检查;React Native 升级同时涉及 JavaScript 包、Android Gradle 配置、iOS Pods 和项目模板文件,锁文件也应一并提交。
建议升级后执行一次干净构建,排除旧产物干扰:
rm -rf node_modules
npm ci
cd android
./gradlew clean
cd ..
npm run android
是否清理 iOS Pods 和 DerivedData 应根据项目情况决定,不必把全量清理作为每次构建的固定动作。
落地检查清单
采用 0.86.2 时,可以按下面的顺序控制风险:
- 直接锁定
react-native@0.86.2,不要引用未发布的 0.86.1; - 在 CI 中确认 npm 依赖和 Maven 原生依赖都能稳定解析;
- 为使用
display: contents的页面补充条件渲染和动态尺寸回归测试; - 在 Android 上覆盖 Reload、退出、重启、后台切换和原生容器卸载;
- 检查自定义原生模块是否在销毁后仍保留监听器、线程或回调;
- 对比升级前后的崩溃日志、ANR、首屏启动和页面切换指标;
- 将 JavaScript 锁文件、Podfile.lock 及必要的 Gradle 变更一起提交。
0.86.2 的重点不是增加可见功能,而是让布局提交与 Android 实例销毁这两条底层路径更可靠。已经采用 0.86.0 的团队可以优先安排补丁升级;正在进行大版本迁移的团队,则应把它作为 0.86 系列的最低验收版本,并把布局与生命周期压力测试纳入发布门禁。