相机拍照流程:从快门按下到JPEG存储的完整旅程

0 阅读16分钟

引言:快门延迟,相机App的生死线

打开系统相机,按下快门,你期望照片立刻被拍下来。但现实是,很多相机App的快门延迟长达几百毫秒,甚至更久——孩子笑容定格的瞬间,咔嚓一声,照片里却是已经低头的脑袋。

快门延迟到底从哪里来?怎么优化到最低?

本文将带你走完Camera2 API拍照流程的每一步:从预捕获的3A锁定准备,到拍照请求的发送与处理,再到ZSL零快门延迟的黑科技原理,最后到JPEG文件的保存。把每个环节说清楚,把每个优化点讲透彻。

一、拍照前的准备:预捕获与3A锁定

很多开发者以为拍照就是调用一下capture(),但真正的高质量拍照,正式曝光前必须先完成一个关键步骤:预捕获序列(Precapture Sequence)

1.1 为什么需要预捕获

拍照和预览是不同的场景。预览可以容忍曝光略有波动,但照片必须保证曝光准确——因为照片就一张,没有"下一帧"来修正。

预捕获的目的是:

  • 锁定AE(自动曝光),防止拍摄瞬间曝光突变
  • 锁定AF(自动对焦),确保主体清晰
  • 触发闪光灯预闪(如果需要),让AE根据实际打光情况重新计算

1.2 实现预捕获序列

// 完整的拍照流程分为三个阶段:预捕获→等待收敛→正式拍照
// 本例使用状态机来管理这三个阶段

private static final int STATE_PREVIEW = 0;           // 正常预览
private static final int STATE_WAITING_LOCK = 1;      // 等待对焦锁定
private static final int STATE_WAITING_PRECAPTURE = 2; // 等待预曝光收敛
private static final int STATE_WAITING_NON_PRECAPTURE = 3; // 等待非预曝光状态
private static final int STATE_PICTURE_TAKEN = 4;     // 已拍照

private int mState = STATE_PREVIEW;

// 第一步:触发预捕获序列
private void lockFocus() {
    try {
        // 触发AF锁定
        mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER,
                CameraMetadata.CONTROL_AF_TRIGGER_START);

        // 更新状态为"等待对焦锁定"
        mState = STATE_WAITING_LOCK;

        // 发送一次性请求(不是Repeating),触发AF
        mCaptureSession.capture(mPreviewRequestBuilder.build(),
                mCaptureCallback, mBackgroundHandler);
    } catch (CameraAccessException e) {
        e.printStackTrace();
    }
}

// 第二步:在CaptureCallback中处理状态变化
private void processCapture(CaptureResult result) {
    switch (mState) {
        case STATE_WAITING_LOCK: {
            Integer afState = result.get(CaptureResult.CONTROL_AF_STATE);
            if (afState == null) {
                // 不支持AF的设备,直接触发预捕获
                runPrecaptureSequence();
            } else if (CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED == afState ||
                       CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED == afState) {
                // AF已锁定(无论是否成功),检查AE状态
                Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE);
                if (aeState == null ||
                    aeState == CaptureResult.CONTROL_AE_STATE_CONVERGED) {
                    // AE已收敛,可以直接拍照
                    mState = STATE_PICTURE_TAKEN;
                    captureStillPicture();
                } else {
                    // AE未收敛,触发预捕获
                    runPrecaptureSequence();
                }
            }
            break;
        }

        case STATE_WAITING_PRECAPTURE: {
            // 等待AE进入预捕获状态
            Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE);
            if (aeState == null ||
                aeState == CaptureResult.CONTROL_AE_STATE_PRECAPTURE ||
                aeState == CaptureRequest.CONTROL_AE_STATE_FLASH_REQUIRED) {
                mState = STATE_WAITING_NON_PRECAPTURE;
            }
            break;
        }

        case STATE_WAITING_NON_PRECAPTURE: {
            // 等待AE离开预捕获状态(意味着收敛完成)
            Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE);
            if (aeState == null ||
                aeState != CaptureResult.CONTROL_AE_STATE_PRECAPTURE) {
                mState = STATE_PICTURE_TAKEN;
                captureStillPicture();
            }
            break;
        }
    }
}

