ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

React Native手写高性能相机插件:从Bridge到原生实现全解析

React Native手写高性能相机插件:从Bridge到原生实现全解析 很多人在React Native项目里做到拍照功能时第一反应是找现成库装上就是一顿猛拍。但一旦遇到特殊需求——自定义取景框、毫秒级连拍、视频流实时帧处理、与原生图像算法打通——第三方库往往就卡壳了。要么阉割功能要么性能稀烂要么没人维护。这篇文章我不打算讲怎么调API直接记录我是如何从零在React Native里用原生模块手搓一个高性能相机插件从通信机制到底层实现再到性能优化全部走一遍附完整代码和踩坑记录。适合谁来读已经写过React Native业务代码但没碰过原生层又想在RN里做深度相机集成的开发者。读完后你至少能搞清楚三件事RN和原生到底是怎么通信的相机的每个核心能力拍照、对焦、切换镜头、帧回调在原生侧怎么实现以及相机这类高频调用场景怎么绕开性能深坑。1. 项目背景与整体设计思路1.1 为什么非要自己做相机插件先说背景。我接到的项目需求本身不复杂做一个巡检类App需要调用相机拍摄设备铭牌但要求照片清晰度能支持OCR识别同时对拍摄速度有硬指标——从点击快门到拿到可上传的图片全流程不超过800毫秒。另外还需要在相机界面上直接叠加一个取景辅助框框内做自动对焦增强。我第一版是直接用react-native-camera当时是基于旧架构的版本集成倒是不难但跑起来问题一堆一是启动取景时白屏时间偏长体感接近1秒二是拍完照后从原生回调到JS侧再转Base64上传内存直接飙升低端安卓机直接闪退三是想要在取景画面里加一个随对焦状态变化的动画框JS侧根本拿不到即时的对焦状态。模块维护也不够活跃遇到问题连个反馈渠道都费劲。说白了第三方库解决的是80%的通用场景剩下20%的边界需求就是自己动手的时机。我当时的决定是用CameraX作为Android侧底层用AVFoundation作为iOS侧底层在上面封装自定义原生模块通过RN Bridge暴露给JS调用。CameraX的好处是它内部已经处理好了生命周期绑定和用例组合比起直接操作Camera2那套繁琐的StateMachine要省心得多AVFoundation在iOS上则是唯一正确选择Apple对相机硬件的封装已经足够合理直接面向AVCaptureSession编程即可。1.2 整体技术选型和架构分层在动手之前我先明确了架构边界。这个插件分三层JS层负责UI渲染、手势交互、调用原生能力、接收回调数据。所有相机渲染不经过JS层取景画面直接由原生View负责JS只放按钮和遮罩。Bridge层封装了Promise通信和EventEmitter事件通道。凡是“一次性返回结果”的操作用Promise例如拍照、切换摄像头“持续流式回调”的操作用事件通道例如对焦状态变化、帧数据回调。原生层Android侧是基于CameraX的CameraManager封装iOS侧是基于AVFoundation的CameraManager封装。每一侧只向外暴露最少量的方法避免JS到原生方法调用链过长。选CameraX还有一个远期考虑CameraX从设计之初就内置了对ProcessCameraProvider的生命周期感知它会自动跟随Activity或Fragment的LifecycleOwner回收资源这对RN的页面路由场景特别关键——页面切换、应用进入后台相机资源不会泄漏。当初Android上相机权限、Surface生命周期、后台释放这三座大山CameraX把前两座基本铲平了。架构上还有一个重要决定取景画面全部在原生层渲染而不是把视频帧送到JS层再通过Image绘制。RN的JS线程和UI线程被Bridge隔开每一帧数据跨线程传输都会增加延迟和内存拷贝取景画面这种每秒钟30次的流量绝不能走JS通道。1.3 功能边界与核心指标拆解我给自己定下的功能范围如下实时取景保持30fps流畅度画面延迟控制在100ms以内拍照输出最大分辨率JPEG质量因子可配置默认在保证OCR可识别的前提下压到最小体积自动对焦/触摸对焦支持返回对焦状态到JS侧支持指定坐标对焦摄像头切换前后摄像头无缝切换切换过程保持取景不中断闪光灯控制支持关闭/开启/自动三态切换曝光补偿支持-2.0到2.0的EV调节。用数字拆完目标再看实现思路就非常清晰了。技术选型都是在这些硬指标倒逼下做出的。下面我从通信原理开始拆把每一步的关键实现和避坑点都讲透。2. 原生模块通信原理与工程初始化2.1 React Native Bridge的三种通信方式RN与原生模块通信本质上是一个跨语言调用的问题。JS侧是JavaScript引擎原生侧是Java/Objective-C它们之间的调用要经过Bridge序列化。理解这一点是后续所有性能优化的前提。最常用的是Promise模式。原生方法执行完后通过Promise把结果返回给JS侧。比如拍照接口原生侧拍完一张图把图片路径返回这个是一次性操作用Promise最合适。Callback模式是老版本的做法需要主动传入回调函数现在已经不推荐用了除非你维护的是老Legacy项目。事件模式则用于持续性的数据下发。原生模块可以把事件主动推送给JS侧JS侧通过NativeEventEmitter进行监听。比如对焦状态变化这种不是由JS主动触发、而是由系统底层产生的事件就需要走事件通道。数据量较大的场景比如帧数据回调也必须走事件通道或者直接落盘而不是通过Promise把整个大对象传过去。2.2 模块注册与初始化流程Android侧继承ReactContextBaseJavaModule实现一个Module类然后在ReactPackage中注册。以我的CameraModule为例public class CameraModule extends ReactContextBaseJavaModule { private final ReactApplicationContext reactContext; public CameraModule(ReactApplicationContext reactContext) { super(reactContext); this.reactContext reactContext; } Override public String getName() { return CameraModule; } }iOS侧新建一个RCTCameraManager类继承RCTViewManager并实现RCTBridgeModule协议。注意如果这个模块还要附带原生View比如相机取景View就必须继承RCTViewManager否则直接继承NSObject即可。示例interface RCTCameraManager : RCTViewManager RCTBridgeModule end implementation RCTCameraManager RCT_EXPORT_MODULE(CameraModule) RCT_EXPORT_METHOD(takePicture:(NSDictionary *)options resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { // 拍照逻辑 } end一个关键细节RN中模块名和ViewManager的注册名必须完全一致这样JS侧才能通过requireNativeComponent拿到原生View。我在最开始集成时就踩过这个坑名字不一致会导致JS调方法时找不到模块报“TurboModuleRegistry.get... could not be found”。2.3 代码初始化与原生环境配置准备在动手写原生代码前工程初始化有些细节值得注意。Android侧需要确保minSdkVersion 21CameraX最低要求是21同时需要声明相机权限uses-permission android:nameandroid.permission.CAMERA /iOS侧需要在Info.plist添加相机用途描述keyNSCameraUsageDescription/key string需要使用相机拍摄设备照片/string如果没有加描述App一调用相机会直接崩溃。iOS相机权限弹窗的文案会直接引用这个描述所以别乱填。3. 核心功能实现取景、拍照、对焦、切换镜头3.1 Android侧CameraX相机初始化实现Android侧我选择CameraX作为底层驱动。CameraX的核心概念是ProcessCameraProvider它负责把相机设备绑定到生命周期然后通过Preview用例把画面输出到PreviewView通过ImageCapture用例完成拍照。初始化时最需要注意的一点是——所有相机操作必须在主线程执行CameraX内部对线程有强约束。如果你的调用是从JS侧过来的RN的Native Modules默认是跑在JS线程的直接调用CameraX会报线程错误。我封装时先统一切换到HandlerExecutor来处理相机调用private ExecutorService cameraExecutor Executors.newSingleThreadExecutor(); private void bindCameraUseCases() { ListenableFutureProcessCameraProvider cameraProviderFuture ProcessCameraProvider.getInstance(reactContext); cameraProviderFuture.addListener(() - { try { ProcessCameraProvider cameraProvider cameraProviderFuture.get(); Preview preview new Preview.Builder() .setTargetRotation(getCurrentScreenRotation()) .build(); ImageCapture imageCapture new ImageCapture.Builder() .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY) .setTargetResolution(new Size(1920, 1080)) .setJpegQuality(85) .build(); cameraProvider.unbindAll(); cameraProvider.bindToLifecycle( lifecycleOwner, CameraSelector.DEFAULT_BACK_CAMERA, preview, imageCapture); } catch (Exception e) { // 异常处理 } }, ContextCompat.getMainExecutor(reactContext)); }这里有两个参数值得展开说。第一个是CAPTURE_MODE_MINIMIZE_LATENCY。CameraX提供两种拍照模式MINIMIZE_LATENCY降低快门延迟和MINIMIZE_MEMORY降低内存占用。我选择了前者因为项目的核心指标就是快门到出图的时间。代价是内存峰值会更高但后续通过图片压缩策略做了平衡。如果你的App同时要跑很多内存操作可以切换成后者。第二个是setJpegQuality(85)。JPEG质量因子85是一个很微妙的平衡点它对干净背景的物体比如白底黑字的铭牌压缩后肉眼几乎无损OCR识别完全不受影响但体积能比100质量降一半以上。最开始我用100一张图动辄6MB传输和存储压力全上来了。后来改成85图片体积稳定在1.5MB左右识别率没有肉眼可见的下降。setTargetResolution(new Size(1920, 1080))是为了限制输出图片的分辨率。大部分手机的相机传感器默认输出是4000x3000这种天文数字但实际业务根本用不到那么高OCR识别1080p已经绰绰有余。限制分辨率同时也能减少内存压力。3.2 iOS侧AVFoundation相机初始化实现iOS侧没有CameraX这种高级封装需要手动操作AVCaptureSession。核心步骤是创建Session -- 配置输入输出 -- 开始运行。我封装了一个CameraManager类implementation CameraManager { AVCaptureSession *_session; AVCaptureDeviceInput *_backCameraInput; AVCaptureDeviceInput *_frontCameraInput; AVCapturePhotoOutput *_photoOutput; } - (void)setupSession { _session [[AVCaptureSession alloc] init]; _session.sessionPreset AVCaptureSessionPreset1920x1080; AVCaptureDevice *backCamera [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo]; _backCameraInput [AVCaptureDeviceInput deviceInputWithDevice:backCamera error:nil]; if ([_session canAddInput:_backCameraInput]) { [_session addInput:_backCameraInput]; } _photoOutput [[AVCapturePhotoOutput alloc] init]; if ([_session canAddOutput:_photoOutput]) { [_session addOutput:_photoOutput]; } } endiOS上有个容易被忽视的问题AVCaptureSession的sessionPreset决定了解析度但并不是所有设备支持所有预设。我在真机测试时发现iPhone SE系列对AVCaptureSessionPreset3840x2160支持不稳定会出现画面抖动的现象。保险的做法是先判断[session canSetSessionPreset:preset]不支持时降级到1080p。3.3 拍照功能的完整实现链路拍照功能是整个插件最核心的链路。它的流程是JS点击按钮 - 原生执行拍照 - 图片落盘到临时目录 - 返回文件路径给JS - JS读取文件上传。Android侧拍照的核心实现ReactMethod public void takePicture(ReadableMap options, Promise promise) { ImageCapture imageCapture imageCaptureUseCase; if (imageCapture null) { promise.reject(E_CAMERA_NOT_READY, 相机尚未初始化); return; } File outputDir new File(reactContext.getCacheDir(), camera); if (!outputDir.exists()) { outputDir.mkdirs(); } File outputFile new File(outputDir, System.currentTimeMillis() .jpg); ImageCapture.OutputFileOptions outputOptions new ImageCapture.OutputFileOptions.Builder(outputFile).build(); imageCapture.takePicture(outputOptions, cameraExecutor, new ImageCapture.OnImageSavedCallback() { Override public void onImageSaved(ImageCapture.OutputFileResults results) { promise.resolve(outputFile.getAbsolutePath()); } Override public void onError(ImageCaptureException exception) { promise.reject(E_CAPTURE_FAILED, exception.getMessage()); } }); }注意这里的内存策略返回文件路径而不是返回Base64字符串。我见过很多人把图片转成Base64再通过Promise传回JS侧一张2MB的图片Base64化之后接近3MBBridge序列化传递一次就已经很慢了再加上JS侧还要再转成字符串Manipulation低端机器直接卡成PPT。正确做法永远是原生侧落盘JS侧拿路径读取。iOS侧的核心实现RCT_EXPORT_METHOD(takePicture:(NSDictionary *)options resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { AVCapturePhotoSettings *settings [AVCapturePhotoSettings photoSettingsWithFormat:{ AVVideoCodecKey: AVVideoCodecTypeJPEG, AVVideoCompressionPropertiesKey: { AVVideoQualityKey: 0.85 } }]; [_photoOutput capturePhotoWithSettings:settings delegate:self]; // 在captureOutput:didFinishProcessingPhoto:error:回调中处理结果 // 写入临时目录后resolve路径 }我遇到的一个iOS特有坑是capturePhotoWithSettings:delegate:的回调方法是强引用的如果当前控制器被提前释放回调永远不会触发。需要在回调里做好弱引用处理并且在dealloc时手动设置_photoOutput.delegate nil避免野指针崩溃。3.4 触摸对焦与对焦状态实时回调对焦是相机体验中最敏感的功能。CameraX里触摸对焦需要调用CameraControl的startFocusAndMetering方法ReactMethod public void focusAt(float x, float y, Promise promise) { if (camera null) { promise.reject(E_CAMERA_NOT_READY, 相机尚未初始化); return; } FocusMeteringAction action new FocusMeteringAction.Builder( new MeteringPointFactory().createPoint(x, y)) .setAutoCancelDuration(2, TimeUnit.SECONDS) .build(); camera.getCameraControl().startFocusAndMetering(action) .addListener(() - { promise.resolve(true); }, ContextCompat.getMainExecutor(reactContext)); }同时需要通过事件通道把对焦状态推回JS侧。CameraX的camera. CameraInfo提供了zoomState和focusState的监听CameraInfo cameraInfo camera.getCameraInfo(); cameraInfo.getFocusState().observe(getMainExecutor(), focusState - { WritableMap event Arguments.createMap(); event.putString(state, focusState.toString()); reactContext.getJSModule(DeviceEventManagerModule.RDeviceEventEmitter.class) .emit(onFocusStateChange, event); });JS侧监听事件import { NativeEventEmitter, NativeModules } from react-native; const cameraEmitter new NativeEventEmitter(NativeModules.CameraModule); useEffect(() { const subscription cameraEmitter.addListener(onFocusStateChange, (event) { // 更新UI对焦框状态 }); return () subscription.remove(); }, []);3.5 前后摄像头无缝切换切换摄像头比较容易踩雷。直接销毁重来会导致取景画面闪黑屏正确的做法是同一个Preview实例只更新绑定的CameraSelectorReactMethod public void switchCamera(Promise promise) { boolean isFront currentCameraLens CameraSelector.LENS_FACING_FRONT; CameraSelector newSelector isFront ? CameraSelector.DEFAULT_BACK_CAMERA : CameraSelector.DEFAULT_FRONT_CAMERA; cameraProvider.unbindAll(); cameraProvider.bindToLifecycle( lifecycleOwner, newSelector, previewUseCase, imageCaptureUseCase); currentCameraLens isFront ? CameraSelector.LENS_FACING_BACK : CameraSelector.LENS_FACING_FRONT; promise.resolve(true); }iOS侧切换摄像头的本质是切换AVCaptureDeviceInputRCT_EXPORT_METHOD(switchCamera:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { [_session beginConfiguration]; AVCaptureDeviceInput *currentInput _session.inputs.firstObject; [_session removeInput:currentInput]; AVCaptureDevice *newDevice (currentInput.device.position AVCaptureDevicePositionBack) ? [AVCaptureDevice defaultDeviceWithDeviceType:AVCaptureDeviceTypeBuiltInWideAngleCamera mediaType:AVMediaTypeVideo position:AVCaptureDevicePositionFront] : [AVCaptureDevice defaultDeviceWithDeviceType:AVCaptureDeviceTypeBuiltInWideAngleCamera mediaType:AVMediaTypeVideo position:AVCaptureDevicePositionBack]; AVCaptureDeviceInput *newInput [AVCaptureDeviceInput deviceInputWithDevice:newDevice error:nil]; if ([_session canAddInput:newInput]) { [_session addInput:newInput]; } [_session commitConfiguration]; resolve(YES); }注意iOS切换摄像头时照片输出_photoOutput不需要重新创建只要Session还在运行输出连接会自动切换到新的输入源上。但会话配置修改时最好在beginConfiguration和commitConfiguration之间完成所有变更一次性提交避免频繁重启session导致画面卡顿。3.6 原生View集成与取景画面渲染取景画面要直接嵌入RN页面层级Android侧需要实现ReactViewManager来暴露自定义Viewpublic class CameraViewManager extends SimpleViewManagerPreviewView { Override public String getName() { return CameraView; } Override protected PreviewView createViewInstance(ThemedReactContext reactContext) { PreviewView previewView new PreviewView(reactContext); previewView.setScaleType(PreviewView.ScaleType.FILL_CENTER); return previewView; } }JS侧使用方式import { requireNativeComponent } from react-native; const CameraView requireNativeComponent(CameraView); function CameraPreview() { return CameraView style{{ flex: 1 }} /; }这里有个实现细节PreviewView需要在相机初始化前就绑定到生命周期但RN View的创建时机是在原生Module初始化之后。我在实际项目里加了一个setCameraReady的触发器当JS侧感知到View已经挂载完成后再调用原生Module的initializeCamera方法避免相机初始化过早导致取景画面黑屏。4. 性能专项优化从启动白屏到内存峰值控制4.1 React Native启动白屏的成因拆解用RN做相机页面启动阶段有一个绕不开的问题白屏。这在第三方库时代就存在自研后我终于彻底理解了它的成因。RN页面启动时会先加载JS Bundle解析执行JS代码然后再渲染原生组件。这个过程是串行的JS引擎冷启动 - Bundle加载 - 首帧渲染命令下发到UI线程 - 原生View真正挂载。相机页面最致命的是CameraX/AVFoundation的初始化也是一个耗时操作如果放在JS渲染完后再触发用户看到的就是“白屏 —— 黑屏 —— 取景画面”三段式加载。我做的优化方案分三块原生View优先渲染让CameraView在JS层还处于加载阶段时就提前占位底层绑定一个原生SurfaceView这个View不依赖JS Bundle加载完成。相机初始化懒触发原生Module的相机初始化不在模块加载时执行而是等CameraView的onSurfaceCreated回调触发后再执行保证取景画面和Surface生命周期严格同步。并行启动关键路径RN页面启动时JS只需要渲染按钮和遮罩层取景画面的渲染完全由原生线程控制不与JS Bundle执行抢时间片。实际操作中Android侧的白屏时间从原来的850ms降到320msiOS侧从700ms降到280ms。体感提升非常明显。4.2 照片压缩策略解析拍照性能的另一半在压缩策略上。CameraX的ImageCapture自带JPEG质量设置但我们可以做得更精细——针对不同业务场景用不同的压缩档位。我实现了三档策略场景JPEG质量目标分辨率备注快速预览701280x720仅用于列表缩略图标准拍摄851920x1080适合OCR/存档高质量922560x1440需要裁切打印时使用分辨率优先原则先控制分辨率再控制质量因子。同样是把体积降到1MB降低分辨率带来的画质损失远小于降低质量因子。4.3 内存峰值控制与OOM防护相机功能最容易触发OOM的内存操作有两个一是拍照时JPEG解码二是把图片读入内存做Base64转换。前者由底层控制后者完全是人祸。我总结的防护清单绝不在JS侧读取完整图片到内存。图片文件路径给到JS后上传走fetch的FormData文件流不经过JS内存。原生侧拍照落盘使用OutputFileOptions直接写入文件不经过Bitmap内存中转。Android侧拍照后的ImageProxy要及时close()否则会阻塞CameraX的ImageReader管线连续拍摄十几张后相机直接罢工。这里说一个真实踩过的坑刚开始连续拍摄时到第15张左右App崩溃日志指向OutOfMemoryError。排查后发现是ImageCapture的OnImageSavedCallback里忘了关闭ImageProxy外层引用导致CameraX的缓冲池被耗尽。加上image.close()之后连续拍摄200张也没有再崩。4.4 事件通道高频回调的性能观察对焦状态回调用的是事件通道但事件通道也不是无限量的。如果你同时对焦状态和帧数据都走事件通道且频率很高RN的DeviceEventManagerModule会在JS侧做大量的事件分发处理容易造成JS线程卡顿。我的建议是高频回调在原生侧先做降频或聚合。比如对焦状态不必每个状态变化都推给JS可以在原生侧维护一个状态机只在状态从锁定变为对焦成功等关键节点时推送一次。帧数据回调更是如此如果做实时帧处理尽量在原生层完成处理只回传处理结果不要往JS侧传原始YUV数据。5. 踩坑记录与自检清单5.1 高频排查问题速查表现象根因解决方案Android取景黑屏Surface未就绪就绑定CameraX等待PreviewView.onSurfaceCreated再初始化iOS切换后台回来相机黑屏Session被系统中断后未重建监听AVCaptureSessionWasInterruptedNotification恢复后重建Session连续拍照后相机停止响应ImageProxy未close确保每一次拍摄回调都释放资源拍照返回慢使用了CAPTURE_MODE_MAXIMIZE_QUALITY切换为MINIMIZE_LATENCY设置合理分辨率前端拿不到图片文件路径使用cacheDir但上传时未拼全路径返回file://前缀的绝对路径JS侧直接用iOS拍照后图片方向不对未处理EXIF方向在AVCapturePhotoOutput层使用photoOutput.connection.videoOrientation修正5.2 生命周期管理中的几个容易翻车的点RN页面的生命周期与原生相机的生命周期天然存在时间差。页面还在过渡动画中JS的useEffect可能已经触发相机初始化页面已经卸载但原生Module的相机线程还在运行。我在实际项目中用了一套简单可靠的对齐方案原生View的onDropViewInstance方法中做资源释放保证View卸载时相机同步释放。Android侧Override public void onDropViewInstance(PreviewView view) { super.onDropViewInstance(view); cameraProvider.unbindAll(); cameraExecutor.shutdown(); }iOS侧在removeFromSuperview时释放Session- (void)willMoveToSuperview:(UIView *)newSuperview { [super willMoveToSuperview:newSuperview]; if (newSuperview nil) { [_session stopRunning]; } }5.3 自检清单项目上线前我整理了一份检查清单新设备测试时逐条过最低端测试设备上连续拍照30张不崩溃、不卡顿、相机不死。拍摄的照片OCR引擎识别率不低于99%。取景画面流畅度在30fps无明显拖影。页面在前后台切换、锁屏解锁后相机能自动恢复取景。相机的拍照延迟不超过800ms。拍照产物在1.5MB以内格式为JPEG方向正确。无内存泄漏反复进入退出拍照页面10次App内存稳定。6. 插件扩展思路讨论做完基础功能后我开始琢磨这个插件的天花板也想给未来可能遇到的问题留条后路。一个是实时帧处理能力。如果业务将来要做扫码、物体识别、实时滤镜现在的基础架构其实已经铺垫好了——CameraX本身支持ImageAnalysis用例AVFoundation本身也支持AVCaptureVideoDataOutput两者都可以输出YUV帧数据。原生侧拿到帧数据后可以直接交给OpenCV或ML Kit推理结果通过事件通道回传。关键是帧处理不要在JS线程做而是开启独立线程池。另一个是JS侧动态配置相机参数。当前的拍照参数是硬编码在原生层的如果希望运营侧能远程调整相机参数可以做一个配置参数透传通道JS侧通过setCameraOptions(options)方法把分辨率、JPEG质量、对焦模式等参数一次性传给原生层原生层根据参数重建用例。这个改造并不复杂但要注意改配置时必须重新bindToLifecycle这个操作需要做好线程同步避免在拍照过程中被插入执行导致状态错乱。还有一个方向是多相机协同——同时调用前置和后置摄像头做双路取景。CameraX可以用两个CameraSelector绑定到同一个生命周期AVFoundation则需要两个AVCaptureSession并行。这个场景少见但如果做双摄直播或者前后对比类应用会非常有用。7. 最终的工程结构参考为了让这次踩坑经验更容易复用我把最终工程结构整理出来供参考。完整代码因为太长就不贴全文了但目录结构很能说明问题CameraPlugin/ ├── android/src/main/java/com/camera/ │ ├── CameraModule.java // RN桥接入口 │ ├── CameraViewManager.java // 原生View管理 │ ├── CameraLifecycleOwner.java // 生命周期管理 │ └── ImageProcessor.java // 图像压缩与处理 ├── ios/ │ ├── CameraManager.h // iOS相机管理器 │ ├── CameraManager.m │ └── CameraViewManager.m // RCTViewManager实现 └── src/ ├── index.ts // JS侧API封装 ├── CameraPreview.tsx // 取景组件 └── types.ts // 类型声明这个结构的特点是JS侧只暴露一个CameraPreview组件和一组相机控制方法所有的实现细节都被隔离在原生层。后续如果换底层驱动比如从CameraX换到Camera2JS侧API完全不用动这是我最满意的一点。做完这个插件后我对React Native的能力边界有了完全不同的认识——RN从来就不是性能瓶颈瓶颈只在于你用不用得惯Bridge这套通信模型。把这个模型吃透相机这种高频、大流量、强交互的场景也能跑得和原生一样顺滑。希望这篇记录能让你少踩几个坑在手机摄像头这块把RN的性能榨到极限。
返回列表