Expo 仓库内 react-native-view-shot 深度解析:从 ViewShot 组件到 RAW/zip-base64 高性能视图截图
2026/9/7 23:28:21 网站建设 项目流程

Expo 仓库内 react-native-view-shot 深度解析:从 ViewShot 组件到 RAW/zip-base64 高性能视图截图

【免费下载链接】expoAn open-source framework for making universal native apps with React. Expo runs on Android, iOS, and the web.项目地址: https://gitcode.com/GitHub_Trending/ex/expo

本篇基于 Expo 仓库中随 Expo Go 一起维护的react-native-view-shot模块(README)展开,系统讲解该库「把 React Native 视图栅格化为图片」的完整 API 体系——ViewShot组件、captureRef命令式 API、captureScreen全屏截图,以及raw+zip-base64高性能截图链路,并结合 Android/iOS/Web 三端源码印证各参数的真实行为,帮助你在 Expo 项目中可靠、高性能地完成视图截图、导出与分享。

一、模块定位与安装

react-native-view-shot的核心能力只有一句话:Capture a React Native view to an image(把一个 React Native 视图捕获成图片)。在 Expo 仓库中,它以源码模块的形式内嵌在 Expo Go 应用的modules目录下,其 package.json 显示版本为4.0.3main入口为src/index.js,并声明了codegenConfig(Android Java 包名fr.greweb.reactnativeviewshot),说明该模块在新架构下通过 Codegen 生成 TurboModule 桩代码。

在独立项目中安装方式如下(README 原文):

yarn add react-native-view-shot # In Expo expo install react-native-view-shot

安装后的原生链接要点(README 原文要求):

  • 在 Xcode 中确认react-native-view-shot已正确 link(必要时手动安装);
  • 0.60.x 之前需要执行react-native link react-native-view-shot
  • 0.60.x 之后依赖 autolink,iOS 端需要执行npx pod-install安装 CocoaPods 依赖。

从仓库结构看,模块包含完整的三端实现:

平台实现文件
JS 主入口src/index.js
JS 原生桥接src/RNViewShot.js、src/specs/NativeRNViewShot.ts
Web 端src/RNViewShot.web.js(基于html2canvas,见 package.json 依赖)
AndroidRNViewShotModule.java、ViewShot.java
iOSRNViewShot.mm
类型定义src/index.d.ts

此外,Expo 仓库在根目录维护了一个针对性补丁 patches/react-native-view-shot.patch:把 iOS 端snapshotContentContainer的类型检查从RCTScrollView放宽为UIScrollView,并直接把该UIView作为绘制目标——这解释了为什么新版文档中该选项在 iOS 上对任意UIScrollView实例生效,而不局限于 RN 的ScrollView包装类。

二、高层 API:ViewShot组件

README 给出的四种典型用法:挂载后手动触发、挂载即捕获、等待图片加载完成、捕获 ScrollView 内容。

