sceneform-内存泄漏分析

使用Sceneform开发过程中遇到内存泄漏问题描述

我们的项目中使用了很多比较大的模型,glb格式,纹理比较大5000x7000这种的。

测试中,发现退出ARActivity,内存使用没有回落。重新进入ARActivity,内存使用量增加。反复进出n次,发明内存占用成倍增加,显然内存泄漏了。

典型情况,一个场景中加载一个300MB的glb模型,重复进入4、5次就内存溢出了。

Sceneform 如何管理内存

我们都知道,在Java语法中,我们创建一个对象通过关键字 new,但是我发现Sceneform创建关键角色 Renderable,不论是ViewRenderable 还是ModelRenderable都是调用的build()方法,如下代码所示:

ViewRenderable.builder()

        .setView(context, view)

        .setVerticalAlignment(ViewRenderable.VerticalAlignment.CENTER)

        .build()

.thenAccept {

renderable = it

        }

ModelRenderable.builder().setSource(activity, pathUri)

        .setIsFilamentGltf(true).build().thenAccept(

                { modelRenderable ->

renderable = modelRenderable

                   })

        .exceptionally(

                { throwable ->

val toast = Toast.makeText(activity, "无法加载模型文件,请确认格式是否正确", Toast.LENGTH_LONG)

                    toast.setGravity(Gravity.CENTER, 0, 0)

                    toast.show()

                    null

                })

如此我们不难看出这个对象JVM已经无法管理了。那么它能管理的是哪个呢?其实是 thenAccept()方法的返回值,有些小伙伴就迷糊了,一会管理,一会又不管理,怎么回事啊?其实这里有两个对象了,当我们调用build()的时候,里面其实对当前的renderable做了copy。请看Renderabel和ModelRenderable代码:


//Renderabel.java

public CompletableFuture<T> build() {

    ...

  // For static-analysis check.,其实registryId是uri

Object registryId = this.registryId;

  if (registryId != null) {

    // See if a renderable has already been registered by this id, if so re-use it.

    //ResourceManager.getInstance().getModelRenderableRegistry();

ResourceRegistry<T> registry = getRenderableRegistry();

    CompletableFuture<T> renderableFuture = registry.get(registryId);

    if (renderableFuture != null) {

      return renderableFuture.thenApply(

              renderable -> getRenderableClass().cast(renderable.makeCopy()));

    }

  }

  T renderable = makeRenderable();

  ...

  return result.thenApply(

      resultRenderable -> getRenderableClass().cast(resultRenderable.makeCopy()));

}

//ModelRenderabel.java

    @Override

public ModelRenderable makeCopy() {

    return new ModelRenderable(this);

}

/

 @hide

/

@Override

protected ModelRenderable makeRenderable() {

    return new ModelRenderable(this);

}

代码中不是makeCopy()方法,就是makeRenderable()其实都是对当前的renderable做了拷贝,然后一份Sceneform自己用,一份给开发者,为什么这么做呢?我先不回答这个问题,我先请问你一个问题,我如果想显示同一个模型显示10次,那么我要加载10次这个模型到引擎内吗?显然不能那么做啊,因为模型我可以复用啊,我加载一次就可以了。对吧。所以从整体看这个设计是节省资源的,二不是浪费资源。这时JVM这边的Renderable对象需要映射到filament引擎一个对象,引擎内的对象其实JVM就够不着管理了。

如何创建一个JVM不管的资源?Builder

既然new的对象Java要负责回收,那我就不能使用这个方式创建,Sceneform采用了构建者模式,创建一个Builder,通过这个Builder可以获取byteBuffer从本地glb文件或远程glb文件,然后把byteBuffer数据传递给filament引擎,去构造模型对象。这个对象其实Java就无法管理了。回收这个对象也只能是filament来做。

资源回收的机制

 要想知道怎么回收的之前,我们先看看怎么创建的资源的

我们创建完Node后,要为其创建renderable,就是在node执行 setRenderable()方法的时候

调用了 RenderableInstance.attachToRenderer(getRendererOrDie())->RenderableInstance.attachFilamentAssetToRenderer().最终调用了filament/Scene.java的addEntity()方法


