Android 相机 USB 连接别写在 Activity 里:锁屏、切页和后台传输怎么设计?(我有手机连接相机demo可以问我要)
开发 Android 相机连接功能时,有一种情况特别容易被忽略:
相机明明还插着,为什么切换一下页面,照片传输就中断了?
例如,摄影师正在使用手机连接微单拍摄。相机拍下一张照片,App 开始下载;这时候用户切换到照片列表,或者手机屏幕熄灭,再回到 App 时,却发现连接状态不对了。
有些项目会重新连接设备,有些项目会丢失当前传输任务,还有些项目直接把 USB 连接写在 Activity 里面,导致页面重新创建时,整个通信流程也跟着重新初始化。
刚开始开发的时候,这样写可能没什么问题。
但一旦进入照片直播、连续拍摄或者长时间导入照片的真实场景,就很容易暴露问题。
我认为这类项目有一个值得提前考虑的原则:
页面负责展示,连接管理器负责设备状态,后台任务负责符合用户预期的持续工作。
这篇就从 Android 生命周期、USB 设备管理和后台任务三个方面,聊聊相机直连功能应该如何设计。
一、为什么不建议把 USB 连接直接写在 Activity 里?
先看一个常见的写法:
class CameraActivity : AppCompatActivity() {
private lateinit var cameraManager: CameraManager
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
cameraManager = CameraManager()
cameraManager.connect()
}
override fun onDestroy() {
cameraManager.disconnect()
super.onDestroy()
}
}
这段代码用来说明问题,里面的 CameraManager 是示意对象,并不是 Android 系统类。
问题出在哪里?
连接的建立和释放完全取决于 Activity 的生命周期。
但 Activity 被销毁,并不一定代表用户不再需要相机连接。
例如:
-
用户旋转屏幕,Activity 可能重新创建。
-
用户从相机页面切换到图片列表。
-
App 临时进入后台。
-
用户打开其他页面查看已经拍摄的照片。
如果上述情况都会触发断开连接,应用就会频繁重新初始化底层设备通信。
这不仅可能增加等待时间,还容易引发资源释放、任务中断和连接状态不同步等问题。
当然,如果产品设计明确要求离开页面就断开相机,那么页面销毁时关闭连接可以是合理行为。
真正的问题不是 Activity 不能管理连接,而是不能在没有明确生命周期设计的情况下,把设备连接和某个页面强绑定。
二、先把页面生命周期和设备生命周期分开
我更倾向于把代码拆成三部分。
┌──────────────────────────┐
│ Activity / UI │
│ 展示照片、状态、操作按钮 │
└─────────────┬────────────┘
│
┌─────────────▼────────────┐
│ ViewModel │
│ 页面状态、用户交互 │
└─────────────┬────────────┘
│
┌─────────────▼────────────┐
│ CameraRepository / │
│ ConnectionManager │
│ USB连接、协议、设备状态 │
└─────────────┬────────────┘
│
▼
Camera
这里有三个关键点。
Activity 负责界面。 页面显示连接状态、传输进度和照片列表,但不应该因为页面重建,就默认销毁仍然需要的设备连接。
ViewModel 负责页面状态和用户操作。 例如发起连接、断开连接、查看当前传输进度。
连接管理器负责设备通信。 统一管理 USB 设备发现、授权结果、连接对象、协议会话和断开事件。
至于连接管理器究竟放在哪里,要取决于产品需求。
如果连接只需要在当前页面打开期间存在,可以由页面作用域管理。
如果用户切换页面后仍然需要连接,那么连接管理器应该具有比单个页面更长的生命周期。
如果用户锁屏后还必须持续处理 USB 设备传来的图片,则还需要进一步设计后台执行机制。
不要因为用了 ViewModel,就默认设备连接已经能够在后台持续运行。ViewModel 的作用主要是管理界面相关状态,并不等同于一个保证持续运行的后台服务。
三、ViewModel 可以这样组织,但不要让它接管所有底层资源
先定义一个简单的页面状态:
data class CameraUiState(
val connected: Boolean = false,
val connecting: Boolean = false,
val transferCount: Int = 0,
val message: String = "等待连接相机"
)
然后让 ViewModel 通过 Repository 获取状态。
以下是接口设计示例,具体实现需要结合项目中的 USB 通信模块调整:
class CameraViewModel(
private val repository: CameraRepository
) : ViewModel() {
val uiState = repository.uiState
fun connectCamera() {
repository.connect()
}
fun disconnectCamera() {
repository.disconnectByUser()
}
}
这里最重要的不是这几个方法,而是职责划分:
connectCamera() 表示用户发起连接。
disconnectCamera() 表示用户主动断开。
页面销毁不应该自动等同于用户主动断开。
如果连接由更长生命周期的对象负责,那么页面销毁时应该清理自己的状态订阅,而不是擅自释放仍被其他业务使用的连接资源。
反过来,如果页面退出本来就意味着停止拍摄,那么就应该明确执行断开流程,而不是让后台连接一直占用资源。
连接是否持续存在,应该由产品规则决定,而不是由某个回调偶然决定。
四、页面离开了,怎么让界面停止观察,却不影响正在进行的传输?
这是另一个很实用的细节。
假设相机仍然连接着手机,后台任务正在下载照片。
用户从相机页面进入照片详情页。
这时候,相机连接管理器仍然可以持有自己的设备状态,而原来的 Activity 不应该继续接收 UI 更新。
使用 Kotlin Flow 时,可以通过 repeatOnLifecycle,让界面只在合适的生命周期状态下收集状态。
例如:
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { state ->
renderCameraState(state)
}
}
}
这里的 renderCameraState() 是示意性的界面更新方法。
这种设计将两件事分开了:
一件事是后台是否还在传输照片。
另一件事是当前页面是否需要接收状态更新。
页面暂时停止收集状态,不应该自动取消一个仍然需要执行的设备传输任务。
当页面重新进入可见状态时,可以从 Repository 获取最新状态,再更新界面,而不是依赖页面销毁前保存的旧状态。
五、手机锁屏以后,USB 相机传输还能继续吗?
这里就涉及 Android 的后台执行限制了。
首先要区分两种情况。
第一种:用户只是切换页面,但不要求锁屏后继续传输。
这种情况下,可以让连接管理器拥有独立于当前 Activity 的生命周期,但不一定需要后台服务。
第二种:用户明确要求锁屏、切换应用之后,仍然继续与已连接的相机通信。
这时候就需要考虑后台执行机制。
Android 官方目前将 connectedDevice 前台服务作为与外部连接设备交互的适用方案之一,其中明确包含通过 USB 与本地设备传输数据的场景。
官方参考:
如果确实需要持续连接相机,可以评估由 connectedDevice 前台服务负责设备通信,并通过通知向用户展示持续运行的连接或传输状态。
但是,这不意味着所有相机 App 都必须使用前台服务。
如果用户离开相机页面就不再需要传输,那么没有必要为了一个短暂任务强行保持后台连接。
同样,前台服务也不能保证应用永远不会被终止。USB 断开、进程被回收和系统资源限制仍然需要处理。
六、Android 新版本下,前台服务不能随便启动
这里有一个特别值得注意的问题。
对于面向 Android 14(API 34)及以上版本的应用,使用前台服务时,需要在 Manifest 中声明相应的服务类型和权限。
对于通过 USB 与外部设备持续交互的场景,可以评估 connectedDevice 类型是否适用。
具体类型要求与使用条件,可以参考:
此外,Android 12 及以上版本对应用处于后台时启动前台服务存在限制,只有满足规定条件或例外情形时才允许启动。
Android 15 及以上版本还对部分 dataSync 前台服务增加了运行时长限制。Android 16 进一步调整了从前台服务启动的后台作业配额规则。
参考:
所以,不能简单地给相机连接功能套上一个 Service,就认为后台问题解决了。
需要先确定真实业务:是否需要持续访问 USB 设备、传输是否由用户明确发起,以及任务是否必须立即完成。
还要注意,前台服务应该在符合系统规则的时机启动,不能指望应用已经进入后台以后,随时都能重新创建服务。
七、USB 连接与云端上传,最好不要绑成同一个后台任务
专业摄影 App 中,照片到达手机后,通常还会上传服务器。
但 USB 传输与网络上传,其实是两种不同的工作。
相机
↓
USB / PTP / MTP
↓
本地照片文件
↓
文件校验
↓
云端上传
↓
业务交付
USB 连接负责从相机拿到文件。
云端上传负责将本地照片发送到远程服务器。
如果将这两个过程直接写在同一个循环中,网络临时断开就可能影响后续相机导入,或者导致整个流程难以恢复。
更合理的做法是:USB 照片导入完成并记录状态后,再由独立上传任务处理网络传输。
对于可延期执行的后台任务,Android 官方建议考虑 WorkManager;对于符合条件、由用户主动发起且需要持续展示进度的长时间数据传输,可以进一步评估 User-Initiated Data Transfer Job。
但这些机制主要用于相应的后台任务场景,并不能替代 USB 相机的底层连接管理。
参考:
这样拆开后,即使云端上传失败,照片仍然保留在手机中;网络恢复之后,可以继续处理尚未完成的上传任务,而不必重新从相机下载整张照片。
八、进程被终止以后,不要指望保存下来的连接对象还能继续工作
这是整个设计中非常重要的一点。
如果 Android 系统终止了应用进程,那么进程内的对象也会随之消失。
例如:
-
UsbDeviceConnection -
当前 PTP/MTP 会话对象
-
传输工作线程
-
尚未持久化的内存队列
这些都不能当作进程终止后仍然存在的资源。
因此,我建议区分两类状态。
运行时状态
包括 USB 连接对象、协议会话、正在执行的传输请求等。
这些状态需要在设备连接恢复后重新建立。
持久化业务状态
包括已经完成的照片记录、待上传文件路径、任务状态和业务标识。
这些状态可以根据业务需求保存到数据库或者持久化存储中。
当应用重新启动时,就可以执行:
应用重新启动
↓
重新发现 USB 设备
↓
检查连接及授权状态
↓
重新建立通信会话
↓
检查未完成的传输任务
↓
恢复可恢复的业务流程
这里要强调,恢复业务任务并不意味着所有 USB 文件传输都可以从断点继续。
具体能否续传,要取决于相机协议和实际设备能力。对于不支持可靠断点续传的场景,可能需要重新获取整个文件,并通过本地任务记录避免重复导入。
九、这也是我为什么把相机连接能力独立整理成 SDK
我自己开发照片直播软件时,需要解决一个很明确的问题:
相机拍下来的照片,怎样才能快速、稳定地进入手机 App?
随着功能逐步完善,我发现它不只是一个 USB 连接动作,还涉及连接生命周期、设备状态管理、照片获取、文件处理以及上层业务如何接收传输结果。
因此,我把项目中的相机有线连接能力独立整理成了 Camera Connect SDK,希望为需要接入专业相机的开发者提供一个可研究、可集成的底层方案。
项目地址:
https://github.com/18818474455/camera-connect-sdk
如果你正在开发 Android 摄影 App、照片直播、影楼系统或者 AI 修图工具,可以先查看仓库中的接入说明与实际支持范围,再根据目标相机和 Android 设备开展测试。
具体的后台持续运行能力,还需要根据 SDK 的实现、宿主 App 的生命周期和目标系统版本进行设计,不应默认认为只要集成 SDK,就能保证所有场景下的连接永远不间断。
总结
开发相机 USB 直连功能时,我认为有一个原则特别值得记住:
Activity 销毁,不等于用户不再需要相机;应用进入后台,也不等于所有任务都应该无条件继续。
页面生命周期、设备连接生命周期和传输任务生命周期应该分别设计。
对于只需要页面内使用的场景,可以采用简单的页面级管理;对于需要跨页面维持连接的场景,应该使用更长生命周期的连接管理器;对于用户明确要求持续运行的 USB 任务,则需要评估符合 Android 规则的后台执行机制。
再配合任务状态持久化、设备重新发现和异常恢复,整条相机传输链路才会更加可靠。
真正成熟的相机 SDK,不只是能连接设备,还应该清楚地定义连接何时建立、何时释放,以及连接中断之后应该怎么办。
#Android开发 #相机SDK #USB开发 #前台服务 #Android生命周期 #PTP #MTP #照片直播 #移动端开发 #程序员