相机预览流程:从Surface到屏幕的每一帧

0 阅读17分钟

引言:预览,不只是"看个大概"

打开相机App的瞬间,你看到的那一帧实时画面,背后经历了什么?

很多开发者以为相机预览就是"把摄像头的数据显示出来"——听起来简单,做起来却暗藏玄机。一个60fps的流畅预览背后,涉及HAL层数据采集、BufferQueue生产者/消费者模型、SurfaceFlinger合成渲染、以及AF/AE/AWB三大自动控制算法的持续收敛,任何一个环节出问题都可能导致画面卡顿、曝光忽明忽暗、焦点游移不定。

本文将带你完整走一遍Camera2 API的预览流程,从Surface的创建到每一帧图像最终出现在屏幕上,把中间的每个技术环节都说清楚。如果你是相机开发的新手,看完本文你会对预览流程有系统性认识;如果你是老手,也许能从3A控制和性能优化部分找到一些新收获。

一、预览的起点:Surface与SurfaceTexture

在Camera2中,预览的本质是将相机数据输出到一个Surface。Surface是Android图形系统的核心抽象,它代表一块可以被渲染的缓冲区队列。预览用的Surface通常来自两个来源:TextureViewSurfaceView

1.1 TextureView vs SurfaceView

这是Camera开发中最常见的选择题,两者各有利弊:

特性TextureViewSurfaceView
渲染方式在View树中渲染(主线程合成)独立Surface,SurfaceFlinger直接合成
动画支持✅ 支持旋转、缩放、透明度动画❌ 不支持普通View动画
性能开销较高(需要GPU合成到View树)较低(硬件直接合成)
圆角/裁剪容易实现需要额外处理
适用场景需要复杂UI效果的预览性能敏感场景、大尺寸预览
// 方案一:使用 TextureView
public class CameraPreviewActivity extends AppCompatActivity
        implements TextureView.SurfaceTextureListener {

    private TextureView mTextureView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mTextureView = new TextureView(this);
        mTextureView.setSurfaceTextureListener(this);
        setContentView(mTextureView);
    }

    @Override
    public void onSurfaceTextureAvailable(SurfaceTexture surface,
                                          int width, int height) {
        // SurfaceTexture 就绪,可以开始打开相机
        openCamera(width, height);
    }

    @Override
    public void onSurfaceTextureSizeChanged(SurfaceTexture surface,
                                            int width, int height) {
        // 尺寸变化,需要重新配置预览(如旋转后)
        configureTransform(width, height);
    }

    @Override
    public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
        // Surface 销毁,需要关闭相机
        closeCamera();
        return true;
    }

    @Override
    public void onSurfaceTextureUpdated(SurfaceTexture surface) {
        // 每帧更新时回调,可用于帧率统计
    }
}

1.2 SurfaceTexture的内部机制

TextureView内部持有一个SurfaceTexture,这是整个预览管道的关键组件。SurfaceTextureBufferQueue拿到相机输出的图像帧后,会将其作为OpenGL ES纹理附着到GPU,再由TextureView渲染到屏幕。

相机HAL → BufferQueue → SurfaceTexture → OpenGL纹理 → TextureView → SurfaceFlinger → 屏幕

SurfaceTexture的核心接口是updateTexImage(),调用后会将最新的帧更新为当前的OpenGL纹理。TextureView会在每次onSurfaceTextureUpdated()回调时自动调用这个方法,你通常不需要手动调用——但如果你需要精确控制帧的消费时机(比如做帧率控制),可以手动接管这个流程。

1.3 选择合适的预览尺寸

这是一个容易踩坑的地方。相机支持的预览尺寸由CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP决定,你需要从中选出最合适的尺寸:

private Size chooseOptimalSize(Size[] choices, int textureViewWidth,
                               int textureViewHeight, Size aspectRatio) {
    // 收集满足最小尺寸要求的候选项
    List<Size> bigEnough = new ArrayList<>();
    List<Size> notBigEnough = new ArrayList<>();

    int w = aspectRatio.getWidth();
    int h = aspectRatio.getHeight();

    for (Size option : choices) {
        // 宽高比必须匹配(允许小误差)
        if (option.getHeight() == option.getWidth() * h / w) {
            if (option.getWidth() >= textureViewWidth &&
                option.getHeight() >= textureViewHeight) {
                bigEnough.add(option);
            } else {
                notBigEnough.add(option);
            }
        }
    }

    // 优先选择满足条件的最小尺寸(节省带宽),否则选最大的
    if (!bigEnough.isEmpty()) {
        return Collections.min(bigEnough, new CompareSizesByArea());
    } else if (!notBigEnough.isEmpty()) {
        return Collections.max(notBigEnough, new CompareSizesByArea());
    } else {
        Log.e(TAG, "找不到合适的预览尺寸");
        return choices[0];
    }
}

