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

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

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

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

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

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

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

1.1 为什么需要预捕获

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

预捕获的目的是:

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

1.2 实现预捕获序列

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

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来接收相机输出的图像数据:

java 复制代码
// 创建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 拍照参数配置

java 复制代码
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文件存储的完整流程:

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

2.4 保存图片文件

java 复制代码
// 实现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 基本连拍实现

java 复制代码
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:

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

查询设备最大连拍帧率

java 复制代码
// 查询特定尺寸和格式下的最短帧时间(单位:纳秒)
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,后续每张曝光完全相同,适合运动抓拍:

java 复制代码
captureBuilder.set(CaptureRequest.CONTROL_AE_LOCK, true);

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

四、ZSL零快门延迟

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

4.1 普通拍照的延迟来源

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

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

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

4.2 ZSL的工作原理

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

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:

java 复制代码
// === 步骤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类来读写这些数据:

java 复制代码
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格式

java 复制代码
// 检查设备是否支持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:

java 复制代码
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()时就打开相机,进入预览状态:

java 复制代码
@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进入前台时就完成,而不是按快门后才创建:

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

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

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

java 复制代码
// 预览时使用连续对焦模式
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字节)
  • 显示缩略图时不要解码全尺寸
java 复制代码
// 只读取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:

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

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

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

七、调试技巧

7.1 拍照延迟分析

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

java 复制代码
// 在快门按下时记录时间
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进行更精细的分析:

bash 复制代码
# 抓取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()方法综合考虑传感器方向和设备旋转:

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

问题4:连拍速度慢

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

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

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

java 复制代码
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录像、音视频同步等核心问题。

参考资料

相关推荐
千里马学框架1 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台1 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone1 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc1 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo1 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077001 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼1 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone1 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen1 天前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone1 天前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui