Android中CameraX调用异常:相机关闭与Future垃圾回收问题
解决DeviceAdminReceiver中调用CameraX拍照时的GC回收异常问题
问题根源分析
- 无效的LifecycleOwner:你的
CameraX类继承了AppCompatActivity,但只是通过val camerax = CameraX()创建实例却未启动Activity,导致它的生命周期完全无效。CameraX的bindToLifecycle需要活跃的生命周期上下文,未启动的Activity无法提供,且这个实例极易被GC回收——这就是异常提示The completer object was garbage collected this future would otherwise never complete的直接原因。 - Receiver生命周期过短:
DeviceAdminReceiver是广播接收器,生命周期极短,onPasswordFailed执行完毕后,局部变量camerax会失去引用,直接被GC回收,导致CameraX的异步操作(比如ProcessCameraProvider的Future)失去上下文,最终抛出异常并关闭相机。 - 文件名错误:原代码中
fileName末尾的/会让系统把它当作目录而非图片文件,导致拍照保存失败。
解决方案
步骤1:重构CameraX工具类,放弃继承Activity
创建独立的相机管理类,手动管理生命周期以适配后台场景:
class CameraManager(private val context: Context) { private lateinit var cameraProviderFuture: ListenableFuture<ProcessCameraProvider> private lateinit var cameraSelector: CameraSelector private var imageCapture: ImageCapture? = null private lateinit var imgCaptureExecutor: ExecutorService // 手动管理生命周期,用于后台相机操作 private val lifecycleRegistry = LifecycleRegistry(this) init { lifecycleRegistry.currentState = Lifecycle.State.CREATED } fun startCamera() { cameraProviderFuture = ProcessCameraProvider.getInstance(context) cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA imgCaptureExecutor = Executors.newSingleThreadExecutor() cameraProviderFuture.addListener({ val cameraProvider = cameraProviderFuture.get() imageCapture = ImageCapture.Builder().build() try { cameraProvider.unbindAll() // 绑定到手动管理的生命周期 cameraProvider.bindToLifecycle(lifecycleRegistry, cameraSelector, imageCapture) Log.d("CameraX", "CameraOpened") } catch (e: Exception) { Log.d("CameraX", "Use case binding failed: ${e.message}") } }, ContextCompat.getMainExecutor(context)) // 将生命周期切换到STARTED状态 lifecycleRegistry.currentState = Lifecycle.State.STARTED } fun takePhoto(onResult: (Boolean, String?) -> Unit) { imageCapture?.let { capture -> val dir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).absolutePath + "/FLP-Intruder/" val imageDir = File(dir) if (!imageDir.exists()) { imageDir.mkdirs() } // 修正文件名,移除末尾的/ val fileName = "JPEG_${System.currentTimeMillis()}_Track.png" val file = File(dir, fileName) Log.d("CameraX", "takePhoto: ${file.absolutePath}") val outputFileOptions = ImageCapture.OutputFileOptions.Builder(file).build() capture.takePicture( outputFileOptions, imgCaptureExecutor, object : ImageCapture.OnImageSavedCallback { override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) { Log.i("CameraX", "The image has been saved in ${file.toUri()}") onResult(true, file.absolutePath) stopCamera() } override fun onError(exception: ImageCaptureException) { Log.d("CameraX", "Error taking photo:${exception.message}") onResult(false, exception.message) stopCamera() } }) } ?: run { onResult(false, "ImageCapture not initialized") } } fun stopCamera() { try { val cameraProvider = cameraProviderFuture.get() cameraProvider.unbindAll() imgCaptureExecutor.shutdown() lifecycleRegistry.currentState = Lifecycle.State.DESTROYED } catch (e: Exception) { Log.d("CameraX", "Error stopping camera: ${e.message}") } } // 实现LifecycleOwner接口,提供手动管理的生命周期 fun getLifecycle(): Lifecycle = lifecycleRegistry }
步骤2:在DeviceAdminReceiver中持有全局引用,避免GC回收
在Receiver中声明全局变量保存CameraManager实例,防止被GC回收:
class MyDeviceAdminReceiver : DeviceAdminReceiver() { // 全局引用,防止相机实例被GC回收 private var cameraManager: CameraManager? = null override fun onPasswordFailed(context: Context, intent: Intent) { super.onPasswordFailed(context, intent) takePictureCameraX(context) } private fun takePictureCameraX(context: Context) { // 检查必要权限 if (ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED || (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q && ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED)) { Log.d("CameraX", "Missing required permissions") return } cameraManager = CameraManager(context) cameraManager?.startCamera() // 使用带SupervisorJob的CoroutineScope,避免内存泄漏 CoroutineScope(Dispatchers.Main + SupervisorJob()).launch { delay(100) // 延迟等待相机初始化,实际可监听绑定完成事件更可靠 cameraManager?.takePhoto { success, msg -> if (success) { Log.d("CameraX", "拍照成功") } else { Log.d("CameraX", "拍照失败: $msg") } // 完成后释放资源 cameraManager = null } } } }
步骤3:适配后台相机权限限制
- Android 12及以上:后台使用相机需申请
CAMERA权限,且建议启动前台服务执行相机操作,避免被系统拦截。 - Android 10及以上:优先使用
MediaStore保存图片,或者申请MANAGE_EXTERNAL_STORAGE权限(仅必要时),避免直接操作外部存储路径。
内容的提问来源于stack exchange,提问作者Hamza Ayaz
相关产品推荐
相关产品推荐

