Search code examples
androidarcoresceneformleakcanary

Sceneform - How to fix leaking SceneView?


I've been using SceneView for loading 3D models for almost a year now but I never understood what caused this leak. I would implement LeakCanary, but just this one leak because I couldn't figure out how to fix the problem.

But now I want to get to the bottom of the issue and try to fix it. I've tested it on two versions of Sceneform -- 1.15.0 and the last version before being deprecated by Google 1.17.1 and the issue happens on both.

The leak happens when you load a model using SceneView in an Activity or Fragment (I've tried both) and you "exit the app" by doing the System home navigation or switching to a different app via the Recent tasks list.

enter image description here

The logs for the leak:

┬───
│ GC Root: System class
│
├─ android.app.ActivityThread class
│    Leaking: NO (MainActivity↓ is not leaking and a class is never leaking)
│    ↓ static ActivityThread.sCurrentActivityThread
├─ android.app.ActivityThread instance
│    Leaking: NO (MainActivity↓ is not leaking)
│    mInitialApplication instance of android.app.Application
│    ↓ ActivityThread.mActivities
├─ android.util.ArrayMap instance
│    Leaking: NO (MainActivity↓ is not leaking)
│    ↓ ArrayMap.mArray
├─ java.lang.Object[] array
│    Leaking: NO (MainActivity↓ is not leaking)
│    ↓ Object[].[1]
├─ android.app.ActivityThread$ActivityClientRecord instance
│    Leaking: NO (MainActivity↓ is not leaking)
│    activity instance of com.example.a3dmodeltester.MainActivity with
│    mDestroyed = false
│    ↓ ActivityThread$ActivityClientRecord.activity
├─ com.example.a3dmodeltester.MainActivity instance
│    Leaking: NO (SceneView↓ is not leaking and Activity#mDestroyed is false)
│    mApplication instance of android.app.Application
│    mBase instance of android.app.ContextImpl
│    ↓ MainActivity.sceneView
├─ com.google.ar.sceneform.SceneView instance
│    Leaking: NO (View attached)
│    View is part of a window view hierarchy
│    View.mAttachInfo is not null (view attached)
│    View.mID = R.id.sceneview
│    View.mWindowAttachCount = 1
│    mContext instance of com.example.a3dmodeltester.MainActivity with
│    mDestroyed = false
│    ↓ SceneView.renderer
│                ~~~~~~~~
├─ com.google.ar.sceneform.rendering.Renderer instance
│    Leaking: UNKNOWN
│    Retaining 2.9 kB in 38 objects
│    ↓ Renderer.viewAttachmentManager
│               ~~~~~~~~~~~~~~~~~~~~~
├─ com.google.ar.sceneform.rendering.ViewAttachmentManager instance
│    Leaking: UNKNOWN
│    Retaining 2.2 kB in 19 objects
│    ↓ ViewAttachmentManager.frameLayout
│                            ~~~~~~~~~~~
╰→ android.widget.FrameLayout instance
​     Leaking: YES (ObjectWatcher was watching this because android.widget.
​     FrameLayout received View#onDetachedFromWindow() callback)
​     Retaining 1.9 kB in 15 objects
​     key = 10550463-895e-443a-898f-11c5131da209
​     watchDurationMillis = 5165
​     retainedDurationMillis = 157
​     View not part of a window view hierarchy
​     View.mAttachInfo is null (view detached)
​     View.mWindowAttachCount = 1
​     mContext instance of com.example.a3dmodeltester.MainActivity with
​     mDestroyed = false

METADATA

Build.VERSION.SDK_INT: 27
Build.MANUFACTURER: Google
LeakCanary version: 2.7
App process name: com.example.a3dmodeltester
Stats: LruCache[maxSize=3000,hits=892,misses=24408,hitRate=3%]
RandomAccess[bytes=1232425,reads=24408,travel=6462427001,range=8963521,size=1161
0814]
Heap dump reason: 1 retained objects, app is not visible
Analysis duration: 5411 ms

Reproducing the leak:

Last version before deprecation:

 implementation 'com.google.ar.sceneform.ux:sceneform-ux:1.17.1'
 debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.7'

I have also tried it with 1.15.0

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    
    <com.google.ar.sceneform.SceneView
        android:id="@+id/sceneview"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@android:color/white"/>
</RelativeLayout>

MainActivity (have also tested on a fragment)

public class MainActivity extends AppCompatActivity {

    private static final String TAG = "MainActivity";

    private static final Vector3   STARTING_CAMERA_POSITION = new Vector3(0f, 1f, 3f);
    private              SceneView sceneView;
    private              Node      node;

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

        sceneView = findViewById(R.id.sceneview);
        Scene scene = sceneView.getScene();
        node = new Node();
        scene.addChild(node);

        ModelRenderable.builder()
                .setSource(this, Uri.parse("Andy.sfb"))
                .build()
                .thenAccept(renderable -> node.setRenderable(renderable))
                .exceptionally(
                        throwable -> {
                            Log.e(TAG, "Unable to load Renderable.", throwable);
                            return null;
                        });
        Camera camera = scene.getCamera();
        camera.setWorldPosition(STARTING_CAMERA_POSITION);
    }

    @Override
    public void onPause() {
        super.onPause();
        sceneView.pause();
    }

    @Override
    public void onResume() {
        super.onResume();
        try {
            sceneView.resume();
        } catch (CameraNotAvailableException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        sceneView.destroy();
    }
}

The .sfb file

enter image description here

https://drive.google.com/file/d/1NchJ2NGNM7NZzRZkq4fewALc7m4dnGG3/view?usp=sharing

To reproduce press the Home or Recent task list button while SceneView is running

enter image description here

I've also tested it with only just the SceneView, but it leaks as well.

enter image description here

How would I fix this leak?

I've also tested it on multiple APIs, emulators, and real devices and it happens on all of them.


Solution

  • I've tried these options to stop the leak but they do not do anything:

    Scene scene = sceneView.getScene();
    scene = null;
    sceneView = null;
    sceneView.destroy();
    Renderer.destroyAllResources();
    Renderer.reclaimReleasedResources();
    

    This however, fixes the leak for me:

    Removing the sceneView.pause(); from

    @Override
    public void onPause()
    

    stops the leak when doing a System home navigation or switching to a different app via the Recent tasks list. (The question in the OP)

    Adding sceneView.pause(); to

    @Override
    public void onDestroy()
    

    Prevents a leak when the activity or fragment is destroyed.

        @Override
        public void onDestroy() {
            super.onDestroy();
            sceneView.pause(); 
        }