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.3,main入口为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 依赖) |
| Android | RNViewShotModule.java、ViewShot.java |
| iOS | RNViewShot.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)时截图,面向极特殊场景;
- 不定义(默认):不自动截图,需用 ref 自己调用
onCapture:定义了captureMode时,捕获成功回调,参数为截图结果 URI;onCaptureFailure:定义了captureMode时,捕获失败回调。
源码印证:组件内部到底做了什么
ViewShot 类实现揭示了几个 README 未展开的关键机制:
- 首次布局等待。组件内部维护
firstLayoutPromise,capture()会先等第一次onLayout事件再执行捕获(L212-L218)。这正是 FAQ 中「可以用ViewShot组件自动等待首次onLayout,避免The content size must not be zero报错」的原理。 - 临时文件自动释放。每次捕获成功后,
onCapture会把上一次捕获的 URI 用 500ms 延迟调用releaseCapture释放(L230-L239),这就是 README 所说的「ViewShot组件在你多次捕获时会用它,对 continuous capture 防止文件泄漏」。 collapsable={false}自动设置。render()返回的View固定带collapsable={false}(L295-L307),直接规避了 FAQ 中 Android 端Trying to resolve view with tag '{tagID}' which doesn't exist的坑。captureMode的防呆检查。开发模式下checkCompatibleProps(L178-L196)会警告两类误用:定义了captureMode却漏传onCapture;continuous/update模式下options.result不是tmpfile(持续截图用 tmpfile 可配合释放机制避免 base64 大字符串在 bridge 上堆积)。- continuous 模式的节流逻辑。
syncCaptureLoop用requestAnimationFrame循环,且只有当上一次捕获结果已经返回(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 类型补充):
| 选项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
fileName | string | - | (仅 Android)输出文件名,至少 3 个字符 |
width/height | number | - | 最终图片尺寸(从 View 边界缩放;想要原始像素尺寸就不要传) |
format | string | png | png/jpg/webm(Android)/raw(Android,ARGB 像素数组) |
quality | number | 1 | 0.0–1.0,仅对有损格式(jpg)有效 |
result | string | tmpfile | "tmpfile"(默认,临时文件,仅应用运行期间存在)、"base64"(裸 base64 字符串,仅小图,避免 bridge 卡顿;注意不是 data uri)、"data-uri"(base64 加 Data URI scheme 头)、"zip-base64"(Android,zip/deflate 压缩后再 base64) |
snapshotContentContainer | bool | false | 为 true 且 view 是 ScrollView 时,按 content container 高度而非容器高度计算 |
handleGLSurfaceViewOnAndroid | bool | false | Android 上捕获 SurfaceView/GL 视图;默认 false 因性能影响显著 |
useRenderInContext | bool | false | (仅 iOS)改用renderInContext代替drawViewHierarchyInRect,部分场景更可用 |
JS 层 validateOptions 会对 options 做校验与强制纠正:非法width/height(非正数)被删除、quality越界回退为 1、非法format/result回退默认值,并在__DEV__下以console.warn逐条提示。平台差异在这里硬编码:webm、raw格式与zip-base64结果只在 Android 被接受(L26-L32)。
原生桥接与三端调用链
JS 侧 captureRef 先做 ref 解包(支持{ current }对象或组件实例),再用findNodeHandle解析出数字 tag,然后调用 TurboModuleRNViewShot.captureRef(tag, options)。桥接规格定义在 NativeRNViewShot.ts:captureRef、captureScreen返回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):基于 释放之前捕获的 URI:对 该方法以原生硬件级截屏方式捕获当前屏幕显示内容,不需要 ref,也不作用于视图层——因此 ScrollView 只截到当前可见部分,无法整屏展开。options 与 源码上这是一个「tag = -1 的 captureRef」:Android 端 captureScreen 直接转调 快照不保证像素级完美,且行为随平台不同。以下是 README 列出的差异与规避方法(测试机型:iPhone 6 / iOS,Nexus 5 / Android)。 Android 端源码与这张表互相印证: README 指出 profiling 发现三大性能因素:bitmap 内存(反)分配、Base64 输出缓冲(反)分配、PNG/JPG 压缩。对应引入的四类优化在 ViewShot.java 中均有实体实现: 注意: 1. 想保存到文件? 2. 快照 Promise 被 reject?Video / GL 等特殊组件不保证可截。失败时 3. 简单视图却得到黑图/空白/报错?对照上文互操作表:含不支持组件的 View,整体快照都可能被污染。 4. 黑色背景代替透明 / 文字周围出现奇怪边框?优先给被截的 view 设置背景色,避免透明像素带来的怪异边缘。 5. Android 报 6. 报 7. 截图尺寸是宽高选项的 2~3 倍?快照结果以真实像素为尺寸,而 RN style 中的 width/height 单位是 point。可在 options 中显式传 8. Android 捕获 GL 视图?需开启 9. 用 以上能力与限制均可在当前仓库的 模块源码目录、补丁文件 与 README 中逐条对照验证。 【免费下载链接】expoAn open-source framework for making universal native apps with React. Expo runs on Android, iOS, and the web.html2canvas渲染 DOM,tmpfile未实现、会告警并降级返回>import { releaseCapture } from "react-native-view-shot"; releaseCapture(uri);tmpfile结果是真正删文件,对其他result类型是 no-op。README 提醒:tmpfile 截图在应用关闭后会自动清理(Android 的CleanTask与createTempFile前缀过滤、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) );captureRef相同。captureRef((double) -1, ...),tag 为 -1 时取Activity的android.R.id.content视图(ViewShot.java L197-L201);iOS 端同理转调captureRef:[-1]并取keyWindow(RNViewShot.mm L29-L34)。Web 端则是把document.body当普通视图处理。六、平台互操作性表(Interoperability Table)
系统 iOS Android Windows View, Text, Image, .. YES YES YES WebView YES YES1 YES gl-react v2 YES NO2 NO3 react-native-video NO NO NO react-native-maps YES NO4 NO3 react-native-svg YES YES maybe? react-native-camera NO YES NO3 <View collapsable={false}>父级包一层再对该父级截图。TextureView子视图会被主动遍历并通过getBitmap+ 变换矩阵合成回主 Canvas(ViewShot.java L380-L398),而SurfaceView只有在handleGLSurfaceViewOnAndroid为 true 时才走PixelCopy(API 24+)或旧版getDrawingCache路径(L399-L427)——这解释了为何 GL 类组件在 Android 上「不报错但截到空图」。七、性能优化:RAW 格式与 zip-base64
getBitmapForScreenshot在一个WeakHashMap支撑的集合里按宽高精确匹配复用 Bitmap,找不到才createBitmap(L499-L563);outputBuffer+ReusableByteArrayOutputStream,预分配 64KB(PREALLOCATE_SIZE),asBuffer(size)直接ByteBuffer.wrap内部数组,避免内存拷贝(L566-L627);copyPixelsToBuffer写出 ARGB 数组,完全绕过压缩(L438-L442);zip-base64用Deflater先压缩再 base64,比Bitmap.compress快。RAW Images
format: "raw"对应一个 ARGB 像素数组,优势是不压缩、极快(README 给出的实测口径:截图本身小于 16ms)。RAW 支持zip-base64、base64、tmpfile三种 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 原文全量整理)
captureRef的 Promise 会 reject(库本身不会崩溃)。对应源码中统一的错误码E_UNABLE_TO_SNAPSHOT(ViewShot.java L68)。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)。The content size must not be zero or negative.?不要「即时」截图:至少等第一次onLayout或加超时,否则 View 尚未就绪(有Image时等其onLoad也安全)。ViewShot组件会自动等待首次onLayout。该报错文本可在 iOS 端 RNViewShot.mm L98 找到出处;Android 端对w <= 0 || h <= 0抛Impossible to snapshot the view: view is invalid(ViewShot.java L348-L350)。width/height强制缩放(Android 端用Bitmap.createScaledBitmap实现,ViewShot.java L430-L435),可能影响清晰度。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;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;data-uri/base64,Android 大图高吞吐场景用raw+zip-base64,在 Node 侧解压重组 PNG;captureScreen()(仅 Android/iOS 原生语义,Web 退化为document.body渲染)。项目地址: https://gitcode.com/GitHub_Trending/ex/expo
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考