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
.
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
https://drive.google.com/file/d/1NchJ2NGNM7NZzRZkq4fewALc7m4dnGG3/view?usp=sharing
To reproduce press the Home
or Recent task list
button while SceneView
is running
I've also tested it with only just the SceneView
, but it leaks as well.
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.
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();
}