☰
Android相机开发:Camera1、Camera2与CameraX选型避坑指南
2026/10/29 15:04:31 网站建设 项目流程

做安卓相机开发这些年,被问得最多的一个问题就是:Camera、Camera2、CameraX 到底该用哪个?提问的人里有刚接手老项目的,代码里还躺着一坨 Camera1 的 open/release;也有新项目从零起步,被 CameraX 的 UseCase 组合绕得头晕;还有做完一轮兼容测试发现某几台机器预览是绿的、某几台机器拍照方向歪了,回来找根因的。安卓相机开发的麻烦从来不在于"能不能调起来",而在于调起来之后能不能在几百款设备上表现一致。这篇文章就围绕这三套 API 的差异、各自的调用套路、参数怎么定、坑在哪里、什么业务场景该选谁来展开,代码都给到能直接抄的程度,踩过的坑也会原样写出来。不管你是第一次写相机模块,还是正在做老项目迁移,看完应该能省掉至少两轮真机回归测试的时间。

1. 三套相机 API 的来龙去脉与整体设计思路

1.1 为什么同一个功能会有三套 API 并存

安卓相机这套东西的历史包袱很重。最早的 Camera 类(现在大家习惯叫 Camera1)是跟着早期系统一起出来的,那会儿手机相机就是"预览 + 拍一张",API 设计得非常扁平:打开相机、设置参数、绑定预览画面、开始预览、拍照、释放。这种设计的优点是上手快,二十行代码能让画面出来;缺点是把几乎所有控制权都锁死了——曝光、对焦、帧率这些底层能力要么没有接口,要么只有一个粗粒度的开关,想调个手动对焦距离都得看厂商心情。

后来系统升级,官方推出了 Camera2,思路完全换了一套:把相机当成一个可以被精细调度的设备,设备出数据流,流要挂到会话上,会话由一串请求驱动,每个请求都能带自己的一套参数。这套模型的表达能力强了不止一个量级,代价是调用方要自己管状态机、管线程、管缓冲区,代码量翻好几倍,而且早期的 Camera2 实现质量参差不齐,某些设备上还不如 Camera1 顺畅,这就导致大量团队"想用又不敢用"。

再往后就是 CameraX。它的出发点很朴素:绝大多数 App 对相机的需求就那几类——预览、拍照、录像、图像分析,为什么不把这些常见需求封装成开箱即用的组件?于是有了 UseCase 这个概念,你把 Preview、ImageCapture、ImageAnalysis 组合起来绑到生命周期上,剩下的兼容性适配交给库自己去处理。CameraX 底层仍然走 Camera2,在个别老设备上会退回 Camera1 的实现路径,所以它本质上是"一层聪明的封装",而不是第四套驱动。

三套 API 并存这件事短期内不会改变,理解它们各自解决什么问题,比死记 API 名字有用得多。

1.2 三套方案的架构差异一句话说清

用一句生活化的类比:Camera1 像是给你一台傻瓜相机,快门、对焦都帮你决定好了,你只需要按下去;Camera2 像是给你一台专业机身加一堆旋钮,每个参数都能拧,但你得知道每个旋钮是干什么的,拧错了画面就废;CameraX 像是一台带场景模式的相机,人像、夜景、连拍都预设好了,需要微调的时候也能进菜单改几个关键项。

落到代码层面,三者的核心差异体现在三个维度上:

维度Camera1Camera2CameraX
控制粒度粗,参数集中在 Parameters细,每个请求独立配置中,UseCase 暴露常用选项
状态管理简单,字符串状态复杂,设备/会话/请求三层状态机由库托管,绑生命周期即可
代码量(预览+拍照)约 100 行约 400 行起约 60 行
兼容性适配靠开发者自己写分支靠开发者自己写分支库内部处理
适合的团队维护老项目有定制成像需求的团队绝大多数新项目

这张表不是让你照着抄选型,而是先建立一个直觉:CameraX 省下来的代码量,本质上是把 Camera2 的状态管理和兼容分支替你做掉了。如果你的需求恰好落在它覆盖的范围内,那没理由不用;一旦需求越界(比如要同时挂两路高分辨率输出、要精确控制每一帧的曝光补偿),就得往下钻。

1.3 选型之前先想清楚的三件事

很多团队选型的纠结,其实是因为需求没梳理清楚就开始比 API。动手前先把这三件事定下来,答案会自己浮出来。

