You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android中CameraX调用异常:相机关闭与Future垃圾回收问题

解决DeviceAdminReceiver中调用CameraX拍照时的GC回收异常问题

问题根源分析

  1. 无效的LifecycleOwner:你的CameraX类继承了AppCompatActivity,但只是通过val camerax = CameraX()创建实例却未启动Activity,导致它的生命周期完全无效。CameraX的bindToLifecycle需要活跃的生命周期上下文,未启动的Activity无法提供,且这个实例极易被GC回收——这就是异常提示The completer object was garbage collected this future would otherwise never complete的直接原因。
  2. Receiver生命周期过短:DeviceAdminReceiver是广播接收器,生命周期极短,onPasswordFailed执行完毕后,局部变量camerax会失去引用,直接被GC回收,导致CameraX的异步操作(比如ProcessCameraProvider的Future)失去上下文,最终抛出异常并关闭相机。
  3. 文件名错误:原代码中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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.17 07:40:38