资讯动态

Android后台拍照实现:前台服务+ Camera2 + ImageReader 锁屏自动采集方案

发布时间:2026/10/14 16:36:06 来源:尧图企业网站定制
简介面向有后台拍照需求的安卓开发者这份资源围绕Service、权限、相机API与图片处理提供一套可运行的工程实现。压缩包共862个文件以398个png界面截图、374个xml布局配置、9个java源码和35个class编译文件为主另有jar、aidl、apk及gradle构建脚本整体5.32MB。目前已有783人学习下载。借助源码与配置可梳理完整链路通过服务承载相机操作动态申请CAMERA权限使用Camera/Camera2的CaptureSession拍照配合WakeLock防休眠并用通知栏反馈状态。对aidl接口和apk打包产物作剖析还能帮助理解进程间通信、打包细节及不同版本兼容性处理兼顾原理与实战。1. Android 后台拍照从服务申请到快门回调的真实实现很多开发者以为后台拍照就是调起系统相机拍一张等到自己接过考勤打卡或巡检取证这类需求时才发现手一离开桌面相机就停工了。这个资源讲的是完全由你的应用接管相机设备通过前台服务常驻、Camera2 采集、ImageReader 回调拿到 JPEG 帧用户在锁屏状态下也能按业务逻辑自动按快门。适合做巡检、考勤、安防类 App 的开发者也是把 Camera2 链路彻底吃透的练手工程。2. 后台拍照的合法性底线前台服务、Camera2 与权限栈2.1 为什么直接拉起系统相机必翻车从 Android 10API 29开始系统对后台应用访问相机设置了明确限制。这里对“后台”的定义很直接没有可见的 Activity也没有运行中的前台服务。如果你在某个 Service 里直接调用startActivity想唤起系统相机绝大多数机器上你只会看到类似Background activity starts denied的日志然后用户什么反应都没有。即便你把系统相机成功拉起来了锁屏状态下用户根本无法操作快门最后就是黑屏或者卡死。有一个朋友做的项目当时用的是 WebView 里嵌入网页拍照模块网页里调系统相机Android 10 之后直接大面积失效最后只能把拍照链路全部搬到原生层。真正能稳定跑通的做法是把相机的打开、预流、拍照、编码全部放在自己的进程里通过前台服务持有相机设备。相机设备从打开到释放都由你的代码控制不依赖界面是否存在。2.2 权限组合清单与类型声明后台拍照涉及的不是一个权限而是一组权限。漏掉任何一个服务都能跑但到关键时刻就会翻车。我整理了一份常用的排查清单权限适用版本作用与说明CAMERA所有版本运行时权限打开相机必需FOREGROUND_SERVICEAndroid 9声明允许使用前台服务FOREGROUND_SERVICE_CAMERAAndroid 14targetSdk 34声明前台服务的类型为相机POST_NOTIFICATIONSAndroid 13前台服务通知需要显示不申请会导致服务启动失败WAKE_LOCK所有版本熄屏后保持 CPU 运行维持相机会话WRITE_EXTERNAL_STORAGEAndroid 10 以下旧版写存储用Android 10 改用 MediaStore 则不需要这里有两个特别容易踩的盲区。第一个是FOREGROUND_SERVICE_CAMERA如果你的targetSdk是 34 但清单里没声明它服务一启动就会抛出SecurityException连startForeground都执行不完。第二个是POST_NOTIFICATIONSAndroid 13 上如果用户拒绝了通知权限前台服务通知不显示系统会认为这个服务不可见随后直接终止它。2.3 前台服务骨架代码后台拍照的服务必须用startForeground启动这是系统认定“你还在前台运行”的唯一依据。下面是最小可用的骨架class CameraForegroundService : Service() { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { // 必须在调用后的5秒内执行否则系统会报 ANR startForeground(NOTIFY_ID, buildNotification()) initCameraManager() return START_STICKY } private fun buildNotification(): Notification { val channel NotificationChannel( CHANNEL_ID, Background Camera, NotificationManager.IMPORTANCE_LOW ) getSystemService(NotificationManager::class.java).createNotificationChannel(channel) return Notification.Builder(this, CHANNEL_ID) .setContentTitle(相机服务运行中) .setContentText(正在等待拍照任务) .setSmallIcon(android.R.drawable.ic_menu_camera) .build() } override fun onDestroy() { releaseCameraAndWakeLock() super.onDestroy() } companion object { private const val NOTIFY_ID 1001 private const val CHANNEL_ID camera_service } }这个骨架只做了两件事把服务声明为前台服务给出用户可见的通知。逻辑上你需要在initCameraManager()里去申请相机权限并准备CameraManager。START_STICKY的返回值表示进程被系统回收后系统会重新创建这个服务并调用onStartCommand但传入的intent会是null所以业务上不能依赖intent里的参数做逻辑判断。参数注意点NOTIFY_ID建议用固定常量不要每次随机生成否则通知栏会叠一堆相同的通知。IMPORTANCE_LOW意味着通知不会弹出声音适合这种常驻后台的服务不会打扰用户。3. 用 Camera2 接管拍照链路从 CameraManager 到 JPEG 帧落地3.1 为什么不用 CameraX 而是 Camera2CameraX 确实封装得很好但它整个设计都围绕“有 UI 的预览”展开。你需要一个PreviewView绑定到生命周期还要保证这个视图处于可见状态。后台场景下没有界面强行建一个不可见的SurfaceView会引入 Surface 创建时机的麻烦往往快门按下去 Surface 还没 ready。更关键的是生命周期绑定关系。CameraX 在LifecycleOwner不可见时会自动关闭相机后台服务里很难声明一个“保持可见”的生命周期。我之前在某项目里用 CameraX 做定时拍照熄屏十分钟后相机设备就被自动释放了日志里能看到相机被 lifecycle 关闭的记录。Camera2 没有这层约束。相机设备打开后只要你的CameraDevice对象没有 close它就一直持有。打开、配置会话、投递请求都由你显式控制后台场景反而是它的主场。3.2 CameraDevice 打开与 CaptureSession 建立先打开相机设备。Camera2 的打开是异步的结果通过StateCallback通知。val manager getSystemService(Context.CAMERA_SERVICE) as CameraManager val cameraId findBackCameraId(manager) ?: return // openCamera() 调用前必须检查运行时权限 if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) ! PackageManager.PERMISSION_GRANTED) return manager.openCamera(cameraId, object : CameraDevice.StateCallback() { override fun onOpened(camera: CameraDevice) { thisCameraForegroundService.cameraDevice camera createSession(camera) } override fun onDisconnected(camera: CameraDevice) { camera.close() } override fun onError(camera: CameraDevice, error: Int) { // error 常见为 ERROR_CAMERA_IN_USE(1)表示设备被其他应用占用 camera.close() } }, backgroundHandler)findBackCameraId的常规做法是遍历manager.cameraIdList用CameraCharacteristics.LENS_FACING_BACK筛选后置摄像头。这里注意backgroundHandler要用独立的HandlerThread不要用主线程的Handler否则回调会被 UI 操作阻塞。onError里除了关闭设备最好把error码打到日志里后续排查能直接知道是不是设备被占。会话建立同样异步。把ImageReader的 Surface 作为唯一输出目标private fun createSession(camera: CameraDevice) { val surface imageReader.surface camera.createCaptureSession( listOf(surface), object : CameraCaptureSession.StateCallback() { override fun onConfigured(session: CameraCaptureSession) { captureSession session startRepeatingPreview(session) } override fun onConfigureFailed(session: CameraCaptureSession) { // 配置失败多是 Surface 已关闭或相机被占用 } }, backgroundHandler ) }createCaptureSession的outputs参数决定了相机数据流向哪里。这里只传了imageReader.surface意思是所有帧都送到ImageReader的缓冲区不经过屏幕。3.3 ImageReader 拿帧与 JPEG 编解码ImageReader是 Camera2 中从相机拿帧的标准出口。我用ImageFormat.JPEG而不是 YUV因为后台拍照最终要保存完整图片JPEG 可以直接落盘省掉一次 YUV 转码val imageReader ImageReader.newInstance(width, height, ImageFormat.JPEG, 2) imageReader.setOnImageAvailableListener({ reader - // acquireLatestImage 会丢弃排队中的旧帧避免延迟堆积 val image reader.acquireLatestImage() ?: returnsetOnImageAvailableListener val buffer image.planes[0].buffer val bytes ByteArray(buffer.remaining()) buffer.get(bytes) image.close() saveToGallery(bytes) }, backgroundHandler)逻辑说明JPEG 格式下Image只有一个 planeplanes[0].buffer里就是完整的 JPEG 字节流不需要额外编码。maxImages参数设成 2是给回调处理留缓冲余量如果设成 1回调稍微慢一点就会因为缓冲区满而丢掉新帧现象就是拍照偶尔成功偶尔黑屏。image.close()这一步不能省。ImageReader的缓冲区数量有限不释放的话几次拍照后整个采集管线就停住了。3.4 把成品写入 MediaStoreAndroid 10 之后不经过 MediaStore 直接往公共目录写文件会被拒绝。我一般用ContentResolver加IS_PENDING标记的方式写入private fun saveToGallery(bytes: ByteArray) { val values ContentValues().apply { put(MediaStore.Images.Media.DISPLAY_NAME, CAM_${System.currentTimeMillis()}.jpg) put(MediaStore.Images.Media.MIME_TYPE, image/jpeg) put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES /CamService) put(MediaStore.Images.Media.IS_PENDING, 1) } val uri contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values) ?: return contentResolver.openOutputStream(uri)?.use { it.write(bytes) } values.clear() values.put(MediaStore.Images.Media.IS_PENDING, 0) contentResolver.update(uri, values, null, null) }逻辑说明IS_PENDING置 1 时相册和其他应用看不到这个文件等写入完成后再更新为 0文件才对外可见。这能避免拍照写入到一半时用户打开相册看到一个损坏的图片。RELATIVE_PATH一定要是 MediaStore 认可的子目录像Pictures/CamService不要用绝对路径。4. 熄屏与后台保活WakeLock、SCREEN_OFF 广播与系统回收策略4.1 熄屏后相机被系统挂起的现象与原因后台拍照最大的坑不在权限而在熄屏。现象很典型亮屏时整个链路都正常锁屏后服务还活着logcat里也能看到服务日志但ImageReader的回调越来越少最后完全不来帧。有些设备上还会直接报ERROR_CAMERA_SERVICE_DIED或者 HAL 断开连接。原因是多层的。首先屏幕关闭后 CPU 会进入深度休眠相机 HAL 没有持续的帧请求就会停摆。其次部分厂商的 ROM 对相机 HAL 做了省电策略屏幕熄灭一段时间就直接释放摄像头资源。你以为服务还在实际相机设备已经脱离了硬件。解决方案有两板斧一是用PARTIAL_WAKE_LOCK保持 CPU 运行二是保持RepeatingRequest一直在投递告诉 HAL“我还要继续采帧”。两者缺一不可。4.2 WakeLock 与屏幕状态监听WakeLock 的获取时机很关键。我的做法是监听系统的亮屏与灭屏广播在灭屏瞬间立刻获取锁而不是等服务轮询val powerManager getSystemService(Context.POWER_SERVICE) as PowerManager val wakeLock powerManager.newWakeLock( PowerManager.PARTIAL_WAKE_LOCK, camera:bg ) val screenReceiver object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { when (intent?.action) { Intent.ACTION_SCREEN_OFF - { // 锁屏立即持有保持 CPU 不睡 wakeLock.acquire(20 * 60 * 1000L) } Intent.ACTION_SCREEN_ON - { if (wakeLock.isHeld) wakeLock.release() } } } } registerReceiver( screenReceiver, IntentFilter().apply { addAction(Intent.ACTION_SCREEN_OFF) addAction(Intent.ACTION_SCREEN_ON) } )逻辑说明PARTIAL_WAKE_LOCK只保持 CPU 运行不会点亮屏幕适合相机后台采帧。acquire()带了 20 分钟超时这是为了防兜底——万一某次没有走到ACTION_SCREEN_ON对应的release()锁还是会在 20 分钟后自动释放不至于让设备一直处于不休眠状态。另外RepeatingRequest也要保持投递。在CaptureSession配置完成后我一般这么启动预流val requestBuilder cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW) requestBuilder.addTarget(imageReader.surface) captureSession.setRepeatingRequest(requestBuilder.build(), null, backgroundHandler)这里每次会话建立后都要重新设置一次因为CaptureSession重建后原来的请求会失效。4.3 前台服务退到后台的保活边界有了前台服务进程优先级已经提到前台级别正常情况下系统不会主动回收。但要留意国内很多定制 ROM 的后台管理策略它们不认系统的优先级而是按自己的白名单规则来限制后台应用。这就不是代码能单独解决的问题了。我建议在应用里做一个引导页提示用户把应用加入电池优化白名单代码里用ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS请求忽略电池优化。这个请求需要声明REQUEST_IGNORE_BATTERY_OPTIMIZATIONS权限能不能跳转到设置页由系统决定但至少给了用户一个操作入口。需要注意电池优化不是一路通到黑的。部分 ROM 上即使用了这个接口仍然要用户进到自启动管理里手动打开开关。代码层面能把该做的都做了剩下的只能靠引导文案。5. 后台拍照高频问题排查四类实测翻车记录下面这些翻车案例都在真实项目里出现过每一条都是“现象 → 原因 → 解决”的结构你可以直接对照自己的日志去定位。5.1 相机打开回调迟迟不来onOpened不触发现象调用openCamera()后StateCallback.onOpened一直没有回调logcat里出现Camera is already in use或CameraDevice was already closed。原因另一应用占用了相机设备或者上一次服务退出时没有正确释放相机。最常见的是扫码组件没有在页面销毁时关闭相机导致你的服务拿不到设备。解决在openCamera()前先遍历CameraCharacteristics检查LENS_FACING之外还要看设备状态调用处包一层try-catch抛出的CameraAccessException里能拿到具体错误码。同时确保服务onDestroy里调用cameraDevice.close()和captureSession.close()释放逻辑不能遗漏。5.2 ImageReader 回调 3 秒才来一帧照片延迟明显现象拍照动作触发后onImageAvailable要等两三秒才回调连续拍照时发现拿到的是上一帧的旧照片。原因ImageReader的handler用了主线程的Handler主线程被 UI 操作或其他任务堵住回调排在后面或者是用了acquireNextImage()它不会丢弃旧帧队列越积越多。解决给ImageReader传独立的HandlerThread的handler回调里改用acquireLatestImage()它永远返回最新的一帧并释放之前的帧。注意这两个改动要同时做只改一个还是会出问题。5.3 拍出来的照片全黑或噪点严重现象照片能保存但内容是一片黑偶尔有几张噪点图。原因没有启动RepeatingRequest。很多人以为拍照时调用capture()就够了但是相机 sensor 默认是不出帧的必须有一个持续请求在推动。没有setRepeatingRequestcapture()拿到的就是 sensor 空跑的数据。解决在onConfigured回调里立刻调用setRepeatingRequest启动预流然后再投递拍照请求。每次CaptureSession重建后都要重新设置一遍这个顺序别搞反。5.4 Android 14 上服务启动直接崩溃现象targetSdk34 的设备上startForeground抛SecurityException日志显示ForegroundServiceStartNotAllowedException。原因Android 14 要求前台服务必须声明具体的类型相机类服务要加上foregroundServiceTypecamera同时还要声明FOREGROUND_SERVICE_CAMERA权限。这两样缺一个都启动失败。解决在AndroidManifest.xml里给service加上android:foregroundServiceTypecamera并在权限声明区加uses-permission android:nameandroid.permission.FOREGROUND_SERVICE_CAMERA/。如果应用还需要兼容到 Android 14 以下的机型这些声明不会影响低版本放心加上即可。6. 进阶定时自动拍照与完整验证方法6.1 定时触发与防重入后台拍照的场景大多是周期性的比如巡检项目每隔 30 秒或者 1 分钟拍一张。我习惯用Handler.postDelayed做自触发循环而不是AlarmManager因为前者能保证和服务的生命周期绑在一起服务停了循环自然停止private val tickRunnable object : Runnable { override fun run() { if (captureSession ! null cameraDevice ! null) { captureOnce() } // 每次执行完再投递下一次避免固定频率导致的请求堆积 backgroundHandler.postDelayed(this, intervalMillis) } } private fun startPeriodicCapture() { backgroundHandler.removeCallbacks(tickRunnable) backgroundHandler.postDelayed(tickRunnable, intervalMillis) }逻辑说明captureOnce()里用TEMPLATE_STILL_CAPTURE创建拍照请求提交一次capture()。这里不需要为照片单独设置新 session直接在已有的 session 上用当前 surface 投递即可极少数机型会需要重建 session但大部分情况这么跑没问题。参数intervalMillis建议不要小于 1000因为两次拍摄之间需要等上一帧的 buffer 完全落盘。6.2 验证链路Logcat、EXIF 与前台服务存活检查代码写完了最怕的是没验证直接上线。我现在每次改完后台相机逻辑会强制走一遍下面的验证流程第一看logcat。过滤关键字CameraForegroundService和ImageReader确认服务启动、session 配置成功、每一帧回调都在按预期频率出现。如果两帧之间间隔超过预定值就要怀疑系统是否对接帧节奏做了限制。第二查照片 EXIF。用ExifInterface读取刚刚保存的 JPEG 文件检查MODEL和DateTimeOriginal字段是否合理。假如MODEL是空的或者时间对不上说明这张图不是从相机 sensor 出来的可能是程序逻辑里复用了旧 buffer。第三确认前台服务存活。连着adb shell dumpsys activity services | grep 你的包名看输出里有没有isForegroundtrue。如果服务在熄屏半小时后变成isForegroundfalse那说明系统还是把服务停掉了需要回头检查通知权限和电池优化白名单。之前有一次上线前我大意了只验证了亮屏时连续拍 10 张都正常结果客户在锁屏状态下用拍出来的全是黑图。后来查下来就是RepeatingRequest在 session 重建后没有重新设置。从那以后我每次改后台相机逻辑都强制锁屏跑 10 分钟再核对一遍日志、EXIF 和前台服务状态这套流程再没让黑图问题漏出去过。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