闪退点

Version: Cocos2dx-Lua 3.16

在某个页面修改后,出现闪退。查看修改记录没有特别的地方,均为修改图片或者条件判断。

启动 XCode 发现闪退位置出现在void Node::onEnterTransitionDidFinish()中, 在其调用子节点的child->onEnterTransitionDidFinish()时,child指针无效。

1352
1353
1354
...
for( const auto &child: _children)
    child->onEnterTransitionDidFinish();

此时的调用栈:

0: void Node::onEnterTransitionDidFinish() -> child->onEnterTransitionDidFinish();
1: void Director::setNextScene() -> _runningScene->onEnterTransitionDidFinish();
2: void Director::drawScene() -> setNextScene();
...

对应 Lua 代码流程:

display.runScene(HomeScene)
HomeScene:ctor() {
    self:addChild(HomeLayer)
}

怀疑

因为是在调用Scene::onEnterTransitionDidFinish()时指针无效,所以猜测是遍历过程中字节点被删除。

查看相关代码:

HomeScene: 没有 onEnterTransitionDidFinish()
HomeLayer: 有 onEnterTransitionDidFinish(),
           但在 onEnterTransitionDidFinish 中没有删除 HomeScene 上的结点。
           而是往 Scene 上新增结点 addChild(GuideLayer)

因为游戏逻辑是只创建一个GuideLayer,重复创建时会删除上次创建的页面。又怀疑是否创建了两次GuideLayer。 通过屏蔽代码发现不添加GuideLayer就不会闪退,但是GuideLayer的确只创建了一次,不存在删除问题。

真相

再次分析流程

HomeScene:onEnterTransitionDidFinish():
    HomeLayer:onEnterTransitionDidFinish(
        addChild(GuideLayer)  // 添加到HomeScene上
    )

HomeScene:onEnterTransitionDidFinish() 闪退

总归问题是出现在HomeScene_children容器上,那么既然不是删除,增加结点会导致容器出问题吗?

查找代码得到_children类型为CCVector,而CCVector通过std::vector实现。

所以出现了常见的问题:在容器迭代过程中删除或插入

在新增结点往std::vector插入的时候,恰好超过容量大小,导致扩容。而std::vector是分配的连续内存, 所以扩容时候会重新分配内存,导致遍历的时候出现child指针无效。

scene::onEnterTransitionDidFinish() {
    for (child: scene->children) {       
        // onEnterTransitionDidFinish里面往scene中addChild
        // 导致children容器扩容,for循环的child指针失效
        // 然后下一轮循环再到这里,child(无效指针)->onEnterTransitionDidFinish()就会闪退
        child->onEnterTransitionDidFinish();  
    }  
}

else

那么是不是就不能在onEnterTransitionDidFinish中添加结点呢?

在这个项目,许多页面都在onEnterTransitionDidFinishHomeScene添加GuideLayer;或者在往前页面添加结点,为啥运行了这么久都没发现问题?

查看代码可以得到,C++onEnterTransitionDidFinish是在遍历完子节点后, 再发送消息调用Lua中的onEnterTransitionDidFinish的:

for( const auto &child: _children)
    child->onEnterTransitionDidFinish();
...
// 调用当前结点 Lua 的 onEnterTransitionDidFinish
ScriptEngineManager::sendNodeEventToLua(this, kNodeOnEnterTransitionDidFinish);

而项目中的页面管理是这样做的:进入游戏创建HomeScene,之后其他页面跳转均在HomeScene上删除和添加,几乎不会创建新 Scene 。

所以后面不会再遇到HomeScene::onEnterTransitionDidFinish遍历时添加子结点的情况;而直接往当前页面添加结点,也是在当前页面的_childre遍历完后再添加的(Lua 中添加)。