Android Camera2 API多摄像头并发开发实战:从架构到优化
2026/7/31 15:58:54 网站建设 项目流程

1. 项目概述:为什么需要同时操作多个摄像头?

在Android应用开发中,处理单个摄像头已经是常规操作,但当你需要同时预览前后摄像头、实现双目视觉、或者接入红外等特殊摄像头时,事情就变得复杂起来。我最初接到类似需求,是为了一个需要实时人脸比对和活体检测的金融类App。客户要求前置摄像头捕捉用户面部,同时后置摄像头扫描身份证件,确保“人证合一”。听起来简单,但Android系统对摄像头资源的独占性管理,让同时打开多个Camera实例成了第一个拦路虎。

这不仅仅是金融场景的需求。在工业检测中,你可能需要多角度同时监控一个零件;在社交或直播应用里,画中画、前后摄像头同屏展示能极大增强互动性;在安防领域,多路视频流同时分析更是基础。因此,掌握多摄像头并发处理技术,是突破许多创新功能瓶颈的关键。这个项目要解决的,就是如何在Android上,以最简洁、稳定的方式,实现多个摄像头的同步打开、预览、控制(缩放、对焦)以及图像获取(截图),并处理好旋转、镜像等显示细节。我会避开那些庞大臃肿的第三方库,带你从Android原生的Camera2 API入手,构建一个清晰、可复用的多摄像头管理框架。

2. 核心架构设计与Camera2 API选型

2.1 为何放弃Camera1与第三方库?

在Android相机开发中,我们主要有Camera1(已废弃但仍有使用)、Camera2(Android 5.0+)以及诸如Google的CameraX等封装库。对于多摄像头并发这种相对底层的需求,我的选择是Camera2 API。

Camera1 API设计陈旧,它通过Camera.open(cameraId)来获取相机实例,但系统对相机服务的全局锁管理,使得几乎不可能稳定地同时打开两个以上的摄像头实例,即使有些设备通过Hack方式实现,也充满了兼容性陷阱和不可预知的崩溃。而Camera2 API采用了全新的、基于管道的异步模型,将相机抽象为一个可以输出图像流的设备,通过CameraManager来管理。这种设计本身就更适合并发操作,允许我们为不同的摄像头创建独立的CameraCaptureSession

为什么不直接用CameraX?CameraX确实极大地简化了生命周期管理和基础用例(如预览、拍照、分析)。然而,在撰写本文时,CameraX对多摄像头并发预览的原生支持仍处于实验阶段(ExperimentalMultipleCameras),且其抽象层级较高,当我们需要精细控制每个摄像头的对焦模式、缩放比例、输出图像格式(如处理红外摄像头YUV数据)时,可能会感到束手束脚。从学习和掌控的角度出发,直接使用Camera2 API能让你透彻理解背后的机制,一旦掌握,便能灵活应对各种复杂场景。

2.2 多摄像头管理核心思路

我们的目标是构建一个MultiCameraManager,其核心职责是:

  1. 枚举与筛选:获取设备上所有可用的摄像头,并根据特性(前后置、红外、逻辑摄像头)进行筛选和配对。
  2. 资源隔离与分配:为每个需要打开的摄像头独立分配资源,包括CameraDeviceCaptureRequest.BuilderSurface(用于预览)和ImageReader(用于截图/分析)。
  3. 会话并行管理:为每个摄像头创建独立的CameraCaptureSession,避免会话间的命令冲突。
  4. 线程安全与同步:确保所有回调(CameraDevice.StateCallbackCameraCaptureSession.StateCallback)在正确的后台线程执行,并妥善处理生命周期(如Activity暂停时释放所有资源)。

一个关键点是理解“逻辑摄像头”。在高端机型上,系统可能将多个物理摄像头(例如,主摄、超广角、长焦)虚拟成一个逻辑摄像头,通过软件切换来提供“无损变焦”等功能。在我们的多摄像头并发场景中,通常需要绕过逻辑摄像头,直接打开特定的物理摄像头ID。这需要通过CameraCharacteristics来检查LENS_FACING(前后置)和SCALER_STREAM_CONFIGURATION_MAP(支持的输出格式和分辨率)。

3. 实战:一步步构建多摄像头预览

3.1 环境准备与权限处理