关键原则:预览尺寸的宽高比必须与拍照尺寸的宽高比保持一致,否则预览画面会出现拉伸变形。这是一个让很多新手困惑的问题——"我的预览怎么看起来胖了?"——十有八九就是宽高比没对齐。

二、配置预览管道:CaptureSession的建立

有了Surface之后,下一步是建立CameraCaptureSession,这是Camera2中代表"相机配置状态"的核心类。

2.1 打开相机设备

private void openCamera(int width, int height) {
    // 权限检查(必须在运行时申请 CAMERA 权限)
    if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
            != PackageManager.PERMISSION_GRANTED) {
        requestCameraPermission();
        return;
    }

    // 设置预览尺寸(宽高比对齐拍照尺寸)
    setUpCameraOutputs(width, height);

    // 调整TextureView的变换矩阵,处理旋转问题
    configureTransform(width, height);

    CameraManager manager = (CameraManager) getSystemService(Context.CAMERA_SERVICE);
    try {
        // 获取相机ID(通常0是后置,1是前置)
        String cameraId = manager.getCameraIdList()[0];

        // 异步打开相机,结果通过 CameraDevice.StateCallback 回调
        manager.openCamera(cameraId, mStateCallback, mBackgroundHandler);
    } catch (CameraAccessException e) {
        e.printStackTrace();
    }
}

private final CameraDevice.StateCallback mStateCallback =
        new CameraDevice.StateCallback() {

    @Override
    public void onOpened(@NonNull CameraDevice cameraDevice) {
        // 相机已打开,保存设备引用,开始创建预览会话
        mCameraDevice = cameraDevice;
        createCameraPreviewSession();
    }

    @Override
    public void onDisconnected(@NonNull CameraDevice cameraDevice) {
        cameraDevice.close();
        mCameraDevice = null;
    }

    @Override
    public void onError(@NonNull CameraDevice cameraDevice, int error) {
        cameraDevice.close();
        mCameraDevice = null;
        // 错误码:ERROR_CAMERA_IN_USE, ERROR_MAX_CAMERAS_IN_USE 等
        Log.e(TAG, "相机打开失败,错误码:" + error);
    }
};

2.2 创建CaptureSession

createCameraPreviewSession()是配置预览的核心方法。Camera2采用"会话"模型:你告诉相机"我要输出到这些Surface",相机硬件会据此配置数据通路:

private void createCameraPreviewSession() {
    try {
        SurfaceTexture texture = mTextureView.getSurfaceTexture();

        // 设置默认缓冲区尺寸(必须!否则缓冲区大小是默认值)
        texture.setDefaultBufferSize(mPreviewSize.getWidth(),
                                     mPreviewSize.getHeight());

        // 从 SurfaceTexture 创建 Surface(相机输出目标)
        Surface previewSurface = new Surface(texture);

        // 构建预览请求(使用 TEMPLATE_PREVIEW 模板)
        mPreviewRequestBuilder = mCameraDevice.createCaptureRequest(
                CameraDevice.TEMPLATE_PREVIEW);
        mPreviewRequestBuilder.addTarget(previewSurface);

        // 创建 CaptureSession,传入输出 Surface 列表
        // 注意:此处可以同时传入预览Surface和拍照Surface,一次性配置
        mCameraDevice.createCaptureSession(
                Arrays.asList(previewSurface, mImageReader.getSurface()),
                new CameraCaptureSession.StateCallback() {

                    @Override
                    public void onConfigured(@NonNull CameraCaptureSession session) {
                        if (mCameraDevice == null) return;

                        mCaptureSession = session;
                        // Session 配置完成,启动预览
                        startPreview();
                    }

                    @Override
                    public void onConfigureFailed(@NonNull CameraCaptureSession session) {
                        // 配置失败,通常是 Surface 格式/尺寸不被支持
                        Log.e(TAG, "CaptureSession 配置失败");
                    }
                },
                mBackgroundHandler
        );
    } catch (CameraAccessException e) {
        e.printStackTrace();
    }
}

