简介面向 Android 开发者深入理解 Camera2 API 的完整资料包围绕相机架构、CaptureRequest 控制、对焦与自动曝光、图像回调、录像、性能优化及兼容性等主题展开适合已有 Android 基础、希望实现专业拍照或视频功能的中高级开发者。压缩包为 zip 格式共 1520 个文件大小约 44.72MB包含 649 个 xml 界面布局与资源、412 个 png 图标、128 个 json 配置、218 个 class 字节码以及 aidl 接口、jar 依赖、dex 与 app-debug.apk 可直接安装的示例应用整体为完整的 Android Studio 工程及构建产物。目前已有 1832 人学习下载。这份资料的价值在于将 Camera2 API 的抽象概念落到可运行代码中既能查看关键接口与类实现也能通过示例 APK 直接体验曝光、对焦、白平衡等参数调整效果对照资源描述的十大知识模块逐项研读可快速搭建自己的相机应用并规避厂商兼容性与性能优化中的常见陷阱。1. 为什么还在折腾Camera2从灰度预览和手动曝光说起做扫码、OCR、文档拍摄或者真正的手动相机类App时你会发现绕不开Android Camera2 API。CameraX确实更省事但它的封装把HAL级别的曝光补偿、对焦区域、RAW输出、逐帧回调都收进了黑匣子Camera2正好反过来它让开发者直接面对CaptureRequest、CaptureSession、ImageReader这套管线把每一帧的流向和参数变化都摊在明面上。这篇实战笔记写给两类人一类是新手想搞清楚Camera2到底怎么组织预览和拍照另一类是有Camera1或CameraX经验、被设备兼容性逼到需要自己控制底层的熟手。下面从核心概念开始一路落到可复现的代码和踩坑记录。2. Camera2核心概念CaptureRequest如何一步步影响一帧画面2.1 一张职责表理清相机管线里的角色Camera2 API最大的入门门槛是它把相机抽象成了多个对象配合工作的管线。先用一张表把角色和职责划清楚角色对应类职责常见误用相机设备CameraDevice与单个物理/逻辑相机建立连接负责打开和关闭当成一次性资源用完不close会话CameraCaptureSession管理一组Surface的输出通道是提交请求的入口以为切换Surface必须重建会话请求CaptureRequest描述“接下来这一帧/一串帧怎么拍”的完整参数和输出目标修改后不重新提交参数不生效结果CaptureResult报告这一帧实际使用的参数和状态认为结果总是和请求一致帧数据ImageReader把帧数据曝光到内存缓冲区供应用直接读取忘记close导致缓冲区耗尽从这张表能看出一个核心观点Camera2不是“调个参数、拍一张照片”的接口而是一个“不断提交请求、不断拿回结果”的循环。请求在某个时间点被消费结果在稍后的时间点带着真实参数返回两者之间的对应关系是理解后面所有代码的关键。2.2 请求、会话、结果的循环关系Camera2默认是“重复请求”的工作模式。你可以把CaptureSession想象成一条流水线CaptureRequest是送进流水线的工单CameraDevice按工单要求曝光出帧最后捕获回调把这份工单的执行结果送回应用侧。我一般会这样组织循环// 1. 在会话配置成功后提交一个持续预览请求 session.setRepeatingRequest(previewRequest, metadataCallback, backgroundHandler); // 2. 在metadataCallback里读取每一帧的真实参数 Override public void onCaptureCompleted(CaptureSession session, CaptureRequest request, TotalCaptureResult result) { // 注意这里拿到的曝光时间、ISO、对焦状态才是硬件真正使用的值 long exposureTime result.get(CaptureResult.SENSOR_EXPOSURE_TIME); int iso result.get(CaptureResult.SENSOR_SENSITIVITY); }这段代码的逻辑是setRepeatingRequest告诉相机“以后每一帧都按这个工单来”CaptureCallback负责接收每帧执行完的结果。如果你要做一个停顿即拍摄的相机就再提交一个一次性的TEMPLATE_STILL_CAPTURE请求拍完再恢复预览请求。参数说明上要注意两个容易忽略的点第一CaptureRequest里设置的曝光、ISO是对HAL的“请求”HAL有权就近修正第二个真正写入JPEG头里的曝光时间要读CaptureResult而不是读你设置的请求值。调试时我经常遇到“我设置了ISO 800照片属性里却是640”的情况原因就是AE还没有完全收敛HAL做了自动微调。2.3 模板选型TEMPLATE_PREVIEW还是TEMPLATE_STILL_CAPTURECameraDevice提供了多套预设模板直接决定了请求的基础参数策略。最常用的是这两个// 预览模板帧率优先AE/AF持续运转 CaptureRequest.Builder previewBuilder cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW); // 拍照模板画质优先允许更长的曝光时间 CaptureRequest.Builder stillBuilder cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);两套模板的区别不是“预览请求不能拍照”而是HAL在自动曝光、自动对焦、降噪策略上的偏好完全不同。预览模板会把目标帧率放在前面曝光时间会被压缩拍照模板则允许曝光时间变长、噪声优化更积极代价是帧率低。所以一个常见的错误做法是把预览用的Builder直接拿走去拍照结果就是照片偏暗或噪声偏多。选择建议如果只是做扫一扫、灰度预览、人脸跟踪用TEMPLATE_PREVIEW就够了要做文档拍摄或手动摄影拍照帧必须单独建TEMPLATE_STILL_CAPTURE请求并且不直接使用预览Surface而是接到ImageReader的Surface上。TEMPLATE_RECORD适合视频录制、TEMPLATE_ZERO_SHUTTER_LAG适合高速连拍这两类场景后面再展开。3. 把预览跑起来最小可用Camera2项目的完整落地3.1 权限声明与相机ID选择Camera2对权限的检查和Camera1不同它要求同时声明相机权限和可见性声明而且从Android 9开始运行时权限申请失败会导致相机打开后立即出现CAMERA_DISCONNECTED。我的做法是打开Activity就先申请Manifest.permission.CAMERA拒绝就直接禁用入口不做那种“首次能聊天、二次才能进”的模糊处理。uses-feature android:nameandroid.hardware.camera.any android:requiredtrue / uses-permission android:nameandroid.permission.CAMERA /相机ID选取上别写死0。多摄机型的ID规则并不统一有的是0/1代表主摄和广角有的0就是前置1才是主摄。我一般这样探测CameraManager manager (CameraManager) getSystemService(Context.CAMERA_SERVICE); for (String cameraId : manager.getCameraIdList()) { CameraCharacteristics chars manager.getCameraCharacteristics(cameraId); Integer facing chars.get(CameraCharacteristics.LENS_FACING); // 优先选后置LENS_FACING_BACK找不到再回退到任一可用ID if (facing ! null facing CameraCharacteristics.LENS_FACING_BACK) { selectedId cameraId; break; } }这段逻辑里值得说明的是index和ID并不等价。getCameraIdList()返回的可能是0、1、2但也可能是非连续的数字串。在多摄场景下厂商对ID顺序并没有统一规范通过LENS_FACING选择朝向是更稳的方案。如果设备是折叠屏或外接摄像头ID列表可能更多但按朝向过滤总是成立的。3.2 打开相机、创建会话、发起持续预览最小可用流程分三步openCamera→createCaptureSession→setRepeatingRequest。下面这段代码把三者串起来注意回调的时序onOpened里能创建会话onConfigured里才允许提交请求。// 第一步打开相机状态回调落在后台线程 manager.openCamera(selectedId, new CameraDevice.StateCallback() { Override public void onOpened(NonNull CameraDevice camera) { device camera; try { // 第二步把预览Surface装进会话的输出去向 device.createCaptureSession( Arrays.asList(previewSurface, imageReader.getSurface()), new CameraCaptureSession.StateCallback() { Override public void onConfigured(NonNull CameraCaptureSession session) { sessionRef.set(session); buildAndStartPreview(); } }, backgroundHandler ); } catch (CameraAccessException e) { // 处理CAMERA_IN_USE或MAX_CAMERAS_IN_USE } } // onDisconnected / onError 都要回调 }, backgroundHandler); // 第三步发起重复预览请求 private void buildAndStartPreview() { CaptureRequest.Builder builder device.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW); builder.addTarget(previewSurface); CaptureRequest request builder.build(); sessionRef.get().setRepeatingRequest(request, callback, backgroundHandler); }逻辑说明createCaptureSession的ListSurface参数决定“这一组会话接受哪些输出目标”之后再提交的请求必须包含至少其中一个Surface否则会抛IllegalArgumentException。Camera2不允许给请求添加不在会话列表里的Surface这一点和直觉相反——很多人以为请求里可以随便加Surface实际必须先建会话、再建请求。参数说明有两个点第一onConfigured回调可能不在主线程触发所有UI相关操作记得再post回主线程第二setRepeatingRequest的请求与capture的请求差异在于前者会持续接管预览后者只作用一帧。如果你在拍照后希望回到预览必须再调用一次setRepeatingRequest。3.3 方向与旋转黑屏问题的高发源头预览黑屏最隐蔽的两个原因一个是Surface尺寸不匹配一个是旋转方向错了。先说旋转前置摄像头输出到TextureView时如果Activity是竖屏就需要把预览帧旋转90度或270度展示否则画面是横的严重时会直接黑屏。// 从传感器朝向和显示器旋转计算最终旋转角 int sensorOrientation chars.get(CameraCharacteristics.SENSOR_ORIENTATION); int displayRotation getWindowManager().getDefaultDisplay().getRotation(); // 后置旋转角 (sensorOrientation - deviceRotation * 因子 360) % 360 // 其中deviceRotation把Surface.ROTATION_0/90/180/270换算成度数常见做法是后置相机旋转角等于(sensorOrientation - deviceRotationDegrees 360) % 360前置要加镜像逻辑公式变成(sensorOrientation deviceRotationDegrees 360) % 360。这里不用硬记HAL给的sensorOrientation经常是90或270你只需要保证TextureView.getTransform()里的旋转角和你测出来的最终值一致。黑屏的第二类原因是Surface配置在会话创建前没准备好。SurfaceTexture需要先设好默认缓冲区尺寸再创建Surface传进会话。相比SurfaceViewTextureView会在主线程回调缺帧时表现得更加隐晦黑屏概率也更高。我的习惯是开启setOpaque(false)并监听第一帧的回调做一个“预览已启动”的UI提示超过两秒没回调就可以断言尺寸或会话配置有问题。4. 手动控制实战曝光、对焦与白平衡的参数级拆解4.1 曝光补偿与ISOAE模式下先把画面暗下来手动相机的核心不是“关掉自动”而是“保留自动引擎、调整它的目标”。大部分场景不需要把AE完全关掉只调节曝光补偿就够了。Camera2里对应三个参数CONTROL_AE_EXPOSURE_COMPENSATION、SENSOR_EXPOSURE_TIME、SENSOR_SENSITIVITY。// 在TEMPLATE_PREVIEW的Builder上叠加手动曝光配置 builder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON); builder.set(CaptureRequest.CONTROL_AE_LAOCK, false); // 注意拼写LOCK不是LAOCK // 曝光补偿范围先查后设 RangeInteger evRange chars.get(CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE); builder.set(CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION, clamp(evValue, evRange.getLower(), evRange.getUpper()));逻辑说明CONTROL_AE_EXPOSURE_COMPENSATION的单位是“步进”每步一般是1/3 EV具体步长要读CONTROL_AE_COMPENSATION_STEP。设计上AE补偿不需要关掉自动曝光它只改变HAL内部的亮度目标。如果你把曝光补偿从0调到-6画面看起来会明显变暗但高光溢出反而减轻这就是文档拍摄压高光的常见思路。真正需要关掉AE的场景是夜景长曝光和固定曝光时间连拍。这种时候要把CONTROL_AE_MODE设为CONTROL_AE_MODE_OFF再手动设置SENSOR_EXPOSURE_TIME和SENSOR_SENSITIVITY两个值必须同时设置只设曝光时间不设ISO会得到一张全黑的帧builder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_OFF); builder.set(CaptureRequest.SENSOR_EXPOSURE_TIME, 33_000_000L); // 33ms builder.set(CaptureRequest.SENSOR_SENSITIVITY, 400); // ISO 400参数说明里有个容易踩的坑曝光时间的单位是纳秒33毫秒要写成33_000_000L写33f就变成33纳秒图片几乎全黑。而ISO的取值范围需要读sensorInfo.sensitivityRange不同设备上限差别很大从800到102400都有。设置非法值时请求会在HAL层被拒绝或悄悄修改调试时很难定位。4.2 点按对焦AF_REGIONS的坐标换算与触发时序相机自动对焦默认是连续对焦CONTINUOUS_PICTURE但点按对焦需要你把屏幕坐标换算成sensor坐标并设置到CONTROL_AF_REGIONS里。Audio作为坐标换算最容易出错的环节因为Camera2的坐标体系和屏幕方向并不一致。// 把点击的屏幕坐标换算成sensor坐标并构造对焦区域 Rect sensorRect chars.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE); float x (event.getX() / viewWidth) * sensorRect.width() sensorRect.left; float y (event.getY() / viewHeight) * sensorRect.height() sensorRect.top; // MeteringRectangle权重最小为1设为0会导致区域被忽略 MeteringRectangle focusArea new MeteringRectangle( (int) (x - 100), (int) (y - 100), 200, 200, 1000); builder.set(CaptureRequest.CONTROL_AF_REGIONS, new MeteringRectangle[]{focusArea}); builder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO); builder.set(CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START);逻辑说明CONTROL_AF_TRIGGER_START是“触发并锁定”的意思相机完成对焦后需要再提交一个CONTROL_AF_TRIGGER_CANCEL的请求来解锁否则之后的连续对焦不会恢复。更精细的做法是监听CaptureResult.CONTROL_AF_STATE从FOCUSED_LOCKED变成ACTIVE_SCAN代表开始对焦回到PASSIVE_SCAN后重新设置AF_TRIGGER_IDLE恢复默认。参数说明屏幕坐标系和sensor坐标系的比例不是直接映射。如果预览画面经过了旋转你必须先做坐标更正。检查镜头镜像状态LENS_RADIAL_DISTORTION虽然不能直接坐标换算但能帮你判断这个机型是否在自拍时做了镜面输出。真正常用的做法是预览View的宽高比和sensor的宽高比不一致时按scaleType.CENTER_CROP先裁剪再手算偏移。4.3 白平衡与色温AWB_OFF后的参数边界白平衡在Camera2里用CONTROL_AWB_MODE控制手动色温则设置COLOR_CORRECTION_MODE和COLOR_CORRECTION_GAINS。很多资料会把“手动白平衡”错误地等同于“改色温值”实际Camera2没有直接暴露色温K而是通过三通道增益控制。builder.set(CaptureRequest.CONTROL_AWB_MODE, CaptureRequest.CONTROL_AWB_MODE_OFF); builder.set(CaptureRequest.COLOR_CORRECTION_MODE, CaptureRequest.COLOR_CORRECTION_MODE_TRANSFORM_MATRIX); // 三通道增益典型日光平衡R1.8, G1.0, B1.4具体数值要结合设备 RggbChannelVector gains new RggbChannelVector(1.8f, 1.0f, 1.0f, 1.4f); builder.set(CaptureRequest.COLOR_CORRECTION_GAINS, gains);逻辑说明RggbChannelVector四通道分别对应RGGB排列的四个子像素而不是三个通道。想偏暖就把R通道增益调高、B通道增益调低想偏冷就反过来。该功能只在AWB关闭时生效如果你只设了COLOR_CORRECTION_GAINS没关AWB大多数设备会忽略设定值。这里的关系是CONTROL_AWB_MODE_AUTO每帧都会覆盖你设置的增益所以手动白平衡必须先OFF再设置色彩校正参数。部分机型还提供CONTROL_AWB_LOCK它在AWB模式下锁定当前色温用途是拍摄连续画面时防止白平衡跳跃如果设备不支持手动增益锁定AWB是退而求其次的方案效果稳定但不区分色温方向。5. 拍照与帧数据采集ImageReader、YUV转JPEG与RAW输出5.1 ImageReader的配置与Image关闭时机ImageReader是应用侧读取帧数据的窗口。预览Surface直连显示器ImageReader则连内存缓冲区适合做拍照、分析、算法前处理。它的最关键参数是图像格式、最大图像数量、宽高。ImageReader reader ImageReader.newInstance(width, height, ImageFormat.YUV_420_888, 3); reader.setOnImageAvailableListener(new ImageReader.OnImageAvailableListener() { Override public void onImageAvailable(ImageReader reader) { Image image reader.acquireLatestImage(); if (image ! null) { processImage(image); // 这里只做数据处理不做耗时IO image.close(); // 必须立刻关闭否则缓冲区耗尽后不再回调 } } }, backgroundHandler);逻辑说明acquireLatestImage()会丢掉已存在的旧帧、只返回最新一帧适合实时处理场景acquireNextImage()按队列顺序返回适合连拍。无论用哪种Image.close()必须在处理完数据后同步调用否则缓冲池被占满之后的帧回调会完全停止表现就是预览黑屏、拍照卡死。参数说明上maxImages设成3已经能应对大部分场景。设1虽然省内存但处理速度稍有波动就会丢帧设5以上在旧设备上容易造成内存压力。需要特别注意的是格式匹配ImageReader的format必须和CaptureRequest里addTarget的Surface格式一致常见坑是在TEMPLATE_STILL_CAPTURE里用了JPEG格式Surface却新建了一个YUV_420_888的ImageReader系统会直接抛异常。5.2 YUV_420_888转JPEG旋转、布局与压缩细节从ImageReader拿到的YUV_420_888数据是三个独立的ByteBuffer分别存储Y、U、V平面。注意它和JPEG编码器的输入格式不完全一致大多数编码器需要连续的NV12或NV21数据所以要自己做一次packing。最稳妥的做法是先用矩阵把Y平面保持不变、UV交错再送给编码器。// 把YUV_420_888的Y平面和UV平面合并成NV21字节数组 private byte[] yuv420ToNv21(Image image, int width, int height) { Image.Plane[] planes image.getPlanes(); byte[] nv21 new byte[width * height * 3 / 2]; // Y平面通常有pixelStride1按行拷贝 ByteBuffer yBuffer planes[0].getBuffer(); int rowStride planes[0].getRowStride(); // U/V平面的pixelStride常见为2需要解包 ByteBuffer uBuffer planes[1].getBuffer(); ByteBuffer vBuffer planes[2].getBuffer(); // ... 省略逐行解包循环 return nv21; }逻辑说明Plane.getRowStride()返回的是行字节数它可能大于宽度因为硬件会做对齐。直接按width逐行拷贝会得到错位的画面。U/V平面的pixelStride指相邻同色像素的间隔通常为2因此不能把整个uBuffer当成一个紧凑平面。这两个布局参数是YUV转码里最隐蔽的坑我见过很多开发者用固定分配方式导致颜色边缘发绿。旋转的处理方式更省事不用在ByteBuffer层面做旋转直接等编码完再对JPEG做Matrix旋转。如果是带EXIF方向的Camera2原生JPEG可以在CaptureResult.JPEG_ORIENTATION里取到方向把旋转动作交给照片库处理如果是自己拼的NV21就绕不开编码前或编码后的旋转。两者的取舍是前置镜头的镜像必须在编码前处理否则拍出来的字方向相反。5.3 RAW与DNG超出JPEG的后期空间手动相机App在硬件支持的前提下输出RAW比直出JPEG更符合“手动”的定位。Camera2里RAW对应ImageFormat.RAW_SENSOR格式是14位或16位 Bayer数据不能直接显示必须先封装成DNG再交给后期软件。// 创建与RAW_SENSOR匹配的ImageReader ImageReader rawReader ImageReader.newInstance(w, h, ImageFormat.RAW_SENSOR, 3); // 在STILL_CAPTURE请求里把rawReader.getSurface()加入目标 builder.addTarget(rawReader.getSurface()); // 收到RAW帧后用DngCreator封装为DNG文件 DngCreator creator new DngCreator(chars, metadataCaptureResult); FileOutputStream fout new FileOutputStream(dngFile); creator.writeByteBuffer(fout, image.getPlanes()[0].getBuffer()); creator.close(); image.close();逻辑说明DngCreator是系统提供的DNG编码器把RAW数据结合相机特性参数转换成标准DNG格式前提是传入的CaptureResult是这份RAW帧对应的结果不能拿几秒前的预览结果来代替。写入时要传入单个Plane的Buffer而不是三平面。如果你发现DNG在PC端打开是全黑往往是ISO或曝光时间设置在RAW请求里没有生效也可能你传入了错误的TotalCaptureResult。这里有一个特征值需要核对LENS_INFO_AVAILABLE_FOCAL_LENGTHS和SENSOR_INFO_COLOR_FILTER_ARRANGEMENT在DNG写入时由chars自动带入但如果设备本身把RAW定义为YCbCr Bayer混合格式就要看REQUEST_AVAILABLE_CAPABILITIES里是否有RAW能力。没有这个capability的旧设备不会响应RAW_SENSOR格式创建ImageReader时直接抛IllegalArgumentException。6. 兼容性避坑指南从黑屏到崩溃的排查实战6.1 打开相机失败CAMERA_IN_USE 与 MAX_CAMERAS_IN_USE 的差异现象调用openCamera后立刻回调onError错误码为ERROR_CAMERA_IN_USE或ERROR_MAX_CAMERAS_IN_USE有时还有ERROR_CAMERA_DISCONNECTED。原因CAMERA_IN_USE表示这台相机被别的进程占用MAX_CAMERAS_IN_USE表示当前进程请求的相机ID总数超过设备上限比如部分设备允许同时打开3路摄像头但你的App已经打开了前置又去开主摄就可能触发后者。CAMERA_DISCONNECTED则通常是系统相机进程异常退出或设备被拔出。解决先检查Camera2内部没有重复打开也就是CameraDevice.StateCallback的onOpened与onDisconnected成对出现。收到CAMERA_IN_USE时用CameraManager.AvailabilityCallback监听onCameraAvailable等资源释放后再重试收到MAX_CAMERAS_IN_USE时不要立即重试先确认是否当前进程占用了其他相机。如果是USB摄像头热插拔导致DISCONNECTED只能提示用户重新插拔代码层面无法恢复。6.2 会话创建失败与尺寸适配现象createCaptureSession回调onConfigureFailedlogcat里能看到Stream configuration failed或类似的硬件层报错另一种隐蔽现象是会话创建成功但预览画面只有上半部分或整屏绿条。原因最常见的是把预览尺寸设成了sensor原始尺寸比如选了一个StreamConfigurationMap里不存在的组合或者同一个Surface同时被预览和ImageReader以两种尺寸使用。Camera2的HAL会对输出组合做合法性限制部分设备要求预览输出的宽高比和JPEG保持一致否则会话配置失败。解决不要自己枚举尺寸直接从StreamConfigurationMap.getOutputSizes(SurfaceTexture.class)里取可用列表先按比例匹配预览View的宽高再从中选最大的一组。选尺寸的顺序应该是——先过滤掉宽高小于480P的再按16:9或4:3匹配最后选最大记住getOutputSizes()返回的顺序不保证分辨率从大到小必须自己排序。6.3 LEGACY级别的特性边界与硬件能力检测现象同一份代码在A机支持手动ISO、RAW、电子防抖在B机上调用对应参数直接无效果CaptureResult里的值没有变化。原因INFO_SUPPORTED_HARDWARE_LEVEL定义了LIMITED和FULL的区别LIMITED设备只支持部分手动控制LEGACY级别更少。很多“祖传”中低端设备标称支持Camera2实际只有LEGACY级别能开CameraSession但不能提供RAW、传感器手动曝光等能力。解决所有进阶功能都要先读能力集再暴露。启动时做一次能力检测int level chars.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL); if (level CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL) { // 可以展示手动曝光、RAW等高级功能 } else if (level CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED) { // 只展示基础控制项蒙版手动曝光滑块 }同级设备之间还存在“名义支持但实际无效”的差异。一个可靠的做法是每次打开相机后提交一个测试请求等收到CaptureResult后判断SENSOR_EXPOSURE_TIME是否确实改变了没改变就在UI层隐藏对应控件而不是报错闪退。这个探测开销很低整个过程在后台Handler上执行用户基本无感。6.4 每次接入新机型时强制走一遍的三个动作兼容性问题的本质是没有在真机上验证。我自己带过太多项目在模拟器上跑得好好的换到实体设备就崩。接入新机型我强制走这三步第一步先打印INFO_SUPPORTED_HARDWARE_LEVEL和所有CONTROL_AE_COMPENSATION_RANGE到日志确认设备声称的能力边界。第二步用后台Handler实现一个“打开相机→建会话→取一帧→关闭”的自动测试任何异常都要留字段做记录不弹窗。第三步验证旋转和镜像拍一张有字的白纸检查预览方向和拍照方向是否一致。这三步之外还有一个实用技巧利用系统自带的在线相机能力查看器这类工具直接读取CameraCharacteristics全部参数能迅速定位是代码问题还是HAL问题。从那以后我每次支持新设备都不再问“为什么模拟器正常”而是默认“真机上一定有某个尺寸或参数是特殊的”把上面三步走完再合代码。多花十分钟能省掉用户反馈里大量的“打开闪退”“画面倒着”这类问题。希望这篇笔记能帮你绕过我已经踩过的坑也欢迎在实际调整参数时对照自己的设备日志观察变化规律。本文还有配套的精品资源点击获取