【内存泄露】奇怪的内存泄露,楼主已经崩溃!!求助各大神!

首先介绍下,这篇帖子是有关app内存持续增加的问题,并且不是简单的内存泄露。。。楼主已崩溃,项目上线在即,这个问题解不了楼主就要被拿去祭天了。。。论坛大神多,只能拜托各位了,帖子有点长,先谢谢了

最近随着游戏里的内容越来越多,频发内存崩溃问题。。。 游戏中的现象如下:如果只打某几个固定的副本关卡,则内存上限是基本可以维持在一个固定值的;但如果是打竞技场,则内存越打越大。。直到崩溃。后来分析下来两者的区别如下:副本里的敌人spine是固定的,而竞技场里的敌人每次都是新的,每次有新的spine载入,则内存上限会被撑大!因此做了以下测试

测试引擎:cocos2djs 3.16
(从3.14.1 ~ 3.16 都会有这个问题,之前的应该也有,楼主没测)

测试代码:

启动时内存:

测试一

新启动app,先加载好贴图,记录当前内存:28.5mb
每次创建一个spine并记录:
第1次创建并删除,内存增加3.5mb;
第2次创建并删除,内存增加1.1mb;
第3次创建并删除,内存增加0.5mb;
第4次创建并删除,内存增加0.4mb;
第5次创建并删除,内存增加0mb;
第6次创建并删除,内存增加0mb;
第7次创建并删除,内存增加0.2mb;
第8次创建并删除,内存增加0.1mb;
第9次创建并删除,内存增加0mb;
第10次创建并删除,内存增加0.1mb;
结束测试, 此时内存:34.4mb

共计增加了 6mb左右
可见在首次创建spine时内存在时增加了最多,后面增长逐渐放缓,但还是会涨

测试二

新启动app,先加载好贴图,记录当前内存
同时创建10个并再删除,内存增加了11mb :sweat_smile:
同时创建1个并删除,内存不变;
同时创建2个并删除,内存不变;
同时创建3个并删除,内存增加0.1mb;
同时创建4个并删除,内存不变;
同时创建7个并删除,内存不变;
同时创建10个并删除,内存增加0.3mb;
同时创建11个并删除,内存增加0.5mb;
同时创建12个并删除,内存增加0.5mb;

以上可见:
1、同时创建了N次再删除后,之后的操作只要同时创建的数量低于N,则内存几乎不变;当同时创建数量超过N后,内存每次都会有明显增加。
2、创建1个spine再删除,重复执行N次,比同时创建N个spine再删除所增加的内存要小

那会不会是spine有内存泄露呢?

测试三

使用xcode自带的leaks检测内存泄露,使用同样的操作,结果如下图


未发现和以上相关的内存泄露(只有一些js和c++交互的泄露,很小,可以无视),通过内存曲线可以看到前后内存正常

会不会是有内存泄露而leaks查不出来?

测试四

将SkeletonRenderer.cpp 中的析构方法里全注释,spine不做任何析构,故意造成内存泄露。测试结果如下:
第1次创建并删除,内存增加4mb;
第2次创建并删除,内存增加1mb;
第3次创建并删除,内存增加1.2mb;
第4次创建并删除,内存增加0.6mb;
第5次创建并删除,内存增加0.9mb;
第6次创建并删除,内存增加1.5mb;
第7次创建并删除,内存增加0.9mb;
第8次创建并删除,内存增加1mb;
第9次创建并删除,内存增加1.1mb;
第10次创建并删除,内存增加0.8mb;


很明显,不调用析构的情况下,首次依然增加4mb,后面每次平均都会稳定增加1mb,并且此时用leaks是可以查到内存泄露的
因此结论是:原spine并没有内存泄露

产生内存的代码

跟踪了spine创建时的代码,发现在app第一次创建spine执行到以下代码时,内存加了3.5~4mb,可以确定多出来的内存就是这里产生的

## but!
后来发现这不仅仅是spine的问题,其他地方也会发生类似的问题!
我修改了FileUtils里的一行代码,将 getPathForFilename 强制调用了100次循环,按理来说不会有内存问题啊,结果。。。


正常情况下,首次启动+载入贴图,内存28.5mb;
强制循环100次,首次启动+载入贴图,内存31.1mb;
强制循环1000次,首次启动+载入贴图,内存56.7mb;

如果说spine是他们第三方自己的问题,那上面的这个循环产生的额外常住内存该如何解释?如何处理。。?

总结现象

