Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Unity] C# 调用 JS 时因 GC 触发导致参数变成无效对象 #681

Closed
1901 opened this issue Mar 2, 2022 · 16 comments · Fixed by #729
Closed

[Unity] C# 调用 JS 时因 GC 触发导致参数变成无效对象 #681

1901 opened this issue Mar 2, 2022 · 16 comments · Fixed by #729
Assignees
Milestone

Comments

@1901
Copy link

1901 commented Mar 2, 2022

详细描述

Unity 2019 的环境中,在 C# 调到 JS 的方法时(图中代码:497 行),传到 JS 的参数,在执行 PuertsDLL.InvokeJSFunction 的时候从对象池里移除掉了,具体可以看图中的调用堆栈。这样导致传到 JS 层的对象就无法正常使用了。

问题分析

目前看起来应该是在 C++ 的 JSEngine.cpp 中的 OnGarbageCollected 方法因为 gc 被触发,然后代码调回到 C# 的 StaticCallbacks.GeneralDestructor 方法,导致对象从对象池中移除。

问题

  1. 请问这个问题要怎么处理或有办法绕过吗?
  2. 因为是 gc 触发导致出现的问题,所以不能稳定重现,是否有办法手动触发 gc 来方便调试?

5C485EE0-9A10-4776-8188-DB8D290F40B1

@1901 1901 changed the title C# 调用 JS 时因 GC 触发导致参数变成无效对象 [Unity] C# 调用 JS 时因 GC 触发导致参数变成无效对象 Mar 2, 2022
@zombieyang
Copy link
Contributor

zombieyang commented Mar 2, 2022

图中看不出你是怎么使用Action的。

原理上来说,每个到C#变成delegate的JS函数,在C++侧都会有一个JSFunction对象,使用PersistentHandler持有它。
而JSFunction的释放是在C#侧generalDelegate的析构函数执行的时候执行的。
也就是C#不GC delegate,C++ JSFunction对象就不会析构,进而JS侧也不可能GC那个JS函数。

所以你这个case就挺离奇,看看你能不能按这个思路先查查。

@1901
Copy link
Author

1901 commented Mar 3, 2022

@zombieyang 感谢回复。

Action 是这样的,在 C# 这边有个事件处理器,在 JS 调用 C# 的方法传入了一个 Function 作为事件监听者。当事件派发的时候,通过这个 Action 回调到 JS 层。

// this.onLoopCompleteBind 是一个 JS 的 Function,传入到 C# 生成那个 Action。
this.onLoopCompleteBind = this.onLoopCompleteDBEvent.bind(this);
this.armatureComponent.addDBEventListener(EventObject.LOOP_COMPLETE, this.onLoopCompleteBind);

private onLoopCompleteDBEvent(type: string, eventObject: dragonBones.EventObject) {
    if (eventObject && eventObject.animationState) { // JS 报错行
        this.onLoopComplete(eventObject);
    }
}

现在的问题并不是这个 JSFunction 被释放了。而是调用这个 Function 的时候传入的参数从 ObjectPool 里被移除了,这样就会导致参数传到 JS 后。在 JS 里使用这个参数的时候(上面代码块中的 eventObject.animationState),调用到 C# 下面的代码,在 Puerts.Utils.GetSelf((int)data, self) 这一步的时候无法取到 eventObject 这个对象,因为对象之前已经从 ObjectPool 中移除掉了。

[Puerts.MonoPInvokeCallback(typeof(Puerts.V8FunctionCallback))]
private static void G_animationState(IntPtr isolate, IntPtr info, IntPtr self, int paramLen, long data)
{
    try
    {
        var obj = Puerts.Utils.GetSelf((int)data, self) as DragonBones.EventObject;
        var result = obj.animationState; // C# 报错行
        Puerts.ResultHelper.Set((int)data, isolate, info, result);
    }
    catch (Exception e)
    {
        Puerts.PuertsDLL.ThrowException(isolate, "c# exception:" + e.Message + ",stack:" + e.StackTrace);
    }
}

@zombieyang
Copy link
Contributor

zombieyang commented Mar 3, 2022

这个EventObject,基类是不是一个C#类,然后JS侧将其extends了?#65

@1901
Copy link
Author

1901 commented Mar 3, 2022

EventObject 是 C# 的一个类,但没有在 JS 层继承他,和 #65 不太一样。

但是今天有点新的发现,在 C# 调到 JS 时传的参数 eventObject 是从一个对象池(非 Puerts.ObjectPool)中取出来了,因为是复用的对象,出问题前也多次将这个对象传入到 JS。猜测是不是 GC 的时候清理之前的对象,导致这次的复用对象也被清理?

@chexiongsheng
Copy link
Collaborator

但是今天有点新的发现,在 C# 调到 JS 时传的参数 eventObject 是从一个对象池(非 Puerts.ObjectPool)中取出来了,因为是复用的对象,出问题前也多次将这个对象传入到 JS。猜测是不是 GC 的时候清理之前的对象,导致这次的复用对象也被清理?

看上去有这么个可能。

https://github.com/Tencent/puerts/blob/master/unity/native_src/Src/JSFunction.cpp#L98
这行之后,这个js对象会被handle持有,不会被gc。
但在此之前,如果那些HandleScope之类的语句会触发gc的话,那个对象有可能被释放。因为那个时候参数放到c++的JsFunction里头只是保存了对象ID而已,并没有在js那增加应用。

@1901
Copy link
Author

1901 commented Mar 3, 2022

@chexiongsheng @zombieyang

感谢两位的回复。

经过测试分析,发现我的应用场景可能确实是跟对象池有关系。但是我做了另外一个测试,也能重现问题。

