使用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)即可