// 触发AE预捕获序列
private void runPrecaptureSequence() {
    try {
        mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER,
                CameraMetadata.CONTROL_AE_PRECAPTURE_TRIGGER_START);
        mState = STATE_WAITING_PRECAPTURE;
        mCaptureSession.capture(mPreviewRequestBuilder.build(),
                mCaptureCallback, mBackgroundHandler);
    } catch (CameraAccessException e) {
        e.printStackTrace();
    }
}

这个三阶段状态机是Camera2官方示例Camera2Basic的核心模式,理解它是掌握Camera2拍照流程的基础。

二、静态拍照流程

预捕获完成后,进入正式拍照环节。

2.1 配置ImageReader

拍照需要一个独立的ImageReader来接收相机输出的图像数据:

// 创建JPEG格式的ImageReader
// 注意:只需要1个缓冲,拍照后立即处理,不需要缓冲多张
private void setupImageReader(Size largestSize) {
    mImageReader = ImageReader.newInstance(
            largestSize.getWidth(),
            largestSize.getHeight(),
            ImageFormat.JPEG,
            1  // 最多1个JPEG缓冲(充足)
    );

    mImageReader.setOnImageAvailableListener(
            new ImageReader.OnImageAvailableListener() {
                @Override
                public void onImageAvailable(ImageReader reader) {
                    // 在后台线程中保存图片
                    mBackgroundHandler.post(new ImageSaver(reader.acquireNextImage()));
                }
            },
            mBackgroundHandler
    );
}

格式选择建议

  • ImageFormat.JPEG:直接输出JPEG,无需手动编码,最常用
  • ImageFormat.RAW_SENSOR / RAW10 / RAW12:输出RAW数据,后期处理灵活,但需要手动处理
  • ImageFormat.YUV_420_888:输出YUV数据,可用于自定义编码(如WebP、HEIF)

2.2 拍照参数配置

private void captureStillPicture() {
    try {
        // 使用 TEMPLATE_STILL_CAPTURE 模板(针对拍照优化:更长的降噪时间等)
        CaptureRequest.Builder captureBuilder = mCameraDevice.createCaptureRequest(
                CameraDevice.TEMPLATE_STILL_CAPTURE);
        captureBuilder.addTarget(mImageReader.getSurface());

        // ===== JPEG 输出参数 =====

        // JPEG质量(0-100),95是高质量但文件较大,85是常用权衡值
        captureBuilder.set(CaptureRequest.JPEG_QUALITY, (byte) 95);

        // JPEG方向:补偿传感器方向和设备旋转,确保照片方向正确
        // 不设置的话,照片可能横着或倒着
        int rotation = getWindowManager().getDefaultDisplay().getRotation();
        captureBuilder.set(CaptureRequest.JPEG_ORIENTATION,
                getJpegOrientation(mCameraCharacteristics, rotation));

        // ===== 3A 状态:继承预览参数(已收敛),锁定不动 =====
        captureBuilder.set(CaptureRequest.CONTROL_AF_MODE,
                CaptureRequest.CONTROL_AF_MODE_AUTO);

        // ===== 可选:GPS位置信息 =====
        Location location = getCurrentLocation();
        if (location != null) {
            captureBuilder.set(CaptureRequest.JPEG_GPS_LOCATION, location);
        }

        // ===== 可选:缩略图配置 =====
        // 大部分设备默认会生成缩略图,也可以手动设置
        captureBuilder.set(CaptureRequest.JPEG_THUMBNAIL_SIZE,
                new Size(320, 240));
        captureBuilder.set(CaptureRequest.JPEG_THUMBNAIL_QUALITY, (byte) 80);

        // 停止连续预览请求(避免干扰拍照帧)
        mCaptureSession.stopRepeating();
        mCaptureSession.abortCaptures();

        // 执行单次拍照
        mCaptureSession.capture(captureBuilder.build(),
                new CameraCaptureSession.CaptureCallback() {
                    @Override
                    public void onCaptureCompleted(@NonNull CameraCaptureSession session,
                                                   @NonNull CaptureRequest request,
                                                   @NonNull TotalCaptureResult result) {
                        // 拍照完成,解锁AF/AE,恢复预览
                        unlockFocus();
                    }
                },
                mBackgroundHandler);

    } catch (CameraAccessException e) {
        e.printStackTrace();
    }
}