示例代码

  • Unity 代码片段一
public static void TestEventObject(Action<string, DragonBones.EventObject> handler)
{
    Game.Instance.ScheduleUpdate(() =>
    {
        var eventObject = new DragonBones.EventObject();
        handler(eventObject.type, eventObject); // 注释 A
        handler(eventObject.type, eventObject); // 注释 B
        handler(eventObject.type, eventObject);
        handler(eventObject.type, eventObject);
        handler(eventObject.type, eventObject);
        handler(eventObject.type, eventObject);
        handler(eventObject.type, eventObject);
        handler(eventObject.type, eventObject);
        handler(eventObject.type, eventObject);
        handler(eventObject.type, eventObject);
    });
}
  • Unity 代码片段二
[Puerts.MonoPInvokeCallback(typeof(Puerts.V8FunctionCallback))]
private static void G_type(IntPtr isolate, IntPtr info, IntPtr self, int paramLen, long data)
{
    try
    {
        var obj = Puerts.Utils.GetSelf((int)data, self) as DragonBones.EventObject;
        var result = obj.type; // 报错行
        Puerts.PuertsDLL.ReturnString(isolate, info, result);
    }
    catch (Exception e)
    {
        Puerts.PuertsDLL.ThrowException(isolate, "c# exception:" + e.Message + ",stack:" + e.StackTrace);
    }
}
  • TypeScript(JavaScript)
onTestEventObject(type: string, eventObject: dragonBones.EventObject): void {
    if (eventObject && eventObject.type) { // 报错行

    }
}

test(): void {
    UnityTest.TestEventObject(this.onTestEventObject.bind(this));    
}

问题分析

  1. C# 代码运行至注释 A 的时候调用 handler 传入 eventObject,JS 层正常得到这个对象,没有问题。
  2. 代码继续运行到注释 B 的时候(具体代码行:PuertsDLL.InvokeJSFunction(...)),V8 的 GC 触发,发现之前 A 处传到 JS 层的 eventObject 已无引用,然后从 ObjectPool 中将其移除。
  3. 等 JS 层 onTestEventObject 方法被调用到,在里面使用 eventObject 的时候,调回到 C# 层(Unity 代码片段二),C# 层根据 objectId 去 ObjectPool 中取 eventObject 时候返回空,导致出现异常。

@zombieyang
Copy link
Contributor

大概明白问题在哪了。顺便确认一下你用的plugin版本是v8还是quickjs?

@1901
Copy link
Author

1901 commented Mar 3, 2022

大概明白问题在哪了。顺便确认一下你用的plugin版本是v8还是quickjs?

v8
版本大概是前天下载的最新版本。

zombieyang added a commit to zombieyang/puerts that referenced this issue Mar 4, 2022
@zombieyang
Copy link
Contributor

https://github.com/Tencent/puerts/tree/681
因为我这边无法复现,麻烦帮忙用这个分支验证一下哈

@1901
Copy link
Author

1901 commented Mar 4, 2022

https://github.com/Tencent/puerts/tree/681 因为我这边无法复现,麻烦帮忙用这个分支验证一下哈

看 commit 信息,似乎改过 native 的代码了,请问哪里可以下载到对应编译好的库文件 macOS/puerts.bundle

@1901
Copy link
Author

1901 commented Mar 4, 2022

https://github.com/Tencent/puerts/tree/681 因为我这边无法复现,麻烦帮忙用这个分支验证一下哈

看 commit 信息,似乎改过 native 的代码了,请问哪里可以下载到对应编译好的库文件 macOS/puerts.bundle

找到了,应该是这个吧?https://github.com/Tencent/puerts/actions/runs/1932215308

@zombieyang
Copy link
Contributor

是的

@1901
Copy link
Author

1901 commented Mar 4, 2022

@zombieyang 经过测试,在上面的例子和我的实际应用场景中都不会再出现错误。

问题应该是已经修复了,再次感谢!

@zombieyang
Copy link
Contributor

好,你先用着,后续我优化好代码之后再合入master

@1901
Copy link
Author

1901 commented Mar 4, 2022

好的,谢谢

@zombieyang zombieyang self-assigned this Mar 9, 2022
bluedoom pushed a commit to bluedoom/puerts that referenced this issue Mar 11, 2022
@bluedoom
Copy link
Contributor

bluedoom commented Mar 14, 2022

我把这个合并到1.2.2上了,情况好了一些,但是没有完全好
bluedoom@b24aefc

image
image
image
复现方法有点尴尬,上面那个Ts代码会每帧执行,然后挂机等,几分钟会跳一个报错。(手动GC没用,感觉需要等引擎自己GC)
另外在合并修复之前,上面错误出现之后还会紧接着出现下面的错误。
image
this.R.Empty.gameObject.SetActive :解释一下,这里面R是Js Object,Empty是Unity Component

bluedoom added a commit to bluedoom/puerts that referenced this issue Mar 14, 2022
(cherry picked from commit 79be1da7ddab1cb55e88117fc5fc6df50569d549)
bluedoom added a commit to bluedoom/puerts that referenced this issue Mar 15, 2022
@zombieyang zombieyang added this to the unity-1.3 milestone Mar 16, 2022
bluedoom added a commit to bluedoom/puerts that referenced this issue Jul 26, 2022
zombieyang pushed a commit to zombieyang/puerts that referenced this issue Aug 11, 2022
zombieyang pushed a commit to zombieyang/puerts that referenced this issue Aug 12, 2022
zombieyang pushed a commit that referenced this issue Oct 14, 2022
mingxxming pushed a commit to mingxxming/puerts that referenced this issue Nov 8, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants