首页 / 资讯中心 / 文章详情

Android后台拍照实战:前台服务与无预览Camera2会话

Android后台拍照实战:前台服务与无预览Camera2会话 ★ FEATURED ARTICLE
简介这份资源面向需要在Android应用后台完成图像采集的开发者聚焦实时监控、后台记录等场景下相机调用受限的问题覆盖服务组件、权限申请、Camera与Camera2 API选型、图片压缩处理、唤醒锁与通知反馈等关键环节适合已具备Android基础、希望打通后台拍照链路的进阶学习者。压缩包为rar格式共862个文件约5.32MB以398个png图片资源和374个xml布局与配置为主另含35个class、9个java源码及8个jar依赖并附带apk安装包、aidl接口定义与gradle构建脚本便于直接运行对照与二次改造。目前已有786人学习下载。资源将后台拍照涉及的权限、服务生命周期与相机操作组织为可运行的工程结构读者可据此理解IntentService与Service两种实现路径的差异掌握拍照后Bitmap压缩保存及功耗控制的处理思路并借助现成工程快速验证兼容性与排错方向。1. 后台拍照这件事先把边界说清楚Android 上做“后台拍照”最容易翻车的地方不是相机 API 本身而是对“后台”两个字的理解。很多需求方嘴里说的是“用户锁屏了也能拍”“App 退到后台还能拍”但落到系统权限模型里这几乎必然撞上相机资源独占、后台启动限制、前台服务类型校验这几堵墙。我见过不止一个项目在这上面反复返工最后发现真正能稳定跑通的是“前台服务 无预览 CameraCaptureSession”这条路线而不是偷偷摸摸在后台开相机。这份资源围绕的就是这个场景不依赖用户可见的预览界面在服务里完成一次拍照并落盘。它适合做考勤打卡抓拍、巡检取证、门禁联动抓图这类需要“无感触发”的功能也适合想搞清楚 Camera2 会话生命周期、ImageReader 取帧、前台服务类型声明的开发者。下面按“能不能用、怎么搭、坑在哪”的顺序拆开讲代码可以直接抄参数我会标清楚哪些能改、哪些别乱动。2. 无预览拍照的会话搭建Camera2 与 CameraX 怎么选2.1 为什么后台场景更偏向 Camera2 的 ImageReader 路线CameraX 的ImageCapture用起来确实省心一行takePicture就能出图但它的设计前提是绑定到一个有生命周期的LifecycleOwner而且默认行为偏向“有预览、有 UI 交互”的场景。后台服务里没有 Activity 的 lifecycle硬塞一个自定义 LifecycleOwner 也能跑但一旦涉及锁屏、息屏、切后台CameraX 内部对 surface 组合和分辨率协商的处理就不那么透明了出问题只能看黑匣子日志。Camera2 虽然啰嗦但链路清楚打开相机 → 创建CaptureSession→ 把ImageReader的 surface 作为输出目标 → 发一次TEMPLATE_STILL_CAPTURE请求 → 在onImageAvailable里拿Image转码落盘。没有预览 surface 参与会话依然能建立这正是后台拍照需要的形态。常见做法是只挂一个ImageReader分辨率选YUV_420_888或JPEG前者方便做后续图像处理后者直接出压缩图省事。选型上我的建议很直接如果只是“拍一张存下来”用ImageReader配JPEG格式少一次编码如果还要做人脸、条码之类的分析用YUV_420_888自己转 NV21 或直接喂给分析库。别在后台场景里贪 CameraX 的便利它的抽象层在无 UI 环境下反而增加排查成本。2.2 打开相机与创建会话的核心代码下面这段是服务里打开相机、建立无预览会话的骨架跑在独立线程的HandlerThread上避免阻塞主线程。class CaptureSessionManager( private val context: Context, private val cameraId: String 0 ) { private lateinit var cameraManager: CameraManager private var cameraDevice: CameraDevice? null private var captureSession: CameraCaptureSession? null private var imageReader: ImageReader? null private val thread HandlerThread(bg-capture).apply { start() } private val handler Handler(thread.looper) fun openAndCapture(onSaved: (File?) - Unit) { cameraManager context.getSystemService(Context.CAMERA_SERVICE) as CameraManager // 选一个支持 JPEG 输出的尺寸别直接用最大分辨率后台场景内存吃紧 val characteristics cameraManager.getCameraCharacteristics(cameraId) val map characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)!! val jpegSize map.getOutputSizes(ImageFormat.JPEG) .filter { it.width 1920 it.height 1080 } .maxByOrNull { it.width * it.height } ?: map.getOutputSizes(ImageFormat.JPEG)[0] imageReader ImageReader.newInstance(jpegSize.width, jpegSize.height, ImageFormat.JPEG, 2) imageReader?.setOnImageAvailableListener({ reader - val image reader.acquireNextImage() ?: returnsetOnImageAvailableListener val file saveJpeg(image) image.close() onSaved(file) }, handler) cameraManager.openCamera(cameraId, object : CameraDevice.StateCallback() { override fun onOpened(device: CameraDevice) { cameraDevice device val surface imageReader!!.surface device.createCaptureSession(listOf(surface), object : CameraCaptureSession.StateCallback() { override fun onConfigured(session: CameraCaptureSession) { captureSession session val request device.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE).apply { addTarget(surface) set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE) set(CaptureRequest.JPEG_ORIENTATION, 90) // 按实际传感器方向调整 } session.capture(request.build(), null, handler) } override fun onConfigureFailed(session: CameraCaptureSession) { onSaved(null) } }, handler) } override fun onDisconnected(device: CameraDevice) { device.close() } override fun onError(device: CameraDevice, error: Int) { device.close(); onSaved(null) } }, handler) } private fun saveJpeg(image: Image): File? { val buffer image.planes[0].buffer val bytes ByteArray(buffer.remaining()).also { buffer.get(it) } val file File(context.getExternalFilesDir(captures), cap_${System.currentTimeMillis()}.jpg) file.parentFile?.mkdirs() file.writeBytes(bytes) return file } }逻辑上分三步先按SCALER_STREAM_CONFIGURATION_MAP挑一个不超过 1080p 的 JPEG 尺寸控制内存峰值再用ImageReader的 surface 单独建会话不挂预览最后发一次TEMPLATE_STILL_CAPTURE请求在回调里把Image的 plane buffer 直接写成文件。参数上JPEG_ORIENTATION必须按设备传感器方向算写死 90 只对部分机型成立正确做法是用SENSOR_ORIENTATION结合当前屏幕旋转做换算否则出来的图是歪的。ImageReader的 maxImages 给 2 就够给大了反而占内存。提示openCamera在部分系统版本上要求调用方处于前台可见状态纯后台直接调会抛CameraAccessException所以这套代码必须跑在前台服务里下一节讲声明。3. 前台服务与权限让系统允许你在“后台”动相机3.1 前台服务类型声明与 Android 版本差异从 Android 9 开始后台应用访问相机受到限制Android 11 之后前台服务如果要使用相机必须在 manifest 里声明foregroundServiceTypecamera并且运行时启动服务时要带上对应类型。少了这个声明服务能起来但一碰相机就报权限或访问异常日志里往往只给一句含糊的CameraAccessException这就是典型的黑匣子现场。manifest 里至少要有这些uses-permission android:nameandroid.permission.CAMERA / uses-permission android:nameandroid.permission.FOREGROUND_SERVICE / uses-permission android:nameandroid.permission.FOREGROUND_SERVICE_CAMERA / service android:name.BgCaptureService android:foregroundServiceTypecamera android:exportedfalse /FOREGROUND_SERVICE_CAMERA是 Android 14 起需要单独申请的权限别漏。启动服务时用startForeground并传入类型val notification buildCaptureNotification() if (Build.VERSION.SDK_INT Build.VERSION_CODES.Q) { startForeground(NOTIFY_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA) } else { startForeground(NOTIFY_ID, notification) }参数说明NOTIFY_ID自己定别和别的通知撞FOREGROUND_SERVICE_TYPE_CAMERA必须和 manifest 里的类型一致不一致系统会直接拒绝。Android 13 以后通知还需要POST_NOTIFICATIONS运行时权限否则前台服务通知不显示虽然服务本身还能跑但用户体验和合规上都别扭。3.2 运行时权限与锁屏场景的处理顺序相机权限是危险权限必须运行时申请。后台拍照的麻烦在于申请权限的时机往往在用户还没触发拍照之前等真正要拍了再去申请弹窗可能被系统拦或者用户根本看不到。我的做法是在进入功能入口时就把CAMERA权限要到服务启动时只做检查不做申请。锁屏场景还要额外注意部分设备在锁屏状态下相机会被系统策略禁用即使前台服务类型正确也打不开。这不是代码能绕过的属于设备策略边界。稳妥方案是在openCamera失败时回退到“解锁后补拍”或者提示用户而不是死循环重试。下面是一个带降级的调用顺序fun tryCaptureWithFallback() { if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) ! PackageManager.PERMISSION_GRANTED) { notifyUser(缺少相机权限无法抓拍) return } captureManager.openAndCapture { file - if (file null) { // 打开失败记录原因交给上层决定是否解锁后重试 scheduleRetryOnUnlock() } else { uploadOrStore(file) } } }这里的scheduleRetryOnUnlock常见做法是注册一个ACTION_USER_PRESENT广播等用户解锁后再补一次。别小看这个降级很多“后台拍照不工作”的反馈根子就是锁屏策略代码本身没毛病。4. 避坑与排查后台拍照最常见的五个翻车点4.1 现象服务起来了相机打不开日志只有 CameraAccessException原因通常是前台服务类型没声明或声明了但启动时没传类型系统认为你在后台非法访问相机。解决核对 manifest 的foregroundServiceTypecamera与startForeground第三个参数是否一致Android 14 还要确认FOREGROUND_SERVICE_CAMERA权限已申请。改完清一次应用数据再测缓存有时会让旧配置残留。4.2 现象拍出来的图是黑的或者全绿原因多半是ImageReader的格式和CaptureRequest输出不匹配或者会话建立时 surface 已经失效。比如用YUV_420_888建 reader 却按 JPEG 去读 plane 0拿到的就是错位数据。解决格式从建 reader 到读图全程统一会话onConfigureFailed里一定要打日志别静默吞掉。另外Image用完必须close()不关会导致后续请求拿不到 buffer表现为第二张开始全黑。4.3 现象第一张正常连续拍第二张就失败原因是ImageReader的 maxImages 太小或者上一张Image没释放buffer 池被占满。解决maxImages 给 2 到 3每次acquireNextImage后无论成败都close连续拍照之间留一点间隔别在onImageAvailable里同步再发一次 capture容易把会话打乱。血泪经验是把拍照请求串行化用一个队列控制比并发怼上去稳得多。4.4 现象图片方向不对横竖颠倒原因是JPEG_ORIENTATION写死了。正确算法是取SENSOR_ORIENTATION再结合Display.getRotation()做补偿公式在 Camera2 官方示例里有核心是(sensorOrientation - deviceRotation 360) % 360这类换算。后台场景没有 ActivityDisplay可能拿不到可以用WindowManager的默认 display 取旋转拿不到就按传感器方向兜底。4.5 现象低内存机型上拍照直接崩或者被系统杀原因是选了最大分辨率单张 JPEG buffer 几十 MB后台服务内存本来就紧。解决按第 2 节的做法限制在 1080p 以内必要时降到 720pImageReader别开大池子拍照完及时释放CameraDevice和会话别让服务长期持有相机。后悔药是加一个onTrimMemory监听内存吃紧时主动关相机。5. 验证与进阶把一次抓拍做成可观测的链路功能能跑通只是及格线真正上线前我会做三件事可观测、可回放、可降级。可观测指的是每次抓拍都记录时间戳、相机 ID、分辨率、耗时、失败原因落到本地环形日志里出问题能回溯可回放指的是保存原图的同时存一份缩略图和元数据方便复现方向、曝光问题可降级指的是相机不可用时走占位图或延后补拍而不是让整个业务流程卡死。验证方法上我一般按这张表逐项过验证项操作预期前台服务类型锁屏后触发抓拍能出图日志无 CameraAccessException权限缺失撤销相机权限再触发走提示分支不崩溃连续抓拍间隔 2 秒连拍 5 张每张都有图无黑图方向横竖屏各拍一张图片方向与场景一致低内存用低端机长时间挂服务不被杀或能自动恢复进阶技巧上如果你要做“静默连拍 选帧”别在ImageReader回调里做重活把Image转成字节后丢给工作线程池回调里只做close和入队。还有一个容易被忽略的点CameraDevice的onDisconnected一定要处理别的应用抢占相机时你会收到这个回调不处理的话服务会一直以为相机还开着后续请求全部超时。我习惯在onDisconnected里把状态机重置为 idle并触发一次延迟重开。从那以后我每次接后台拍照需求都强制先跑一遍“锁屏 撤权 连拍 低端机”这四组用例再谈业务逻辑。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站