// 计算JPEG方向(处理传感器方向+设备旋转的综合旋转角度)
private int getJpegOrientation(CameraCharacteristics c, int deviceOrientation) {
    if (deviceOrientation == android.view.OrientationEventListener.ORIENTATION_UNKNOWN)
        return 0;

    int sensorOrientation = c.get(CameraCharacteristics.SENSOR_ORIENTATION);

    // 将设备旋转角度离散化(0/90/180/270)
    deviceOrientation = (deviceOrientation + 45) / 90 * 90;

    // 前置摄像头需要镜像处理
    boolean facingFront = c.get(CameraCharacteristics.LENS_FACING) ==
            CameraCharacteristics.LENS_FACING_FRONT;
    if (facingFront) deviceOrientation = -deviceOrientation;

    // 计算最终旋转角度
    return (sensorOrientation + deviceOrientation + 360) % 360;
}

2.3 拍照数据流图

下图展示了从按下快门到JPEG文件存储的完整流程:

04-01-still-capture-flow.png

整个流程分为App层准备(步骤①~④)、系统层处理(步骤⑤~⑦)、App层后处理(步骤⑧~⑨)三个大阶段,每个阶段都有可以优化的点。

2.4 保存图片文件

// 实现Runnable以便在后台线程执行文件保存
private static class ImageSaver implements Runnable {
    private final Image mImage;

    ImageSaver(Image image) {
        mImage = image;
    }

    @Override
    public void run() {
        ByteBuffer buffer = mImage.getPlanes()[0].getBuffer();
        byte[] bytes = new byte[buffer.remaining()];
        buffer.get(bytes);

        // 写入JPEG数据到临时文件
        File file = createJpegFile();
        try (FileOutputStream output = new FileOutputStream(file)) {
            output.write(bytes);
        } catch (IOException e) {
            Log.e(TAG, "保存图片失败", e);
        } finally {
            mImage.close(); // 必须!释放ImageReader缓冲区
        }

        // Android 10+ 推荐使用 MediaStore API 保存到相册
        saveToMediaStore(file);
    }
}

// Android 10+ 方式:通过 ContentResolver 保存到 MediaStore
private void saveToMediaStore(File sourceFile) {
    ContentValues values = new ContentValues();
    values.put(MediaStore.Images.Media.DISPLAY_NAME,
            "IMG_" + System.currentTimeMillis() + ".jpg");
    values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg");
    values.put(MediaStore.Images.Media.RELATIVE_PATH,
            Environment.DIRECTORY_PICTURES + "/MyCamera");

    // Android 10+ 需要先插入记录获取URI,再写入数据
    ContentResolver resolver = getContentResolver();
    Uri uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);

    if (uri != null) {
        try (OutputStream os = resolver.openOutputStream(uri)) {
            // 将临时文件数据写入MediaStore
            Files.copy(sourceFile.toPath(), os);
            // 标记文件写入完成(去掉PENDING状态)
            values.clear();
            values.put(MediaStore.Images.Media.IS_PENDING, 0);
            resolver.update(uri, values, null, null);
        } catch (IOException e) {
            Log.e(TAG, "MediaStore保存失败", e);
            resolver.delete(uri, null, null);
        }
    }
}

三、连拍(Burst Capture)

快速连续拍摄多张照片,需要用captureBurst()而不是反复调用capture()

3.1 基本连拍实现

