简介这份资源面向需要在应用退居后台时仍能调用相机完成拍摄的 Android 开发者聚焦实时监控、图像记录等场景下的实现思路。内容围绕 Service 后台运行、CAMERA 运行时权限申请、Camera 与 Camera2 两套 API 的取舍、Bitmap 压缩与图片处理、WakeLock 防止休眠中断、通知反馈及电池优化等关键环节展开适合已具备 Android 基础、希望打通后台拍照链路的进阶学习者参考。压缩包共 862 个文件约 5.32MB以 png 图片、xml 布局与配置、class 与 java 源码、jar 依赖为主另含 aidl、gradle 构建脚本及 apk 产物便于直接导入工程对照调试。目前已有 786 人学习下载可借助完整工程结构与源码理解服务声明、权限请求和相机调用的组织方式减少自行摸索的成本。1. 后台拍照这件事先把边界说清楚去年做一个人脸打卡的模块产品经理的原话是「用户点一下打卡按钮App 退到后台也能把照片传上来」。听起来简单真动手才发现坑在系统层Android 从 6.0 开始收紧权限从 8.0 开始限制后台服务从 10 开始连后台启动 Activity 都给你拦了。所谓「后台拍照」本质不是让相机在后台偷偷工作而是让拍照这个动作在用户无感知或弱感知的情况下完成同时不违反系统的前台服务约束。这个项目就是围绕这个诉求做的一套可运行 Demo用 Camera2 拿到预览与图像数据用前台服务承载生命周期用文件落盘和回调把结果交回业务层。适合正在做打卡、巡检、证件采集这类需要「点了就走」的开发者也适合想搞懂 Camera2 会话机制的人拿来当脚手架。2. 技术选型与工程结构为什么是 Camera2 加前台服务2.1 相机 API 的取舍项目没有用已废弃的android.hardware.Camera也没有直接上 CameraX。原因很实际CameraX 封装度高但后台场景下对ImageAnalysis和ImageCapture的调度控制不够细尤其是需要在没有预览界面的情况下拿到一帧 JPEG 时CameraX 的用例绑定会强制要求一个PreviewView或至少一个 Surface。Camera2 虽然啰嗦但CameraCaptureSession允许只绑定ImageReader的 Surface不依赖任何可见 View这正是后台拍照需要的。代价是要自己处理状态机。Camera2 的打开、配置、会话创建、请求下发是异步回调链任何一步顺序错了就是黑屏或者CameraAccessException。项目里用了一个CameraSessionManager把这条链收敛成「打开 → 配置 → 建会话 → 拍照 → 关闭」五个状态业务层只调一个capture()。2.2 目录结构工程是标准的单模块 Android 项目包结构按职责切分没有过度分层app/src/main/java/com/example/backgroundcamera/ ├── camera/ │ ├── CameraSessionManager.kt // Camera2 状态机与回调 │ ├── ImageSaver.kt // JPEG 落盘与 EXIF 写入 │ └── CaptureRequestBuilder.kt // 请求参数组装 ├── service/ │ ├── CaptureForegroundService.kt // 前台服务承载拍照生命周期 │ └── CaptureNotification.kt // 通知渠道与常驻通知 ├── ui/ │ ├── MainActivity.kt // 触发入口与结果展示 │ └── PermissionGate.kt // 权限申请封装 ├── data/ │ └── CaptureRepository.kt // 结果回调与文件路径管理 └── util/ ├── FileProviderHelper.kt // 跨进程文件共享 └── Threads.kt // 相机线程与 IO 线程camera包只关心「怎么拍到一张图」service包只关心「怎么让拍照在后台活下来」data包负责把结果交出去。这种切法的好处是换相机实现比如以后迁 CameraX只动camera包换调度方式比如改成 WorkManager 触发只动service包。2.3 前台服务为什么绕不开Android 8.0 之后后台应用不能随意启动服务必须用startForegroundService()并在 5 秒内调用startForeground()挂一个常驻通知。Android 10 之后后台启动 Activity 被默认禁止所以「拍完弹个预览页」这种设计在后台场景下直接失效只能走通知或者回调到已存在的界面。项目的做法是拍照全程在一个ForegroundService里跑通知栏显示「正在采集图像」拍完更新通知为「采集完成」点击通知回到MainActivity看结果。这样既满足系统约束又给了用户一个可感知的出口不至于变成「偷偷拍照」的黑匣子。// CaptureForegroundService.kt 关键片段 override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { // 必须在 5 秒内挂上前台通知否则系统抛 ANR 级异常 startForeground(NOTIFICATION_ID, CaptureNotification.build(this, 正在采集图像)) val outputFile File(cacheDir, capture_${System.currentTimeMillis()}.jpg) sessionManager.capture(outputFile) { success - // 回调在主线程之外更新通知要切回主线程 mainHandler.post { CaptureNotification.update(this, if (success) 采集完成 else 采集失败) stopSelf(startId) // 任务结束主动停服务避免常驻耗电 } } return START_NOT_STICKY // 被杀后不自动重启避免重复采集 }这段代码里三个参数值得说NOTIFICATION_ID要固定否则每次startForeground都会新建一条通知START_NOT_STICKY是刻意选的后台拍照属于一次性任务系统杀掉后重启反而会造成重复采集stopSelf(startId)带上 startId 是为了只停掉本次启动不影响可能存在的其他任务。通知渠道在CaptureNotification里用NotificationChannel建重要性设为IMPORTANCE_LOW避免每次拍照都响一声。3. Camera2 会话链路从打开相机到拿到 JPEG3.1 打开相机与选择镜头CameraManager.openCamera()是整条链的起点它返回的是异步回调onOpened里才能拿到CameraDevice。项目里封装成挂起函数风格用CountDownLatch或者suspendCancellableCoroutine都行这里用后者更符合 Kotlin 习惯。// CameraSessionManager.kt private suspend fun openCamera(cameraId: String): CameraDevice suspendCancellableCoroutine { cont - cameraManager.openCamera(cameraId, object : CameraDevice.StateCallback() { override fun onOpened(device: CameraDevice) { if (cont.isActive) cont.resume(device) } override fun onDisconnected(device: CameraDevice) { device.close() if (cont.isActive) cont.resumeWithException(IllegalStateException(相机被占用)) } override fun onError(device: CameraDevice, error: Int) { device.close() if (cont.isActive) cont.resumeWithException(CameraAccessException(error)) } }, cameraHandler) }cameraHandler绑定在一条独立的HandlerThread上不能是主线程。这是血泪经验相机回调里做任何耗时操作都会拖慢帧率主线程上更是直接卡 UI。onDisconnected通常意味着另一个应用抢占了相机比如用户切到系统相机这时候必须close()并让业务层知道否则后续请求全部失败。镜头选择上项目默认取后置主摄遍历cameraManager.cameraIdList用CameraCharacteristics.LENS_FACING过滤LENS_FACING_BACK再按SENSOR_INFO_PIXEL_ARRAY_SIZE排序取最大。前置摄像头在后台场景下容易触发镜像和旋转问题除非业务明确要自拍否则不建议默认用。3.2 配置输出 Surface 与创建会话后台拍照不需要预览所以ImageReader是唯一的输出目标。ImageReader.newInstance(width, height, ImageFormat.JPEG, maxImages)里maxImages设 2 就够设大了占内存设 1 在某些机型上会因为缓冲区未释放导致丢帧。// 创建 ImageReader 并绑定到会话 val imageReader ImageReader.newInstance( targetWidth, targetHeight, ImageFormat.JPEG, 2 ) imageReader.setOnImageAvailableListener({ reader - val image reader.acquireNextImage() ?: returnsetOnImageAvailableListener try { // JPEG 格式下 plane[0] 就是完整压缩数据 val buffer image.planes[0].buffer val bytes ByteArray(buffer.remaining()).also { buffer.get(it) } imageSaver.save(bytes, outputFile) onResult(true) } catch (e: Exception) { onResult(false) } finally { image.close() // 必须关闭否则 maxImages 耗尽后不再回调 } }, ioHandler) val surface imageReader.surface 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, computeOrientation()) } device.createCaptureSession(listOf(surface), object : CameraCaptureSession.StateCallback() { override fun onConfigured(session: CameraCaptureSession) { session.capture(request.build(), null, cameraHandler) } override fun onConfigureFailed(session: CameraCaptureSession) { onResult(false) } }, cameraHandler)几个参数要盯住TEMPLATE_STILL_CAPTURE是拍照模板会自动带上合适的曝光和对焦策略CONTROL_AF_MODE_CONTINUOUS_PICTURE让对焦持续工作避免拍出糊图但后台场景下如果镜头被遮挡对焦会反复拉风箱可以退化成CONTROL_AF_MODE_AUTO只对一次JPEG_ORIENTATION必须根据设备旋转和镜头方向算算错了照片就是横的这是最常见的翻车点。computeOrientation()的逻辑是读SensorOrientation结合Display.rotation和前后置做加减。后置一般是(sensorOrientation deviceRotation * 90) % 360前置还要额外镜像。项目里把这段单独放在CaptureRequestBuilder方便单测。3.3 分辨率与画质权衡targetWidth/targetHeight不是随便填的。直接从StreamConfigurationMap.getOutputSizes(ImageFormat.JPEG)里取最大尺寸在低端机上会分配巨大缓冲区拍照瞬间内存飙升甚至 OOM。项目的做法是取「不超过 1920×1080 的最大可用尺寸」兼顾清晰度和内存。val sizes characteristics.get( CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP )?.getOutputSizes(ImageFormat.JPEG) ?: emptyArray() val target sizes.filter { it.width 1920 it.height 1080 } .maxByOrNull { it.width * it.height } ?: sizes.minByOrNull { it.width * it.height }!!这段逻辑先过滤再取最大兜底取最小保证任何机型都能拿到一个合法尺寸。注意getOutputSizes返回的尺寸列表在不同厂商 ROM 上差异很大有的机型会返回一些非标准比例业务层如果要做裁剪最好在拿到图之后再处理不要在请求阶段假设比例。4. 权限、生命周期与避坑排查4.1 权限申请的正确姿势后台拍照涉及三个权限CAMERA、FOREGROUND_SERVICEAndroid 13 之后还要POST_NOTIFICATIONS否则前台服务通知不显示用户看不到采集状态。CAMERA是运行时权限必须在触发拍照前拿到不能在服务里临时申请——服务里没有 Activity 上下文申请了也没法弹框。PermissionGate里用registerForActivityResult(RequestPermission())封装申请前先checkSelfPermission被拒绝后要区分「首次拒绝」和「勾选不再询问」后者只能引导去设置页。这里有个细节Android 11 之后shouldShowRequestPermissionRationale在「不再询问」时返回 false和「首次未申请」一样所以要用SharedPreferences记一个「已申请过」的标记来区分。4.2 生命周期与相机释放相机是独占资源onPause不释放切到别的应用再回来大概率onDisconnected。项目的策略是MainActivity的onStop里通知服务停止采集并释放相机onStart里重新初始化。服务本身不持有 Activity 引用通过CaptureRepository的单例回调把结果传回避免内存泄漏。// MainActivity.kt override fun onStop() { super.onStop() // 界面不可见时主动收尾避免相机被系统回收后状态错乱 if (isFinishing.not() repository.isCapturing()) { repository.cancelCapture() } }cancelCapture()内部会session.close()、device.close()、imageReader.close()顺序不能反先关会话再关设备最后关 Reader反了会抛IllegalStateException。4.3 避坑排查五条踩过的坑现象一拍照回调一直不触发。原因多半是ImageReader的maxImages被占满没释放或者setOnImageAvailableListener的 Handler 线程已退出。解决确保每次acquireNextImage后image.close()Handler 用存活的HandlerThread服务销毁时quitSafely()。现象二照片方向不对人像横躺。原因是JPEG_ORIENTATION没算或算错。解决按SensorOrientation加设备旋转计算前置额外加镜像标志拍完用 EXIF 校验一次。现象三Android 12 以上服务启动即崩。报ForegroundServiceStartNotAllowedException。原因是应用在后台时调了startForegroundService。解决从可见界面触发或者用WorkManager加setExpedited走豁免路径别在BroadcastReceiver里直接起。现象四部分机型拍出来全黑。原因是会话创建时只绑了ImageReader的 Surface某些 ROM 要求至少有一个预览 Surface 才肯出图。解决加一个 1×1 的SurfaceTexture作为占位预览目标不显示但满足驱动要求。现象五连续拍照第二次失败。原因是第一次的CaptureSession没关就建新的。解决每次拍照完session.close()或者复用同一个会话连续下发capture请求项目选的是后者性能更好。提示调试相机问题时dumpsys media.camera能看到当前相机被谁占用比盲猜快得多。5. 二次开发把 Demo 改成能进生产的采集模块拿到这个工程后最常改的三个方向是换触发方式、加图像处理、接业务回调。换触发方式最典型的是从「点按钮」改成「定时或事件驱动」这时候把MainActivity里的启动逻辑抽到CaptureRepository.trigger()由WorkManager或BroadcastReceiver调用即可服务本身不用动。加图像处理的话在ImageSaver.save()之前插一步。JPEG 字节流可以直接交给BitmapFactory.decodeByteArray做压缩、加水印、人脸检测都行。但要注意别在相机回调线程里做重活项目里ImageSaver跑在独立的 IO 线程处理完再回调。// ImageSaver.kt 扩展点 fun save(bytes: ByteArray, outputFile: File, processor: (ByteArray) - ByteArray { it }) { val processed processor(bytes) // 默认恒等业务可注入压缩/水印 outputFile.outputStream().use { it.write(processed) } writeExif(outputFile, orientation) // EXIF 单独写避免处理破坏元数据 }这个processor参数就是留给二次开发的钩子默认不处理需要时传一个 lambda 进去。writeExif单独抽出来是因为很多图像处理库会丢掉 EXIF方向信息一丢前面算的JPEG_ORIENTATION就白费了。验证改造是否成功我一般跑三步先在模拟器上确认权限流程和通知展示再用两台中端真机一高一低测连续拍 10 张的稳定性最后用adb shell dumpsys meminfo看拍照前后内存有没有持续上涨。相机模块的内存泄漏很隐蔽拍几十张才崩早测早省事。有个习惯我保持了几年任何涉及相机的改动都在onImageAvailable里加一行日志打印image.width/height/format跑一遍再删。因为相机问题十有八九是「你以为的尺寸和实际拿到的尺寸不一致」这行日志比任何断点都直接。希望这套结构能帮你少走几段弯路。本文还有配套的精品资源点击获取
企业数字化 ERP 产品动态
相关推荐
LMMSE信道均衡技术原理与5G实践优化 1. 信道均衡技术背景解析在无线通信系统中,信号经过多径信道传输后会产生码间干扰(ISI),这种干扰会导致接收端信号严重失真。我十年前第一次用示波器观察这种失真时,发现原本清晰的数字信号波形变成了"拖尾"严重的模糊信号。信道均… · 2026/11/4 5:11:11
SolidWorks矩形命令5种绘制方法详解与实战技巧 1. SolidWorks矩形命令全面解析作为一名使用SolidWorks超过8年的机械设计师,我经常看到新手在草图绘制阶段就陷入困境。矩形作为最基本的草图元素之一,看似简单却暗藏玄机。今天我就结合自己的项目经验,带大家彻底掌握SolidWorks中的矩形命令… · 2026/11/4 5:10:11
公司电脑监控在管什么:终端管理、数据防泄漏与合规边界 上周有位刚入职的朋友半夜给我发消息,第一句话就是“我怎么知道公司是不是在监控我电脑”。他刚领到一台公司配的笔记本,装完办公软件之后发现系统里凭空多出一个不认识的后台服务,托盘区还常年挂着一个点不开的灰色图标。这种不安我太熟悉了… · 2026/11/4 5:10:11
E24标准电阻表:硬件工程师的工程直觉基石 1. 为什么一张E24标准电阻表值得你打印贴在工位上?“常用标准电阻表 | E24标准电阻表”——这行字看起来平淡无奇,像电子实验室墙角那张泛黄的旧海报。但在我拆解过上百块故障电路板、调试过三千多个模拟前端、帮二十多位硬件新人绕过“阻值选错导致运放… · 2026/11/4 5:49:18
Games101作业0:开箱即用的图形学实验环境配置指南 简介:本资源是GAMES101《计算机图形学与混合现实研讨会》课程2020年春季学期的首份实践作业指南,面向初学图形学、需快速搭建统一开发环境的高校学生及自学者。文档系统讲解了基于VirtualBox虚拟机的全流程配置方案,涵盖虚拟机安装࿰… · 2026/11/4 5:49:18
E24电阻标准值原理与工程应用全解析 1. 项目概述:为什么一张E24电阻表能成为电子工程师的“随身字典”你拆开一块老式收音机主板,发现一个标着“4K7”的贴片电阻;调试一个运放电路时,手册建议用“220R”做反馈;画PCB前查BOM清单,看到“3M3”“… · 2026/11/4 5:49:18
Highcharts桑基图:数据流向可视化实战指南 1. 桑基图:数据流向可视化的终极武器第一次接触桑基图是在分析电商用户行为路径时,面对复杂的跳转关系,折线图和柱状图完全无法清晰展示用户在不同页面间的流转。直到发现Highcharts的桑基图模块,才真正解决了这个痛点。桑基图&am… · 2026/11/4 5:49:18
曲线曲面积分本质解析:从物理动机到空间思维建模 1. 这不是“背公式”的终点,而是理解空间积分的真正起点“考研数学——曲线曲面积分篇”,这八个字在每年九月之后,几乎会准时出现在某高校自习室的草稿纸边角、某线上题库APP的错题本首页、以及无数备考者凌晨三点合上《复习全书》时那一声无… · 2026/11/4 5:49:18
SQL Server表数据量统计与空间监控实战 1. 需求背景与场景解析在日常数据库管理和性能优化工作中,了解数据库中各个表的数据量分布是DBA和开发人员的常见需求。特别是在以下场景中,这种需求尤为突出:数据库迁移前的容量评估:需要知道哪些表占用了主要空间,以… · 2026/11/4 5:48:18
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 3:16:49
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 6:04:24
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 5:52:46
AI辅助技术栈评估:提升研发决策效率60% 1. 项目背景与核心价值技术决策一直是研发团队面临的重大挑战。随着技术生态的快速演进,技术栈的选择、维护和迭代变得愈发复杂。我们团队在过去两年里,先后经历了三次重大技术栈迁移,每次都需要投入大量人力进行技术评估和风险分析。直到去年… · 2026/11/4 0:00:17
从生成Token到直接决策:Jev如何重塑AI Agent工程实践 1. 从"生成文字"到"拍板决策":Jev 到底在解决什么问题大多数人第一次接触大模型,脑子里建立的模型是"输入问题、输出文字"。你问它今天天气怎么样,它给你一段描述;你让它写个周报,它给你… · 2026/11/4 0:00:17
32G统一内存跑大模型:内存账、TPS实测与端云分流 想在一台 32G 统一内存的小主机上把大模型跑成"日常可用",这件事我前后折腾了小半年。标题里说的这台 Mac mini M6,是我们这次讨论的目标平台,具体到手规格以实物为准;但下面这套推算方法和实测思路,对任何统… · 2026/11/4 0:00:17
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 3:16:49
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 6:04:24
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 5:52:46