引言:快门延迟,相机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文件存储的完整流程:
整个流程分为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的核心思路是:不等你按快门,提前把帧存好了。
ZSL分为两个并行阶段:
阶段一(持续运行,用户无感知):
- Preview持续输出帧的同时,HAL还输出一路全尺寸RAW/YUV数据
- App将这些帧存入一个环形缓冲队列(通常3-10帧)
- 每帧都附带完整的CaptureResult元数据(3A状态、时间戳等)
阶段二(快门按下后立即执行):
- 从缓冲队列中选出"时间戳最近且3A已收敛"的帧
- 构建重处理(Reprocessing)请求,将该帧送回HAL
- HAL对这帧做精细的ISP二次处理(更高质量的降噪、锐化等)
- 输出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拍照流程的核心技术点:
-
预捕获三阶段状态机:AF锁定→AE预捕获→正式拍照,是Camera2 Official的标准模式。理解状态机是避免拍照模糊和曝光错误的关键。
-
JPEG参数配置:质量、方向、GPS、缩略图是四个必须关注的参数。JPEG方向尤其容易出错,需要综合传感器方向和设备旋转角度计算。
-
连拍实现:
captureBurst()一次提交多个请求;ImageReader的maxImages决定连拍帧数上限;AE锁定策略影响连拍的曝光一致性。 -
ZSL零快门延迟:通过预存环形缓冲 + Reprocessing机制,将快门延迟从300ms+压缩到50ms以内。适用于光线充足且不需要闪光的场景。
-
性能优化核心:提前打开相机、预热Session、使用连续3A、所有IO操作在后台线程,是把快门延迟压到最低的四个关键实践。
下一篇我们将进入录像领域,深入解析相机录像流程——MediaRecorder如何与Camera2协作,Surface录像模式、手动MediaCodec录像、音视频同步等核心问题。