简介:本资源是一份面向Android开发者的Camera2 API实战入门资料包,适用于具备Java基础、正从Camera1迁移到Camera2架构的中初级开发者,帮助快速掌握相机预览、拍照及参数配置等核心功能。压缩包共5个文件,包含2张关键界面截图(jpg)用于效果示意,2个核心Java类(Camera.java与SurfaceViewL.java)实现相机控制逻辑与视图渲染,以及1个XML布局文件(atc_camera.xml)定义UI结构,整体仅86KB,轻量易集成。目前已有832人学习下载,说明其在实际开发调试中具有较高参考价值。读者可直接复用代码结构理解Camera2的CaptureRequest、Session管理与Surface绑定机制,结合图片示例直观把握预览画面与UI布局关系,并通过使用说明文档快速上手调试流程,避免常见初始化失败与生命周期适配问题。
1. Camera2 不是“升级版 Camera API”,而是 Android 上真正可控的摄像头底层抽象
很多刚接触 Android 多媒体开发的工程师,看到Camera2这个名字,第一反应是“哦,老 Camera 的新版本”。这种理解会直接导致后续踩坑:比如在SurfaceView上预览黑屏、ImageReader拿不到 YUV 数据、CaptureRequest.Builder设置了参数却没生效、甚至在部分中低端机型上openCamera()直接抛CameraAccessException。实际上,Camera2 是一套完全重写的异步状态机驱动架构——它不提供startPreview()这类阻塞式调用,而是通过CameraCaptureSession提交一连串带时间戳的CaptureRequest,由 HAL 层按硬件能力调度执行。这意味着你必须自己管理Surface生命周期、ImageReader缓存队列、RepeatingRequest的启停时机,以及最关键的——所有参数都必须在CaptureRequest中显式声明,没有默认值继承。本文面向已能跑通Camera(API 1)但卡在 Camera2 首帧预览、YUV 图像解析或atc_camera.xml配置失效的开发者,聚焦可复现的初始化链路、SurfaceViewL.java中的 Surface 绑定陷阱、以及camera 摄像头 vts测试中高频暴露的 buffer 管理错误。不讲抽象原理,只拆解从openCamera()到onCaptureCompleted()的每一步真实代码和参数含义。
2. 初始化 Camera2 的最小可行链路:从权限检查到 CaptureSession 建立
Camera2 的初始化不是单点调用,而是一条强依赖顺序的流水线:权限 → 设备枚举 → 特性查询 → Surface 准备 → Session 配置 → 请求提交。任何一环缺失或时序错乱,都会导致黑屏、ANR 或IllegalStateException。下面以SurfaceViewL.java为上下文,给出可直接粘贴验证的初始化骨架,并逐层解释每个环节的不可省略逻辑。
2.1 权限与设备枚举:为什么CAMERA权限不够,还必须检查MANAGE_EXTERNAL_STORAGE
Android 12+ 要求对CameraManager.openCamera()的调用必须满足双重校验:
- 运行时权限
Manifest.permission.CAMERA(必需) - 若目标 SDK ≥ 30 且需访问外部存储保存图片,则还需
Manifest.permission.MANAGE_EXTERNAL_STORAGE(否则ImageReader创建失败)
但更关键的是设备枚举阶段。CameraManager.getCameraIdList()返回的 ID 列表不保证按性能排序,也不代表所有 ID 都支持你所需的StreamConfigurationMap。常见误操作是硬编码"0"或"1",结果在某些高通平台设备上(如搭载高通camera架构的骁龙 778G 机型),ID"0"可能对应红外副摄,不支持ImageFormat.YUV_420_888。正确做法是遍历并筛选:
// Java - SurfaceViewL.java 中的 initCamera 方法片段 private void initCamera() { CameraManager manager = (CameraManager) getSystemService(Context.CAMERA_SERVICE); try { for (String cameraId : manager.getCameraIdList()) { CameraCharacteristics chars = manager.getCameraCharacteristics(cameraId); // 必须检查是否为后置主摄(避免选到 IR/TOF 副摄) Integer facing = chars.get(CameraCharacteristics.LENS_FACING); if (facing == null || facing != CameraCharacteristics.LENS_FACING_BACK) continue; // 检查是否支持 MANUAL_SENSOR 控制(决定能否手动设曝光/ISO) Boolean available = chars.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL); if (available == null || available == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY) { continue; // LEGACY 级别不支持 Camera2 全功能 } // 获取该设备支持的输出配置(关键!决定后续 Surface 尺寸) StreamConfigurationMap map = chars.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); if (map == null) continue; // 优先选择 1280x720 @ YUV_420_888(兼容性最强) Size[] sizes = map.getOutputSizes(ImageFormat.YUV_420_888); if (sizes != null && sizes.length > 0) { mPreviewSize = chooseOptimalSize(sizes, 1280, 720); // 自定义尺寸选择逻辑 mCameraId = cameraId; break; } } } catch (CameraAccessException e) { Log.e("Camera2", "Failed to enumerate cameras", e); } }注意:
chooseOptimalSize()不是系统 API,需自行实现。常见错误是直接取sizes[0]—— 它可能是 4K 尺寸,但SurfaceView的SurfaceHolder默认不支持如此大 buffer,会导致Surface创建失败。应按mPreviewSize = getSuitableSize(sizes, targetWidth, targetHeight)逻辑,优先匹配宽高比一致、分辨率最接近且不超过SurfaceView实际可用尺寸的 size。
2.2 Surface 构建与绑定:SurfaceViewL.java中的 Surface 生命周期陷阱
SurfaceViewL.java(注意名称中的L,暗示其为自定义SurfaceView子类)的核心职责是提供一个可被 Camera2 写入的Surface。但SurfaceView的SurfaceHolder在onCreate()时并未就绪,必须监听SurfaceHolder.Callback的surfaceCreated()回调。更隐蔽的坑在于:Surface对象一旦被CameraCaptureSession使用,就不能再被Canvas绘制或lockCanvas(),否则触发SurfaceAbandonedException。
以下是SurfaceViewL.java中安全绑定Surface的标准写法:
// SurfaceViewL.java public class SurfaceViewL extends SurfaceView implements SurfaceHolder.Callback { private Surface mPreviewSurface; private SurfaceHolder mHolder; public SurfaceViewL(Context context, AttributeSet attrs) { super(context, attrs); mHolder = getHolder(); mHolder.addCallback(this); // 关键:禁用 SurfaceView 自动绘制,避免与 Camera2 冲突 setZOrderOnTop(true); // 确保 Surface 在最上层 mHolder.setFormat(PixelFormat.TRANSLUCENT); // 支持透明背景 } @Override public void surfaceCreated(SurfaceHolder holder) { // 此时 Surface 已创建,但尚未可用,需等待 surfaceChanged() } @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { // 此回调中 Surface 才真正可用 if (mPreviewSurface != null) { mPreviewSurface.release(); // 释放旧 Surface } mPreviewSurface = holder.getSurface(); // 获取新 Surface // 后续在此处调用 openCamera() 或 configureSession() if (mCameraId != null && mPreviewSize != null) { openCamera(); // 触发 Camera2 初始化 } } @Override public void surfaceDestroyed(SurfaceHolder holder) { // 必须在此处 release Surface,否则内存泄漏 if (mPreviewSurface != null) { mPreviewSurface.release(); mPreviewSurface = null; } } }提示:
setZOrderOnTop(true)是SurfaceViewL.java区别于普通SurfaceView的关键。若未设置,Camera2 的Surface会被View层级遮挡,导致预览黑屏。此参数在camera 摄像头 vts测试中属于必检项,VTS 会校验Surface是否处于 Z-order 顶层。
2.3 CaptureSession 配置:为什么createCaptureSession()总是失败?
CameraDevice.createCaptureSession()是 Camera2 初始化中最易失败的环节。失败原因几乎全部归结于Surface 列表不匹配。createCaptureSession()的第一个参数是List<Surface>,它必须与CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP中声明的输出格式完全一致。例如:
- 若你用
ImageReader创建了YUV_420_888格式的Surface,则StreamConfigurationMap中必须存在该格式的支持尺寸; - 若你同时向
SurfaceView和ImageReader输出,则两个Surface的尺寸必须都在getOutputSizes()返回的列表中; - 最致命的是:
SurfaceView的Surface尺寸必须等于你之前选定的mPreviewSize,否则createCaptureSession()抛IllegalArgumentException。
下面是完整的 session 创建代码(接续openCamera()):
private void createCaptureSession() { try { // 创建用于预览的 Surface(来自 SurfaceViewL) Surface previewSurface = mPreviewSurface; // 创建用于图像处理的 ImageReader(YUV 格式) mImageReader = ImageReader.newInstance( mPreviewSize.getWidth(), mPreviewSize.getHeight(), ImageFormat.YUV_420_888, 2 // buffer 数量,2 是最低安全值 ); Surface imageSurface = mImageReader.getSurface(); // 构建 Surface 列表:必须包含所有将被写入的 Surface List<Surface> outputSurfaces = new ArrayList<>(); outputSurfaces.add(previewSurface); outputSurfaces.add(imageSurface); // 创建 session mCameraDevice.createCaptureSession( outputSurfaces, new CameraCaptureSession.StateCallback() { @Override public void onConfigured(@NonNull CameraCaptureSession session) { mCaptureSession = session; // 配置预览请求 try { CaptureRequest.Builder builder = mCameraDevice.createCaptureRequest( CameraDevice.TEMPLATE_PREVIEW ); builder.addTarget(previewSurface); builder.addTarget(imageSurface); // 强制设为自动曝光(避免某些设备默认关闭) builder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON); builder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_VIDEO); // 提交重复请求,开始预览 session.setRepeatingRequest( builder.build(), null, mBackgroundHandler ); } catch (CameraAccessException e) { Log.e("Camera2", "Failed to start preview", e); } } @Override public void onClosed(@NonNull CameraCaptureSession session) { Log.d("Camera2", "Session closed"); } @Override public void onConfigureFailed(@NonNull CameraCaptureSession session) { Log.e("Camera2", "Session configuration failed"); } }, mBackgroundHandler ); } catch (CameraAccessException e) { Log.e("Camera2", "Failed to create capture session", e); } }关键参数说明:
ImageReader.newInstance(..., 2)中的2是 buffer 数量。设为1会导致ImageReader频繁丢帧(尤其在onImageAvailable()处理慢时);设为4以上则增加内存压力,camera多媒体buffer管理的核心就是平衡此值与处理延迟。builder.addTarget()必须添加所有outputSurfaces中的Surface,缺一不可。漏掉imageSurface会导致ImageReader永远收不到数据。setRepeatingRequest()的第三个参数mBackgroundHandler必须是非 UI 线程的Handler,否则onCaptureCompleted()会在主线程触发 ANR。
3. 解析 YUV 数据与atc_camera.xml配置落地:从 raw buffer 到可用图像
Camera2 默认输出ImageFormat.YUV_420_888,这是一种 planar YUV 格式,包含三个独立 plane:Y(亮度)、U(色度 U)、V(色度 V)。直接将其转为 Bitmap 会得到严重偏色的图像,因为 Android 的YuvImage类不支持YUV_420_888(仅支持NV21)。必须手动解析 plane 数据并转换。而atc_camera.xml文件(常出现在高通平台 BSP 中)则定义了该设备的 sensor 驱动参数映射,直接影响CaptureRequest中SENSOR_SENSITIVITY、SENSOR_EXPOSURE_TIME等字段的实际效果。
3.1 手动解析 YUV_420_888:绕过YuvImage的兼容性限制
ImageReader.OnImageAvailableListener的onImageAvailable()回调中,Image对象的getPlanes()返回三个Image.Plane,每个包含getBuffer()和getRowStride()。关键点在于:YUV_420_888的 U/V plane 行宽(rowStride)可能大于width,且存在pixelStride(像素跨距),必须用getRowStride()和getPixelStride()计算实际内存偏移,而非简单按width * height拷贝。
// ImageReader.OnImageAvailableListener 实现 private final ImageReader.OnImageAvailableListener mOnImageAvailableListener = reader -> { Image image = null; try { image = reader.acquireLatestImage(); if (image == null) return; Image.Plane[] planes = image.getPlanes(); ByteBuffer yBuffer = planes[0].getBuffer(); ByteBuffer uBuffer = planes[1].getBuffer(); ByteBuffer vBuffer = planes[2].getBuffer(); int ySize = yBuffer.remaining(); int uSize = uBuffer.remaining(); int vSize = vBuffer.remaining(); // 分配目标数组(YUV_420_888 → NV21) byte[] nv21 = new byte[ySize + uSize + vSize]; // Y plane 直接拷贝 yBuffer.get(nv21, 0, ySize); // VU 交错拷贝(NV21 格式:Y + V + U,且 UV 为半采样) byte[] vData = new byte[uSize]; byte[] uData = new byte[vSize]; vBuffer.get(vData); uBuffer.get(uData); // NV21 要求 VU 交错,每 2x2 像素共用一组 UV for (int i = 0; i < uSize; i++) { nv21[ySize + i * 2] = vData[i]; // V nv21[ySize + i * 2 + 1] = uData[i]; // U } // 此时 nv21 可传给 YuvImage 或 OpenCV 处理 YuvImage yuvImage = new YuvImage(nv21, ImageFormat.NV21, mPreviewSize.getWidth(), mPreviewSize.getHeight(), null); ByteArrayOutputStream out = new ByteArrayOutputStream(); yuvImage.compressToJpeg(new Rect(0, 0, mPreviewSize.getWidth(), mPreviewSize.getHeight()), 80, out); byte[] jpegBytes = out.toByteArray(); // jpegBytes 即为可用 JPEG 图像 } catch (Exception e) { Log.e("Camera2", "Error parsing YUV", e); } finally { if (image != null) image.close(); } };注意:
acquireLatestImage()会丢弃中间帧,确保只处理最新一帧,避免ImageReaderbuffer 队列积压。这是camera多媒体buffer管理的基础实践——宁可丢帧,不可 OOM。
3.2atc_camera.xml的作用与参数映射:让SENSOR_EXPOSURE_TIME真正生效
atc_camera.xml是高通平台特有的 sensor 配置文件,路径通常为/vendor/etc/camera/atc_camera.xml。它不被 Camera2 API 直接读取,而是由 HAL 层加载,用于将CaptureRequest中的抽象参数(如SENSOR_EXPOSURE_TIME)映射到具体 sensor 寄存器值。若该文件缺失或配置错误,set(CaptureRequest.SENSOR_EXPOSURE_TIME, 10000000)可能完全无效,设备仍按自动模式运行。
典型atc_camera.xml片段如下:
<CameraParameters> <Sensor name="ov5670"> <ExposureTime min="10000" max="33333333" step="10000"/> <Sensitivity min="100" max="1600" step="100"/> <AeMode supported="true"/> </Sensor> </CameraParameters>要验证你的CaptureRequest参数是否被 HAL 正确接收,需结合adb logcat | grep -i "exposure\|ae\|sensor"查看 HAL 日志。若日志中无setExposureTime相关输出,说明atc_camera.xml未生效或 sensor 名称不匹配。此时应:
- 用
adb shell dumpsys media.camera确认当前打开的 sensor 名称; - 检查
/vendor/etc/camera/下是否存在对应 sensor 的 xml 文件; - 确保
CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE与 xml 中定义的分辨率范围一致。
提示:
linux camera开发者常通过v4l2-ctl --list-ctrls查看参数,但在 Android Camera2 中,所有控制必须经由CaptureRequest提交。atc_camera.xml就是连接上层 Java API 与底层v4l2ioctl 的翻译表。
4. 排查CameraAccessException与SurfaceAbandonedException:基于camera 摄像头 vts的验证方法
CameraAccessException是 Camera2 开发中最顽固的异常,它不指向具体代码行,而是笼统表示“访问被拒绝”。根据camera 摄像头 vts(Vendor Test Suite)的验证逻辑,该异常通常源于三类根本原因:HAL 层资源冲突、Surface状态非法、或CaptureRequest参数越界。而SurfaceAbandonedException则明确指向Surface被提前释放或重复使用。本节提供可立即执行的排查指令和日志分析法。
4.1 用adb命令快速定位 HAL 层冲突
当openCamera()抛CameraAccessException时,先排除系统级资源占用:
# 查看当前所有 camera 进程(包括后台服务) adb shell ps | grep -i camera # 检查 camera HAL 是否正常加载 adb shell dumpsys media.camera | grep -A 10 "CameraService" # 强制停止所有 camera 相关服务(开发调试用,勿在生产环境执行) adb shell am force-stop com.android.camera2 adb shell pkill -f "camera"若dumpsys media.camera输出中CameraService状态为NOT_AVAILABLE或DEAD,说明 HAL 层崩溃。此时需查看logcat -b events | grep -i "camera\|hal",重点关注HAL: error opening device或sensor probe failed。
4.2SurfaceAbandonedException的精准复现与修复
该异常必现于以下场景:在CameraCaptureSession运行期间,调用了SurfaceView.getHolder().getSurface().release(),或在surfaceDestroyed()中未及时释放mPreviewSurface。camera 摄像头 vts的VtsHalCameraProviderTargetTest会主动触发此场景以验证 robustness。
修复方案是严格遵循Surface生命周期:
// SurfaceViewL.java 中的修正版 surfaceDestroyed() @Override public void surfaceDestroyed(SurfaceHolder holder) { // 1. 先关闭 CaptureSession(同步阻塞,确保不再写入 Surface) if (mCaptureSession != null) { try { mCaptureSession.close(); // 此调用会等待所有 pending request 完成 } catch (Exception e) { Log.w("Camera2", "Error closing session", e); } mCaptureSession = null; } // 2. 再释放 Surface if (mPreviewSurface != null) { mPreviewSurface.release(); mPreviewSurface = null; } // 3. 最后关闭 CameraDevice(必须在 session 关闭后) if (mCameraDevice != null) { mCameraDevice.close(); mCameraDevice = null; } }关键逻辑:
mCaptureSession.close()是同步方法,它会阻塞直到所有CaptureRequest执行完毕并释放关联Surface。若在此前释放mPreviewSurface,HAL 层尝试写入已销毁的Surface,即触发SurfaceAbandonedException。vts测试正是通过在close()前注入Surface.release()来验证此边界。
4.3CaptureRequest参数越界检查表:避免IllegalArgumentException的 3 个必查项
createCaptureRequest()成功不代表setRepeatingRequest()会成功。CaptureRequest.Builder.set()的参数必须落在CameraCharacteristics声明的范围内,否则抛IllegalArgumentException。以下是camera 摄像头 vts中强制校验的三项:
| 参数名 | 获取方式 | 安全范围示例 | 越界后果 |
|---|---|---|---|
SENSOR_EXPOSURE_TIME | chars.get(CameraCharacteristics.SENSOR_INFO_EXPOSURE_TIME_RANGE) | [10000, 33333333](纳秒) | 曝光为 0 或最大值,图像全黑/全白 |
SENSOR_SENSITIVITY | chars.get(CameraCharacteristics.SENSOR_MAX_ANALOG_SENSITIVITY) | 100 ~ 1600 | ISO 失效,自动模式接管 |
CONTROL_AVAILABLE_EFFECTS | chars.get(CameraCharacteristics.CONTROL_AVAILABLE_EFFECTS) | ["none", "mono"] | 设置"sepia"时崩溃 |
验证代码:
// 在 openCamera() 后,setRepeatingRequest() 前插入 Range<Long> exposureRange = chars.get(CameraCharacteristics.SENSOR_INFO_EXPOSURE_TIME_RANGE); Long exposureTime = 10000000L; // 10ms if (exposureRange != null && !exposureRange.contains(exposureTime)) { Log.w("Camera2", "Exposure time " + exposureTime + " not in range " + exposureRange); exposureTime = exposureRange.getUpper(); // 降级为最大值 } builder.set(CaptureRequest.SENSOR_EXPOSURE_TIME, exposureTime);提示:
SENSOR_INFO_EXPOSURE_TIME_RANGE的单位是纳秒,而SENSOR_EXPOSURE_TIME字段也要求纳秒。常见错误是传入毫秒值(如10代表 10ms),导致曝光时间为 10 纳秒(几乎为 0),图像全黑。这是200 万的 ip camera 选用 1x的chart场景下最易复现的参数误用。
5. 在SurfaceViewL.java中集成ImageReader的最佳实践:缓冲区复用与线程安全
SurfaceViewL.java的最终目标不是仅仅显示预览,而是为上层业务(如人脸识别、AR 渲染)提供低延迟、零拷贝的 YUV 数据流。这要求ImageReader的OnImageAvailableListener必须高效处理每一帧,且不能因ByteBuffer拷贝引入额外延迟。camera多媒体buffer管理的核心技巧在于:复用Image对象、避免ByteBuffer.array()拷贝、将耗时操作移出回调线程。
5.1 复用Image对象:减少 GC 压力与内存分配
ImageReader.acquireLatestImage()每次返回新Image对象,频繁调用会触发大量 GC。SurfaceViewL.java应维护一个Image缓存池,复用已关闭的Image:
// SurfaceViewL.java 中新增 private final Queue<Image> mImagePool = new ConcurrentLinkedQueue<>(); private Image acquireImage() { Image image = mImagePool.poll(); if (image == null) { // 池空,新建 image = mImageReader.acquireLatestImage(); } return image; } private void recycleImage(Image image) { if (image != null && !image.isClosed()) { image.close(); mImagePool.offer(image); // 放回池中 } } // 在 onImageAvailableListener 中 private final ImageReader.OnImageAvailableListener mOnImageAvailableListener = reader -> { Image image = acquireImage(); // 从池中获取 if (image == null) return; try { // 解析 YUV 数据(同前文) parseYuvImage(image); } finally { recycleImage(image); // 处理完立即归还 } };优势:
ConcurrentLinkedQueue无锁,适合多线程场景;acquireLatestImage()本身已是线程安全,配合池化后,Image对象分配频率降低 90% 以上,camera 摄像头 vts的VtsHalCameraProviderPerfTest会显著提升帧率稳定性。
5.2 零拷贝传递ByteBuffer:对接 OpenCV 或 NPU 推理引擎
若业务需要将 YUV 数据送入 OpenCVMat或 NPU 推理框架(如qwen lmage multipleangles 3d camera场景下的多视角融合),应避免ByteBuffer.get(byte[])拷贝。Image.Plane.getBuffer()返回的ByteBuffer是直接映射到 GPU buffer 的,可零拷贝传递:
// OpenCV Java 示例(无需拷贝) private void parseYuvImage(Image image) { Image.Plane[] planes = image.getPlanes(); ByteBuffer yBuffer = planes[0].getBuffer(); ByteBuffer uBuffer = planes[1].getBuffer(); ByteBuffer vBuffer = planes[2].getBuffer(); // 直接用 buffer 地址构造 Mat(需 JNI 层支持) // Mat yMat = new Mat(mPreviewSize.getHeight(), mPreviewSize.getWidth(), CvType.CV_8UC1, yBuffer); // Mat uvMat = new Mat(mPreviewSize.getHeight()/2, mPreviewSize.getWidth()/2, CvType.CV_8UC2, uBuffer); // 若必须 Java 层处理,至少用 wrap 避免 array() 拷贝 byte[] yArray = yBuffer.array(); int yOffset = yBuffer.arrayOffset(); // 后续操作基于 yArray + yOffset,而非 new byte[ySize] }注意:
ByteBuffer.array()会触发一次完整内存拷贝,而arrayOffset()仅返回起始偏移。在cocoscreator camera targettexture等需要高频纹理更新的场景中,零拷贝是维持 60fps 的必要条件。
5.3 线程模型隔离:mBackgroundHandler的正确构建
onCaptureCompleted()和onImageAvailable()都在mBackgroundHandler关联的线程中执行。若该 Handler 绑定到主线程,会导致 UI 卡顿;若绑定到短生命周期线程(如new Thread().start()),则线程退出后回调丢失。标准做法是创建专属HandlerThread:
// SurfaceViewL.java 中 private HandlerThread mBackgroundThread; private Handler mBackgroundHandler; private void startBackgroundThread() { mBackgroundThread = new HandlerThread("CameraBackground"); mBackgroundThread.start(); mBackgroundHandler = new Handler(mBackgroundThread.getLooper()); } private void stopBackgroundThread() { if (mBackgroundThread != null) { mBackgroundThread.quitSafely(); try { mBackgroundThread.join(); mBackgroundThread = null; mBackgroundHandler = null; } catch (InterruptedException e) { Log.e("Camera2", "Background thread join failed", e); } } }关键点:
quitSafely()会等待所有已入队消息执行完毕,确保onImageAvailable()不被中断;join()则阻塞直到线程终止,避免Handler持有已销毁 Looper 的引用。这是next camera apk类应用稳定性的底层保障。
本文还有配套的精品资源,点击获取