首先,确保你的AndroidManifest.xml中声明了必要的权限:

<uses-permission android:name="android.permission.CAMERA" /> <!-- 如果需要在外部存储保存截图 --> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28" /> <!-- 针对Android 10以下 --> <uses-feature android:name="android.hardware.camera" android:required="false" /> <uses-feature android:name="android.hardware.camera.autofocus" android:required="false"/>

注意,从Android 6.0 (API 23)开始,需要在运行时动态申请CAMERA权限。对于多摄像头,你只需要申请一次相机权限,该权限即涵盖对所有摄像头资源的访问。

一个健壮的权限申请和摄像头枚举流程如下:

// 在Activity/Fragment中 private lateinit var cameraManager: CameraManager private val cameraIdList = mutableListOf<String>() private val cameraCharacteristicsMap = mutableMapOf<String, CameraCharacteristics>() private fun setupCameras() { cameraManager = getSystemService(Context.CAMERA_SERVICE) as CameraManager try { // 1. 获取所有摄像头ID val ids = cameraManager.cameraIdList for (id in ids) { val characteristics = cameraManager.getCameraCharacteristics(id) cameraCharacteristicsMap[id] = characteristics // 2. 筛选出我们需要的摄像头(例如:前后置各一个) val lensFacing = characteristics.get(CameraCharacteristics.LENS_FACING) // 根据需求筛选,例如找前置和后置 if (lensFacing == CameraCharacteristics.LENS_FACING_FRONT) { frontCameraId = id } else if (lensFacing == CameraCharacteristics.LENS_FACING_BACK) { backCameraId = id } cameraIdList.add(id) } // 3. 检查设备是否支持多摄像头并发(非必须,但可做友好提示) val concurrentCameraIds = cameraManager.concurrentCameraIds Log.d(TAG, "支持并发的摄像头组合: $concurrentCameraIds") } catch (e: CameraAccessException) { Log.e(TAG, "访问摄像头信息失败", e) } }

注意concurrentCameraIds是Android 11 (API 30)引入的,用于查询系统明确支持同时打开的摄像头组合。在低版本设备上,即使系统未明确报告,我们仍可以尝试打开多个摄像头,但失败率会更高,必须做好异常处理。

3.2 创建独立的预览Surface与CaptureSession

这是最核心的一步。我们需要为每个摄像头准备一个Surface供其渲染画面。通常,这个Surface来自TextureViewSurfaceView。为了实现同屏预览,我们可以在布局中放置两个TextureView

// 假设我们有两个TextureView:textureViewFront, textureViewBack private fun openCamera(cameraId: String, textureView: TextureView) { // 确保在后台线程操作 val backgroundHandler = HandlerThread("CameraBackground_$cameraId").apply { start() } val handler = Handler(backgroundHandler.looper) try { // 1. 打开摄像头设备 cameraManager.openCamera(cameraId, object : CameraDevice.StateCallback() { override fun onOpened(device: CameraDevice) { // 保存device引用,例如放入Map: cameraDevicesMap[cameraId] = device cameraDevicesMap[cameraId] = device // 2. 创建预览请求构建器 val previewRequestBuilder = device.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW).apply { // 3. 从TextureView获取Surface val surface = Surface(textureView.surfaceTexture.apply { setDefaultBufferSize(previewSize.width, previewSize.height) // 需要先计算合适的预览尺寸 }) addTarget(surface) // 将此Surface作为输出目标 // 设置预览参数,如自动对焦、自动曝光 set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE) set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH) } // 4. 创建CaptureSession,仅使用这一个Surface device.createCaptureSession(listOf(surface), object : CameraCaptureSession.StateCallback() { override fun onConfigured(session: CameraCaptureSession) { // 保存session引用 captureSessionsMap[cameraId] = session // 5. 设置重复请求,开始预览 session.setRepeatingRequest(previewRequestBuilder.build(), null, handler) } override fun onConfigureFailed(session: CameraCaptureSession) { Log.e(TAG, "摄像头 $cameraId 的CaptureSession创建失败") } }, handler) } override fun onDisconnected(device: CameraDevice) { /* 清理资源 */ } override fun onError(device: CameraDevice, error: Int) { /* 处理错误 */ } }, handler) } catch (e: CameraAccessException) { Log.e(TAG, "打开摄像头 $cameraId 失败", e) } }

