Thracian
Thracian

Reputation: 67218

Bitmap in onPictureTaken method is not garbage collected

I'm using Camera1 Api to test some features with SurfaceView, TextureView.

Bitmap created with bitmap = BitmapFactory.decodeByteArray(data, 0, data.length); is never recycled and memory is never claimed back even though bitmap.recycle() and System.gc() is called.

Here are the few thread about recycling Bitmap but none of them works.

  1. Recycling Bitmap does not free memory
  2. Memory usage does not decrease even I recycle bitmaps

This is the code i use with SurfaceView with image with 4160, and height 3120 which returns a Bitmap about 50mb.

CameraActivity

public class CameraActivity extends Activity {
    private static final String SAVE_DIR = "Folder";
    private Camera mCamera;
    private FrameLayout preview;
    private CameraPreview mPreview;

    public static final int MEDIA_TYPE_IMAGE = 1;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // Create an instance of Camera
        if (checkCameraHardware(this)) {
            mCamera = getCameraInstance();
        }

        if (mCamera == null) {
            Toast.makeText(this, "Camera null ", Toast.LENGTH_SHORT).show();
            return;
        }
        // Create our Preview view and set it as the content of our activity.
        mPreview = new CameraPreview(this, mCamera);
        preview = (FrameLayout) findViewById(R.id.camera_preview);
        preview.addView(mPreview);

        Button captureButton = (Button) findViewById(R.id.button_capture);
        captureButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // get an image from the camera
                mCamera.takePicture(null, null, mPicture);

            }
        });
    }

    /**
     * Check if this device has a camera
     */
    private boolean checkCameraHardware(Context context) {
        if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA)) {
            // this device has a camera
            return true;
        } else {
            // no camera on this device
            return false;
        }
    }

    /**
     * A safe way to get an instance of the Camera object.
     */
    public Camera getCameraInstance() {
        Camera c = null;
        try {
            c = Camera.open(); // attempt to get a Camera instance
            setCameraDisplayOrientation(this, CameraInfo.CAMERA_FACING_BACK, c);
        } catch (Exception e) {
            // Camera is not available (in use or does not exist)
        }
        return c; // returns null if camera is unavailable
    }

    public void setCameraDisplayOrientation(Activity activity, int cameraId, android.hardware.Camera camera) {
        android.hardware.Camera.CameraInfo info = new android.hardware.Camera.CameraInfo();
        android.hardware.Camera.getCameraInfo(cameraId, info);
        int rotation = activity.getWindowManager().getDefaultDisplay().getRotation();
        int orientation = getResources().getConfiguration().orientation;

        int degrees = 0;
        switch (rotation) {
            case Surface.ROTATION_0:
                degrees = 0;
                break;
            case Surface.ROTATION_90:
                degrees = 90;
                break;
            case Surface.ROTATION_180:
                degrees = 180;
                break;
            case Surface.ROTATION_270:
                degrees = 270;
                break;
        }

        int result;
        if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
            result = (info.orientation + degrees) % 360;
            result = (360 - result) % 360; // compensate the mirror
        } else { // back-facing
            result = (info.orientation - degrees + 360) % 360;
        }
        camera.setDisplayOrientation(result);
        Camera.Parameters params = camera.getParameters();
        params.setRotation(90);
        camera.setParameters(params);
    }

    private PictureCallback mPicture = new PictureCallback() {

        @Override
        public void onPictureTaken(byte[] data, Camera camera) {
            long startTime = System.currentTimeMillis();

            File pictureFile = getOutputMediaFile(MEDIA_TYPE_IMAGE);
            FileOutputStream fos = null;
            Bitmap bitmap = null;

            if (pictureFile == null) {
                return;
            }

            try {
                fos = new FileOutputStream(pictureFile);
                bitmap = BitmapFactory.decodeByteArray(data, 0, data.length);
                bitmap.compress(CompressFormat.JPEG, 100, fos);

            } catch (FileNotFoundException e) {
                System.out.println("CameraActivityonPictureTaken() File not found: " + e.getMessage());
            } finally {
                try {
                    if (fos != null) {
                        fos.close();
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }

                fos = null;
                pictureFile = null;

                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
                    System.out.println("CameraActivity onPictureTaken() Bitmap recycled: " + bitmap.isRecycled() + ", size: " + bitmap.getAllocationByteCount() / (1024) + "kb");
                }
                bitmap.recycle();
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
                    System.out.println("CameraSurfaceTextureListener onPictureTaken() Bitmap recycled: " + bitmap.isRecycled() + ", size: " + bitmap.getAllocationByteCount() / (1024) + "kb");
                }


                bitmap = null;
                System.gc();

                long finishTime = System.currentTimeMillis();
                System.out.println("CameraActivity onPictureTaken() TIME: " + (finishTime - startTime) + "ms");
                mPreview.refreshPreview();
            }
        }
    };

    /**
     * Create a File for saving an image or video
     */
    private File getOutputMediaFile(int type) {
        // To be safe, you should check that the SDCard is mounted
        // using Environment.getExternalStorageState() before doing this.

        File mediaStorageDir = new File(Environment.getExternalStorageDirectory(), "MyCameraApp");
        // This location works best if you want the created images to be shared
        // between applications and persist after your app has been uninstalled.

        // Create the storage directory if it does not exist
        if (!mediaStorageDir.exists()) {
            if (!mediaStorageDir.mkdirs()) {
                Log.d("MyCameraApp", "failed to create directory");
                return null;
            }
        }

        // Create a media file name
        String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(new Date());
        File mediaFile;
        if (type == MEDIA_TYPE_IMAGE) {
            mediaFile = new File(mediaStorageDir.getPath() + File.separator + "IMG_" + timeStamp + ".jpg");
        } else {
            return null;
        }

        return mediaFile;
    }

    @Override
    protected void onPause() {
        super.onPause();
        releaseCamera(); // release the camera immediately on pause event
        if (preview != null && mPreview != null) {
            preview.removeView(mPreview);
            mPreview = null;
        }
    }

    @Override
    protected void onResume() {
        super.onResume();


        // Create an instance of Camera
        if (checkCameraHardware(this)) {
            if (mCamera == null) {
                mCamera = getCameraInstance();
                System.out.println("onResume() mCamera: " + mCamera);
                if (mPreview == null) {
                    // Create our Preview view and set it as the content of our
                    // activity.
                    mPreview = new CameraPreview(this, mCamera);
                    System.out.println("onResume() preview child count: " + preview.getChildCount());
                    preview.removeAllViews();
                    preview.addView(mPreview);
                } else {
                    mPreview.refreshPreview();
                }
            }
        }
    }

    private void releaseCamera() {
        if (mCamera != null) {
            mCamera.release(); // release the camera for other applications
            mCamera = null;
        }
    }
}