重要细节createCaptureSession()传入的Surface列表决定了相机的工作模式,这个配置一旦完成就不能修改——如果你需要增减输出目标(比如开始录像),必须关闭当前Session并重新创建一个新的。这也是为什么在Camera2中,同时配置好预览Surface和拍照Surface是最佳实践。

2.3 预览数据流架构图

下图展示了从App层到最终显示的完整数据流路径:

03-01-camera-preview-flow.png

整个预览管道可以分为五层:

  • App层:TextureView/Surface 作为数据接收端
  • CameraService层:负责Session管理和请求调度
  • HAL3层:相机硬件抽象层,驱动实际传感器
  • BufferQueue层:生产者/消费者模型的图像缓冲队列
  • Display层:SurfaceFlinger负责最终的屏幕合成

三、启动连续预览:setRepeatingRequest详解

3.1 TEMPLATE_PREVIEW模板

Camera2提供了多个CaptureRequest模板,TEMPLATE_PREVIEW是专为预览场景优化的:

private void startPreview() {
    try {
        // 使用 TEMPLATE_PREVIEW 模板,已预设适合预览的参数
        // - 自动曝光模式:CONTROL_AE_MODE_ON
        // - 自动白平衡:CONTROL_AWB_MODE_AUTO
        // - 自动对焦:CONTROL_AF_MODE_CONTINUOUS_PICTURE(大多数设备)
        // - 噪点消除:NOISE_REDUCTION_MODE_FAST

        // 设置AF模式为连续自动对焦
        mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE,
                CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);

        // 设置闪光灯自动(适用于有闪光灯的设备)
        mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE,
                CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);

        // 构建请求
        mPreviewRequest = mPreviewRequestBuilder.build();

        // 启动连续预览(重复请求)
        mCaptureSession.setRepeatingRequest(
                mPreviewRequest,
                mCaptureCallback,     // 每帧的回调
                mBackgroundHandler    // 回调线程
        );
    } catch (CameraAccessException e) {
        e.printStackTrace();
    }
}

setRepeatingRequest()capture()的区别:

  • capture():单次拍摄,执行一次后停止
  • setRepeatingRequest():持续重复,直到调用stopRepeating()或Session关闭
  • setRepeatingBurst():持续重复一个请求序列(常用于连拍)

3.2 CaptureCallback:预览帧的元数据

每帧预览完成后,CameraService会回调CaptureCallback,传递该帧的元数据(注意:不是图像数据,图像数据通过Surface直接传输):

private final CameraCaptureSession.CaptureCallback mCaptureCallback =
        new CameraCaptureSession.CaptureCallback() {

    @Override
    public void onCaptureProgressed(@NonNull CameraCaptureSession session,
                                    @NonNull CaptureRequest request,
                                    @NonNull CaptureResult partialResult) {
        // 部分结果(Partial Result):某些参数提前到达,如AF/AE状态
        // 可用于更快响应AF状态变化,减少延迟
        processPreviewResult(partialResult);
    }

    @Override
    public void onCaptureCompleted(@NonNull CameraCaptureSession session,
                                   @NonNull CaptureRequest request,
                                   @NonNull TotalCaptureResult result) {
        // 完整结果:该帧所有元数据都已可用
        processPreviewResult(result);
    }

    @Override
    public void onCaptureFailed(@NonNull CameraCaptureSession session,
                                @NonNull CaptureRequest request,
                                @NonNull CaptureFailure failure) {
        // 帧处理失败(可能因为硬件临时故障或资源不足)
        Log.w(TAG, "预览帧处理失败,原因:" + failure.getReason());
    }
};

private void processPreviewResult(CaptureResult result) {
    // 读取AF状态
    Integer afState = result.get(CaptureResult.CONTROL_AF_STATE);
    // 读取AE状态
    Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE);
    // 读取AWB状态
    Integer awbState = result.get(CaptureResult.CONTROL_AWB_STATE);

    // 可以根据这些状态更新UI(如显示对焦框)
}

四、3A自动控制:让每一帧都"好看"

3A控制(AF自动对焦、AE自动曝光、AWB自动白平衡)是相机预览质量的核心。三个算法协同工作,持续分析每一帧图像,动态调整相机参数。

03-02-3a-control-mechanism.png

4.1 AF自动对焦