关键点解析

  • 独立线程:每个摄像头最好拥有自己独立的HandlerThreadHandler,避免回调阻塞。线程名最好带上cameraId以便调试。
  • Session独立:每个CameraDevice创建自己独立的CaptureSession。这是实现并发预览的关键,确保两个摄像头的图像流处理管道互不干扰。
  • Surface绑定Surface来自TextureView,我们需要先为其设置一个合适的缓冲区大小(setDefaultBufferSize),这个大小应该从摄像头支持的预览尺寸列表中选取一个与TextureView比例最匹配、分辨率适中的尺寸。

3.3 计算与选择最佳预览尺寸

摄像头支持的输出尺寸千差万别,选择一个合适的预览尺寸至关重要,它影响性能、功耗和预览质量。我们需要从StreamConfigurationMap中获取支持的尺寸,并筛选。

private fun chooseOptimalPreviewSize(cameraId: String, textureView: TextureView): Size { val characteristics = cameraCharacteristicsMap[cameraId]!! val map = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)!! // 获取摄像头支持的所有输出尺寸(针对SurfaceTexture类,即预览) val supportedSizes = map.getOutputSizes(SurfaceTexture::class.java) val displayRotation = windowManager.defaultDisplay.rotation val isPortrait = displayRotation == Surface.ROTATION_0 || displayRotation == Surface.ROTATION_180 val viewWidth = if (isPortrait) textureView.height else textureView.width val viewHeight = if (isPortrait) textureView.width else textureView.height val aspectRatio = viewWidth.toFloat() / viewHeight.toFloat() // 筛选逻辑:1. 比例匹配;2. 分辨率不超过1080p(兼顾性能与清晰度);3. 选择最大可用尺寸。 var optimalSize = supportedSizes[0] for (size in supportedSizes) { // 有些设备返回的尺寸可能为null或0 if (size.width <= 0 || size.height <= 0) continue // 计算当前尺寸的比例(需要考虑传感器方向,这里简化处理) val ratio = size.width.toFloat() / size.height // 允许比例有微小误差,例如0.01 if (Math.abs(ratio - aspectRatio) > 0.01) continue // 限制最大分辨率 if (size.width > 1920 || size.height > 1920) continue // 选择面积最大的一个 if (size.width * size.height > optimalSize.width * optimalSize.height) { optimalSize = size } } Log.i(TAG, "摄像头 $cameraId 选择的预览尺寸: ${optimalSize.width}x${optimalSize.height}") return optimalSize }

实操心得:不要盲目选择最高分辨率。对于预览而言,1080p(1920x1080)通常是性能与清晰度的最佳平衡点。过高的分辨率会加重GPU渲染负担,增加功耗,对后续的图像分析(如人脸识别)也没有额外好处,反而可能因为需要降采样而增加延迟。

4. 高级功能实现:控制、截图与显示处理

4.1 独立控制缩放与对焦

每个摄像头的控制(缩放、对焦、曝光)都是通过更新其对应的CaptureRequest并提交给CaptureSession来实现的。关键在于操作独立的CaptureRequest.Builder

数码变焦(Zoom)

