项目中有一个tmx文件,16M,加载这个tiledmap的时候,非常慢非常慢,在VisualStudio中通过性能探查器,进行了一波分析:
从饼状图中可以看到性能压力集中在文件IO上,追查到性能热点代码,是在解析csv数据:
unsigned char *buffer; TMXLayerInfo* layer = tmxMapInfo->getLayers().back(); tmxMapInfo->setStoringCharacters(false); std::string currentString = tmxMapInfo->getCurrentString(); vector<string> gidTokens; istringstream filestr(currentString); string sRow; // 因为地图是1000*1000,好在csv数据只有一行,所以这里只会执行一次 while(getline(filestr, sRow, '\n')) { string sGID; istringstream rowstr(sRow); // 但是因为csv是通过,分割,所以间接导致gidTokens会非常的大 // 性能热点函数getline while (getline(rowstr, sGID, ',')) { gidTokens.push_back(sGID); } } // 32-bits per gid buffer = (unsigned char*)malloc(gidTokens.size() * 4); if (!buffer) { CCLOG("cocos2d: TiledMap: CSV buffer not allocated."); return; } uint32_t* bufferPtr = reinterpret_cast<uint32_t*>(buffer); // 这里是百万次以上循环,有一定的性能压力 for(auto gidToken : gidTokens) { auto tileGid = (uint32_t)strtoul(gidToken.c_str(), nullptr, 10); *bufferPtr = tileGid; bufferPtr++; } layer->_tiles = reinterpret_cast<uint32_t*>(buffer); tmxMapInfo->setCurrentString("");
getline
是 C++ 标准库中的一个函数,其作用是从输入流中读取一行数据并存储到指定的字符串对象中。
std::istream& getline(std::istream& is, std::string& str, char delim);
其中,is
表示输入流对象,str
表示要存储读取结果的字符串对象,delim
表示分隔符(默认为换行符 \n
)。当遇到分隔符时,getline
函数会停止读取并返回流对象。该函数会忽略分隔符本身,并将其从输入流缓冲区中删除。
getline
函数在输入流中读取一行数据时,需要逐个字符地读取,并检查每个字符是否为分隔符。由于字符的读取和检查是严格按照顺序进行的,因此这种方法会导致在处理大量数据时的性能问题。
不同encoding对比
encoding | 第1次/第2次 | size |
csv | 21812ms/21508ms | 16.2M |
base64 | 1670ms /610ms | 42.1M |
zlib | 1183ms/105ms | 471K |
gzip | 1189ms/130ms | 472k |
以上是测试超大tiledmap地图的加载性能报告,观察到一个非常有趣的现象,zlib的第1次加载和第2次加载耗费的时间差距有点大,初步猜测是因为加载纹理导致的
- 第1次加载时,可以看到在addImage上花费了大量时间
- 第2次加载时,在xml解析上花费了大量时间:
这个结果是符合我们的猜测,所以如果要加快tmx的创建,可以预加载tmx所使用的纹理图片。
结论
超大地图,尽量保存采用zlib、gzip的方式保存数据,会降低IO操作,提高tiledmap的性能,同时减少tmx文件大小,理论上这个问题在cocos creator中也会遇到。