private void attachFilamentAssetToRenderer() {

        FilamentAsset currentFilamentAsset = filamentAsset;

        if (currentFilamentAsset != null) {

            int[] entities = currentFilamentAsset.getEntities();

            Preconditions.checkNotNull(attachedRenderer)

                    .getFilamentScene()

                    .addEntity(currentFilamentAsset.getRoot());

            Preconditions.checkNotNull(attachedRenderer).getFilamentScene().addEntities(entities);

        }

    }

//  filament/Scene.java

/

  Adds an {@link Entity} to the <code>Scene</code>.

 

 @param entity the entity is ignored if it doesn't have a {@link RenderableManager} component

                or {@link LightManager} component.<br>

                A given {@link Entity} object can only be added once to a <code>Scene</code>.

 /

public void addEntity(@Entity int entity) {

    nAddEntity(getNativeObject(), entity);

}

在Java的世界,一切new的对象我们可以让虚拟机来自动回收,但是Builder显然不符合这个,我们只有手工回收,具体怎么做呢?

我们来看看回收的流程

在页面销毁时,首先调用了SceneView这个成员的 destroy()方法


public void destroy() {

    if (renderer != null) {

      //add this method to release memory.

      reclaimReleasedResources();

      //这个方法销毁资源,具体是FilamentEngineWrapper调用自己的destroy方法,但是官方只调用了3个,共有16个

      renderer.dispose();

      renderer = null;

    }

  }

我们看到它其实调用了2个重要方法 ,第一个是reclaimReleasedResources()方法它最终调用到了ResourceManager.getInstance().reclaimReleasedResources();