private void startBurstCapture(int burstCount) {
    try {
        CaptureRequest.Builder captureBuilder = mCameraDevice.createCaptureRequest(
                CameraDevice.TEMPLATE_STILL_CAPTURE);
        captureBuilder.addTarget(mImageReader.getSurface());
        captureBuilder.set(CaptureRequest.JPEG_QUALITY, (byte) 85);

        // 构建连拍请求列表(每张参数相同,也可以逐张差异化)
        List<CaptureRequest> burstRequests = new ArrayList<>();
        for (int i = 0; i < burstCount; i++) {
            burstRequests.add(captureBuilder.build());
        }

        mCaptureSession.stopRepeating();

        // 提交连拍请求
        mCaptureSession.captureBurst(burstRequests,
                new CameraCaptureSession.CaptureCallback() {
                    private int mCompletedCount = 0;

                    @Override
                    public void onCaptureCompleted(@NonNull CameraCaptureSession session,
                                                   @NonNull CaptureRequest request,
                                                   @NonNull TotalCaptureResult result) {
                        mCompletedCount++;
                        Log.d(TAG, "连拍完成第 " + mCompletedCount + " 张");

                        if (mCompletedCount == burstCount) {
                            // 全部完成,恢复预览
                            unlockFocus();
                        }
                    }
                },
                mBackgroundHandler);

    } catch (CameraAccessException e) {
        e.printStackTrace();
    }
}

3.2 连拍速度的物理限制

连拍速度受多重因素限制:

1. 传感器帧率上限:不同分辨率下,传感器帧率不同。全分辨率下通常是8-15fps,降分辨率可以达到60fps+。

2. ISP处理能力:每帧图像都需要ISP进行降噪、色调映射等处理,高质量模式下耗时更长。

3. JPEG编码速度:硬件JPEG编码通常比传感器帧率快,但RAW输出时软件编码可能成为瓶颈。

4. ImageReader缓冲区maxImages决定了最多能缓存多少帧,超出后会丢帧。连拍时建议设置为5-10:

// 连拍场景的ImageReader配置
mImageReader = ImageReader.newInstance(
        captureSize.getWidth(),
        captureSize.getHeight(),
        ImageFormat.JPEG,
        10  // 支持最多10帧的缓冲队列
);

查询设备最大连拍帧率

// 查询特定尺寸和格式下的最短帧时间(单位:纳秒)
StreamConfigurationMap map = mCameraCharacteristics.get(
        CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);

long minFrameDuration = map.getOutputMinFrameDuration(
        ImageFormat.JPEG,
        captureSize
);

// 转换为帧率
double maxFps = 1e9 / minFrameDuration;
Log.d(TAG, String.format("最大连拍帧率:%.1f fps", maxFps));

3.3 连拍中的AE策略

连拍时通常有两种AE策略选择:

固定曝光(推荐):第一张拍前锁定AE,后续每张曝光完全相同,适合运动抓拍:

captureBuilder.set(CaptureRequest.CONTROL_AE_LOCK, true);

自适应曝光:每张都让AE独立调整,适合光线快速变化的场景,但会有曝光跳变。

四、ZSL零快门延迟

ZSL(Zero Shutter Lag)是高端相机应用的标配技术,目标是把快门延迟压缩到接近于零。

4.1 普通拍照的延迟来源

普通拍照流程(不用ZSL)的典型延迟:

用户按快门 → [AF锁定: ~100ms][AE预捕获: ~150ms][传感器曝光: ~30ms][ISP处理: ~100ms] → 照片就绪
总延迟: 300~500ms

这300-500ms就是快门延迟的主要来源。用户按下快门时,画面内容已经改变了!

4.2 ZSL的工作原理

ZSL的核心思路是:不等你按快门,提前把帧存好了

04-02-zsl-reprocessing.png

ZSL分为两个并行阶段:

阶段一(持续运行,用户无感知)

  1. Preview持续输出帧的同时,HAL还输出一路全尺寸RAW/YUV数据
  2. App将这些帧存入一个环形缓冲队列(通常3-10帧)
  3. 每帧都附带完整的CaptureResult元数据(3A状态、时间戳等)

阶段二(快门按下后立即执行)

  1. 从缓冲队列中选出"时间戳最近且3A已收敛"的帧
  2. 构建重处理(Reprocessing)请求,将该帧送回HAL
  3. HAL对这帧做精细的ISP二次处理(更高质量的降噪、锐化等)
  4. 输出JPEG(耗时约50ms,远低于普通拍照的300ms+)

4.3 实现ZSL

Camera2的ZSL通过**Reprocessing(重处理)**机制实现,需要使用ImageWriter将帧送回HAL:

