1. 从Camera1到Camera2为什么我们需要Pipeline如果你是从Camera1 API时代过来的Android开发者第一次接触Camera2 API时大概率会被它那套复杂的CameraCaptureSession、CaptureRequest、ImageReader给绕晕。Camera1时代我们调用takePicture相机就拍照调用startPreview画面就显示一切看起来简单直接。但这种“简单”背后是系统帮你做了所有决定你就像一个坐在自动驾驶汽车里的乘客只能按几个固定的按钮无法干预驾驶过程。当你想实现一些高级功能比如手动对焦、手动曝光、高速连拍或者处理原始图像数据时就会处处碰壁。Camera2 API的设计哲学正是为了解决这个根本问题将相机的控制权交还给开发者。它不再把相机看作一个“黑盒”而是将其抽象为一个由多个组件Sensor、ISP、3A算法等构成的、可配置的数据处理流水线也就是我们常说的Pipeline。理解Camera2 Pipeline是掌握Camera2 API的钥匙。它不是一个具体的类而是一套概念模型。简单来说你可以把它想象成一个现代化的、高度自动化的相机工厂。图像传感器Sensor是原料采集车间它持续不断地采集光线信号RAW数据。这些原始数据随后被送入图像信号处理器ISP车间进行降噪、色彩插值、白平衡、色调映射等一系列复杂的加工。加工后的成品如YUV或JPEG格式的图像被分拣到不同的出货口一个口连着屏幕预览一个口连着存储器拍照还可能有一个口连着网络视频编码推流。Camera2 API让你成为了这个工厂的调度员。你可以配置流水线决定开几条生产线创建几个CaptureSession每条生产线生产什么规格的产品配置Surface输出目标如SurfaceView、ImageReader、MediaCodec的Surface。下达生产指令通过CaptureRequest告诉工厂下一批产品要按照什么参数生产例如ISO设为800对焦模式设为AF-C启用光学防抖。处理产品从指定的出货口ImageReader领取加工好的图像数据进行自定义的后处理或分析。这套模型带来的核心优势是灵活性与高性能。你可以为预览、拍照、录像分别创建最优化的Pipeline实现零快门延迟拍照ZSL可以同时向多个目标输出不同格式的数据可以精细控制每一个成像参数。但与之对应的是陡峭的学习曲线和更高的复杂度。很多初学者照着Demo把预览跑起来后一旦想加入拍照功能或者处理YUV数据就会陷入IllegalStateException或图像方向错误的泥潭其根本原因就是对Pipeline的工作机制理解不透。接下来的内容我将结合自己踩过的坑和项目中的实际应用为你彻底拆解Camera2 Pipeline的各个环节从创建到配置从请求到回调让你不仅能“跑通”代码更能“驾驭”这套强大的系统。2. Pipeline的核心组件与生命周期Session、Request与Surface要搭建一个Camera2应用你需要和几个核心类打交道它们共同构成了Pipeline的骨架。理解它们各自的职责和生命周期是避免各种诡异Bug的前提。2.1 CameraManager、CameraDevice与CameraCharacteristics这是Pipeline的“入口”和“蓝图”。CameraManager 系统服务用于发现和连接相机设备。通过getCameraIdList()获取可用的相机ID如“0”代表后置主摄“1”代表前置摄像头。CameraDevice 代表一个已打开的物理相机设备。它是所有操作的起点通过它的createCaptureSession方法来创建Pipeline的核心——CameraCaptureSession。CameraCharacteristics 相机的“能力说明书”。在打开相机前你必须通过CameraManager.getCameraCharacteristics(cameraId)获取它。这份说明书至关重要它告诉你这个相机支持哪些分辨率、有哪些可用的传感器、支持哪些对焦模式、最大帧率是多少等等。任何不查询CameraCharacteristics就硬编码参数的行为都是在给自己挖坑。比如不是所有相机都支持CONTROL_AF_MODE_CONTINUOUS_PICTURE连续对焦也不是所有相机都能输出RAW格式数据。2.2 CameraCaptureSession流水线工厂这是Pipeline的实体也是最容易出状态问题的地方。你可以通过CameraDevice.createCaptureSession来创建它但这个过程是异步的。创建Session时你需要传入一个ListSurface。这个列表定义了这条流水线的所有输出目的地。这里有一个关键约束一旦Session创建成功其输出的Surface集合就被固定下来在整个Session生命周期内无法增删只能替换通过setRepeatingRequest或capture方法传入的CaptureRequest所携带的Surface必须是创建Session时提供的那个集合的子集。这就像你建工厂时规划了三条出货传送带分别对应预览SurfaceView、拍照ImageReader、录像MediaCodec Surface工厂建好后你不能凭空增加或拆除一条传送带但你可以决定每次生产时让产品走哪几条传送带通过CaptureRequest的addTarget方法。Session的生命周期状态需要仔细处理配置中调用createCaptureSession后进入此状态。此时不能进行其他操作。就绪onConfigured回调被触发Session创建成功。此时可以开始发送CaptureRequest。关闭调用close()方法后或CameraDevice断开后。进入此状态的Session不能再被使用所有关联的Surface也可能被释放。一个常见的错误是在Activity的onPause时没有正确关闭Session和CameraDevice导致下次onResume时发生状态冲突。最佳实践是在onPause中关闭Session和CameraDevice在onResume中重新走一遍完整的初始化流程。2.3 CaptureRequest与CaptureResult生产指令与质检报告CaptureRequest.Builder 用于构建生产指令。你可以通过CameraDevice.createCaptureRequest(int templateType)来获取一个构建器。templateType参数是一个捷径它提供了一些预设的配置如TEMPLATE_PREVIEW: 适用于预览的默认设置。TEMPLATE_STILL_CAPTURE: 适用于高质量静态照片的默认设置通常会触发自动对焦和测光并采用最高画质JPEG编码。TEMPLATE_RECORD: 适用于视频录制的默认设置通常会锁定对焦和曝光并采用适合视频的帧率和防抖配置。 但请注意这只是个起点你完全可以也经常需要覆盖这些预设参数。CaptureRequest 构建好的、不可变的生产指令。它包含了所有成像参数如CONTROL_AF_MODE,SENSOR_EXPOSURE_TIME,JPEG_QUALITY以及本次生产的目标Surface列表通过addTarget(Surface)添加。CaptureResult 当一帧图像被传感器捕获时相机硬件和算法实际采用的参数报告。你可以在CaptureCallback.onCaptureCompleted中拿到它。CaptureRequest是你“要求”的参数CaptureResult是相机“实际执行”的参数。在复杂光照或快速场景变化下两者可能不完全一致。例如你请求的曝光时间可能因为超出传感器范围而被自动修正。2.4 Surface产品的目的地Surface是Android中用于跨进程传递图像数据的缓冲区生产者接口。在Camera2中它代表Pipeline输出图像的终点。常见的Surface来源有SurfaceView / TextureView: 用于预览。系统会自动管理其缓冲队列你只需要获取它的Surface并传给Session即可。ImageReader: 这是获取图像数据用于自定义处理的最重要工具。你可以创建指定格式如ImageFormat.JPEG,ImageFormat.YUV_420_888、指定尺寸和最大图像数量的ImageReader然后从其getSurface()方法获取Surface。当有新的图像可用时ImageReader的回调会被触发你可以在其中通过acquireLatestImage()或acquireNextImage()获取Image对象进而访问其中的图像数据。MediaCodec.createInputSurface(): 用于视频录制将相机数据直接喂给编码器。Allocation.getSurface()(RenderScript) 或OpenGL ES 的 EGLSurface: 用于直接进行GPU处理。一个极其重要的原则是你必须确保这些Surface的生命周期被妥善管理并且在它们无效如SurfaceView被销毁后不再被Camera2 Pipeline使用。否则会导致应用崩溃或相机服务无响应。3. 构建一个健壮的双重Pipeline预览与拍照理论说再多不如看实战。我们来实现一个最常见的场景同时支持预览和拍照。这个场景需要构建一个包含两个输出目标的Pipeline。3.1 初始化与权限检查首先当然是清单文件权限和动态权限申请这个基础步骤不再赘述。我们直接从获取相机特性开始。private void setUpCamera(int cameraId) { CameraManager manager (CameraManager) context.getSystemService(Context.CAMERA_SERVICE); try { // 1. 获取相机特性这是所有决策的基础 CameraCharacteristics characteristics manager.getCameraCharacteristics(String.valueOf(cameraId)); // 2. 获取相机传感器方向用于后续纠正图像显示方向 Integer sensorOrientation characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION); mSensorOrientation sensorOrientation ! null ? sensorOrientation : 0; // 3. 获取流配置映射它告诉我们该相机支持哪些输出尺寸 StreamConfigurationMap map characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); if (map null) { throw new RuntimeException(Cannot get stream configuration map.); } // 4. 选择预览尺寸通常选择与屏幕比例最接近的、小于等于1080P的尺寸以平衡性能与画质 Size[] previewSizes map.getOutputSizes(SurfaceTexture.class); // 对于TextureView Size optimalPreviewSize chooseOptimalSize(previewSizes, 1920, 1080, new Size(16, 9)); mPreviewSize optimalPreviewSize; // 5. 选择拍照尺寸选择最大可用的JPEG尺寸或者你需要的特定高分辨率 Size[] jpegSizes map.getOutputSizes(ImageFormat.JPEG); Size largestJpegSize Collections.max(Arrays.asList(jpegSizes), new CompareSizesByArea()); mPhotoSize largestJpegSize; // 6. 检查相机是否支持所需的硬件级别和功能例如是否支持手动控制 Integer hardwareLevel characteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL); mIsLegacyDevice hardwareLevel ! null hardwareLevel CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY; // 7. 打开相机设备异步 manager.openCamera(String.valueOf(cameraId), mStateCallback, mBackgroundHandler); } catch (CameraAccessException e) { Log.e(TAG, Camera access exception, e); } }chooseOptimalSize是一个你需要自己实现的工具方法用于根据比例、最大宽高等条件筛选出最合适的尺寸。3.2 创建输出Surface与CaptureSession相机打开成功后在CameraDevice.StateCallback.onOpened回调中我们开始创建Pipeline。private CameraDevice.StateCallback mStateCallback new CameraDevice.StateCallback() { Override public void onOpened(NonNull CameraDevice cameraDevice) { mCameraDevice cameraDevice; // 创建预览和拍照的Surface createCameraPreviewSession(); } // ... onDisconnected, onError 处理 }; private void createCameraPreviewSession() { try { // 1. 配置TextureView用于预览 SurfaceTexture texture mTextureView.getSurfaceTexture(); if (texture null) return; // 设置TextureView的缓冲区尺寸为选定的预览尺寸 texture.setDefaultBufferSize(mPreviewSize.getWidth(), mPreviewSize.getHeight()); Surface previewSurface new Surface(texture); // 2. 创建ImageReader用于拍照JPEG格式 // 参数2最大图像数量建议设为2或3以平衡内存和延迟。1可能导致缓冲区不足。 mImageReader ImageReader.newInstance(mPhotoSize.getWidth(), mPhotoSize.getHeight(), ImageFormat.JPEG, 3); mImageReader.setOnImageAvailableListener(mOnImageAvailableListener, mBackgroundHandler); Surface photoSurface mImageReader.getSurface(); // 3. 构建一个用于预览的CaptureRequest mPreviewRequestBuilder mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW); mPreviewRequestBuilder.addTarget(previewSurface); // 将预览Surface加入目标 // 4. 创建CaptureSession同时传入预览和拍照两个Surface // 注意此时photoSurface也被加入了Session但预览请求并未将其设为目标。 // 这意味着流水线会为photoSurface分配资源但默认的预览流不会向它输出数据。 mCameraDevice.createCaptureSession(Arrays.asList(previewSurface, photoSurface), new CameraCaptureSession.StateCallback() { Override public void onConfigured(NonNull CameraCaptureSession session) { mCaptureSession session; // Session创建成功开始发送预览请求 try { // 设置自动对焦模式为连续对焦适用于预览 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); // 构建请求 CaptureRequest previewRequest mPreviewRequestBuilder.build(); // 设置重复请求驱动预览持续进行 mCaptureSession.setRepeatingRequest(previewRequest, null, mBackgroundHandler); } catch (CameraAccessException e) { Log.e(TAG, Failed to start preview., e); } } Override public void onConfigureFailed(NonNull CameraCaptureSession session) { Toast.makeText(getContext(), Failed to create camera session., Toast.LENGTH_SHORT).show(); } }, null // 可以传入Handlernull则使用当前线程的Looper ); } catch (CameraAccessException e) { Log.e(TAG, Camera access exception during session creation, e); } }关键点解析createCaptureSession的Surface列表我们传入了previewSurface和photoSurface。这意味着这条流水线有能力向这两个目标输出数据。但具体向哪个目标输出由每次的CaptureRequest决定。预览请求的目标mPreviewRequestBuilder.addTarget(previewSurface)所以这个重复请求只会把图像数据送到预览的SurfaceView不会送到ImageReader。这节省了不必要的编码和内存拷贝开销。ImageReader的缓冲区我们设置了最大图像数为3。如果处理太慢比如拍照后保存到磁盘耗时过长缓冲区被占满相机新的帧就无法写入可能导致预览卡顿或拍照失败。因此在ImageReader的回调中处理完Image后必须尽快调用image.close()释放缓冲区。3.3 实现拍照切换请求目标当用户点击拍照按钮时我们需要让Pipeline生产一张JPEG图片。private void takePicture() { if (mCameraDevice null || mCaptureSession null) return; try { // 1. 首先停止持续的预览请求避免干扰 mCaptureSession.stopRepeating(); // 也可以调用mCaptureSession.abortCaptures()来取消所有未完成的请求更彻底 // 2. 创建一个专门用于拍照的CaptureRequest.Builder // 使用TEMPLATE_STILL_CAPTURE模板它包含了一些适合拍照的默认优化如降噪、锐化 final CaptureRequest.Builder captureBuilder mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE); // 3. 将拍照的SurfaceImageReader的Surface设为目标 captureBuilder.addTarget(mImageReader.getSurface()); // 4. 设置JPEG方向。相机传感器图像可能是横屏的需要根据设备方向旋转。 int rotation getWindowManager().getDefaultDisplay().getRotation(); int jpegOrientation (mSensorOrientation ORIENTATIONS.get(rotation) 360) % 360; captureBuilder.set(CaptureRequest.JPEG_ORIENTATION, jpegOrientation); // 5. 可以设置JPEG质量等参数 captureBuilder.set(CaptureRequest.JPEG_QUALITY, (byte) 95); // 6. 对于静态拍照通常希望使用单次对焦AF-S并锁定曝光AE-LOCK // 但在连续对焦模式下也可以直接触发一次对焦。这里我们假设预览时已是连续对焦。 // 如果要精确控制可以先发送一个对焦锁定请求再发送拍照请求构成一个序列。 // 7. 发送单次拍照请求 mCaptureSession.capture(captureBuilder.build(), new CameraCaptureSession.CaptureCallback() { Override public void onCaptureCompleted(NonNull CameraCaptureSession session, NonNull CaptureRequest request, NonNull TotalCaptureResult result) { // 拍照捕获完成但图像数据可能还在处理中稍后会在ImageReader回调中收到 Log.d(TAG, Capture completed.); // 拍照完成后立即恢复预览 try { // 重新开始预览的重复请求 mCaptureSession.setRepeatingRequest(mPreviewRequestBuilder.build(), null, mBackgroundHandler); } catch (CameraAccessException e) { Log.e(TAG, Failed to restart preview after capture., e); } } Override public void onCaptureFailed(NonNull CameraCaptureSession session, NonNull CaptureRequest request, NonNull CaptureFailure failure) { Log.e(TAG, Capture failed: failure.getReason()); // 失败后也需要尝试恢复预览 try { mCaptureSession.setRepeatingRequest(mPreviewRequestBuilder.build(), null, mBackgroundHandler); } catch (CameraAccessException e) { Log.e(TAG, Failed to restart preview after failed capture., e); } } }, mBackgroundHandler); } catch (CameraAccessException e) { Log.e(TAG, Camera access exception during capture, e); } }关键点与避坑指南停止预览在发送拍照请求前停止预览重复请求 (stopRepeating()) 是个好习惯。因为相机硬件处理能力有限同时处理高帧率预览流和高分辨率拍照流可能导致资源竞争增加拍照延迟或失败概率。对于高性能设备也可以不停止但这需要更精细的测试。目标切换注意captureBuilder.addTarget(mImageReader.getSurface())。这个请求只将ImageReader的Surface设为目标预览Surface不再是目标。因此在拍照的瞬间预览画面可能会冻结因为没有数据输出了。这就是为什么我们在onCaptureCompleted后要立刻恢复预览请求。方向处理JPEG_ORIENTATION是元数据它告诉JPEG解码器如何旋转图像。你必须在拍照请求中正确设置否则保存的图片方向可能是错的。计算方向时要结合传感器安装方向(SENSOR_ORIENTATION) 和设备当前朝向。恢复预览无论拍照成功还是失败在CaptureCallback中恢复预览是保证用户体验连贯性的关键。否则预览将一直黑屏。3.4 处理拍照结果ImageReader的回调拍照请求成功后图像数据会被写入ImageReader的缓冲区。private final ImageReader.OnImageAvailableListener mOnImageAvailableListener new ImageReader.OnImageAvailableListener() { Override public void onImageAvailable(ImageReader reader) { // 必须在后台线程中处理避免阻塞回调线程我们设置了Handler try (Image image reader.acquireLatestImage()) { if (image null) { return; } ByteBuffer buffer image.getPlanes()[0].getBuffer(); byte[] bytes new byte[buffer.remaining()]; buffer.get(bytes); // 获取JPEG方向信息从CaptureResult中获取更准确这里简化处理 // 实际项目中可以将方向信息通过某种方式如AtomicInteger从CaptureCallback传递过来。 int orientation mLastJpegOrientation; // 假设已保存 // 在后台线程保存图片 saveImageToFile(bytes, orientation); } catch (Exception e) { Log.e(TAG, Failed to process image, e); } // Image使用try-with-resources自动关闭释放缓冲区 } };重要提醒acquireLatestImage()会获取最新的一帧并丢弃旧的适合拍照场景。对于视频帧分析可能需要acquireNextImage()。无论如何获取Image后必须尽快处理并调用close()否则缓冲区泄漏会导致相机后续帧无法写入整个Pipeline卡死。4. 高级话题ZSL、YUV处理与性能优化掌握了基础的双重Pipeline我们可以探讨一些更高级的应用场景这些场景更能体现Camera2 Pipeline的威力。4.1 零快门延迟ZSL的实现思路ZSL的核心是让拍照几乎在按下快门的瞬间完成没有对焦和测光的等待时间。Camera2实现ZSL的典型方案是使用一个三路输出的Pipeline预览流低分辨率YUV或直接到SurfaceView。ZSL流一个持续循环写入的、固定大小如3-5帧的ImageReader格式为ImageFormat.PRIVATE或ImageFormat.YUV_420_888取决于设备支持。这个流以最高帧率运行但分辨率可能低于拍照分辨率。拍照流高分辨率JPEG的ImageReader。工作流程常时相机同时向预览流和ZSL流输出数据。ZSL流像一个环形缓冲区总是保存着最近几帧。拍照时不发送新的拍照请求而是从ZSL环形缓冲区中选取一帧最清晰、曝光最合适的图像通常结合陀螺仪数据选择抖动最小的一帧将其重新处理Reprocess成高分辨率JPEG。重新处理通过createReprocessCaptureRequest方法将ZSL流中的Image作为输入发送一个重新处理的请求输出到拍照流的ImageReader。这样做的好处是拍照动作避开了传感器捕获和ISP处理的时间延迟极低。但实现复杂需要处理TotalCaptureResult与图像的匹配、重新处理会话的创建等。4.2 处理YUV_420_888数据很多计算机视觉或高级滤镜功能需要处理YUV数据。ImageFormat.YUV_420_888是Android上表示YUV数据的通用格式。当你从ImageReader获取到一个ImageFormat.YUV_420_888格式的Image时它通常包含三个平面PlaneY平面、U平面、V平面。Image.Plane[] planes image.getPlanes(); ByteBuffer yBuffer planes[0].getBuffer(); // Y data ByteBuffer uBuffer planes[1].getBuffer(); // U data ByteBuffer vBuffer planes[2].getBuffer(); // V data int yRowStride planes[0].getRowStride(); // 可能包含padding int yPixelStride planes[0].getPixelStride(); // 通常为1 int uvRowStride planes[1].getRowStride(); int uvPixelStride planes[1].getPixelStride(); // 通常为2因为UV是亚采样的最大的坑在于rowStride和pixelStride。由于内存对齐要求每一行数据的末尾可能有填充字节padding。你不能简单地假设data.length width * height * 1.5。必须使用rowStride和pixelStride来正确访问像素数据否则图像会错乱。通常需要自己编写代码或使用RenderScript、libyuv等库来正确裁剪和转换。4.3 Pipeline性能调优与常见陷阱Surface尺寸与格式匹配创建Session时传入的Surface尺寸必须在相机的StreamConfigurationMap支持的范围之内。混合不同格式如YUV和JPEG的Surface时要确保相机支持这种组合。有些旧设备或特定模式下可能不支持同时输出高分辨率JPEG和高帧率YUV。Handler与线程管理几乎所有Camera2的回调方法都可以传入一个Handler来指定回调线程。强烈建议使用一个专用的后台线程HandlerThread来处理这些回调尤其是ImageReader的图像可用回调因为图像处理如保存、编码、分析通常是耗时操作放在主线程会导致UI卡顿甚至ANR。状态同步Camera2 API是高度异步且状态敏感的。你必须确保操作序列化。例如不要在onConfigured回调进行中又去调用createCaptureSession。一个实用的模式是使用一个简单的状态机如IDLE,OPENING,SESSION_CONFIGURING,PREVIEW,CAPTURING来管理相机生命周期。资源释放这是内存泄漏和崩溃的重灾区。必须遵循严格的释放顺序首先在CaptureSession上调用close()。然后在CameraDevice上调用close()。最后释放ImageReader(close()) 和其他的Surface。 最好在Activity/Fragment的onPause中执行这些清理操作并在onResume中重建。兼容性处理一定要通过CameraCharacteristics检查设备能力。例如通过INFO_SUPPORTED_HARDWARE_LEVEL判断是LEGACY,LIMITED,FULL还是LEVEL_3设备。LEGACY设备对Camera2的支持非常有限可能不支持手动控制或某些输出格式组合你可能需要回退到更简单的配置或使用Camera1 API。对焦与测光区域Camera2允许你设置特定的对焦和测光区域CONTROL_AF_REGIONS,CONTROL_AE_REGIONS。坐标系统是相对于传感器有效区域的(x, y, width, height)范围是[0, 1000)。你需要将屏幕触摸坐标转换到这个坐标系。设置后相机可能会需要几帧来完成对焦和曝光调整期间CaptureResult中的对焦状态 (CONTROL_AF_STATE) 会变化需要监听这个状态来给用户反馈如对焦框动画。理解并熟练运用Camera2 Pipeline意味着你完全掌控了移动设备上最强大的图像采集能力。从简单的拍照预览到复杂的多流处理、自定义算法集成这套模型提供了坚实的基础。虽然初期学习成本较高但一旦掌握其灵活性和性能优势是旧API无法比拟的。在实际项目中建议从官方Camera2Basic示例开始逐步添加功能并始终将设备兼容性和资源管理放在首位。