You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android Camera2 API连续拍摄第三张图片存相册延迟问题

问题分析与解决方案

核心问题定位

连续拍摄三张图片时第三张保存耗时过长,主要由以下几个关键问题导致:

1. 每次拍照重复创建CameraCaptureSession与ImageReader

代码在每次拍照时都会新建CameraCaptureSession和ImageReader,但未及时释放旧的Session资源。相机硬件资源有限,连续创建Session会引发资源竞争,第三次拍摄时相机服务需要等待资源释放,进而拉长整体耗时。

2. Location回调阻塞图像处理流程

在ImageReader.OnImageAvailableListener中实时调用userLocation.getLastLocation(),如果位置请求是异步排队执行,第三次拍摄时可能因前两次的位置请求未完成导致阻塞,拖慢保存流程。

3. 图像数据处理与文件操作的线程冲突

将ByteBuffer拷贝到byte[]的操作在相机后台线程执行,但后续的文件写入、EXIF修改、MediaScanner扫描未分离到独立IO线程,会占用相机处理线程,导致延迟累积。


具体修复方案

方案1:复用CameraCaptureSession,避免重复创建

不要每次拍照都新建CaptureSession,而是在预览初始化时创建一次,拍照时直接复用该Session提交拍摄请求。

代码修改示例:

// 成员变量保存Session
private CameraCaptureSession mCaptureSession;

// 初始化预览时创建Session(原createCameraPreview方法内)
private void createCameraPreview() {
    List<Surface> outputSurfaces = new ArrayList<>();
    outputSurfaces.add(new Surface(binding.textUreView.getSurfaceTexture()));
    
    cameraDevice.createCaptureSession(outputSurfaces, new CameraCaptureSession.StateCallback() {
        @Override
        public void onConfigured(@NonNull CameraCaptureSession session) {
            mCaptureSession = session;
            // 启动预览请求
            try {
                CaptureRequest previewRequest = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW)
                        .addTarget(outputSurfaces.get(0))
                        .build();
                mCaptureSession.setRepeatingRequest(previewRequest, null, mBackgroundHandler);
            } catch (CameraAccessException e) {
                showErrorDialog(getString(R.string.error_title_camera), getString(R.string.camera_access_exception, e.getMessage()));
            }
        }

        @Override
        public void onConfigureFailed(@NonNull CameraCaptureSession session) {
            session.close();
        }
    }, mBackgroundHandler);
}

// 拍照方法直接复用已有Session
private void takePicture() {
    try {
        CameraManager manager = (CameraManager) getActivity().getSystemService(Context.CAMERA_SERVICE);
        CameraCharacteristics characteristics = manager.getCameraCharacteristics(cameraDevice.getId());
        Size[] jpegSizes = Objects.requireNonNull(characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)).getOutputSizes(ImageFormat.JPEG);
        int width = jpegSizes[0].getWidth();
        int height = jpegSizes[0].getHeight();

        ImageReader reader = ImageReader.newInstance(width, height, ImageFormat.JPEG, 2);
        // 配置CaptureRequest.Builder逻辑保持不变...
        
        if (mCaptureSession != null && mCaptureSession.isOpen()) {
            // 添加ImageReader的Surface到Session临时目标
            mCaptureSession.capture(captureBuilder.build(), captureListener, mBackgroundHandler);
        }
    } catch (CameraAccessException e) {
        throw new RuntimeException(e);
    }
}

方案2:提前缓存Location,避免拍照时实时请求

在页面初始化或预览启动时预获取位置并缓存,拍照时直接使用缓存数据,避免位置请求的异步排队延迟。

代码修改示例:

// 成员变量缓存位置
private Location mCachedLocation;

@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
    super.onViewCreated(view, savedInstanceState);
    // 预获取位置
    userLocation.getLastLocation(location -> {
        mCachedLocation = location;
    });
}

// 图像回调中使用缓存位置
ImageReader.OnImageAvailableListener readerListener = imageReaderListener -> {
    try (Image image = imageReaderListener.acquireLatestImage()) {
        if (image == null) {
            showUserMessage(getString(R.string.message_image_capture_failed));
            return;
        }

        ByteBuffer buffer = image.getPlanes()[0].getBuffer();
        byte[] bytes = new byte[buffer.capacity()];
        buffer.get(bytes);

        // 优先使用缓存位置,为空时再重新获取
        if (mCachedLocation != null) {
            processImageSave(bytes, mCachedLocation.getLatitude(), mCachedLocation.getLongitude());
        } else {
            userLocation.getLastLocation(location -> {
                mCachedLocation = location;
                processImageSave(bytes, location.getLatitude(), location.getLongitude());
            });
        }
    } catch (Exception e) {
        showUserMessage(getString(R.string.message_image_processing_error));
        cameraViewModel.setCameraException(e);
    }
};

方案3:将文件操作移至独立IO线程

创建专门的IO线程池处理文件写入、EXIF修改和MediaScanner扫描,避免阻塞相机后台线程。

代码修改示例:

// 单例IO线程池
private ExecutorService mIoExecutor = Executors.newSingleThreadExecutor();

private void processImageSave(byte[] bytes, double latitude, double longitude) {
    mIoExecutor.submit(() -> {
        try {
            String savedFileName = saveImageWithLocation(bytes, latitude, longitude);
            // 回到主线程更新UI和ViewModel
            getActivity().runOnUiThread(() -> {
                cameraViewModel.setImageFinalName(savedFileName);
                showUserMessage(getString(R.string.message_image_save_successful));
            });
        } catch (IOException e) {
            getActivity().runOnUiThread(() -> {
                showUserMessage(getString(R.string.message_image_save_failed));
                cameraViewModel.setCameraException(e);
            });
        }
    });
}

// 原saveImageWithLocation方法保持不变,确保在IO线程执行

方案4:优化ImageReader资源配置

  • 将ImageReader.newInstance的maxImages参数设置为2(足够处理连续拍摄,避免资源浪费)
  • 确保每次acquireLatestImage()后通过try-with-resources自动关闭Image实例(你的代码已实现,无需修改)

额外优化建议

  • 避免在saveImageWithLocation中频繁更新ViewModel的setImageFileName和setFileDirectory,仅在必要时执行
  • 检查ensureDirectoryExists()方法,确保目录创建是一次性操作,避免重复判断IO状态
  • Android Q及以上版本,使用MediaStore API替代MediaScannerConnection,更高效地将图片插入系统相册,避免扫描延迟

内容的提问来源于stack exchange,提问作者Luai

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 11:22:02