public long reclaimReleasedResources() {

    long resourcesInUse = 0;

    for (ResourceHolder registry : resourceHolders) {

      resourcesInUse += registry.reclaimReleasedResources();

    }

    return resourcesInUse;

这里是统计资源使用次数,如果当使用次数为0时,我们就可以通知native层可以回收资源了。否则我们不知道什么时候应该让native层回收。

第二个是renderer.dispose()()方法;它代码更简单明了

/ @hide /

  public void dispose() {

    //这里调用了自己的 onDetachedFromSurface,执行了 engine.destorySwapChain()

    filamentHelper.detach(); // call this before destroying the Engine (it could call back)

    final IEngine engine = EngineInstance.getEngine();

    if (indirectLight != null) {

      engine.destroyIndirectLight(indirectLight);

    }

    engine.destroyRenderer(renderer);

    engine.destroyView(view);

    reclaimReleasedResources();

  }

我们看到这里其实直接调用了filament的引擎去销毁资源。

我们再跟一下代码


// FilamentEngineWrapper.java 

@Override

public void destroyView(View view) {

  engine.destroyView(view);

}

// Engine.java 

/

  Destroys a {@link View} and frees all its associated resources.

 @param view the {@link View} to destroy

 /

public void destroyView(@NonNull View view) {

    assertDestroy(nDestroyView(getNativeObject(), view.getNativeObject()));

    view.clearNativeObject();

}

// filament/View.java

void clearNativeObject() {

    mNativeObject = 0;

}

从上面我们可以清晰的知道native层怎么回收的了。其实到这里你有没有发现一个大问题,build的时候可以去做了attachToRenderer()方法,那么回收的时候怎么没有detachFromRenderer()方法,从命名上来看这是一对方法啊需要成对 的调用啊,只调用一个绝对又问题,至于答案我们后面讲。我们来思考另外一个问题,Sceneform自己留的一份什么时候回收的呢?

Build里面为什么要有Makecopy,创建之后不光是开发者有一份,Sceneform还cache了一份,这个也要清理

Sceneform使用了copy机制,当然它也会为这个机制提供内存回收的方法,就是统计对象引用计数来管理

如果我们想让Sceneform回收它自己cache的Renderable,就只需要把对象的引用数设置0即可。

代码如下

// ResourceManager.java

public long reclaimReleasedResources() {

    long resourcesInUse = 0;

    for (ResourceHolder registry : resourceHolders) {

      resourcesInUse += registry.reclaimReleasedResources();

    }

    return resourcesInUse;

// ResourceRegistry.java

@Override

public long reclaimReleasedResources() {

  // Resources held in registry are also held by other ResourceHolders.  Return zero for this one

  // and do

  // counting in the other holders.

return 0;

}

// CleanupRegistry.java 这里什么时候设置为0呢?

public long reclaimReleasedResources() {

CleanupItem<T> ref = (CleanupItem<T>) referenceQueue.poll();

  while (ref != null) {

    if (cleanupItemHashSet.contains(ref)) {

      ref.run();

      cleanupItemHashSet.remove(ref);

    }

ref = (CleanupItem<T>) referenceQueue.poll();

  }

  Log.d("steven","");

  return cleanupItemHashSet.size();

}

小结一下,我们创建Renderable 先调用Renderable.build()方法,这个过程里面会对创建的Renderable makeCopy,这样就有两个Renderable,一份ResourceManager来管理,SceneView.destroy()方法执行时会调用 ResourceManager.getInstance().reclaimReleasedResources()方法来释放资源。二另外一份开发者使用,我释放的方式仅仅是设置Renderable为null,这样根本不会释放资源,导致内存泄漏

SceneForm框架下Node对Resouce的管理机制

Node的active,enabled两个关键属性,与Node的生命周期的关系

首先我们来看看Node的生命周期,Node的LifecycleListener接口说明了它有三个状态:


/ Interface definition for callbacks to be invoked when node lifecycle events occur. /

public interface LifecycleListener {

  /

    Notifies the listener that {@link #onActivate()} was called.

   

 @param node the node that was activated

   /

void onActivated(Node node);

  /

    Notifies the listener that {@link #onUpdate(FrameTime)} was called.

   

 @param node the node that was updated

 @param frameTime provides time information for the current frame

   /

void onUpdated(Node node, FrameTime frameTime);

  /

    Notifies the listener that {@link #onDeactivate()} was called.

   

 @param node the node that was deactivated

   /

void onDeactivated(Node node);

}

第一个是onActivated-激活状态,啥时候激活的呢?调用activate方法时激活的,那这个方法什么时候被调用了?请看下面的代码


private void updateActiveStatusRecursively() {

  final boolean shouldBeActive = shouldBeActive();

  if (active != shouldBeActive) {

    if (shouldBeActive) {

      activate();

} else {

      deactivate();

    }

  }

  for (Node node : getChildren()) {

    node.updateActiveStatusRecursively();

  }

}

private boolean shouldBeActive() {

  if (!enabled) {

    return false;

  }

  if (scene == null) {

    return false;

  }

  if (parentAsNode != null && !parentAsNode.isActive()) {

    return false;

  }

  return true;

}

private void activate() {

  AndroidPreconditions.checkUiThread();

  if (active) {

    // This should NEVER be thrown because updateActiveStatusRecursively checks to make sure

    // that the active status has changed before calling this. If this exception is thrown, a bug

    // was introduced.

throw new AssertionError("Cannot call activate while already active.");

  }

  active = true;

  if ((scene != null && !scene.isUnderTesting()) && renderableInstance != null) {

    renderableInstance.attachToRenderer(getRendererOrDie());

  }

  if (lightInstance != null) {

    lightInstance.attachToRenderer(getRendererOrDie());

  }

  if (collider != null && scene != null) {

    collider.setAttachedCollisionSystem(scene.collisionSystem);

  }

  onActivate();

  for (LifecycleListener lifecycleListener : lifecycleListeners) {

    lifecycleListener.onActivated(this);

  }

}

private void deactivate() {

  AndroidPreconditions.checkUiThread();

  if (!active) {

    // This should NEVER be thrown because updateActiveStatusRecursively checks to make sure

    // that the active status has changed before calling this. If this exception is thrown, a bug

    // was introduced.

throw new AssertionError("Cannot call deactivate while already inactive.");

  }

  active = false;

  if (renderableInstance != null) {

    renderableInstance.detachFromRenderer();

  }

  if (lightInstance != null) {

    lightInstance.detachFromRenderer();

  }

  if (collider != null) {

    collider.setAttachedCollisionSystem(null);

  }

  onDeactivate();

  for (LifecycleListener lifecycleListener : lifecycleListeners) {

    lifecycleListener.onDeactivated(this);

  }

}

由上面的代码我们看到,active默认为false, activate()方法执行后就被设置成了true,注意这里如果active已经被设置成true,便不能再次设置成true,上面代码写了会报 AssertionError错误。

第二个状态是 onDeactivated,这个是执行了deactivate()方法之后设置的。看上面的代码我们不难发现,这个状态和第一种状态互斥的。我们这里注意一下,就是这种状态设置后,renderableInstance,lightInstance 等资源都从renderer里面移除掉了。这里我为什么强调这个呢?因为开发中我们常遇到一种需求,在条件1下我不想显示Node,条件2下我想显示,就不能通过改变这个状态来实现,因为这个一旦设置过,资源就被释放了。你的节点也就不能用了。那么谁会改变这个状态呢?

就是它:


  Sets the enabled state of this node. Note that a Node may be enabled but still inactive if it

  isn't part of the scene or if its parent is inactive.

 

 @see #isActive()

 @param enabled the new enabled status of the node

 /

public final void setEnabled(boolean enabled) {

  AndroidPreconditions.checkUiThread();

  if (this.enabled == enabled) {

    return;

  }

  this.enabled = enabled;

  updateActiveStatusRecursively();

}

那如果我们只是想隐藏Node呢?Node提供了另外一个好用的方法


public void setParent(@Nullable NodeParent parent) {

  AndroidPreconditions.checkUiThread();

if (parent == this.parent) {

return;

  }

  // Disallow dispatching transformed changed here so we don't

  // send it multiple times when setParent is called.

allowDispatchTransformChangedListeners = false;

if (parent != null) {

    // If this node already has a parent, addChild automatically removes it from its old parent.

    parent.addChild(this);

} else if (this.parent != null) {

this.parent.removeChild(this);

  }

allowDispatchTransformChangedListeners = true;

  // Make sure transform changed is dispatched.

  markTransformChangedRecursively(WORLD_DIRTY_FLAGS, this);

}

这个方法的注释说的很详细了。我们仔细阅读会发现,如果我们调用这个方法传递的值为null时,这个node会从他的父节点分离,也就是不显示了。

第三种状态简单些 onUpdated,更新状态,它的调用顺序是Choreographer.callbackRecord()-> SceneView.doFrameNoRepost()->Scene.dispatchUpdate()->Node.dispatchUpdate()。可以看出只要node 是active的,每帧画面都在调用这个方法。

到此我们可以简单的总结一下Node的生命周期过程,如果我们创建的类型是AnchorNode(AnchorNode(Anchor anchor)),初始化会调用 setAnchor()方法,这个方法直接调用了setChildrenEnabled(boolean flag)方法,入参为anchor是否为空,很明显,这里的anchor不为空,所以setChildrenEnabled(true),这个方法会遍历所有孩子执行child.setEnabled(true)。还有一个为无参的构造函数,什么都没做,但是Node.dispatchUpdate()方法会调用AnchorNode.updateTrackedPose()方法,这个方法最终调用了child.setEnabled(enabled)。至此。AnchorNode创建后都执行了Node.setEnabled(enabled),也就激活了Node。这样三个状态都齐活了。

Node树状数据结构,树状结构和递归更新active属性的过程

通过阅读源码,可以看出Sceneform的Node层级结构(the company net so bad)
[图片上传失败...(image-827d39-1630460815476)]

总结

  • SceneForm会build出来JVM无法GC的图形对象

  • 图形对象在SceneForm框架下,托管给Node

  • Node不用了,一定要setEnable(false)

  • 场景销毁,destroyAllResource

  • setEnable(false)之后不能setEnable(true),如果想让node隐藏,保留原先的position,rotation,scale,setParent(null)即可

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,039评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,426评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,417评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,868评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,892评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,692评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,416评论 3 419
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,326评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,782评论 1 316
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,957评论 3 337
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,102评论 1 350
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,790评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,442评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,996评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,113评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,332评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,044评论 2 355

推荐阅读更多精彩内容