This is the Memory profiler of the image saving process. App is using 16 mb RAM while running camera preview. When i touch button to save image it goes up to 110mb while saving, it starts in about 25.00s, i didn't use a thread to check it visually, app freeze while saving, then it's reduced to 75 mb and keeps at this level if i do not manually GC or pause app using home button. I manually GC'ed at 43.00s. I left the app open and Bitmap wasn't still garbage collected after 7 minutes. I also checked CameraKit app and CameraView, they are not also GC'ed after picture is taken. Isn't there any way to claim memory from Bitmap manually.

How is it also possible to check if an Activity leaks with new Memory Profiler and create .hprof file?

Memory Profiler of the image saving process Memory Profile 2

I also test Camera2 Api code. Here is the memory profile for this code.

Memory Profiler Camera2 Api Dashed lines are object allocations which have a sawtooth pattern, objects GC'ed on the edge, but all memories are stable and not following object allocation pattern. How is this possible?

Upvotes: 0

Views: 209

Answers (1)

Nabin Bhandari
Nabin Bhandari

Reputation: 16419

I also found this issue earlier and I don't know why this happens but I think you shouldn't really worry about it.

As the documentation of the recycle method says that it is not guaranteed for the bitmap resources to be freed up immediately after this method is called.

To explain why you shouldn't worry, the memory will be freed up when you try to allocate a new image in the memory. Try taking a new picture and check the memory, the memory will not be added. Or even better, try taking 5 pictures, you will see that it will not take memory for 5 images as the memory will be freed up when creating new Bitmap.

Upvotes: 1

Related Questions