AF(Auto Focus)的核心任务是驱动镜头运动,使目标物体在传感器上成像最清晰。

常用AF模式

// 连续对焦(预览阶段推荐):相机持续检测并调整焦点
mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE,
        CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);

// 单次对焦:触发一次后锁定
mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE,
        CaptureRequest.CONTROL_AF_MODE_AUTO);

// 手动对焦:直接指定镜头位置(0.0f=无穷远,≥10.0f=近距离)
mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE,
        CaptureRequest.CONTROL_AF_MODE_OFF);
mPreviewRequestBuilder.set(CaptureRequest.LENS_FOCUS_DISTANCE, 5.0f);

AF状态机

AF系统是一个状态机,关键状态包括:

  • CONTROL_AF_STATE_INACTIVE:未激活
  • CONTROL_AF_STATE_PASSIVE_SCAN:连续对焦扫描中
  • CONTROL_AF_STATE_PASSIVE_FOCUSED:连续对焦已收敛(焦点稳定)
  • CONTROL_AF_STATE_ACTIVE_SCAN:单次对焦执行中
  • CONTROL_AF_STATE_FOCUSED_LOCKED:对焦成功并锁定
  • CONTROL_AF_STATE_NOT_FOCUSED_LOCKED:对焦失败并锁定(画面可能模糊)

实现点击对焦

// 手指触摸屏幕时,将触摸区域转换为传感器坐标,设置AF测量区域
private void tapToFocus(MotionEvent event) {
    // 获取传感器活跃区域
    Rect sensorArraySize = mCameraCharacteristics.get(
            CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE);

    // 将触摸坐标转换为传感器坐标
    int x = (int)(event.getX() / mTextureView.getWidth() * sensorArraySize.width());
    int y = (int)(event.getY() / mTextureView.getHeight() * sensorArraySize.height());

    // 创建50x50像素的测量区域,权重为MeteringRectangle.METERING_WEIGHT_MAX
    int halfTouchWidth = 50;
    int halfTouchHeight = 50;
    MeteringRectangle focusArea = new MeteringRectangle(
            Math.max(x - halfTouchWidth, 0),
            Math.max(y - halfTouchHeight, 0),
            halfTouchWidth * 2,
            halfTouchHeight * 2,
            MeteringRectangle.METERING_WEIGHT_MAX - 1
    );

    // 取消之前的连续对焦,切换到单次对焦模式
    mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE,
            CaptureRequest.CONTROL_AF_MODE_AUTO);
    mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_REGIONS,
            new MeteringRectangle[]{focusArea});

    // 发送AF触发指令
    mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER,
            CameraMetadata.CONTROL_AF_TRIGGER_START);

    try {
        mCaptureSession.capture(mPreviewRequestBuilder.build(),
                mAfCaptureCallback, mBackgroundHandler);
    } catch (CameraAccessException e) {
        e.printStackTrace();
    }
}

4.2 AE自动曝光

AE(Auto Exposure)控制传感器曝光时间(快门速度)、ISO感光度和镜头光圈(如果是可变光圈镜头),让画面亮度保持在合适范围。

AE模式和测光区域

// 自动曝光,不带闪光灯
mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE,
        CaptureRequest.CONTROL_AE_MODE_ON);

// 自动曝光补偿:-3 到 +3 EV(具体范围由 AE_COMPENSATION_RANGE 决定)
// 1步 = 1/3 EV(由 AE_COMPENSATION_STEP 决定)
mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION, 3); // +1 EV

// 设置AE测光区域(与AF区域类似,但权重影响曝光计算)
mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_REGIONS,
        new MeteringRectangle[]{aeArea});

// 锁定AE(拍照前常用,防止拍摄瞬间曝光突变)
mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_LOCK, true);

AE状态机关键状态

  • CONTROL_AE_STATE_CONVERGED:曝光已收敛(画面亮度稳定)
  • CONTROL_AE_STATE_SEARCHING:正在搜索合适曝光值(画面可能闪烁)
  • CONTROL_AE_STATE_LOCKED:曝光已锁定
  • CONTROL_AE_STATE_FLASH_REQUIRED:环境太暗,建议触发闪光

4.3 AWB自动白平衡

AWB(Auto White Balance)分析画面中的白色/灰色区域,推断当前光源的色温,并调整各颜色通道增益,使白色物体看起来真正是白色。

