Sceneform - 如何修复泄漏的 SceneView?

Sceneform - How to fix leaking SceneView?

我使用 SceneView 加载 3D 模型已经将近一年了,但我一直不明白是什么导致了这种泄漏。我会实现 LeakCanary,但只有这一次泄漏,因为我不知道如何解决这个问题。

但现在我想弄清问题的根源并尝试解决它。我已经在 Sceneform 的两个版本上对其进行了测试——1.15.0 以及被 Google 1.17.1 弃用之前的最后一个版本,这两个版本都出现了这个问题。

当您在 ActivityFragment 中使用 SceneView 加载模型(我都试过了)并且您通过执行 System home navigation 或通过 Recent tasks list.

切换到不同的应用程序

泄漏日志:

┬───
│ 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

重现漏洞:

弃用前的最后一个版本:

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

我也试过 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(也测试过片段)

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();
    }
}

.sfb 文件

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

要重现,请在 SceneView 为 运行

时按 HomeRecent task list 按钮

我也只用 SceneView 测试过它,但它也会泄漏。

我该如何解决这个漏洞?

我还在多个 API、模拟器和真实设备上对其进行了测试,并且它发生在所有这些设备上。

正如你所说的最后一个版本1.17.1 一些爱好者正在研究 maintained version。也许这个版本修复了您的内存泄漏问题。

我尝试了这些选项来阻止泄漏,但它们没有任何作用:

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

然而,这为我修复了漏洞:

中删除sceneView.pause();
@Override
public void onPause()

在执行系统主页导航或通过“最近的任务”列表切换到其他应用程序时停止泄漏。 (OP 中的问题)

sceneView.pause(); 添加到

@Override
public void onDestroy()

在 activity 或片段被销毁时防止泄漏。

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