// === 步骤1:建立ZSL所需的特殊CaptureSession ===
private void createZslSession() {
    // 检查设备是否支持重处理
    Integer maxNumInputStreams = mCameraCharacteristics.get(
            CameraCharacteristics.REQUEST_MAX_NUM_INPUT_STREAMS);
    if (maxNumInputStreams == null || maxNumInputStreams < 1) {
        Log.w(TAG, "设备不支持Reprocessing,退回普通拍照");
        createNormalSession();
        return;
    }

    // 创建输入流配置(用于ZSL帧回传)
    InputConfiguration inputConfig = new InputConfiguration(
            mCaptureSize.getWidth(),
            mCaptureSize.getHeight(),
            ImageFormat.PRIVATE  // ZSL专用格式
    );

    // 创建ZSL环形缓冲(本质是ImageReader,用于存储预捕获帧)
    mZslImageReader = ImageReader.newInstance(
            mCaptureSize.getWidth(),
            mCaptureSize.getHeight(),
            ImageFormat.PRIVATE,  // ZSL使用PRIVATE格式,不需要CPU访问
            10  // 缓冲10帧
    );

    // 使用 createReprocessableCaptureSession 创建支持重处理的Session
    mCameraDevice.createReprocessableCaptureSession(
            inputConfig,
            Arrays.asList(previewSurface, mZslImageReader.getSurface(),
                          mJpegImageReader.getSurface()),
            new CameraCaptureSession.StateCallback() {
                @Override
                public void onConfigured(@NonNull CameraCaptureSession session) {
                    mCaptureSession = session;
                    // 创建ImageWriter,用于将ZSL帧送回HAL
                    mImageWriter = ImageWriter.newInstance(
                            session.getInputSurface(), 2);
                    startZslPreview();
                }
                @Override
                public void onConfigureFailed(@NonNull CameraCaptureSession session) {
                    Log.e(TAG, "ZSL Session配置失败");
                }
            },
            mBackgroundHandler);
}

// === 步骤2:预览时持续存储ZSL帧 ===
// ZSL帧通过mZslImageReader的Surface自动接收,
// 在onImageAvailable中将帧和CaptureResult一起存入环形缓冲

private final LinkedList<Pair<Image, TotalCaptureResult>> mZslRingBuffer =
        new LinkedList<>();
private static final int ZSL_BUFFER_SIZE = 7;

// 在 onImageAvailable 中存储帧
mZslImageReader.setOnImageAvailableListener(reader -> {
    Image image = reader.acquireLatestImage();
    if (image != null) {
        // 存入环形缓冲,超出时丢弃最旧的
        synchronized (mZslRingBuffer) {
            mZslRingBuffer.add(new Pair<>(image, mLatestCaptureResult));
            if (mZslRingBuffer.size() > ZSL_BUFFER_SIZE) {
                Pair<Image, TotalCaptureResult> oldest = mZslRingBuffer.removeFirst();
                oldest.first.close(); // 释放最旧帧
            }
        }
    }
}, mBackgroundHandler);

// === 步骤3:快门按下时,取缓冲帧进行重处理 ===
private void captureWithZsl() {
    Pair<Image, TotalCaptureResult> zslFrame = null;

    synchronized (mZslRingBuffer) {
        // 从后往前找:时间戳最近且AE已收敛的帧
        for (int i = mZslRingBuffer.size() - 1; i >= 0; i--) {
            Pair<Image, TotalCaptureResult> candidate = mZslRingBuffer.get(i);
            Integer aeState = candidate.second.get(CaptureResult.CONTROL_AE_STATE);
            if (aeState != null &&
                aeState == CaptureResult.CONTROL_AE_STATE_CONVERGED) {
                zslFrame = candidate;
                break;
            }
        }
    }

    if (zslFrame == null) {
        Log.w(TAG, "没有合适的ZSL帧,退回普通拍照");
        captureStillPicture();
        return;
    }

    try {
        // 构建重处理请求(基于已有的CaptureResult,继承其3A状态)
        CaptureRequest.Builder reprocessBuilder =
                mCameraDevice.createReprocessCaptureRequest(zslFrame.second);
        reprocessBuilder.addTarget(mJpegImageReader.getSurface());
        reprocessBuilder.set(CaptureRequest.JPEG_QUALITY, (byte) 95);
        reprocessBuilder.set(CaptureRequest.JPEG_ORIENTATION,
                getJpegOrientation(mCameraCharacteristics,
                        getWindowManager().getDefaultDisplay().getRotation()));

        // 将ZSL帧送回HAL的输入流
        mImageWriter.queueInputImage(zslFrame.first);

        // 提交重处理请求
        mCaptureSession.capture(reprocessBuilder.build(),
                mZslCaptureCallback, mBackgroundHandler);

    } catch (CameraAccessException e) {
        e.printStackTrace();
    }
}