// 自动白平衡(绝大多数场景推荐)
mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AWB_MODE,
        CaptureRequest.CONTROL_AWB_MODE_AUTO);

// 预设场景模式(用于特殊光源场景)
mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AWB_MODE,
        CaptureRequest.CONTROL_AWB_MODE_DAYLIGHT);    // 日光(5500K)
mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AWB_MODE,
        CaptureRequest.CONTROL_AWB_MODE_INCANDESCENT); // 白炽灯(3000K)
mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AWB_MODE,
        CaptureRequest.CONTROL_AWB_MODE_FLUORESCENT);  // 荧光灯(4000K)

// 锁定AWB(与AE一起锁定,保持拍摄前后颜色一致)
mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AWB_LOCK, true);

4.4 3A收敛状态的监控

在进行拍照前,通常需要等待3A收敛完成,否则可能拍出模糊或曝光不正确的照片:

// 综合检查3A是否已收敛
private boolean is3AConverged(CaptureResult result) {
    Integer afState = result.get(CaptureResult.CONTROL_AF_STATE);
    Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE);

    // AF收敛条件:已对焦锁定、或者连续对焦稳定
    boolean afReady = (afState == null) ||
            afState == CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED ||
            afState == CaptureResult.CONTROL_AF_STATE_PASSIVE_FOCUSED;

    // AE收敛条件:已收敛或已锁定
    boolean aeReady = (aeState == null) ||
            aeState == CaptureResult.CONTROL_AE_STATE_CONVERGED ||
            aeState == CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED ||
            aeState == CaptureResult.CONTROL_AE_STATE_LOCKED;

    return afReady && aeReady;
}

五、预览帧的图像数据获取

上文提到,CaptureCallback只携带元数据,实际图像数据通过Surface直接传输。如果你需要访问预览帧的像素数据(比如做实时滤镜、二维码扫描、AI推理),需要使用ImageReader

5.1 配置ImageReader

// 创建 ImageReader,指定格式和最大帧缓冲数量
// YUV_420_888 是Camera2原生格式,CPU处理效率高
mImageReader = ImageReader.newInstance(
        mPreviewSize.getWidth(),
        mPreviewSize.getHeight(),
        ImageFormat.YUV_420_888,
        2  // 最多缓冲2帧(过多会增加延迟,过少可能丢帧)
);

mImageReader.setOnImageAvailableListener(mOnImageAvailableListener,
        mBackgroundHandler);

// 将 ImageReader 的 Surface 添加到 CaptureSession 的输出目标
// (需要在 createCaptureSession() 时一起传入)
mPreviewRequestBuilder.addTarget(mImageReader.getSurface());

5.2 处理YUV图像帧

private final ImageReader.OnImageAvailableListener mOnImageAvailableListener =
        new ImageReader.OnImageAvailableListener() {

    @Override
    public void onImageAvailable(ImageReader reader) {
        // 必须调用 acquireLatestImage(),且必须在 try-with-resources 中关闭
        // 忘记关闭会导致缓冲区耗尽,预览卡死
        try (Image image = reader.acquireLatestImage()) {
            if (image == null) return;

            // YUV_420_888 格式:Y平面(亮度) + U平面(色度) + V平面(色度)
            Image.Plane[] planes = image.getPlanes();
            ByteBuffer yBuffer = planes[0].getBuffer();  // Y分量
            ByteBuffer uBuffer = planes[1].getBuffer();  // U分量
            ByteBuffer vBuffer = planes[2].getBuffer();  // V分量

            int ySize = yBuffer.remaining();
            int uSize = uBuffer.remaining();
            int vSize = vBuffer.remaining();

            byte[] nv21 = new byte[ySize + uSize + vSize];
            yBuffer.get(nv21, 0, ySize);
            vBuffer.get(nv21, ySize, vSize);       // NV21: V在U前面
            uBuffer.get(nv21, ySize + vSize, uSize);

            // 将 NV21 转换为 Bitmap(如果需要显示或进一步处理)
            YuvImage yuvImage = new YuvImage(nv21, ImageFormat.NV21,
                    image.getWidth(), image.getHeight(), null);

            // 进行图像分析(如二维码识别、人脸检测等)
            analyzeFrame(yuvImage, image.getWidth(), image.getHeight());

        } catch (Exception e) {
            Log.e(TAG, "处理预览帧时出错", e);
        }
    }
};

