使用共享对象
学习如何使用 Expo 模块 API 中的共享对象。
共享对象允许你将 Android 和 iOS 的长期存在的原生实例暴露给你应用的 JavaScript/TypeScript,而无需控制它们的生命周期。它们可以用于保持大型状态对象的存活,例如已解码的位图,而不是每次组件挂载时都创建一个新的原生实例。
🌐 Shared objects let you expose long-lived native instances from Android and iOS to your app's JavaScript/TypeScript without giving control of their lifecycle. They can be used to keep heavy state objects, such as a decoded bitmap, alive across React components, rather than spinning up a new native instance every time a component mounts.
在本指南中,让我们了解什么是共享对象以及它们如何在原生平台上实现。
🌐 In this guide, let's understand what a shared object is and how they are implemented in native platforms.
什么是共享对象?
🌐 What is a shared object?
共享对象是一种自定义类,它通过 Expo 模块将来自 Android 和/或 iOS 的原生实例桥接到你的应用的 JavaScript/TypeScript 代码。在 Kotlin 和 Swift 的原生端,通过继承 SharedObject 来声明该类,并在模块定义中使用 Class() 将其暴露。共享对象在 JavaScript 和原生端都不再持有引用时会自动被释放。
🌐 A shared object is a custom class that bridges a native instance from Android and/or iOS to your app's JavaScript/TypeScript code through an Expo module. On the native side in Kotlin and Swift, you declare the class by inheriting from SharedObject and expose it in your module definition using Class(). A shared object is deallocated automatically once neither JavaScript nor native holds a reference.
为什么使用共享对象?
🌐 Why use shared objects?
大型媒体资源,例如图片,在解码到内存后可能会超过几兆字节。如果没有使用共享对象,在应用的不同部分之间传递这些资源会迫使每个部分每次都从磁盘重新加载并多次解码相同的文件。这可能会增加内存压力,造成 I/O 瓶颈,掉帧,或导致电量消耗增加。
🌐 Large media assets, like images, can exceed several megabytes once decoded into memory. Without shared objects, passing these assets between different parts of your app forces each part to reload from the disk every time and decode the same file multiple times. This can increase memory pressure, create an I/O bottleneck, drop frames, or cause battery drain.
共享对象通过在内存中保持一个原生实例的存活,同时让多个 JavaScript 引用指向它,从而解决了这个问题。
🌐 Shared objects solve this by keeping a single native instance alive in memory while multiple JavaScript references point to it.
示例:无需磁盘 I/O 的图片操作
🌐 Example: Image manipulation without disk I/O
要理解共享对象,可以考虑这样一个例子:你需要旋转并翻转应用用户选择的图片,然后在对图片进行处理后在你的应用中显示它。
🌐 To understand shared objects, consider an example where you need to rotate and flip an image picked by the app user and then display that image in your app after manipulating it.
没有共享对象
🌐 Without shared objects
从历史上看,本地模块通常以无状态的方式编写,每个函数独立操作,调用之间不维护状态。如果你想对同一个对象(例如图片文件)执行两个独立操作,你需要在两个地方从磁盘加载该对象,并每次重复进行 I/O 操作。
🌐 Historically, native modules were often written in a stateless way, where each function operated independently without maintaining state between calls. If you wanted to perform two separate operations on the same object (such as an image file), you would load it from disk in both places and repeat the I/O operation each time.
如果没有共享对象,ImagePicker 会从诸如 "file:///path/to/image.jpg" 的文件 URI 中读取内容,并将图片解码到内存中。图片处理模块随后读取相同的 URI 并再次将图片解码到内存中。当应用用户调用一个变换方法(例如 rotate())来旋转图片时,模块会将旋转后的图片保存到一个新文件中。最后,当将新 URI 传递给 Image 组件时,它会再次从磁盘解码图片以呈现图片。这个工作流程会导致两次或更多次的解码和磁盘读取操作。
🌐 Without shared objects, ImagePicker reads from a file URI such as "file:///path/to/image.jpg" and decodes the image into memory. The image manipulator module then reads the same URI and decodes the image into memory again. When the app user calls a transform method (for example, rotate()) to rotate the image, the module saves the rotated image to a new file. Finally, when the new URI is passed to the Image component, it decodes the image from disk again to render the image. This workflow results in two or more decode and disk read operations.
使用共享对象
🌐 With shared objects
使用共享对象时,相同的场景会更加高效。ImagePicker 读取 URI 并将图片解码为共享对象一次。当应用用户调用变换方法(例如 rotate())时,模块会操作内存中的位图而不写入磁盘。如果需要文件输出,可以调用显式的保存函数(例如图片处理器中的 saveAsync),否则变换只会保留在内存中。
🌐 The same scenario becomes much more efficient with shared objects. ImagePicker reads the URI and decodes the image once into a shared object. When the app user calls a transform method (for example, rotate()), the module manipulates the in-memory bitmap without writing to the disk. If you need a file output, call an explicit save function (for example, saveAsync in an image manipulator), otherwise the transforms stay in memory only.
最后,共享对象被传递给 Image 组件,这次图片是从内存中渲染的。整个工作流只需要一次磁盘读取和一次解码操作,所有转换都在内存中完成。
🌐 Finally, the shared object is passed to the Image component and this time the image is rendered from memory. The entire workflow requires just one disk read and one decode operation, with all transformations happening in memory.
性能提升显著。通过消除多余的磁盘I/O和解码操作,内存中只保留一个位图,而不是多个副本。这可以降低CPU使用率,有助于延长电池寿命,并降低因内存压力导致崩溃的风险。
🌐 The performance gains are significant. By eliminating redundant disk I/O and decode operations, you keep only a single bitmap in memory instead of multiple copies. This reduces CPU usage, which helps preserve battery life, and lowers the risk of crashes from memory pressure.
共享对象还可以解锁更方便的面向对象的 API 结构。你可以在一个长时间存在的实例(例如 rotate()、flipX()、renderAsync())上暴露方法,让调用者在这个有状态的对象上进行链式操作,而不是暴露一组无状态的函数。
🌐 Shared objects also unlock a more convenient, object-oriented API shape. You can expose methods on a long-lived instance (for example, rotate(), flipX(), renderAsync()) and let callers chain operations on that stateful object, instead of exposing a flat set of stateless functions.
使用共享对象的实现
🌐 Implementation with shared objects
既然你已经明白了共享对象的用途,让我们来看一个最小实现,该实现演示了前一个示例的核心概念。
🌐 Now that you understand why shared objects are useful, let's look into a minimal implementation that demonstrates the core concepts of the previous example.
这个示例创建了一个简单的图片处理模块,该模块从文件路径加载图片,在内存中应用变换(旋转和翻转),并提供一个其他模块可以使用的共享引用。
🌐 The example creates a simple image manipulation module that loads an image from a file path, applies transforms (rotate and flip) in memory, and exposes a shared reference that other modules can consume.
安卓实现
🌐 Android implementation
在 Android 中,你可以从 expo.modules.kotlin.sharedobjects.SharedObject 提供的 SharedObject 类创建一个共享对象。该类管理解码后的位图,并提供操作它的方法。实现方式是只在内存中保留当前图片,并在原地应用变换,因此只有在旋转或翻转等变换生成新位图时,才会分配新的位图:
🌐 In Android, you create a shared object from SharedObject class provided by expo.modules.kotlin.sharedobjects.SharedObject. This class manages the decoded bitmap and exposes methods to manipulate it. The implementation keeps only the current image in memory and applies transforms in place, so you allocate a new bitmap only when a transformation like rotation or flip produces one:
import android.graphics.Bitmap import android.graphics.BitmapFactory import android.graphics.Matrix import expo.modules.kotlin.modules.Module import expo.modules.kotlin.modules.ModuleDefinition import expo.modules.kotlin.sharedobjects.SharedObject class ImageRef : SharedRef<Bitmap>() class SimpleImageContext( runtimeContext: RuntimeContext, bitmap: Bitmap ) : SharedObject(runtimeContext) { private var current: Bitmap = bitmap fun rotate(degrees: Float) = apply { val matrix = Matrix().apply { postRotate(degrees) } current = Bitmap.createBitmap(current, 0, 0, current.width, current.height, matrix, true) } fun flipX() = apply { val matrix = Matrix().apply { preScale(-1f, 1f) } current = Bitmap.createBitmap(current, 0, 0, current.width, current.height, matrix, true) } fun render(): ImageRef = ImageRef(current, runtimeContext) override fun sharedObjectDidRelease() { if (!current.isRecycled) current.recycle() } }
上面的示例与 iOS 的实现非常相似。然而,在 Android 上有一个不同之处:sharedObjectDidRelease() 方法。这个生命周期回调在 JavaScript 释放对共享对象的所有引用时被调用,提供了清理原生资源的机会。
🌐 The above example is quite similar to the iOS implementation. However, there is one difference on Android: the sharedObjectDidRelease() method. This lifecycle callback is invoked when JavaScript releases all references to the shared object, providing an opportunity to clean up native resources.
当这个类的结果传递给另一个模块时,render 方法会返回一个 ImageRef,它是一种专用的 SharedRef<Bitmap> 类型,expo-image 和其他能够识别图片的模块已经可以理解这种类型。
🌐 When the result of this class is passed to another module, the render method returns an ImageRef, which is a specialized SharedRef<Bitmap> type that expo-image and other image-aware modules already understand.
该模块定义公开了一个异步函数用于创建上下文,以及一个类定义用于绑定方法。Expo 模块 API 使用声明式语法,你可以在其中指定模块名称、创建实例的函数,以及将方法映射到共享对象的类定义:
🌐 The module definition exposes an async function to create the context and a class definition to bind methods. The Expo Modules API uses a declarative syntax where you specify the module name, functions to create instances, and a class definition that maps methods to the shared object:
import android.graphics.Bitmap import android.graphics.BitmapFactory import expo.modules.kotlin.modules.Module import expo.modules.kotlin.modules.ModuleDefinition class SimpleImageModule : Module() { override fun definition() = ModuleDefinition { Name("SimpleImageModule") AsyncFunction("createContextAsync") { path: String -> val bitmap = BitmapFactory.decodeFile(path) ?: throw Exceptions.IllegalArgument("Unable to decode image at $path") SimpleImageContext(runtimeContext, bitmap) } Class<SimpleImageContext>("Context") { Function("rotate") { ctx: SimpleImageContext, degrees: Float -> ctx.rotate(degrees) } Function("flipX") { ctx: SimpleImageContext -> ctx.flipX() } AsyncFunction("renderAsync") Coroutine { ctx: SimpleImageContext -> ctx.render() } } } }
在上面的例子中,createContextAsync 函数从文件路径解码位图并返回一个新的 SimpleImageContext 实例。一旦上下文存在,rotate 和 flipX 函数会同步运行,因为它们仅在内存中操作。renderAsync 函数被标记为异步,以表示它可能涉及复制或为其他模块使用准备位图。
🌐 In the above example, createContextAsync function decodes the bitmap from the file path and returns a new SimpleImageContext instance. Once the context exists, the rotate and flipX functions run synchronously because they only manipulate in memory. The renderAsync function is marked async to signal that it might involve copying or preparing the bitmap for consumption by other modules.
iOS 实现
🌐 iOS implementation
在 iOS 中,你可以通过继承 ExpoModulesCore 提供的 SharedObject 类来创建一个共享对象。该类管理解码后的位图,并提供操作它的方法。实现只在内存中保留当前图片,并对其原地应用变换:
🌐 In iOS, you create a shared object by inheriting from SharedObject class provided by ExpoModulesCore. This class manages the decoded bitmap and exposes methods to manipulate it. The implementation keeps only the current image in memory and applies transforms in place:
import ExpoModulesCore import UIKit final class ImageRef: SharedRef<UIImage> {} final class SimpleImageContext: SharedObject { private var current: UIImage init(path: String) throws { guard let data = try? Data(contentsOf: URL(fileURLWithPath: path)), let image = UIImage(data: data) else { throw Exceptions.InvalidArgument() } self.current = image super.init() } func rotate(by degrees: Double) { current = current.rotated(degrees: degrees) } func flipX() { current = current.withHorizontallyFlippedOrientation() } func render() -> ImageRef { return ImageRef(current) } }
在上面的例子中,SimpleImageContext 读取一个图片文件并在内存中保留一个 UIImage。rotate 和 flipX 方法会修改内存中的当前图片,而不会接触磁盘。
🌐 In the above example, SimpleImageContext reads an image file and keeps a single UIImage in memory. The rotate and flipX methods mutate the current image in memory without touching the disk.
当这个类的结果传递给另一个模块时,render 方法会返回一个 ImageRef,它是一种专用的 SharedRef<UIImage> 类型,expo-image 和其他能够识别图片的模块已经可以理解这种类型。
🌐 When the result of this class is passed to another module, the render method returns an ImageRef, which is a specialized SharedRef<UIImage> type that expo-image and other image-aware modules already understand.
现在,通过共享对象类定义,你可以通过模块定义来暴露它。Expo 模块 API 使用声明式语法,你可以在其中指定模块名称、创建实例的函数,以及将方法映射到共享对象的类定义:
🌐 Now, with the shared object class definition, you can expose it through your module definition. The Expo Modules API uses a declarative syntax where you specify the module name, functions to create instances, and a class definition that maps methods to the shared object:
public final class SimpleImageModule: Module { public func definition() -> ModuleDefinition { Name("SimpleImageModule") AsyncFunction("createContextAsync") { (path: String) -> SimpleImageContext in return try SimpleImageContext(path: path) } Class("Context", SimpleImageContext.self) { Function("rotate") { (ctx, degrees: Double) -> SimpleImageContext in ctx.rotate(by: degrees) return ctx } Function("flipX") { (ctx: SimpleImageContext) -> SimpleImageContext in ctx.flipX() return ctx } AsyncFunction("renderAsync") { (ctx: SimpleImageContext) -> ImageRef in return ctx.render() } } } }
在上述示例中,createContextAsync 是一个异步函数,因为从磁盘加载和解码图片是一个 I/O 操作。一旦上下文存在,rotate 和 flipX 函数会同步运行,因为它们只操作内存中的数据。renderAsync 函数被标记为异步,以表示它可能涉及复制或准备位图以供其他模块使用,尽管在这个简单的示例中,它会立即返回。
🌐 In the above example, the createContextAsync is an asynchronous function because loading and decoding an image from disk is an I/O operation. Once the context exists, the rotate and flipX functions run synchronously because they only manipulate in memory. The renderAsync function is marked async to signal that it might involve copying or preparing the bitmap for consumption by other modules, though in this simple example, it returns immediately.
在你的应用中使用共享对象
🌐 Using a shared object in your app
你现在可以在应用的 JavaScript/TypeScript 代码中使用共享对象,从路径加载图片,创建已加载图片的上下文,链式应用内存中的变换,渲染以获取共享引用,然后将该引用传递给 Image 组件:
🌐 You can now use the shared object in your app's JavaScript/TypeScript code to load the image from the path, create a context of the loaded image, chain in-memory transforms, render to get a shared reference, and then pass that reference to an Image component:
import { useState } from 'react'; import { Button } from 'react-native'; import { Image } from 'expo-image'; import type { SharedRef } from 'expo'; import SimpleImageModule from 'simple-image-module'; // The native custom module import { pickImageAsync } from './pickImage'; // The custom TypeScript function export function SharedImageExample() { const [context, setContext] = useState(null); const [result, setResult] = useState<SharedRef<'image'> | null>(null); const load = async () => { const uri = await pickImageAsync(); if (!uri) { return; } const ctx = await SimpleImageModule.createContextAsync(uri); setContext(ctx); setResult(await ctx.renderAsync()); }; const rotateAndFlip = async () => { if (!context) { return; } setResult(await context.rotate(90).flipX().renderAsync()); }; return ( <> <Button title="选择图片" onPress={load} /> <Button title="旋转90° + X翻转" onPress={rotateAndFlip} disabled={!context} /> {result && <Image source={result} style={{ width: 200, height: 200 }} />} </> ); }
在上述示例中,React 组件只使用由图片操作器上下文转换过的原生图片,该图片已存在于内存中,并由共享对象(ImageRef)引用。因此,图片视图可以在下一个帧上立即显示该图片,而链式变换从不接触文件系统。
🌐 In the above example, the React component only consumes the native image transformed by the image manipulator context, which is already in memory and referenced by the shared object (ImageRef). As a result, the image view can display the image immediately on the next frame, and chained transforms never touch the file system.
JavaScript API 使用 ImagePicker 选择图片,该方法返回一个标准文件 URI。这个 URI 会被传递给自定义原生模块,以在 SharedImageExample() 中创建一个共享对象:
🌐 The JavaScript API picks an image using ImagePicker, which returns a standard file URI. This URI is handed to a custom native module to create a shared object in SharedImageExample():
import * as ImagePicker from 'expo-image-picker'; export async function pickImageAsync() { const result = await ImagePicker.launchImageLibraryAsync({ quality: 1, allowsMultipleSelection: false, }); if (result.canceled || !result.assets?.length) { return null; } // At this point we still have a disk URI. // The native module will lift it into a shared object. return result.assets[0].uri; }
在上述示例中,ImagePicker 不需要了解共享对象。它返回它应该返回的内容,即文件路径。你的本地模块负责将该路径转换为其他 Expo 模块可以使用的共享对象,例如来自 expo-image 的 Image。
🌐 In the above example, ImagePicker doesn't need to know about shared objects. It returns what it should, which is a file path. Your native module is responsible for transforming that path into a shared object that other Expo modules can work with, such as Image from expo-image.
使用共享对象的 Expo 库
🌐 Expo libraries that use shared objects
一些使用共享对象的 Expo SDK 库 示例及其用途:
🌐 Some examples of Expo SDK libraries that use shared objects and their purpose:
expo-image库使用SharedObject来保持解码操作的活性,视图组件在 Android 上接受SharedRef<Bitmap>,在 iOS 上接受SharedRef<UIImage>。这种设计允许图片在模块之间传递而无需再次解码。想了解更多,请查看expo-image库的源代码:Android 和 iOS。expo-image-manipulator库展示了如何处理异步操作、排队多个操作以及提供简洁的 JavaScript API。要了解更多,请查看expo-image-manipulator库的源代码:Android 和 iOS。expo-sqlite库使用共享对象来在调用之间保持数据库、会话和语句句柄,同时协调对底层数据库的访问。想要了解更多,请查看expo-sqlite库的 Android 和 iOS 源代码。expo/fetch库使用共享对象来保持请求和响应的生命周期,以支持流式传输、取消和重定向处理,同时提供与 JavaScript fetch 兼容的 API。要了解更多,请查看expo/fetch库的源代码:Android 和 iOS。
共享对象的性能优势
🌐 Performance benefits of shared objects
使用共享对象可以带来多种性能提升,例如:
🌐 Using shared objects provides several performance improvements, such as:
- 减少磁盘输入/输出: 通过一次读取操作,而不是跨不同模块或函数调用进行多次读取
- 更少的解码操作: 昂贵的解码(例如 JPEG/PNG 转位图)只进行一次,而不是反复进行
- 降低内存压力: 内存中只存在一个解码实例,而不是多个副本
- 更快的操作: 内存中的转换比基于磁盘的转换快得多
- 避免掉帧: 减少 I/O 阻塞可以让界面交互更加流畅
其他资源
🌐 Additional resources
共享对象解决了许多与 Expo APIs 相关的基本问题,同时还开启了一种全新的面向对象 API 设计方式。