PAYATHON 2026

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 也不代表图片加载或动画已经完成,这些内容需要分别等待对应的完成事件。

升级后仍有问题,可按以下顺序检查:

  1. 先对只有 View 和 Text 的简单容器截图,确认原生模块能正常工作。
  2. 检查 ref 是否指向棋盘外层的原生 View,而非未转发 ref 的自定义组件。
  3. 检查隐藏弹层时是否同时卸载、替换或隐藏了棋盘。
  4. 如果棋盘使用 GL、SurfaceView、视频等特殊渲染内容,检查对应组件是否支持截图。普通视图截图成功,也不能保证这些内容能被完整捕获。组件支持与截图限制

备注:内容仅供参考。