注意ImageReader的帧获取有两个方法:

  • acquireLatestImage():丢弃所有旧帧,只获取最新一帧(推荐!适合实时处理)
  • acquireNextImage():按顺序获取,可能造成处理延迟堆积

5.3 预览与ImageReader共存的性能代价

同时使用TextureView预览和ImageReader获取帧,意味着相机数据会被复制两份(分别发到两个Surface)。对于高分辨率或高帧率场景,这会显著增加内存带宽和CPU负载。

常见优化方案:

  • 降低ImageReader分辨率:分析任务不需要全分辨率,640x480往往够用
  • 降低ImageReader帧率:只分析每N帧,通过时间戳过滤跳过多余帧
  • GPU加速:使用OpenGL/RenderScript直接在GPU上处理SurfaceTexture的纹理

六、性能优化:追求流畅与低延迟

6.1 帧率控制

Camera2可以通过AE_TARGET_FPS_RANGE精确控制帧率:

// 查询支持的帧率范围
Range<Integer>[] fpsRanges = mCameraCharacteristics.get(
        CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES);

// 选择最高帧率(通常是[30,30]固定30fps或[15,30]可变帧率)
Range<Integer> targetFps = null;
for (Range<Integer> range : fpsRanges) {
    if (range.getUpper() >= 30) {
        if (targetFps == null ||
                range.getLower() > targetFps.getLower()) {
            targetFps = range;
        }
    }
}

// 设置目标帧率
if (targetFps != null) {
    mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE,
            targetFps);
}

固定帧率(如[30,30])vs 可变帧率(如[15,30])的选择:

  • 固定30fps:画面更稳定,视觉体验更好,但弱光下可能曝光不足
  • 可变帧率:弱光时降低帧率以获得更长曝光时间,画面亮度更好,但可能出现帧率抖动

6.2 减少预览延迟

预览延迟(从光线进入镜头到屏幕显示的时间)影响用户体验。主要优化点:

1. 使用SurfaceView代替TextureView:SurfaceView绕过View树,直接由SurfaceFlinger合成,延迟约少1个VSYNC周期(16ms @60fps)。

2. 合理设置BufferQueue深度ImageReadermaxImages参数控制缓冲队列深度,值越小延迟越低,但丢帧风险越高。预览场景推荐设置为2。

3. 避免主线程处理:所有CameraService回调都应在后台线程执行,避免阻塞UI线程:

// 创建专用的HandlerThread(相机后台线程)
private HandlerThread mBackgroundThread;
private Handler mBackgroundHandler;

private void startBackgroundThread() {
    mBackgroundThread = new HandlerThread("CameraBackground");
    mBackgroundThread.start();
    mBackgroundHandler = new Handler(mBackgroundThread.getLooper());
}