4.4 ZSL适用场景与局限

适用场景

  • 日常拍照(光线充足,3A容易收敛)
  • 人像抓拍(要求即时响应)
  • 连拍场景(减少首帧延迟)

不适用场景

  • 需要闪光灯的暗光场景(ZSL帧是在闪光前拍摄的,曝光不足)
  • 慢速快门拍摄(ZSL帧的曝光时间由3A决定,不适合长曝光)
  • 手动曝光模式(ZSL帧的参数固定,无法用于手动控制)

这也是为什么真正的相机App会根据当前场景自动切换ZSL和普通拍照模式,而不是无脑用ZSL。

五、拍照后处理

5.1 EXIF元数据写入

EXIF(Exchangeable Image File Format)是JPEG文件内嵌的元数据,包含拍摄时的相机参数、GPS位置、时间戳等信息。Android提供了ExifInterface类来读写这些数据:

private void writeExifData(File jpegFile, TotalCaptureResult captureResult) {
    try {
        ExifInterface exif = new ExifInterface(jpegFile.getAbsolutePath());

        // ===== 相机参数 =====

        // 快门速度(曝光时间)
        Long exposureTime = captureResult.get(CaptureResult.SENSOR_EXPOSURE_TIME);
        if (exposureTime != null) {
            // 单位:纳秒,转换为秒的分数形式,如 "1/100"
            double expSec = exposureTime / 1e9;
            exif.setAttribute(ExifInterface.TAG_EXPOSURE_TIME,
                    String.valueOf(expSec));
        }

        // ISO感光度
        Integer sensitivity = captureResult.get(CaptureResult.SENSOR_SENSITIVITY);
        if (sensitivity != null) {
            exif.setAttribute(ExifInterface.TAG_ISO_SPEED_RATINGS,
                    sensitivity.toString());
        }

        // 光圈值(f/number)
        Float aperture = captureResult.get(CaptureResult.LENS_APERTURE);
        if (aperture != null) {
            exif.setAttribute(ExifInterface.TAG_F_NUMBER,
                    String.valueOf(aperture));
        }

        // 焦距(毫米)
        Float focalLength = captureResult.get(CaptureResult.LENS_FOCAL_LENGTH);
        if (focalLength != null) {
            exif.setAttribute(ExifInterface.TAG_FOCAL_LENGTH,
                    focalLength + "/1");
        }

        // ===== 时间信息 =====
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy:MM:dd HH:mm:ss", Locale.US);
        exif.setAttribute(ExifInterface.TAG_DATETIME,
                sdf.format(new Date(System.currentTimeMillis())));

        // ===== GPS位置(如果有权限且已获取位置)=====
        Location loc = mLastKnownLocation;
        if (loc != null) {
            exif.setGpsInfo(loc);
        }

        exif.saveAttributes();

    } catch (IOException e) {
        Log.e(TAG, "写入EXIF失败", e);
    }
}

5.2 Android 15的HEIF/Ultra HDR支持

Android 15对拍照格式做了重要扩展:

Ultra HDR格式