import ViewShot from "react-native-view-shot"; function ExampleCaptureOnMountManually { const ref = useRef(); useEffect(() => { // on mount ref.current.capture().then(uri => { console.log("do something with ", uri); }); }, []); return ( <ViewShot ref={ref} options={{ fileName: "Your-File-Name", format: "jpg", quality: 0.9 }}> <Text>...Something to rasterize...</Text> </ViewShot> ); } // alternative function ExampleCaptureOnMountSimpler { const ref = useRef(); const onCapture = useCallback(uri => { console.log("do something with ", uri); }, []); return ( <ViewShot onCapture={onCapture} captureMode="mount"> <Text>...Something to rasterize...</Text> </ViewShot> ); } // waiting an image function ExampleWaitingCapture { const ref = useRef(); const onImageLoad = useCallback(() => { ref.current.capture().then(uri => { console.log("do something with ", uri); }) }, []); return ( <ViewShot ref={ref}> <Text>...Something to rasterize...</Text> <Image ... onLoad={onImageLoad} /> </ViewShot> ); } // capture ScrollView content // NB: you may need to go the "imperative way" to use snapshotContentContainer with the scrollview ref instead function ExampleCaptureOnMountSimpler { const ref = useRef(); const onCapture = useCallback(uri => { console.log("do something with ", uri); }, []); return ( <ScrollView> <ViewShot onCapture={onCapture} captureMode="mount"> <Text>...The Scroll View Content Goes Here...</Text> </ViewShot> </ScrollView> ); }

Props(README 原文):

  • children:要栅格化的实际内容;
  • options:与captureRef方法相同的 options;
  • captureMode(string):
    • 不定义(默认):不自动截图,需用 ref 自己调用capture()
    • "mount":挂载时捕获一次。注意它不会等待图片加载——若内容有Image,应使用默认模式(无 captureMode),在Image#onLoad后手动调用viewShotRef.capture()
    • "continuous"(EXPERIMENTAL):持续不断地大量截图,面向极特殊场景;
    • "update"(EXPERIMENTAL):每次 React 重绘(on did update)时截图,面向极特殊场景;
  • onCapture:定义了captureMode时,捕获成功回调,参数为截图结果 URI;
  • onCaptureFailure:定义了captureMode时,捕获失败回调。

源码印证:组件内部到底做了什么

ViewShot 类实现揭示了几个 README 未展开的关键机制:

  1. 首次布局等待。组件内部维护firstLayoutPromisecapture()会先等第一次onLayout事件再执行捕获(L212-L218)。这正是 FAQ 中「可以用ViewShot组件自动等待首次onLayout,避免The content size must not be zero报错」的原理。
  2. 临时文件自动释放。每次捕获成功后,onCapture会把上一次捕获的 URI 用 500ms 延迟调用releaseCapture释放(L230-L239),这就是 README 所说的「ViewShot组件在你多次捕获时会用它,对 continuous capture 防止文件泄漏」。
  3. collapsable={false}自动设置render()返回的View固定带collapsable={false}(L295-L307),直接规避了 FAQ 中 Android 端Trying to resolve view with tag '{tagID}' which doesn't exist的坑。
  4. captureMode的防呆检查。开发模式下checkCompatibleProps(L178-L196)会警告两类误用:定义了captureMode却漏传onCapturecontinuous/update模式下options.result不是tmpfile(持续截图用 tmpfile 可配合释放机制避免 base64 大字符串在 bridge 上堆积)。
  5. continuous 模式的节流逻辑syncCaptureLooprequestAnimationFrame循环,且只有当上一次捕获结果已经返回(lastCapturedURI变化)才发起下一次捕获(L247-L259),天然形成「上一帧未结束则不抢跑」的背压控制。

另外,capture()在组件卸载后会返回一个永不 resolve 的neverEndingPromise(L8、L216),即「组件卸载后你永远不会收到回调」,避免悬空 Promise。

三、captureRef(view, options):低层命令式 API

import { captureRef } from "react-native-view-shot"; captureRef(viewRef, { format: "jpg", quality: 0.8, }).then( (uri) => console.log("Image saved to", uri), (error) => console.error("Oops, snapshot failed", error) );

返回图片 URI 的 Promise。view是 React Native 组件的 ref;options完整清单(README 原文 + index.d.ts 类型补充):

选项类型默认值说明
fileNamestring-(仅 Android)输出文件名,至少 3 个字符
width/heightnumber-最终图片尺寸(从 View 边界缩放;想要原始像素尺寸就不要传)
formatstringpngpng/jpg/webm(Android)/raw(Android,ARGB 像素数组)
qualitynumber10.0–1.0,仅对有损格式(jpg)有效
resultstringtmpfile"tmpfile"(默认,临时文件,仅应用运行期间存在)、"base64"(裸 base64 字符串,仅小图,避免 bridge 卡顿;注意不是 data uri)、"data-uri"(base64 加 Data URI scheme 头)、"zip-base64"(Android,zip/deflate 压缩后再 base64)
snapshotContentContainerboolfalse为 true 且 view 是 ScrollView 时,按 content container 高度而非容器高度计算
handleGLSurfaceViewOnAndroidboolfalseAndroid 上捕获 SurfaceView/GL 视图;默认 false 因性能影响显著
useRenderInContextboolfalse(仅 iOS)改用renderInContext代替drawViewHierarchyInRect,部分场景更可用

JS 层 validateOptions 会对 options 做校验与强制纠正:非法width/height(非正数)被删除、quality越界回退为 1、非法format/result回退默认值,并在__DEV__下以console.warn逐条提示。平台差异在这里硬编码:webmraw格式与zip-base64结果只在 Android 被接受(L26-L32)。

原生桥接与三端调用链

JS 侧 captureRef 先做 ref 解包(支持{ current }对象或组件实例),再用findNodeHandle解析出数字 tag,然后调用 TurboModuleRNViewShot.captureRef(tag, options)。桥接规格定义在 NativeRNViewShot.ts:captureRefcaptureScreen返回Promise<string>releaseCapture同步返回。

Android 侧(RNViewShotModule.java):

  • captureRef通过 Fabric 的FabricUIManager.addUIBlock把实际绘制投递到 UI 线程(L102-L103),ViewShot类实现UIBlock接口;
  • tmpfile的临时文件由 createTempFile 创建:在内部/外部 cache 目录中选剩余空间更大的一方落盘,默认前缀ReactNative-snapshot-image,传入fileName时则以其为前缀;
  • 模块invalidate()时触发CleanTask清理两个 cache 目录中所有该前缀的残留文件(L122-L160),对应 README「tmpfile 截图在应用关闭后自动清理」;
  • releaseCapture只删除位于 cache 目录内的文件(L54-L64),是一种路径安全约束。

iOS 侧(RNViewShot.mm):

  • 通过uiManager addUIBlock拿到viewRegistry解析 tag(L53-L68);
  • 尺寸小于 0.1 时回退到view.bounds.size(或 ScrollView 的contentSize),仍为 0/负数则拒绝并报错The content size must not be zero or negative(L94-L100)——这正是 FAQ 中该报错的来源;
  • useRenderInContext二选一:renderInContext无法捕获渐变或完整 ScrollView 内容,但适合大视图;drawViewHierarchyInRect在大视图上会静默失败并产出空白图(L114-L122 的注释写得很直白);
  • 图片编码在后台队列执行:jpg 用UIImageJPEGRepresentation(image, quality),其余走 PNG;base64/data-uri/tmpfile(经RCTTempFilePath落到临时目录.../ReactNative/)三条输出路径(L144-L185);
  • releaseCapture仅删除临时目录ReactNative子路径下的文件(L36-L46)。

Web 侧(RNViewShot.web.js):基于html2canvas渲染 DOM,tmpfile未实现、会告警并降级返回>import { releaseCapture } from "react-native-view-shot"; releaseCapture(uri);

释放之前捕获的 URI:对tmpfile结果是真正删文件,对其他result类型是 no-op。README 提醒:tmpfile 截图在应用关闭后会自动清理(Android 的CleanTaskcreateTempFile前缀过滤、iOS 的临时目录机制印证了这一点),一般场景不必手动处理;但continuous这类高频捕获场景下,ViewShot组件已内置「捕获成功后延迟 500ms 释放上一个 URI」的防泄漏逻辑。

五、captureScreen():Android 与 iOS 专属

import { captureScreen } from "react-native-view-shot"; captureScreen({ format: "jpg", quality: 0.8, }).then( (uri) => console.log("Image saved to", uri), (error) => console.error("Oops, snapshot failed", error) );

该方法以原生硬件级截屏方式捕获当前屏幕显示内容,不需要 ref,也不作用于视图层——因此 ScrollView 只截到当前可见部分,无法整屏展开。options 与captureRef相同。

源码上这是一个「tag = -1 的 captureRef」:Android 端 captureScreen 直接转调captureRef((double) -1, ...),tag 为 -1 时取Activityandroid.R.id.content视图(ViewShot.java L197-L201);iOS 端同理转调captureRef:[-1]并取keyWindow(RNViewShot.mm L29-L34)。Web 端则是把document.body当普通视图处理。

六、平台互操作性表(Interoperability Table)

快照不保证像素级完美,且行为随平台不同。以下是 README 列出的差异与规避方法(测试机型:iPhone 6 / iOS,Nexus 5 / Android)。

系统iOSAndroidWindows
View, Text, Image, ..YESYESYES
WebViewYESYES1YES
gl-react v2YESNO2NO3
react-native-videoNONONO
react-native-mapsYESNO4NO3
react-native-svgYESYESmaybe?
react-native-cameraNOYESNO3
  1. 需要用<View collapsable={false}>父级包一层再对该父级截图。
  2. 返回空图(不是 Promise reject)。
  3. 组件本身缺少该平台支持。
  4. 可改用 react-native-maps 自带的 takeSnapshot 能力。

Android 端源码与这张表互相印证:TextureView子视图会被主动遍历并通过getBitmap+ 变换矩阵合成回主 Canvas(ViewShot.java L380-L398),而SurfaceView只有在handleGLSurfaceViewOnAndroid为 true 时才走PixelCopy(API 24+)或旧版getDrawingCache路径(L399-L427)——这解释了为何 GL 类组件在 Android 上「不报错但截到空图」。

七、性能优化:RAW 格式与 zip-base64

README 指出 profiling 发现三大性能因素:bitmap 内存(反)分配、Base64 输出缓冲(反)分配、PNG/JPG 压缩。对应引入的四类优化在 ViewShot.java 中均有实体实现:

  • 可复用 Bitmap 池getBitmapForScreenshot在一个WeakHashMap支撑的集合里按宽高精确匹配复用 Bitmap,找不到才createBitmap(L499-L563);
  • 可复用输出缓冲:静态outputBuffer+ReusableByteArrayOutputStream,预分配 64KB(PREALLOCATE_SIZE),asBuffer(size)直接ByteBuffer.wrap内部数组,避免内存拷贝(L566-L627);
  • RAW 格式:直接copyPixelsToBuffer写出 ARGB 数组,完全绕过压缩(L438-L442);
  • ZIP deflate 压缩zip-base64Deflater先压缩再 base64,比Bitmap.compress快。

RAW Images

format: "raw"对应一个 ARGB 像素数组,优势是不压缩、极快(README 给出的实测口径:截图本身小于 16ms)。RAW 支持zip-base64base64tmpfile三种 result。RAW 磁盘文件内容格式为:${width}:${height}|${base64}(Android 端 saveToRawFileOnDevice 写出的resolution头正是%d:%d|zip-base64/base64路径中仅 RAW 会拼接该头,见 L271-L303)。

zip-base64 与 RAW 的配合用法(README 原文示例)

const fs = require("fs"); const zlib = require("zlib"); const PNG = require("pngjs").PNG; const Buffer = require("buffer").Buffer; const format = Platform.OS === "android" ? "raw" : "png"; const result = Platform.OS === "android" ? "zip-base64" : "base64"; captureRef(this.ref, { result, format }).then((data) => { // expected pattern 'width:height|', example: '1080:1731|' const resolution = /^(\d+):(\d+)\|/g.exec(data); const width = (resolution || ["", 0, 0])[1]; const height = (resolution || ["", 0, 0])[2]; const base64 = data.substr((resolution || [""])[0].length || 0); // convert from base64 to Buffer const buffer = Buffer.from(base64, "base64"); // un-compress data const inflated = zlib.inflateSync(buffer); // compose PNG const png = new PNG({ width, height }); png.data = inflated; const pngData = PNG.sync.write(png); // save composed PNG fs.writeFileSync(output, pngData); });

注意:zlib.inflate打包 PNG 是 CPU 密集型操作,README 建议用process.fork()子进程方式做 raw → PNG 的转换;示例服务端代码还需yarn add pngjs。README 备注该代码已在大型商业项目中验证(此处按原文保留其自述,不作额外引申)。

八、Troubleshooting / FAQ(README 原文全量整理)

1. 想保存到文件?

  • 存到相机相册:使用 react-native-cameraroll;
  • 存到任意文件路径:使用 react-native-fs 之类工具;
  • 更复杂需求可自写原生模块。

2. 快照 Promise 被 reject?Video / GL 等特殊组件不保证可截。失败时captureRef的 Promise 会 reject(库本身不会崩溃)。对应源码中统一的错误码E_UNABLE_TO_SNAPSHOT(ViewShot.java L68)。

3. 简单视图却得到黑图/空白/报错?对照上文互操作表:含不支持组件的 View,整体快照都可能被污染。

4. 黑色背景代替透明 / 文字周围出现奇怪边框?优先给被截的 view 设置背景色,避免透明像素带来的怪异边缘。

5. Android 报Trying to resolve view with tag '{tagID}' which doesn't exist要截取的View 必须collapsable={false},某些内容甚至需要包一层<View collapsable={false}>才可截图,否则该 view 不对应任何原生 UI View。直接改用ViewShot组件即可——其render()固定设置了collapsable={false}(src/index.js L300)。

6. 报The content size must not be zero or negative.不要「即时」截图:至少等第一次onLayout或加超时,否则 View 尚未就绪(有Image时等其onLoad也安全)。ViewShot组件会自动等待首次onLayout。该报错文本可在 iOS 端 RNViewShot.mm L98 找到出处;Android 端对w <= 0 || h <= 0Impossible to snapshot the view: view is invalid(ViewShot.java L348-L350)。

7. 截图尺寸是宽高选项的 2~3 倍?快照结果以真实像素为尺寸,而 RN style 中的 width/height 单位是 point。可在 options 中显式传width/height强制缩放(Android 端用Bitmap.createScaledBitmap实现,ViewShot.java L430-L435),可能影响清晰度。

8. Android 捕获 GL 视图?需开启handleGLSurfaceViewOnAndroid

/** * if true and when view is a SurfaceView or have it in the view tree, view will be captured. * False by default, because it can have signoficant performance impact */ handleGLSurfaceViewOnAndroid?: boolean;

9. 用expo-sharing分享截图结果?tmpfile(默认 result)最适配,调用shareAsync前记得给结果补上file://前缀:

captureRef(viewRef) .then((uri) => Sharing.shareAsync(`file://${uri}`, options))

九、小结:在 Expo 项目中的选型建议

  • 一次性、可控时机截图:优先ViewShot组件 + 手动capture()(等待首次布局、自动释放旧 tmpfile、自动collapsable={false},三个坑全部规避);
  • 整页长图snapshotContentContainer: true(iOS 依赖 Expo 补丁放宽到UIScrollView),或命令式地对 ScrollView ref 调captureRef
  • 需要分享/上传文件:默认tmpfile,配合file://前缀交给expo-sharing
  • 需要字符串直传(Web/桥传场景):小图用data-uri/base64,Android 大图高吞吐场景用raw+zip-base64,在 Node 侧解压重组 PNG;
  • 全屏截图captureScreen()(仅 Android/iOS 原生语义,Web 退化为document.body渲染)。

以上能力与限制均可在当前仓库的 模块源码目录、补丁文件 与 README 中逐条对照验证。

【免费下载链接】expoAn open-source framework for making universal native apps with React. Expo runs on Android, iOS, and the web.项目地址: https://gitcode.com/GitHub_Trending/ex/expo

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询