private void stopBackgroundThread() {
    mBackgroundThread.quitSafely();
    try {
        mBackgroundThread.join();
        mBackgroundThread = null;
        mBackgroundHandler = null;
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

6.3 预览旋转处理

这是Camera2开发中最令人头疼的问题之一。相机传感器有自己的"自然方向"(通常横屏),而手机屏幕默认竖屏显示,加上前摄还有镜像问题,不处理好就会出现预览画面旋转90°或左右镜像的情况。

private void configureTransform(int viewWidth, int viewHeight) {
    // 获取相机传感器方向(相对于设备自然方向的旋转角度)
    int sensorOrientation = mCameraCharacteristics.get(
            CameraCharacteristics.SENSOR_ORIENTATION);

    // 获取当前屏幕旋转角度
    int rotation = getWindowManager().getDefaultDisplay().getRotation();

    Matrix matrix = new Matrix();
    RectF viewRect = new RectF(0, 0, viewWidth, viewHeight);
    RectF bufferRect = new RectF(0, 0,
            mPreviewSize.getHeight(), mPreviewSize.getWidth());

    float centerX = viewRect.centerX();
    float centerY = viewRect.centerY();

    if (Surface.ROTATION_90 == rotation || Surface.ROTATION_270 == rotation) {
        bufferRect.offset(centerX - bufferRect.centerX(),
                         centerY - bufferRect.centerY());
        matrix.setRectToRect(viewRect, bufferRect, Matrix.ScaleToFit.FILL);
        float scale = Math.max(
                (float) viewHeight / mPreviewSize.getHeight(),
                (float) viewWidth / mPreviewSize.getWidth());
        matrix.postScale(scale, scale, centerX, centerY);
        matrix.postRotate(90 * (rotation - 2), centerX, centerY);
    }

    mTextureView.setTransform(matrix);
}

七、调试工具与常见问题排查

7.1 Camera2调试利器

1. CameraManager.openCamera() 失败原因分析

错误码原因解决方案
ERROR_CAMERA_IN_USE另一个应用正在使用相机等待其释放或提高App优先级
ERROR_MAX_CAMERAS_IN_USE打开的相机数量超出系统限制关闭不需要的相机设备
ERROR_CAMERA_DISABLED相机被设备策略禁用检查企业设备策略
ERROR_CAMERA_DEVICE相机硬件发生致命错误重新打开相机
ERROR_CAMERA_SERVICECameraService崩溃系统级问题,通常需要重启

2. 预览卡顿排查

# 查看相机相关进程状态
adb shell ps -A | grep camera

# 抓取Camera相关日志
adb logcat -s CameraDevice CameraService Camera2 | grep -v "DEBUG"

# 查看SurfaceFlinger帧率统计(是否有丢帧)
adb shell dumpsys SurfaceFlinger | grep "FPS"

# 查看相机服务状态
adb shell dumpsys media.camera

3. 3A状态追踪

开发阶段可以在onCaptureCompleted()中打印3A状态,观察收敛过程:

private void logCaptureResult(CaptureResult result) {
    String[] AF_STATES = {"INACTIVE", "PASSIVE_SCAN", "PASSIVE_FOCUSED",
                          "ACTIVE_SCAN", "FOCUSED_LOCKED", "NOT_FOCUSED_LOCKED"};
    String[] AE_STATES = {"INACTIVE", "SEARCHING", "CONVERGED",
                          "LOCKED", "FLASH_REQUIRED", "PRECAPTURE"};

    Integer afState = result.get(CaptureResult.CONTROL_AF_STATE);
    Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE);

    if (afState != null && aeState != null) {
        Log.d(TAG, String.format("AF: %s | AE: %s",
                AF_STATES[afState], AE_STATES[aeState]));
    }
}

7.2 常见坑位汇总

  1. 预览画面拉伸变形:预览尺寸宽高比与SurfaceTexture/TextureView比例不匹配,调用configureTransform()修正。

  2. 预览黑屏:忘记调用texture.setDefaultBufferSize()设置SurfaceTexture的缓冲区尺寸,默认1x1显示黑屏。

  3. ImageReader满了,预览卡住acquireLatestImage()获取到Image后忘记调用close(),缓冲区耗尽后新帧无法入队。

  4. 相机Session重建后闪屏:拍照或切换录像模式时需要重建Session,新旧Session切换瞬间会短暂黑屏,可以在TextureView上盖一个渐变动画掩盖。

  5. 前置摄像头预览左右镜像:前摄传感器通常有镜像标志(LENS_FACING_FRONT),需要在configureTransform()中额外处理水平翻转。

八、实战案例:实现完整的相机预览

综合以上内容,下面是一个完整的、可运行的相机预览示例:

public class CameraPreviewFragment extends Fragment {

    private static final String TAG = "CameraPreview";

    private TextureView mTextureView;
    private CameraDevice mCameraDevice;
    private CameraCaptureSession mCaptureSession;
    private CaptureRequest.Builder mPreviewRequestBuilder;
    private HandlerThread mBackgroundThread;
    private Handler mBackgroundHandler;
    private Size mPreviewSize;

    // === 生命周期管理 ===

    @Override
    public void onResume() {
        super.onResume();
        startBackgroundThread();
        if (mTextureView.isAvailable()) {
            openCamera(mTextureView.getWidth(), mTextureView.getHeight());
        } else {
            mTextureView.setSurfaceTextureListener(mSurfaceTextureListener);
        }
    }

    @Override
    public void onPause() {
        closeCamera();
        stopBackgroundThread();
        super.onPause();
    }

    // === TextureView 监听器 ===

    private final TextureView.SurfaceTextureListener mSurfaceTextureListener =
            new TextureView.SurfaceTextureListener() {
        @Override
        public void onSurfaceTextureAvailable(SurfaceTexture st, int w, int h) {
            openCamera(w, h);
        }
        @Override
        public void onSurfaceTextureSizeChanged(SurfaceTexture st, int w, int h) {
            configureTransform(w, h);
        }
        @Override
        public boolean onSurfaceTextureDestroyed(SurfaceTexture st) {
            return true;
        }
        @Override
        public void onSurfaceTextureUpdated(SurfaceTexture st) {}
    };

    // === 相机打开/关闭 ===

    private void openCamera(int width, int height) {
        if (ContextCompat.checkSelfPermission(requireContext(),
                Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
            return;
        }

        CameraManager manager = (CameraManager)
                requireContext().getSystemService(Context.CAMERA_SERVICE);
        try {
            String cameraId = manager.getCameraIdList()[0];
            CameraCharacteristics characteristics =
                    manager.getCameraCharacteristics(cameraId);

            StreamConfigurationMap map = characteristics.get(
                    CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
            Size[] outputSizes = map.getOutputSizes(SurfaceTexture.class);
            mPreviewSize = chooseOptimalSize(outputSizes, width, height,
                    outputSizes[0]); // 简化:用第一个尺寸的宽高比

            manager.openCamera(cameraId, new CameraDevice.StateCallback() {
                @Override
                public void onOpened(@NonNull CameraDevice camera) {
                    mCameraDevice = camera;
                    createCaptureSession();
                }
                @Override
                public void onDisconnected(@NonNull CameraDevice camera) {
                    camera.close();
                }
                @Override
                public void onError(@NonNull CameraDevice camera, int error) {
                    camera.close();
                    Log.e(TAG, "相机错误: " + error);
                }
            }, mBackgroundHandler);

        } catch (CameraAccessException e) {
            Log.e(TAG, "相机访问异常", e);
        }
    }

    private void createCaptureSession() {
        try {
            SurfaceTexture texture = mTextureView.getSurfaceTexture();
            texture.setDefaultBufferSize(mPreviewSize.getWidth(),
                                         mPreviewSize.getHeight());
            Surface surface = new Surface(texture);

            mPreviewRequestBuilder = mCameraDevice.createCaptureRequest(
                    CameraDevice.TEMPLATE_PREVIEW);
            mPreviewRequestBuilder.addTarget(surface);
            mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE,
                    CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);

            mCameraDevice.createCaptureSession(
                    Collections.singletonList(surface),
                    new CameraCaptureSession.StateCallback() {
                        @Override
                        public void onConfigured(@NonNull CameraCaptureSession session) {
                            mCaptureSession = session;
                            try {
                                mCaptureSession.setRepeatingRequest(
                                        mPreviewRequestBuilder.build(),
                                        null, mBackgroundHandler);
                            } catch (CameraAccessException e) {
                                Log.e(TAG, "启动预览失败", e);
                            }
                        }
                        @Override
                        public void onConfigureFailed(@NonNull CameraCaptureSession session) {
                            Log.e(TAG, "Session 配置失败");
                        }
                    }, mBackgroundHandler);

        } catch (CameraAccessException e) {
            Log.e(TAG, "创建 CaptureSession 失败", e);
        }
    }

    private void closeCamera() {
        if (mCaptureSession != null) {
            mCaptureSession.close();
            mCaptureSession = null;
        }
        if (mCameraDevice != null) {
            mCameraDevice.close();
            mCameraDevice = null;
        }
    }
}

总结

本文完整梳理了Camera2 API预览流程的核心技术链路:

  1. Surface创建:TextureView/SurfaceView各有适用场景,SurfaceTexture是连接相机数据和GPU渲染的桥梁。预览尺寸的宽高比对齐是最容易忽略的细节。

  2. CaptureSession配置:Session是Camera2的核心配置容器,创建时需要一次性传入所有输出Surface。任何对输出目标的修改都需要重建Session。

  3. setRepeatingRequest与TEMPLATE_PREVIEW:持续预览的本质是持续发送重复的CaptureRequest,TEMPLATE_PREVIEW提供了优化过的默认参数,按需覆盖即可。

  4. 3A控制协同:AF/AE/AWB三个算法共享输入帧,相互独立地收敛,通过CaptureResult反馈状态。实现点击对焦、曝光补偿等功能需要理解各自的状态机。

  5. 性能优化关键点:后台线程处理相机回调、合理的ImageReader缓冲深度、正确处理旋转变换是保证流畅预览体验的基础。

掌握了预览流程,你已经具备了Camera2开发的核心基础。下一篇我们将在预览的基础上,深入探讨静态图像拍摄流程——从预捕获的3A锁定、到RAW/JPEG格式选择、再到拍照结果的完整处理链路。

参考资料