// 检查设备是否支持Ultra HDR输出
StreamConfigurationMap map = mCameraCharacteristics.get(
        CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
boolean supportsUltraHdr = false;

// Android 15新增:通过JPEG_R格式支持Ultra HDR
if (android.os.Build.VERSION.SDK_INT >= 34) {
    Size[] ultaHdrSizes = map.getOutputSizes(ImageFormat.JPEG_R);
    supportsUltraHdr = (ultaHdrSizes != null && ultaHdrSizes.length > 0);
}

if (supportsUltraHdr) {
    // 使用Ultra HDR格式(JPEG_R = JPEG + Gainmap)
    mImageReader = ImageReader.newInstance(
            captureSize.getWidth(), captureSize.getHeight(),
            ImageFormat.JPEG_R, 1);
}

HEIF格式(需要MediaCodec编码,不是直接来自Camera2): Android 8.0+支持通过HeifWriter将YUV帧编码为HEIF:

HeifWriter heifWriter = new HeifWriter.Builder(outputPath,
        width, height, HeifWriter.INPUT_MODE_BITMAP)
        .setQuality(90)
        .build();
heifWriter.start();
heifWriter.addBitmap(bitmap);
heifWriter.stop(3000 /* timeout ms */);
heifWriter.close();

六、性能优化:把快门延迟压到最低

6.1 快门延迟的关键优化点

优化点1:预先打开相机

很多App在点击拍照按钮时才打开相机,这会增加几百毫秒的初始化延迟。正确做法是在onResume()时就打开相机,进入预览状态:

@Override
protected void onResume() {
    super.onResume();
    startBackgroundThread();
    // 只要TextureView准备好就立刻打开相机
    if (mTextureView.isAvailable()) {
        openCamera(mTextureView.getWidth(), mTextureView.getHeight());
    } else {
        mTextureView.setSurfaceTextureListener(mSurfaceTextureListener);
    }
}

优化点2:提前预热CaptureSession

createCaptureSession()是一个耗时操作(100-300ms),应该在App进入前台时就完成,而不是按快门后才创建:

// 进入预览时,同时配置好拍照Session(预览Surface + 拍照Surface一起传入)
mCameraDevice.createCaptureSession(
        Arrays.asList(previewSurface, mImageReader.getSurface()), // 同时配置两个
        sessionCallback, mBackgroundHandler);

优化点3:连续3A vs 按需3A

连续自动对焦(CONTROL_AF_MODE_CONTINUOUS_PICTURE)会持续运行,按下快门时AF已经基本收敛,无需额外等待。相比按需对焦(按下快门才开始对焦)可节省100ms以上:

// 预览时使用连续对焦模式
mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE,
        CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);

6.2 内存优化

拍照会产生大量内存占用,以1200万像素JPEG(12MP)为例:

  • JPEG压缩后:约4-6MB
  • 解码为Bitmap后:约36MB(4000×3000×3字节)
  • 显示缩略图时不要解码全尺寸
// 只读取EXIF信息而不解码全图(Android 12+)
ExifInterface exif = new ExifInterface(inputStream);
// 获取缩略图(内嵌在JPEG中,不需要解码全图)
Bitmap thumbnail = exif.getThumbnailBitmap();

// 使用 BitmapFactory.Options 降采样
BitmapFactory.Options opts = new BitmapFactory.Options();
opts.inSampleSize = 4; // 缩小4倍采样
Bitmap preview = BitmapFactory.decodeFile(filePath, opts);

6.3 避免ANR的最佳实践

相机所有操作都应在后台线程执行,否则容易触发ANR:

// ❌ 错误:在主线程中等待拍照完成
mCaptureSession.capture(request, null, null); // null handler = 主线程回调,危险!

// ✅ 正确:指定后台handler处理回调
mCaptureSession.capture(request, mCaptureCallback, mBackgroundHandler);

// ✅ 正确:文件IO也在后台线程
mBackgroundHandler.post(() -> saveJpegToFile(imageBytes));

七、调试技巧

7.1 拍照延迟分析

通过在关键节点打时间戳来定位延迟瓶颈:

// 在快门按下时记录时间
long t0 = System.nanoTime();
lockFocus();

// 在预捕获完成时
long t1 = System.nanoTime();
Log.d(TAG, String.format("预捕获耗时:%.1f ms", (t1 - t0) / 1e6));

// 在onCaptureCompleted中
long t2 = System.nanoTime();
Log.d(TAG, String.format("拍照总耗时:%.1f ms", (t2 - t0) / 1e6));

也可以用systrace进行更精细的分析:

# 抓取Camera相关的systrace
adb shell atrace --async_start -b 32768 camera hal
adb shell atrace --async_stop -o /data/local/tmp/camera_trace.ctrace
adb pull /data/local/tmp/camera_trace.ctrace

7.2 常见问题排查

问题1:照片出现模糊

