camera-samples icon indicating copy to clipboard operation
camera-samples copied to clipboard

Camera X orientation memory leak

Open ali72-20 opened this issue 7 months ago • 1 comments

CameraActivity causes a memory leak when using CameraX with Jetpack Compose. LeakCanary reports that the ProcessCameraProvider retains a reference to the destroyed CameraActivity through the LifecycleCameraProviderImpl and its internal lifecycleCameraKeys map. Even after calling unbindAll() in onDestroy(), the activity is not properly garbage collected, leading to a memory leak.

Image Image Image

ali72-20 avatar Jul 13 '25 13:07 ali72-20

` package com.example.cameraxapp

import android.Manifest import android.content.pm.ActivityInfo import android.content.pm.PackageManager import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.camera.lifecycle.ProcessCameraProvider import androidx.camera.view.CameraController import androidx.camera.view.LifecycleCameraController import androidx.camera.view.PreviewView import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.viewinterop.AndroidView import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat import com.example.cameraxapp.ui.theme.CameraXAppTheme

class MainActivity : ComponentActivity() { lateinit var controller : LifecycleCameraController

 override fun onCreate(savedInstanceState: Bundle?) {
     super.onCreate(savedInstanceState)

     requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR
     if (!hasRequiredPermission()) {
         ActivityCompat.requestPermissions(
             this,
             CAMERAX_PERMISSIONS,
             0
         )
     }
    controller = LifecycleCameraController(applicationContext).apply {
         setEnabledUseCases(CameraController.IMAGE_CAPTURE or CameraController.VIDEO_CAPTURE)

     }
     setContent {
         CameraXAppTheme {
             // The main screen content which now manages its own controller.
             MainScreenContent(controller = controller)
         }
     }
 }

 /**
  * Checks if all required permissions for CameraX are granted.
  */
 private fun hasRequiredPermission(): Boolean {
     return CAMERAX_PERMISSIONS.all {
         ContextCompat.checkSelfPermission(
             applicationContext,
             it
         ) == PackageManager.PERMISSION_GRANTED
     }
 }

 override fun onDestroy() {
     controller.unbind()
     ProcessCameraProvider.getInstance(this).get().unbindAll()
     super.onDestroy()

 }

 companion object {
     // Define the required permissions for the camera.
     private val CAMERAX_PERMISSIONS = arrayOf(
         Manifest.permission.CAMERA
     )
 }

}

@Composable fun MainScreenContent( controller: LifecycleCameraController ) { val context = LocalContext.current val lifecycleOwner = LocalLifecycleOwner.current

 // Remember controller once per composition


 // Clean up when composable leaves composition
 DisposableEffect(controller) {
     onDispose {
         controller.unbind()
     }
 }

 Box(
     modifier = Modifier.fillMaxSize(),
     contentAlignment = Alignment.Center
 ) {
     CameraPreview(controller, lifecycleOwner, Modifier.fillMaxSize())
     Text("Camera Preview", Modifier.align(Alignment.BottomCenter))
 }

}

@Composable fun CameraPreview( controller: LifecycleCameraController, lifecycleOwner: androidx.lifecycle.LifecycleOwner, modifier: Modifier = Modifier ) { AndroidView( factory = { ctx -> PreviewView(ctx).apply { this.controller = controller controller.bindToLifecycle(lifecycleOwner) } }, modifier = modifier, onRelease = { controller.unbind() } ) }`

ali72-20 avatar Jul 13 '25 15:07 ali72-20