第一件:这个相机模块要跑在什么设备范围上。如果 App 面向的是企业内部分发的设备,机型就那几款,那兼容性成本几乎为零,直接选控制力最强的方案。如果是面向大众市场,机型覆盖面从旗舰到入门都有,那兼容性就是主要成本,CameraX 的优势会立刻放大。

第二件:成像结果要不要"可控"。这里说的可控不是指滤镜,而是指原始数据的处理链路——要不要拿 YUV 帧做实时分析、要不要控制曝光的收敛速度、要不要在拍照时锁定白平衡。只要涉及这些,Camera1 基本出局,CameraX 能做但要看清它暴露的接口够不够,剩下就是 Camera2 的活儿。

第三件:团队里有没有人真正懂相机管线。Camera2 的坑不在于写不出来,而在于出问题时能不能定位。预览卡住是会话没配置好、是 Surface 被释放了、还是请求队列堵了,这些都需要有人能看懂状态回调。如果团队里没人愿意啃这块,硬上 Camera2 大概率会变成长期维护负担。

提示:这三件事里,第二件最容易被低估。很多团队一开始只想要"能拍照",做着做着产品经理加了实时美颜、加了扫码识别、加了证件边缘检测,这些全都需要拿到帧数据,一旦早期选了 Camera1,后期就是推倒重来。

2. Camera1:老而弥坚的够用主义

2.1 Camera1 的核心调用链路

Camera1 的调用顺序几乎是固定的,像一个必须按顺序按下的机械按钮:Camera.open()拿到设备实例,getParameters()取参数集,改完再setParameters()塞回去,setPreviewDisplay()把预览画面接到一个 Surface 上,startPreview()开始出画面,需要拍照时调takePicture(),最后stopPreview()加release()收尾。

这条链路里最容易出问题的是顺序。参数必须在设置预览之前改完,尤其分辨率相关的项;预览画面必须绑定在 Surface 已经创建完成之后,绑定在surfaceCreated之前会直接抛异常;release()之后这个实例就废了,想再用必须重新open()。

另一个必须处理的是方向。Camera1 不会帮你把画面转正,传感器方向和屏幕方向是两回事,你得自己算一个旋转角度喂给setDisplayOrientation(),拍照存下来的 JPEG 还要另外写 EXIF 方向。前后摄的算法完全不一样,前置还要额外做一次水平镜像,否则用户看到的自拍和存下来的照片左右是反的。

2.2 一段能直接用的 Camera1 预览与拍照代码

下面这段是常见的落地形态,省略了权限请求和视图布局,重点看顺序和参数的取法:

