8. Photo 拍照流程:TakePicture触发、StillCaptureRequest构建、JPEG回调与编码、Exif信息写入与缩略图生成
拍照,说白了就是让相机传感器“咔嚓”一下,然后把数据拿回来处理成一张照片。嗯,听起来简单,但Android框架里这一套流程其实挺绕的。我最早接触Camera2时,光是把TakePicture回调调通就折腾了两天——后来发现是CaptureRequest的模板没选对。
这一章,我们就把拍照的完整链路拆开来看。从App调用takePicture()开始,到最终拿到一张带Exif和缩略图的JPEG文件,每一步我都会结合项目里的实际踩坑经验来讲。
8.1 TakePicture的触发:从App到CameraService
你想想看,App里调用mCameraDevice.createCaptureSession()之后,怎么触发拍照?
其实就一行:
mCaptureSession.capture(mStillCaptureRequest, mCaptureCallback, mBackgroundHandler);
但这一行背后,CameraFramework做了三件事:
- 锁住3A(自动对焦/自动曝光/自动白平衡):防止拍照瞬间参数被重新计算。
- 插入一个“预拍”帧:有些设备需要先跑一次AE/AF收敛,再真正曝光。
- 下发真正的StillCaptureRequest:这个Request的模板是
TEMPLATE_STILL_CAPTURE。
核心要点:TakePicture并不是立即曝光,而是先等3A稳定。我曾在某款低端机上遇到过拍照模糊的问题,后来发现是AE还没收敛就拍了——解决方案是在capture之前手动调用mCaptureSession.setRepeatingRequest(mPreviewRequest)跑几帧预热。
我个人习惯在触发拍照前加一个日志:
Log.d("CameraShot", "capture() called, current AE state: " + aeState);
这样能快速定位是3A问题还是HAL层的问题。
8.2 StillCaptureRequest的构建:模板、参数与TAG
构建拍照用的Request,和预览Request最大的区别在于:
- 模板用
TEMPLATE_STILL_CAPTURE,而不是TEMPLATE_PREVIEW。 - 需要显式设置
CONTROL_AE_MODE和CONTROL_AF_MODE。 - 必须指定输出目标(Surface)为
ImageReader或MediaRecorder。
代码示例:
CaptureRequest.Builder stillBuilder =
mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);
stillBuilder.addTarget(mImageReader.getSurface());
// 设置自动对焦模式
stillBuilder.set(CaptureRequest.CONTROL_AF_MODE,
CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
// 设置自动曝光模式
stillBuilder.set(CaptureRequest.CONTROL_AE_MODE,
CaptureRequest.CONTROL_AE_MODE_ON);
// 设置JPEG质量
stillBuilder.set(CaptureRequest.JPEG_QUALITY, (byte) 95);
// 设置缩略图尺寸
stillBuilder.set(CaptureRequest.JPEG_THUMBNAIL_SIZE,
new Size(512, 384));
mStillCaptureRequest = stillBuilder.build();
避坑指南:我曾经在构建Request时忘记设置JPEG_THUMBNAIL_SIZE,结果生成的缩略图是0x0,导致相册App显示异常。后来我养成了习惯——只要用TEMPLATE_STILL_CAPTURE,就显式设置缩略图尺寸。
这里有个细节:JPEG_QUALITY是0-100的整数,但HAL层可能只支持几个固定档位。我建议设95,既保证画质又不会让文件太大。
8.3 JPEG回调与编码:从ImageReader到byte[]
拍照完成后,数据会通过ImageReader.OnImageAvailableListener回调回来。嗯,这里要注意:回调回来的不是JPEG文件,而是一个Image对象。
标准做法是:
ImageReader.OnImageAvailableListener readerListener = new ImageReader.OnImageAvailableListener() {
@Override
public void onImageAvailable(ImageReader reader) {
Image image = reader.acquireLatestImage();
if (image == null) return;
// 获取JPEG数据
ByteBuffer buffer = image.getPlanes()[0].getBuffer();
byte[] jpegData = new byte[buffer.remaining()];
buffer.get(jpegData);
// 关闭Image,释放资源
image.close();
// 后续处理:写入Exif、生成缩略图、保存文件
processJpegData(jpegData);
}
};
警告:acquireLatestImage()必须及时调用并关闭,否则会导致ImageReader队列阻塞,后续拍照会卡死。我见过线上崩溃日志,就是因为在回调里做了耗时操作没及时close()。
JPEG编码其实是由HAL层完成的。App拿到的byte[]已经是完整的JPEG流,包含图像数据和部分Exif信息(比如厂商、型号、拍摄时间)。但有些Exif字段(比如GPS、旋转角度)需要App自己写入。
8.4 Exif信息写入:让照片更完整
Exif是什么?说白了就是照片的“身份证”。里面记录了拍摄参数、设备信息、地理位置等。Android提供了ExifInterface类来读写。
写入Exif的典型流程:
public static void writeExif(byte[] jpegData, ExifData data) {
try {
ExifInterface exif = new ExifInterface(new ByteArrayInputStream(jpegData));
// 写入方向
exif.setAttribute(ExifInterface.TAG_ORIENTATION,
String.valueOf(data.orientation));
// 写入GPS
if (data.latitude != null) {
exif.setAttribute(ExifInterface.TAG_GPS_LATITUDE,
data.latitude);
exif.setAttribute(ExifInterface.TAG_GPS_LONGITUDE,
data.longitude);
}
// 写入日期
exif.setAttribute(ExifInterface.TAG_DATETIME,
new SimpleDateFormat("yyyy:MM:dd HH:mm:ss",
Locale.US).format(new Date()));
// 保存回JPEG
exif.saveAttributes();
} catch (IOException e) {
Log.e("ExifWriter", "Failed to write Exif", e);
}
}
个人经验:ExifInterface的saveAttributes()会重写整个JPEG文件,如果文件很大(比如20MB),这个操作会比较耗时。我建议在后台线程做,并且不要在主线程调用。
另外,有些Exif字段是只读的(比如IMAGE_WIDTH),写入时会静默失败。嗯,这个坑我踩过——当时想改分辨率信息,结果怎么改都不生效,后来查文档才发现是只读的。
8.5 缩略图生成:两种方式,各有优劣
缩略图有两种生成方式:
| 方式 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| HAL内嵌缩略图 | 在JPEG编码时,HAL直接生成缩略图并嵌入Exif的Thumbnail区域 |
速度快,无需二次解码 | 缩略图尺寸固定,无法自定义 |
| App端生成 | 拿到JPEG后,用BitmapFactory解码再压缩 |
灵活,可控制尺寸和质量 | 需要额外解码,耗时较长 |
我个人更推荐使用HAL内嵌缩略图,因为省事。但如果你需要自定义缩略图(比如加水印或裁剪),那就得走App端生成。
App端生成缩略图的代码:
public static byte[] generateThumbnail(byte[] jpegData, int maxSize) {
Bitmap bitmap = BitmapFactory.decodeByteArray(jpegData, 0, jpegData.length);
if (bitmap == null) return null;
// 计算缩放比例
int width = bitmap.getWidth();
int height = bitmap.getHeight();
float scale = Math.min((float) maxSize / width, (float) maxSize / height);
Matrix matrix = new Matrix();
matrix.postScale(scale, scale);
Bitmap thumb = Bitmap.createBitmap(bitmap, 0, 0, width, height, matrix, true);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
thumb.compress(Bitmap.CompressFormat.JPEG, 80, baos);
thumb.recycle();
bitmap.recycle();
return baos.toByteArray();
}
避坑指南:BitmapFactory.decodeByteArray()在解码大图时可能会OOM。我建议先通过BitmapFactory.Options的inJustDecodeBounds获取尺寸,再计算合适的采样率。嗯,这个技巧在相册类App里很常用。
8.6 整体流程SVG图
下面这张图展示了从TakePicture触发到最终保存JPEG的完整数据流。我特意把HAL层和App层的边界画清楚了,方便你理解职责划分。
8.7 总结与避坑清单
拍照流程看起来步骤多,但核心就三条线:
- 触发线:takePicture() → 构建Request → capture()
- 数据线:传感器 → JPEG编码 → ImageReader回调 → byte[]
- 后处理线:Exif写入 → 缩略图生成 → 文件保存
最后,我整理了一份避坑清单,都是我在项目里真实遇到过的:
- 忘记设置缩略图尺寸:导致缩略图为0x0,相册显示异常。
- ImageReader未及时close():队列阻塞,后续拍照卡死。
- Exif写入耗时过长:在主线程调用导致ANR。
- BitmapFactory OOM:解码大图前没有计算采样率。
- 3A未收敛就拍照:照片模糊或曝光不准。
嗯,如果你能把上面这五点都避开,拍照流程基本就稳了。剩下的就是根据具体设备微调参数——比如某些三星设备对JPEG_QUALITY的响应和骁龙平台不一样,这个只能靠实测。