1、spine首次创建并删除,会遗留3~4mb内存放不掉,之后创建增加的速度会放缓,但还会持续增加;
2、创建一个再删除,重复10次,共遗留6mb内存;但同时创建10个spine再删除,会遗留11mb内存。。。这两种操作都是总计创建了10个spine,但内存结果却完全不一样:innocent:
3、如果有多个spine (A、B、C、D),每个独立单位首次创建都会多3~4mb放不掉;
4、遗留的内存大小和json文件的复杂度有关,简单的spine并不会遗留太大,使用二进制创建会小一些;
5、cocos2dx在各种地方都会出现以上莫名的内存增加问题,亲测cocos2djs 创建一万个精灵再删除,内存会多3mb左右,如果在FileUtils里把上面的额外循环加上,会大更多:sweat_smile:
6、使用cc.game.restart()也找不回丢失的内存

。。。wtf ?? 内存去哪了??

从未想过会遇到这样的问题,一定是楼主太年轻。。。求论坛各位大神帮忙分析,该如何解决这些莫名的内存增大。。项目上线在即,楼主不想被祭天:joy:
谢谢各位!

附上测试代码:cocos2djs_316_内存测试.zip (1.9 MB)
cocos new testMemory -l js 创建工程,把附件里的文件覆盖进项目即可

1赞

用二进制的SPINE数据格式,
然后再每次用完一个SPINE,释放的时候 自己手动去释放每个材质图片,已经材质图片可能会生成的spriteFrame
可能会有点用。。。

我也遇到同样问题,大虾赶紧出来秀一波

试过了,没效果… :joy:

看你的测试,有没有做析构都增加一样的内存,不是说明析构没有释放掉需要释放的内存?或者析构没有被调用到?

看还有没有多余引用没有移除,从c++过来的习惯就是,一个new对应一个delete,光new不delete的都是流氓。js里面就不知道了,之前new的对象,不用的时候我手动=null,也不知道有效没

析构肯定是调用到了, 不析构的情况下每次都会稳定增加1mb左右; 析构的情况下泄露的会明显小很多。
在leaks里观察内存前后是正常的, 申请的内存都释放了… 但实际app内存确实增加了, leaks里看不到

试过了,无效,测试后手动调用 cc.sys.garbageCollect() 也一样, 只能回收1mb不到的js内存, 泄露肯定发生在c++层:joy:

我们也遇到同样问题了,坐等引擎组解决。

求引擎组的大神们关注一下啊…

顶一下

建议先创建好N个放到缓存里不释放做成对象池

getPathForFilename循环调用100次会导致Objective-c中autorelease pool内的对象增加,需要到系统事件循环结束后才会被系统清理。原因是getPathForFilename会调用getFullPathForDirectoryAndFilename

std::string FileUtilsApple::getFullPathForDirectoryAndFilename(const std::string& directory, const std::string& filename) const
{
    if (directory[0] != '/')
    {
        NSString* dirStr = [NSString stringWithUTF8String:directory.c_str()]; // 创建临时autorelease对象
        // The following logic is used for remove the "../" in the directory
        // Because method "pathForResource" will return nil if the directory contains "../".
        auto theIdx = directory.find("..");
        if (theIdx != std::string::npos && theIdx > 0) {
            NSMutableArray<NSString *>* pathComps = [NSMutableArray arrayWithArray:[dirStr pathComponents]]; // 创建临时Array对象
            NSUInteger idx = [pathComps indexOfObject:@".."];
            while (idx != NSNotFound && idx > 0) {          // if found ".." & it's not at the beginning of the string
                [pathComps removeObjectAtIndex: idx];       // remove the item ".."
                [pathComps removeObjectAtIndex: idx - 1];   // remove the item before ".."
                idx = [pathComps indexOfObject:@".."];      // find ".." again
            }
            dirStr = [NSString pathWithComponents:pathComps];
        }

        NSString* fullpath = [pimpl_->getBundle() pathForResource:[NSString stringWithUTF8String:filename.c_str()]
                                                             ofType:nil
                                                        inDirectory:dirStr]; // 创建临时NSString对象
        if (fullpath != nil) {
            return [fullpath UTF8String];
        }
    }
    else
    {
        std::string fullPath = directory+filename;
        // Search path is an absolute path.
        if ([s_fileManager fileExistsAtPath:[NSString stringWithUTF8String:fullPath.c_str()]]) {
            return fullPath;
        }
    }
    return "";
}

是的,如果引擎改为ARC就不会产生临时的autorelease对象了。或者直接用 initWithXXX接口,确保不用的时候release即可。但是这很容易出错。毕竟看这个逻辑,有好几个分支去return。

所以楼主不用纠结getPathForFilename的内存问题,因为不是泄露。

好的! 感谢回复!

楼主是怎么处理这种情况的呢?

spine的问题有什么解决方案吗??? @jare