public class Camera1Controller implements SurfaceHolder.Callback { private Camera camera; private SurfaceHolder holder; private int cameraId = 0; // 0 通常为后摄 @Override public void surfaceCreated(SurfaceHolder holder) { this.holder = holder; openCamera(); } private void openCamera() { try { camera = Camera.open(cameraId); Camera.Parameters params = camera.getParameters(); // 1. 先选预览尺寸,优先挑接近 16:9 且不超过 1280x720 的 Camera.Size previewSize = chooseSize( params.getSupportedPreviewSizes(), 1280, 720); params.setPreviewSize(previewSize.width, previewSize.height); // 2. 拍照尺寸可以和预览不同,但比例要对齐,否则取景框和成片不一致 Camera.Size pictureSize = chooseSize( params.getSupportedPictureSizes(), 1920, 1080); params.setPictureSize(pictureSize.width, pictureSize.height); // 3. 对焦模式:优先连续对焦,不支持就退回自动 List<String> focusModes = params.getSupportedFocusModes(); if (focusModes.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) { params.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE); } else if (focusModes.contains(Camera.Parameters.FOCUS_MODE_AUTO)) { params.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO); } params.setPictureFormat(ImageFormat.JPEG); camera.setParameters(params); // 4. 方向修正,必须在 setPreviewDisplay 之前 camera.setDisplayOrientation(getDisplayOrientation(cameraId)); camera.setPreviewDisplay(holder); camera.startPreview(); } catch (IOException | RuntimeException e) { releaseCamera(); } } public void takePhoto(File output) { if (camera == null) return; camera.takePicture(null, null, (data, cam) -> { try (FileOutputStream fos = new FileOutputStream(output)) { fos.write(data); } catch (IOException e) { // 落盘失败,记录日志并提示用户重试 } finally { cam.startPreview(); // 拍完必须回到预览态,否则画面冻住 } }); } private void releaseCamera() { if (camera != null) { camera.stopPreview(); camera.setPreviewCallback(null); camera.release(); camera = null; } } private int getDisplayOrientation(int id) { Camera.CameraInfo info = new Camera.CameraInfo(); Camera.getCameraInfo(id, info); WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); int rotation = wm.getDefaultDisplay().getRotation(); int degrees = rotation == Surface.ROTATION_0 ? 0 : rotation == Surface.ROTATION_90 ? 90 : rotation == Surface.ROTATION_180 ? 180 : 270; int result; if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) { result = (info.orientation + degrees) % 360; result = (360 - result) % 360; // 前置镜像 } else { result = (info.orientation - degrees + 360) % 360; } return result; } }

尺寸选择那段有个实用技巧:不要直接取getSupportedPreviewSizes()的第一个,那个往往是最大分辨率,预览会非常卡。正确做法是按"目标面积 + 宽高比匹配度"排序,取第一个宽高比和目标比例偏差在 0.05 以内的候选。这一步没做,横屏应用在竖屏手机上预览必然变形。

2.3 Camera1 绕不开的四个老问题

第一个是重复 open 导致的崩溃。onResume里调一次、surfaceCreated里再调一次,第二次就会拿到 null 或者抛异常。规矩很简单:谁 open 谁 release,用一个状态标记守住,open 前后都做判空。

第二个是预览拉伸。根因是 SurfaceView 的实际尺寸和相机输出的宽高比不一致,Camera1 不会帮你裁切,只会把画面拉满。处理方式是在surfaceChanged里根据预览尺寸重新计算 View 的布局参数,用FrameLayout包一层,让 SurfaceView 按比例撑满并居中,超出的部分裁掉。

第三个是拍照后的画面冻结。takePicture成功回调里如果忘了startPreview(),用户会看到一张静止图,以为 App 卡死了。这个疏忽在测试阶段往往发现不了,因为测试同学拍完就退出了页面。

第四个是内存和线程问题。takePicture的 JPEG 回调在大分辨率下会有几百毫秒延迟,如果在回调里做耗时操作(比如直接上传),主线程会被拖住。更麻烦的是setPreviewCallback系列接口,回调里拿到的 byte 数组如果不及时处理,帧率会断崖式下跌,正确的写法是用setPreviewCallbackWithBuffer复用缓冲区。

提示:Camera1 在系统层面已经被标记为不推荐使用,新项目不要从它起步。但如果接手的是维护型项目,把它的 open/release 生命周期、方向计算、尺寸选择这三块理干净,往往就能解决大部分用户反馈。

3. Camera2:能力全开但要自己造轮子

3.1 Camera2 的三层模型:设备、会话、请求

Camera2 的心智模型要换一次。它不再是"一个相机对象",而是一条流水线:CameraDevice代表硬件设备,CameraCaptureSession代表一次数据流的配置(你把几个 Surface 交给它,它负责往上送帧),CaptureRequest代表一帧的完整配置(曝光、对焦、目标 Surface 都写在里面)。

这条流水线上有三个必须理解的机制。一是模板,TEMPLATE_PREVIEW、TEMPLATE_STILL_CAPTURE、TEMPLATE_RECORD是官方给的预设参数包,你在此基础上改自己关心的字段,比从零构造靠谱得多。二是重复请求,预览之所以能持续出画面,是因为你通过setRepeatingRequest挂了一个不断重复的请求,想改参数就重新提交一个,想暂停就stopRepeating。三是异步状态回调,设备打开、会话配置、每一帧的捕获结果全是通过CameraCaptureSession.CaptureCallback回来的,所有后续操作必须在回调里推进,写成顺序执行必然出错。

线程模型也是必答题。相机相关的所有调用都应该放在一个专门的后台线程上,配一个HandlerThread,主线程只负责 UI 和把结果丢回去。很多"预览第一次能出来、切后台再回来就黑屏"的问题,根因就是线程混用导致状态回调时序乱了。

3.2 一套可运行的 Camera2 预览加拍照骨架

public class Camera2Controller { private CameraDevice device; private CameraCaptureSession session; private ImageReader previewReader, captureReader; private HandlerThread bgThread; private Handler bgHandler; private String cameraId = "0"; public void start(Context ctx, SurfaceTexture texture, int width, int height) { bgThread = new HandlerThread("camera2"); bgThread.start(); bgHandler = new Handler(bgThread.getLooper()); texture.setDefaultBufferSize(width, height); Surface previewSurface = new Surface(texture); previewReader = ImageReader.newInstance(width, height, ImageFormat.YUV_420_888, 2); captureReader = ImageReader.newInstance(4000, 3000, ImageFormat.JPEG, 2); captureReader.setOnImageAvailableListener(reader -> { Image image = reader.acquireNextImage(); try { ByteBuffer buf = image.getPlanes()[0].getBuffer(); byte[] jpeg = new byte[buf.remaining()]; buf.get(jpeg); // 拿到 jpeg 后落盘或上传,注意不要在这里做重活 } finally { image.close(); // 必须关,否则下一帧拿不到 } }, bgHandler); CameraManager manager = (CameraManager) ctx.getSystemService(Context.CAMERA_SERVICE); try { manager.openCamera(cameraId, new CameraDevice.StateCallback() { @Override public void onOpened(CameraDevice camera) { device = camera; createSession(previewSurface); } @Override public void onDisconnected(CameraDevice camera) { camera.close(); device = null; } @Override public void onError(CameraDevice camera, int error) { camera.close(); device = null; } }, bgHandler); } catch (SecurityException | CameraAccessException e) { // 权限缺失或被占用 } } private void createSession(Surface previewSurface) { try { CaptureRequest.Builder builder = device.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW); builder.addTarget(previewSurface); builder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE); builder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON); device.createCaptureSession( Arrays.asList(previewSurface, captureReader.getSurface()), new CameraCaptureSession.StateCallback() { @Override public void onConfigured(CameraCaptureSession s) { session = s; try { session.setRepeatingRequest(builder.build(), null, bgHandler); } catch (CameraAccessException e) { /* 记录日志 */ } } @Override public void onConfigureFailed(CameraCaptureSession s) { /* 释放重试 */ } }, bgHandler); } catch (CameraAccessException e) { // 会话配置失败 } } public void capture() { if (session == null || device == null) return; try { CaptureRequest.Builder b = device.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE); b.addTarget(captureReader.getSurface()); b.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE); b.set(CaptureRequest.JPEG_ORIENTATION, computeJpegOrientation()); session.capture(b.build(), null, bgHandler); } catch (CameraAccessException e) { // 失败处理 } } }

这段代码里有几个容易被忽略但非常关键的点。ImageReader.newInstance的第三个参数决定输出格式,预览用 YUV 是为了省内存,拍照用 JPEG 是为了免去自己编码;第四个参数是最大缓冲帧数,设成 2 就够,设太大反而占内存。acquireNextImage拿到的Image必须close(),不关的话后续帧全部堆积,表现就是预览突然不动了。

JPEG_ORIENTATION那一项一定要填。Camera2 不会自动把照片转正,不设这个值,竖着拍出来的照片存进相册是横的,用户会以为是 App 坏了。

3.3 会话、线程与状态管理的实战要点

会话的生命周期管理是 Camera2 最容易翻车的地方。切换前后摄、切换分辨率、从拍照模式切到录像模式,都需要重建会话。重建的正确姿势是先stopRepeating(),再session.close(),然后重新走createCaptureSession。如果直接建新会话而不关旧的,某些设备会返回onConfigureFailed,表现就是切一次摄像头之后就再也不出画面。

API 30 之后多了一个SessionConfiguration的新接口,用 Executor 代替 Handler,这是官方推荐的新写法,但在旧版本上还得用老接口。兼容写法是按版本判断,两套都留着,别指望一个接口通吃。

还有一个隐形的坑是设备的独占性。相机是全局资源,别的 App 拿着的时候你这边openCamera会直接失败,回调里给的是onError或者干脆不回调。所以打开失败要有重试和提示,不能静默吞掉。切后台时按规范要close()掉设备和会话,回到前台重新打开,这样既省电也避免被系统强制回收。

提示:Camera2 调试时最好把每个状态回调都打上日志,包括onOpened、onConfigured、onCaptureCompleted。出问题时看日志的时序,比盲猜快十倍。

4. CameraX:UseCase 驱动的现代方案

4.1 UseCase 模型解决了什么问题

CameraX 把所有相机能力拆成了若干个 UseCase:Preview负责出画面,ImageCapture负责拍照,ImageAnalysis负责给开发者喂帧数据,VideoCapture负责录像。你把这些 UseCase 组合起来,通过bindToLifecycle绑到某个 LifecycleOwner 上,库内部会自动处理打开设备、配置会话、生命周期暂停恢复这些事。

这个模型的价值在于"组合即配置"。想同时预览和扫码?把Preview和ImageAnalysis一起绑上去就行。想预览加拍照?换成ImageCapture。不需要关心底层的 Surface 怎么分配,也不用管会话什么时候重建。

组合是有约束的。同一时间绑定的 UseCase 数量受设备能力限制,低端设备上同时绑三个高分辨率 UseCase 会直接绑定失败,抛IllegalArgumentException。另外分辨率也不是随便定的,得先查CameraInfo.getSupportedFrameRateRanges和各个 UseCase 的能力范围,用ResolutionSelector表达你的偏好而不是硬指定。

生命周期这块是 CameraX 最省心的地方。绑到 Activity 或 Fragment 的 lifecycle 上之后,页面onStop时库会自动停掉相机,onStart时自动恢复,你不用写任何onPause/onResume里的相机代码。这一点在老项目迁移时收益特别明显——原来散落在各处的相机开关逻辑可以整段删掉。

4.2 CameraX 落地:预览、拍照、分析的完整组合

class CameraXFragment : Fragment() { private lateinit var previewView: PreviewView private var imageCapture: ImageCapture? = null private val analysisExecutor = Executors.newSingleThreadExecutor() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { val providerFuture = ProcessCameraProvider.getInstance(requireContext()) providerFuture.addListener({ val provider = providerFuture.get() val preview = Preview.Builder() .setResolutionSelector( ResolutionSelector.Builder() .setAspectRatioStrategy(AspectRatioStrategy.RATIO_16_9_FALLBACK_AUTO_STRATEGY) .build() ) .build() .also { it.setSurfaceProvider(previewView.surfaceProvider) } imageCapture = ImageCapture.Builder() .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY) .setTargetRotation(view.display.rotation) .build() val analysis = ImageAnalysis.Builder() .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) .build() .also { it.setAnalyzer(analysisExecutor) { proxy -> handleFrame(proxy) } } val selector = CameraSelector.DEFAULT_BACK_CAMERA try { provider.unbindAll() provider.bindToLifecycle(this, selector, preview, imageCapture, analysis) } catch (e: IllegalArgumentException) { // 该设备不支持这个组合,降级:去掉 analysis 再试一次 } }, ContextCompat.getMainExecutor(requireContext())) } private fun handleFrame(proxy: ImageProxy) { try { // 这里做扫码、人脸、OCR 等实时处理 // 注意 proxy.image 的有效期只到 close() 之前 } finally { proxy.close() // 不关会导致后续帧不再回调 } } private fun shoot() { val capture = imageCapture ?: return val file = File(requireContext().cacheDir, "pic_${System.currentTimeMillis()}.jpg") val options = ImageCapture.OutputFileOptions.Builder(file).build() capture.takePicture(options, ContextCompat.getMainExecutor(requireContext()), object : ImageCapture.OnImageSavedCallback { override fun onImageSaved(output: ImageCapture.OutputFileResults) { // 保存成功,刷新相册 } override fun onError(exc: ImageCaptureException) { // 按 exc.imageCaptureError 分类处理 } }) } }

几个参数值得单独说。STRATEGY_KEEP_ONLY_LATEST表示分析永远是处理最新一帧,旧帧直接丢,这个策略对扫码这类"只要当前画面"的场景是正解,能避免帧堆积拖垮内存;如果业务需要每一帧都不丢(比如逐帧合成),就得换成阻塞策略,但要自己控制下游处理速度,否则必然堆积。

CAPTURE_MODE_MINIMIZE_LATENCY和CAPTURE_MODE_MAXIMIZE_QUALITY的区别在于前者优先出片速度,后者会做多帧合成,画质更好但慢。日常拍照场景选前者,证件、文档类场景选后者。

setTargetRotation一定要设成当前界面的旋转值,并且界面旋转时更新它。这一项不设,横竖屏切换后拍出来的照片方向就是错的,而且这个错误在不同设备上的表现还不太一样。

4.3 扩展能力与兼容性处理

CameraX 除了基础 UseCase,还有一套 Extensions,把厂商的夜景、人像、HDR 能力封装成了统一的接口:CameraExtensions里可以查isExtensionAvailable,可用就通过Extender启用。这部分能力在实际效果上差异很大,同一套调用在不同设备上的画质表现可能天差地别,落地前必须在目标机型上做一次效果对比,不能只看接口能不能调通。

PreviewView有两个模式:PERFORMANCE用 SurfaceView,功耗低延迟小,但不支持在预览上叠加复杂动画;COMPATIBLE用 TextureView,可以随便叠 View,但功耗略高。做贴纸、人脸框这类叠加效果,选后者省事。

绑定失败的处理策略也很实际。前面提到低端设备上三个 UseCase 绑不上,正确做法是捕获异常后逐级降级:先去掉分析,再不行把 Preview 换成低分辨率,最后兜底只绑预览。别让用户看到一个白屏加一个崩溃弹窗。

提示:CameraX 的版本迭代比较快,ResolutionSelector这类新接口在旧版本里没有。项目里如果依赖的是较老的版本,分辨率偏好要用setTargetResolution表达,两者不要混用。

5. 三者横向对比与选型决策

5.1 一份能直接拿去开会的能力对照表

能力项Camera1Camera2CameraX
预览出画面支持,代码量小支持,需自建会话支持,绑定即用
静态拍照支持支持,可控参数多支持,两种模式
帧数据回调有,格式受限支持 YUV/Raw通过 ImageAnalysis
手动曝光/对焦基本不支持完整支持有限支持
多路输出不支持支持受组合约束
生命周期托管无无有
老设备兼容好差(早期实现质量参差)库内部兜底
调试难度低高中

这张表里"调试难度"这一行值得多说两句。Camera1 的问题往往一眼看得出来(黑屏、变形、方向错),改起来也直接。Camera2 的问题经常表现为"偶发",跑十次错一次,日志里看不出所以然,需要抓完整的回调时序。CameraX 的问题大多集中在绑定时抛异常,反而最好定位,因为异常类型和错误码都写得很明确。

5.2 不同业务场景下的选型建议

扫码、卡证识别、人脸活体这类"取帧做算法"的场景,首选 CameraX。ImageAnalysis已经把帧回调、旋转信息、格式转换处理得比较顺手,省下来的时间可以全投到算法侧。唯一要注意的是帧率和分辨率的平衡,分析用 1280x720 足够,别贪高。

自定义相机界面、带专业参数调节(快门时间、ISO、手动对焦)的场景,只有 Camera2 能满足。这类需求通常出现在摄影类、工业检测类应用里,用户群明确,机型范围可控,兼容性成本可以接受。

需要同时输出多路高分辨率数据的场景,比如一边预览一边录制一边存原图,CameraX 的组合约束会卡住你,得回退到 Camera2 自己分配 Surface。

维护型老项目,如果现有 Camera1 代码能跑、需求没有新增,不建议为了新而新去重构。相机模块的重构风险远大于收益,真要做也应该在新功能点上逐步替换,而不是一次性推倒。

5.3 迁移路径与混合使用的现实考量

从 Camera1 迁到 CameraX 是相对平滑的一条路,因为两者的调用心智接近,都是"配置好就用"。迁移时按页面拆,先挑一个相机依赖最轻的页面试点,跑一轮真机回归,确认没问题再铺开。

从 Camera2 迁回 CameraX 的情况比较少见,通常是团队里没人能长期维护 Camera2 那套状态机了。这种情况下建议做一层抽象:定义自己的CameraController接口,内部实现可以随时替换,上层业务代码不动。这层抽象在项目早期就该有,哪怕只有一种实现。

混合使用也是可行的。主流程用 CameraX 保证开发效率和兼容性,个别需要精细控制的环节(比如专业模式)单独开一个 Camera2 的页面,两者互不干扰。前提是切换时把前一个相机彻底释放,不能同时持有两个设备的会话。

6. 常见问题与排查技巧实录

6.1 预览黑屏、拉伸、方向错乱速查

黑屏是最高频的问题,排查顺序建议固定下来:先确认权限(相机权限没给的话open会直接失败),再确认是否有其他页面或后台服务占着相机,然后确认预览 Surface 是否已经就绪,最后看会话是否配置成功。按这个顺序走,八成问题能在前三步定位。

预览拉伸的根因几乎都是宽高比不匹配。判断方法很简单:把预览分辨率的宽高比和预览控件的宽高比算出来对比,不一致就必然变形。解决办法是让控件按相机输出的比例布局,多出来的部分裁切,而不是让画面去适配控件。

方向错乱在三个 API 里各有各的解法:Camera1 靠setDisplayOrientation加 EXIF 方向;Camera2 靠JPEG_ORIENTATION;CameraX 靠setTargetRotation。三者共同的坑是前置摄像头的镜像处理,处理不当会导致"预览是正的,存下来是反的"。

6.2 对焦、曝光与闪光灯的异常处理

对焦失败最典型的表现是画面一直模糊,尤其在近距离拍文档时。Camera2 里连续对焦模式配CONTROL_AF_TRIGGER_START可以强制触发一次对焦,但触发完记得把 trigger 重置成 idle,否则后续帧会一直卡在触发状态。CameraX 的对焦可以通过CameraControl.startFocusAndMetering指定一个对焦点,点按对焦就靠它。

曝光异常的常见现象是画面忽明忽暗。根因通常是自动曝光的区域跟着画面内容乱跳,解决办法是设置测光区域并开启 AE 锁定,等画面稳定后再拍。Camera2 里对应CONTROL_AE_REGIONS和CONTROL_AE_LOCK,CameraX 里则通过FocusMeteringAction同时指定对焦和测光区域。

闪光灯的问题集中在时序上。先触发闪光再配置帧,拍出来是黑的;配置帧的时候把闪光模式写进请求,才是正确顺序。另外某些设备在预览常亮模式下不支持闪光灯,调用前要查FLASH_INFO_AVAILABLE。

6.3 性能、内存与卡顿问题

相机模块的内存问题大多来自图像缓冲。Camera2 的ImageReader如果maxImages设得太大,加上忘记close(),几个来回就能把内存吃满,表现是 GC 频繁、预览掉帧。规矩就一条:acquire之后必须close,用 try-finally 包起来。

CameraX 的同类问题出在ImageProxy上,原因和解决办法完全一样。分析线程用单线程 Executor 而不是每次新建,能避免线程数暴涨。如果分析耗时超过一帧的时间,就说明下游处理太重,需要降分辨率或者跳帧。

预览卡顿还有一个容易被忽略的原因:把相机回调放在主线程。setPreviewCallback里做任何超过 16 毫秒的事都会掉帧,哪怕只是打日志。

6.4 机型兼容避坑清单

下面这张表是从实际回归测试里攒出来的,遇到对应现象可以直接对照:

现象可能原因处理方式
某类设备预览全绿或全紫YUV 转 RGB 时色彩空间假设错误改用系统提供的转换工具,别手写公式
拍照后照片是横的未写 EXIF 方向或未设旋转参数补上方向字段
切前后摄后黑屏旧会话未关闭先 stopRepeating 再 close
绑三个 UseCase 抛异常设备能力不足降级绑定,去掉分析
分析回调只来一次ImageProxy 未 closefinally 里关闭
后台回来相机打不开设备被其他应用占用或未释放回前台重试并提示
高分屏设备预览帧率低预览分辨率选得过大限制在 1280x720 以内

提示:兼容问题的排查一定要落到具体设备上,不要靠模拟器。模拟器的相机实现和真机差别太大,很多问题在上面根本复现不出来。

最后分享一个我自己一直在用的调试习惯:给相机模块加一个开关,打开后把每个关键节点的耗时和状态码打印到界面上(不是日志里),真机测试时让测试同学直接截图反馈。相机问题的现场往往转瞬即逝,靠事后看日志猜,效率低太多。这套东西写起来不到一百行,但每次出问题能省下的沟通成本远超投入。

另外提一句关于版本升级的心态。CameraX 的迭代节奏不算慢,新版本会修掉一批设备兼容问题,也会引入新接口。我的做法是锁一个大版本不轻易动,等业务侧有明确收益(比如需要新的分辨率控制接口)时再整体升,升级前把相机相关的回归用例全跑一遍。相机这块的改动,宁可慢一步,也别在发版前一天动。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询