AS3将函数作为参数传递会导致内存泄漏。

8

我有一个函数,它将另一个函数作为参数。大致如下:

public function onHits(target : Shape, callback : Function) : void

我通过将一个成员函数作为参数传递来使用它,这个函数应该在传递的目标物体碰撞时被调用。这个函数每帧会被多次调用。因此,使用方法如下:

//code...
CollisionManager.onHits(myShape, onHitCB);
//code...

触发函数:

On hit 函数:

public function onHitCB(hitObject : *) : void 
{
    //removed all code to test this problem
}

当我这样做时,我会出现内存泄漏。我已经将问题隔离到onHits方法,并注释掉其他所有内容。onHits是一个没有任何代码的空方法,onHitCB也是空的。如果我注释掉对onHits的调用,就不会有内存泄漏,如果我传递null而不是onHitCB,则也不会有内存泄漏。
所以很明显当我将onHitCB作为参数传递时出现了问题。所以我认为可能是因为Flash分配了一些内存来创建函数指针,但我在调试模式下每帧都调用System.gc(),泄漏仍然存在。这意味着这要么是SDK的错误,要么我没有做正确的事情。
我通过保留指向函数的变量并在我的对象构造函数中分配它找到了一个奇怪的解决方法:
private var func : Function;

public function MyObject() 
{
    func = onHitCB;
}

即使我仍将onHitCB作为参数传递,这将清除内存泄漏。 这意味着获取onHitCB的不是“getter”函数,而是其他原因导致了内存泄漏?

我很困惑。这怎么可能导致内存泄漏:

public function MyObject() 
{
}

public function update() : void
{
    CollisionManager.onHits(myShape, onHitCB);//empty function
}

public function onHitCB(hitObject : *) : void 
{
    //removed all code to test this problem
}

但不包括这个吗? :

private var func : Function;
public function MyObject() 
{
    func = onHitCB;
}

public function update() : void
{
    CollisionManager.onHits(myShape, onHitCB);//empty function
}

public function onHitCB(hitObject : *) : void 
{
    //removed all code to test this problem
}

有没有不需要这样解决的方法?

为什么不将onHitCB作为CollisionManager的类成员?听起来你的函数正在失去作用域。在onHits的最后一行尝试callback=null; - The_asMan
尝试在onHits结束时将回调设置为null,但仍存在泄漏问题。到目前为止,保留对函数的本地引用是我能找到的唯一解决方法。 - Godfather
4个回答

5
"[...]当您将方法作为参数传递时,绑定方法会自动创建。绑定方法确保this关键字始终引用定义方法的对象或类。 来源"
"听起来好像创建对方法的引用并不使用简单的getter。一个新的方法闭包对象被生成。所以你的假设是正确的。"
"我想知道为什么这些引用不会被缓存到每个实例中,并且为什么它们不会被垃圾回收。最好避免创建多个引用。只引用一次方法正是我在多个地方使用该方法时要做的事情,所以大多数情况下,我不会称其为变通方法,而是DRY实践。当然,在您的示例中,这是有意义的,假设方法引用将使用简单的getter。"

我明白了,这证实了我的猜想,即每次传递函数时都会创建一个对象。然而,我仍然不明白为什么在保留本地引用的同时仍然传递函数(而不是本地引用)将不再创建内存泄漏。 - Godfather
我认为只有在直接使用方法名称(在赋值或用作参数时)时才会生成它。因此,func = onHitCB; 只会生成一次方法闭包,使用 func 作为参数不会导致其再次生成。 - kapex
1
是的,但我没有将func作为参数使用,我仍在使用onHitCB(如果您仔细查看上面的2个示例,它们都使用onHitCB调用onHits),这对我来说是最大的谜团。 我现在最好的解释是该方法闭包被缓存为弱引用,通常会被垃圾收集器清除,但由于某种未知原因而不会清除。 由于它是一个弱引用,所以需要在每次调用时重新生成,但由于我在本地保留了一个引用,它足够长时间保持活动状态,以便在以后的调用中重复使用。 - Godfather

1

想要了解在使用函数式技术时到底会导致什么样的内存泄漏问题,可以查看http://www.developria.com/2010/12/functional-actionscript-part-1.html 进行了解更多信息。另外,请注意,使用这样的静态方法是非常不好的做法(http://misko.hevery.com/code-reviewers-guide/flaw-brittle-global-state-singletons/)。你现在才刚开始遇到使用这种技术所引起的众多问题。由于你的项目还处于早期阶段,因此你可能需要考虑其他编程方式。


0

我不确定你在onHits函数中的代码是关于什么的,但如果它不需要额外的时间在另一个周期内完成,那么我建议你这样做:

static public function onHits(target : Shape) : *
{
    // do what you need

    // return the hitObject;
    return hitObject;
}

并且

public function update() : void
{
    // parse the object direc to to the function.
    onHitCB ( CollisionManager.onHits(myShape) );
}

public function onHitCB(hitObject : *) : void 
{
    if ( hitObject == null )
        return;

    // if it not null then do all your calculations.
    //removed all code to test this problem
}

目前,onHits 函数什么也没有做。我已经从中删除了所有代码,但是仅仅通过传递一个函数指针来调用它将会创建一个内存泄漏。我可以通过改变代码的设计来避免这个问题,但是仅仅传递一个函数指针不应该导致这个问题 (>.<) - Godfather

-1
这就是为什么我们不会在面向对象编程中做这种事情的原因。
你最好的选择是正确地添加回调到CollisionManager类中。
当你保留一个本地引用时,它可以被垃圾回收,因为该函数永远不会失去作用域,因为那个变量一直持有引用。
一旦某些东西失去了作用域,几乎不可能对其进行垃圾回收。

尝试这样做并观察如何失去作用域。
private var somevar:String = 'somevar with a string';
public function MyObject() 
{
}

public function update() : void
{
    CollisionManager.onHits(myShape, onHitCB);//empty function
}

public function onHitCB(hitObject : *) : void 
{
    trace(this.somevar) // scope should be lost at this point and somevar should be null or toss an error.
}

这不应该工作吗?我不确定您所说的失去范围是什么意思(函数指针保留对它们所属的实例的引用,因此可以使用正确的this参数调用它们)。通过参数传递函数可以正常工作,并像应该的那样跟踪“带有字符串的somevar”(我想是这样的吧?)。我不明白为什么传递必须在检测到碰撞时调用的回调函数可能是“糟糕的设计”,每个对象都可以并且可能应该以不同的方式处理碰撞。这在多种情况下都被使用(我认为box2D就是这样做的),因为它更加灵活。 - Godfather
好的,这个例子不太好。如果我有时间,我会为您发布一个编辑版本,更好地解释我所说的内容。 - The_asMan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接