fun setZoom(cameraId: String, zoomRatio: Float) { val device = cameraDevicesMap[cameraId] ?: return val characteristics = cameraCharacteristicsMap[cameraId] ?: return val session = captureSessionsMap[cameraId] ?: return // 1. 检查摄像头是否支持缩放 val zoomRange = characteristics.get(CameraCharacteristics.CONTROL_ZOOM_RATIO_RANGE) val minZoom = zoomRange?.lower ?: 1.0f val maxZoom = zoomRange?.upper ?: 1.0f val clampedZoom = zoomRatio.coerceIn(minZoom, maxZoom) // 2. 更新该摄像头的预览请求构建器 val previewBuilder = previewRequestBuildersMap[cameraId] ?: return previewBuilder.set(CaptureRequest.CONTROL_ZOOM_RATIO, clampedZoom) // 3. 提交新的请求 session.setRepeatingRequest(previewBuilder.build(), null, null) }

触摸对焦

fun handleTouchToFocus(cameraId: String, x: Float, y: Float, textureView: TextureView) { val characteristics = cameraCharacteristicsMap[cameraId] ?: return val session = captureSessionsMap[cameraId] ?: return val previewBuilder = previewRequestBuildersMap[cameraId] ?: return // 1. 将触摸点坐标转换为传感器坐标系下的MeteringRectangle val sensorArraySize = characteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE)!! val meteringRect = calculateMeteringRectangle(x, y, sensorArraySize, textureView) // 2. 创建对焦请求 previewBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_CANCEL) previewBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO) val focusMetering = MeteringRectangle(meteringRect, MeteringRectangle.METERING_WEIGHT_MAX) previewBuilder.set(CaptureRequest.CONTROL_AF_REGIONS, arrayOf(focusMetering)) previewBuilder.set(CaptureRequest.CONTROL_AE_REGIONS, arrayOf(focusMetering)) // 3. 先提交一个单次请求触发对焦 session.capture(previewBuilder.build(), null, null) // 4. 再重新设置触发器为IDLE,并恢复连续对焦的预览 previewBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_IDLE) session.setRepeatingRequest(previewBuilder.build(), null, null) }

4.2 高质量截图保存

使用ImageReader来捕获高质量的静态图片。每个摄像头可以关联一个独立的ImageReader

// 在打开摄像头时,同时创建ImageReader private fun prepareImageReader(cameraId: String): ImageReader { val characteristics = cameraCharacteristicsMap[cameraId]!! val map = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)!! // 选择最大的JPEG输出尺寸用于拍照 val largestJpegSize = map.getOutputSizes(ImageFormat.JPEG).maxByOrNull { it.width * it.height }!! // 创建ImageReader,最多缓冲2张图像 val imageReader = ImageReader.newInstance(largestJpegSize.width, largestJpegSize.height, ImageFormat.JPEG, 2) imageReader.setOnImageAvailableListener({ reader -> // 在后台线程处理图像 val image = reader.acquireLatestImage() image?.use { val buffer = it.planes[0].buffer val bytes = ByteArray(buffer.remaining()) buffer.get(bytes) // 保存bytes到文件(需在后台线程执行IO) saveImageToFile(bytes, cameraId) } }, backgroundHandlerMap[cameraId]) // 在创建CaptureSession时,将这个ImageReader的Surface也加入目标列表 // val surfaces = listOf(previewSurface, imageReader.surface) // device.createCaptureSession(surfaces, ...) return imageReader } // 触发截图 fun captureStillPicture(cameraId: String) { val session = captureSessionsMap[cameraId] ?: return val device = cameraDevicesMap[cameraId] ?: return val previewBuilder = previewRequestBuildersMap[cameraId] ?: return // 1. 停止预览,防止画面变化 session.stopRepeating() // 2. 创建专门的拍照请求 val captureBuilder = device.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE).apply { addTarget(imageReaderMap[cameraId]?.surface!!) // 指向ImageReader的Surface // 继承预览的部分设置,如对焦模式 set(CaptureRequest.CONTROL_AF_MODE, previewBuilder.get(CaptureRequest.CONTROL_AF_MODE)) // 设置JPEG方向、质量等 set(CaptureRequest.JPEG_ORIENTATION, getJpegOrientation(cameraId)) set(CaptureRequest.JPEG_QUALITY, 95.toByte()) } // 3. 执行拍照 session.capture(captureBuilder.build(), null, null) // 4. 拍照完成后,恢复预览 session.setRepeatingRequest(previewBuilder.build(), null, null) }

注意事项ImageReader的回调onImageAvailable可能在任意线程被调用,务必在其中进行快速的图像获取操作,然后将耗时的保存、处理工作抛到另一个工作线程,避免阻塞相机流水线。

4.3 处理旋转、镜像与画面适配

摄像头传感器的方向、设备自然方向、UI方向三者不一致时,会导致预览画面旋转。此外,前置摄像头通常需要做水平镜像。

计算JPEG方向

private fun getJpegOrientation(cameraId: String): Int { val characteristics = cameraCharacteristicsMap[cameraId]!! val sensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION) ?: 0 val deviceRotation = windowManager.defaultDisplay.rotation return (sensorOrientation + ORIENTATIONS.get(deviceRotation) + 360) % 360 } companion object { private val ORIENTATIONS = SparseIntArray().apply { append(Surface.ROTATION_0, 90) append(Surface.ROTATION_90, 0) append(Surface.ROTATION_180, 270) append(Surface.ROTATION_270, 180) } }

TextureView的变换矩阵: 为了在预览时正确显示,我们需要对TextureView应用变换矩阵。这通常在TextureView.SurfaceTextureListeneronSurfaceTextureAvailable回调中设置。

override fun onSurfaceTextureAvailable(surface: SurfaceTexture, width: Int, height: Int) { val cameraId = ... // 关联的摄像头ID val characteristics = cameraCharacteristicsMap[cameraId]!! val previewSize = chooseOptimalPreviewSize(cameraId, textureView) val sensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION) ?: 0 val isFrontCamera = characteristics.get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_FRONT // 计算变换矩阵 val matrix = Matrix() val viewWidth = textureView.width.toFloat() val viewHeight = textureView.height.toFloat() val previewWidth = previewSize.width.toFloat() val previewHeight = previewSize.height.toFloat() // 1. 计算缩放,使预览填满View并保持比例 val scaleX = viewWidth / previewWidth val scaleY = viewHeight / previewHeight val scale = max(scaleX, scaleY) // 或 min,取决于你是想填满还是包含 matrix.setScale(scale, scale) // 2. 平移,使画面居中 matrix.postTranslate((viewWidth - previewWidth * scale) / 2, (viewHeight - previewHeight * scale) / 2) // 3. 应用传感器旋转 matrix.postRotate(sensorOrientation.toFloat(), viewWidth / 2, viewHeight / 2) // 4. 如果是前置摄像头,应用水平镜像 if (isFrontCamera) { matrix.postScale(-1f, 1f, viewWidth / 2, viewHeight / 2) } textureView.setTransform(matrix) }

这个矩阵计算是正确显示画面的核心,顺序很重要:先缩放平移,再旋转,最后镜像。

5. 双目摄像头、红外与人脸识别的特殊集成

5.1 双目摄像头同步策略

双目摄像头通常指两个物理位置固定、参数一致的摄像头,用于计算深度信息。在Android上,它们可能表现为两个独立的cameraId

关键挑战是同步:确保左右摄像头捕获的图像时间戳尽可能接近。Camera2 API的CaptureRequest.CONTROL_AE_MODE设置为OFF并手动设置相同的曝光时间(SENSOR_EXPOSURE_TIME)有助于同步,但硬件同步通常需要厂商特定的扩展API。

一个实用的软件同步方案是:

  1. 同时触发:使用CameraManagercreateCaptureSessionsetRepeatingRequest尽可能同时(在同一帧回调内)对两个摄像头进行操作。
  2. 时间戳对齐:使用ImageReader获取图像时,记录Image.getTimestamp()。在后续处理中,根据时间戳配对左右图像。
  3. 外部触发:对于高精度需求,可以考虑使用外部硬件信号触发两个摄像头同时采集,但这超出了标准API范畴。

5.2 接入红外(IR)摄像头

红外摄像头通常输出YUV或RAW格式的数据,而非标准的JPEG。你需要:

  1. 识别摄像头:通过CameraCharacteristicsLENS_FACINGSENSOR_INFO_COLOR_FILTER_ARRANGEMENT等特性,或尝试打开后检查支持的输出格式来识别红外摄像头。
  2. 配置ImageReader:使用ImageFormat.YUV_420_888ImageFormat.RAW_SENSOR格式创建ImageReader
  3. 处理YUV数据:从Image对象的planes中获取Y、U、V分量数据,进行后续处理(如转换为灰度图、温度分析等)。
val imageReader = ImageReader.newInstance(width, height, ImageFormat.YUV_420_888, 2) imageReader.setOnImageAvailableListener({ reader -> val image = reader.acquireLatestImage() image?.use { val yPlane = it.planes[0] // Y分量 val uPlane = it.planes[1] // U分量 val vPlane = it.planes[2] // V分量 // 处理YUV数据... } }, handler)

5.3 集成人脸识别与活体检测

这属于计算机视觉范畴,我们的多摄像头框架为其提供了稳定的视频流输入。通常流程是:

  1. 获取预览帧:除了用于显示的预览Surface,可以再为每个摄像头创建一个ImageReader(格式为ImageFormat.YUV_420_888ImageFormat.PRIVATE),用于获取分析用的帧数据。
  2. 选择分析库:可以使用Google ML Kit的Face DetectionAPI,或集成更专业的SDK如虹软、商汤等。
  3. 数据传递:在ImageReader的回调中,将Image对象或从中提取的字节数组、Bitmap,传递给识别模块。务必注意Image对象必须及时关闭(.close().use{}),否则会迅速耗尽相机缓冲区。
  4. 活体检测:通常需要多帧或特定动作(眨眼、张嘴、摇头)。可以利用双目摄像头获取的深度信息进行3D活体检测,准确度更高。我们的多摄像头框架为这种方案提供了基础设施——你可以同时获取前置RGB摄像头和红外(或另一颗前置)摄像头的图像流,供算法进行融合判断。

6. 避坑指南与性能优化

6.1 常见问题与排查

  1. CameraAccessException: MAX_CAMERAS_IN_USE

    • 原因:系统或其它应用占用了摄像头资源。
    • 解决:确保在onPauseonDestroy中正确关闭所有摄像头(调用CameraDevice.close())。检查是否有其它相机应用在后台运行。
  2. 预览画面拉伸、变形或方向错误

    • 原因TextureView的变换矩阵计算错误,或预览尺寸选择不当。
    • 排查:打印出选择的预览尺寸、TextureView的宽高、传感器方向,逐步检查矩阵计算每一步的结果。确保setDefaultBufferSize设置正确。
  3. 同时打开失败,但单独打开正常

    • 原因:设备硬件或驱动限制。并非所有设备都支持多路摄像头并发高分辨率流。
    • 解决:降低预览分辨率(如降至720p),或尝试先打开一个摄像头,再延迟几百毫秒打开第二个。使用CameraManager.concurrentCameraIds(API 30+)进行能力查询。
  4. 应用卡顿或发热严重

    • 原因:预览分辨率过高,或ImageReader回调中进行了耗时操作阻塞了相机线程。
    • 优化:将预览分辨率限制在1080p以下。确保ImageReader回调中只做最必要的图像获取和传递,将保存、分析等操作移交到单独的ExecutorService或协程中。
  5. 前后摄像头帧率不同步

    • 现象:同屏预览时,两个画面刷新速度肉眼可见不一致。
    • 原因:两个摄像头可能运行在不同的帧率下(例如,一个30fps,一个60fps),或者CPU/GPU负载不均衡。
    • 缓解:在创建预览请求时,尝试通过CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE将两个摄像头的帧率范围设置为一致(如[30,30])。但这并非所有设备都支持。

6.2 性能优化要点

  • 资源按需创建:不要一次性为所有摄像头创建所有资源(如ImageReader)。只在需要截图或分析时,才创建并添加对应的ImageReaderSurface到CaptureSession
  • 及时释放:在摄像头不可见(如切换到其他Fragment)时,及时关闭摄像头释放资源。CameraDeviceCaptureSessionImageReader都是重量级对象。
  • 复用CaptureRequest.Builder:对于预览请求,在CaptureSession创建后,应复用同一个CaptureRequest.Builder,只更新需要改变的参数(如缩放比例),然后提交新请求。
  • 后台线程管理:为每个摄像头使用独立的HandlerThread,避免回调堆积相互影响。使用ExecutorService管理图像保存、分析等CPU密集型任务。
  • 内存管理ImageReadermaxImages参数不要设置过大,2-3张通常足够。及时调用Image.close()

构建一个健壮的多摄像头应用,就像指挥一个小型交响乐团,每个乐器(摄像头)都需要独立的乐谱(配置)和指挥棒(会话),但又必须在统一的节拍(主线程/生命周期)下协同工作。从权限管理、设备枚举到会话创建、画面控制,每一步的严谨处理都至关重要。经过多个项目的打磨,我发现最稳定的方案往往不是最复杂的,而是那些充分考虑了设备差异性和生命周期、并做了大量异常捕获和回退处理的方案。当你成功在屏幕上看到两个摄像头流畅同步预览时,那种成就感会告诉你,这些细致的工作都是值得的。

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

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

立即咨询