React Native 0.77+ 中 react-native-view-shot 截图失败如何修复?
结论
React Native 0.77.3 启用 Fabric 后,应先将 react-native-view-shot 从 3.8.0 升级到支持 New Architecture 且兼容当前 RN 版本的版本,例如固定使用 5.0.1,再重新编译 Android 和 iOS 应用。
截图 ref 应指向实际的原生 View,并在布局和棋盘内容绘制完成后调用 captureRef。增加 _mounted 和非空检查有助于处理生命周期问题,但解决不了原生视图查找路径的兼容性问题。
关于 Fabric 和版本支持,有两点需要纠正:
- Fabric 仍然使用数字
reactTag,不能把这条报错解释为“Fabric 已取消数字 tag”。RN 0.77.3 的FabricUIManager仍提供resolveView(int reactTag)。源码 5.0.1并非首次支持 Fabric 的版本。项目文档注明从4.0+开始支持 New Architecture;5.0.0发布记录要求 RN>=0.76.0,5.0.1则是修复 Android 特定截图崩溃的补丁版本。项目文档、5.0.0 发布记录、5.0.1 发布记录
为什么 ref 不为空,截图仍然失败?
Failed to snapshot view tag 17576 表示该原生视图截图失败。仅凭这条消息,无法确定问题出在视图查找、布局还是后续绘制,需要结合 Android Logcat 或 Xcode 控制台中的原生异常判断。
在这个环境下,应优先排查旧版库与 Fabric 的兼容性。3.8.0 的 Android 实现通过 NativeViewHierarchyManager.resolveView(tag) 查找视图,iOS 实现则通过 bridge.uiManager 的 viewRegistry 查找视图,都依赖旧架构的视图管理路径。Android 源码、iOS 源码
这些检查能确认的范围有限:
_mounted === true只能说明当前组件仍处于挂载状态。- ref 非空说明 JavaScript 端持有引用。
- 即使两项检查都通过,也不能保证截图模块能通过当前渲染架构找到目标原生视图。
React Native 从 0.76 起就默认启用 New Architecture,并非从 0.77 才开始。React Native 0.76 发布说明
升级依赖并重新构建
以固定升级到 5.0.1 为例,在项目根目录执行:
yarn add react-native-view-shot@5.0.1
使用 npm 的项目可执行:
npm install --save-exact react-native-view-shot@5.0.1
Android 清理并重新构建:
cd android
./gradlew clean
cd ..
npx react-native run-android
iOS 安装更新后的原生依赖,再重新构建:
cd ios
bundle exec pod install
cd ..
npx react-native run-ios
如果项目没有使用 Bundler 管理 CocoaPods,可使用 pod install。
该依赖包含原生代码,Fast Refresh 或重启 Metro 无法替代原生编译。后续升级前,应检查目标版本要求的最低 RN 版本,避免直接安装与 RN 0.77.3 不兼容的新主版本。
调整截图和分享代码
给棋盘增加一个专门用于截图的原生 View,设置 collapsable={false},并等待首次有效布局。下面的组件通过 children 接收棋盘内容:
import React, { useRef, useState } from 'react';
import { Button, View } from 'react-native';
import { captureRef } from 'react-native-view-shot';
import Share from 'react-native-share';
export default function BoardShare({ children }) {
const boardRef = useRef(null);
const sharingRef = useRef(false);
const [layoutReady, setLayoutReady] = useState(false);
const shareBoard = async () => {
if (!layoutReady || !boardRef.current || sharingRef.current) {
return;
}
sharingRef.current = true;
let stage = '截图';
try {
const uri = await captureRef(boardRef.current, {
format: 'png',
result: 'tmpfile',
});
// 截图期间页面可能已经退出。
if (!boardRef.current) {
return;
}
stage = '分享';
// 某些平台返回文件路径,某些平台返回 file:// URI。
const url = uri.startsWith('/') ? `file://${uri}` : uri;
await Share.open({
message: 'Check this out!',
url,
type: 'image/png',
failOnCancel: false,
});
} catch (error) {
console.error(`${stage}失败`, error);
} finally {
sharingRef.current = false;
}
};
return (
<>
<View
ref={boardRef}
collapsable={false}
onLayout={({ nativeEvent: { layout } }) => {
setLayoutReady(layout.width > 0 && layout.height > 0);
}}
>
{children}
</View>
<Button
title="分享棋盘"
disabled={!layoutReady}
onPress={shareBoard}
/>
</>
);
}
collapsable={false} 用于保留截图目标对应的原生视图,onLayout 用于确认目标已有有效尺寸。项目文档也建议从这两项入手排查。截图故障排查
原代码中,.then() 没有返回 Share.open() 的 Promise,导致后面的 .catch() 无法捕获分享阶段的异步错误。示例使用 await Share.open(),并通过 stage 区分截图失败和分享失败。设置 failOnCancel: false 可以避免将用户取消分享视为失败。Share.open 文档
隐藏弹层和特殊棋盘的注意事项
原代码先执行 this._gameOverView.hide(),再等待一秒截图。固定等待一秒,并不能确认隐藏动画、图片加载或棋盘绘制已经完成。
如果截图不需要包含结算弹层,可以把弹层放在截图容器之外。如果必须等弹层隐藏后再截图,应使用弹层组件提供的关闭完成回调,具体名称取决于组件。首次 onLayout 也不代表图片加载或动画已经完成,这些内容需要分别等待对应的完成事件。
升级后仍有问题,可按以下顺序检查:
- 先对只有
View和Text的简单容器截图,确认原生模块能正常工作。 - 检查 ref 是否指向棋盘外层的原生
View,而非未转发 ref 的自定义组件。 - 检查隐藏弹层时是否同时卸载、替换或隐藏了棋盘。
- 如果棋盘使用 GL、
SurfaceView、视频等特殊渲染内容,检查对应组件是否支持截图。普通视图截图成功,也不能保证这些内容能被完整捕获。组件支持与截图限制
备注:内容仅供参考。