可能原因:

  • AF未收敛就拍照 → 检查是否等待AF_STATE_FOCUSED_LOCKED
  • 快门速度太慢导致运动模糊 → 检查SENSOR_EXPOSURE_TIME是否过长
  • 手抖 → 建议开启OIS(光学防抖):captureBuilder.set(CaptureRequest.LENS_OPTICAL_STABILIZATION_MODE, LENS_OPTICAL_STABILIZATION_MODE_ON)

问题2:照片曝光不准确

可能原因:

  • AE未收敛就拍照 → 检查AE_STATE_CONVERGED状态
  • 测光区域不合适 → 设置CONTROL_AE_REGIONS指向主体
  • 曝光补偿设置不当 → 检查CONTROL_AE_EXPOSURE_COMPENSATION

问题3:照片方向不对(旋转90°或镜像)

最常见原因是没有正确设置JPEG_ORIENTATION。使用getJpegOrientation()方法综合考虑传感器方向和设备旋转:

# 查看照片的EXIF方向标志
adb shell exiftool /sdcard/DCIM/test.jpg | grep Orientation

问题4:连拍速度慢

# 查看ImageReader的缓冲状态(是否满了)
adb logcat -s Camera2-ImageReader
# 查看HAL处理耗时
adb logcat -s CameraHAL | grep "processCaptureResult"

八、实战案例:实现完整拍照功能

把以上内容整合成一个可运行的完整示例(关键代码片段):

public class CameraActivity extends AppCompatActivity {

    // 状态机
    private int mState = STATE_PREVIEW;

    // 拍照按钮点击
    public void onCaptureClicked(View view) {
        // 先检查3A是否已收敛
        if (mIs3AConverged) {
            // 已收敛,直接拍照(跳过预捕获)
            captureStillPicture();
        } else {
            // 需要等待3A收敛
            lockFocus();
        }
    }

    // 在CaptureCallback中实时更新3A状态
    private boolean mIs3AConverged = false;

    private void updateConvergedState(CaptureResult result) {
        Integer afState = result.get(CaptureResult.CONTROL_AF_STATE);
        Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE);

        boolean afOk = afState == null ||
                afState == CaptureResult.CONTROL_AF_STATE_PASSIVE_FOCUSED ||
                afState == CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED;

        boolean aeOk = aeState == null ||
                aeState == CaptureResult.CONTROL_AE_STATE_CONVERGED;

        mIs3AConverged = afOk && aeOk;
    }

    // 拍照后恢复预览(解锁AF/AE)
    private void unlockFocus() {
        try {
            // 取消AF触发
            mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER,
                    CameraMetadata.CONTROL_AF_TRIGGER_CANCEL);
            mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE,
                    CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);

            mCaptureSession.capture(mPreviewRequestBuilder.build(),
                    mCaptureCallback, mBackgroundHandler);

            // 恢复连续预览
            mState = STATE_PREVIEW;
            mCaptureSession.setRepeatingRequest(mPreviewRequest,
                    mCaptureCallback, mBackgroundHandler);

        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
    }
}

总结

本文完整覆盖了Camera2拍照流程的核心技术点:

  1. 预捕获三阶段状态机:AF锁定→AE预捕获→正式拍照,是Camera2 Official的标准模式。理解状态机是避免拍照模糊和曝光错误的关键。

  2. JPEG参数配置:质量、方向、GPS、缩略图是四个必须关注的参数。JPEG方向尤其容易出错,需要综合传感器方向和设备旋转角度计算。

  3. 连拍实现captureBurst()一次提交多个请求;ImageReader的maxImages决定连拍帧数上限;AE锁定策略影响连拍的曝光一致性。

  4. ZSL零快门延迟:通过预存环形缓冲 + Reprocessing机制,将快门延迟从300ms+压缩到50ms以内。适用于光线充足且不需要闪光的场景。

  5. 性能优化核心:提前打开相机、预热Session、使用连续3A、所有IO操作在后台线程,是把快门延迟压到最低的四个关键实践。

下一篇我们将进入录像领域,深入解析相机录像流程——MediaRecorder如何与Camera2协作,Surface录像模式、手动MediaCodec录像、音视频同步等核心问题。

参考资料