事情是这样的,我在V2.4.0的更新说明上看到:
原生平台上使用 FreeType 渲染 Label: 提升 Label 初始化速度,Android 提升 5 倍左右,iOS 提升 3 倍
原生平台上Label提升了这么多性能,不免让人很期待,于是我作了一个小Demo,界面如下:

4个按钮分别实例化4个Prefab,Prefab里面有三十多个Label,每个Prefab对Label设置不同的CacheMode:
- 按钮1:使用系统字体,CacheMode为Char。
- 按钮2:使用系统字体,CacheMode为None。
- 按钮3:使用一个14M的TTF字体,CacheMode为Char。
- 按钮4:使用一个14M的TTF字体,CacheMode为None。
其中只有按钮3会用到原生的Label优化,Prefab实例化为结点之后,点击结点会Destroy掉,然后点按钮可以重复实例化。
Prefab的内容如下:
我将Demo编译成APK后,运行起来看效果,本以为Prefab3会有很惊喜的性能表现,结果却让人遗憾:
- 按钮1:第1次实例化为666ms,以后实例化为75ms。诡异的是和按钮2相比,显示效果有点虚。
- 按钮2:第1次实例化为208ms,以后实例化为177ms。
- 按钮3:第1次实例化为679ms,以后实例化为193ms;诡异的是每实例化几次,耗时就会回到600多毫秒,如此重复。
- 按钮4:第1次实例化为190ms,以后实例化为183ms。
说好的提升5倍呢?实际结果却比None还要差。例子都是很简单的,不会有使用姿势不对的问题。
带着这个疑问我去翻看了C++层的源代码,位置在resources\cocos2d-x\cocos\2d里面。好家伙,不看不知道,一看发现了好几个问题。我把问题列在这里,引擎同学们好好看看,这就是一个游戏引擎该有的代码质量吗?
问题1:类名拼写错误
TTFLabelAtals,这个不用多说,修正它就是。
问题2:TTFLabelAtals重复调用init函数。
TTFLabelAtals在构造函数中会调用init:
TTFLabelAtals::TTFLabelAtals(const std::string &fontPath, float fontSize, LabelLayoutInfo *info)
:_fontName(fontPath), _fontSize(fontSize), _info(info)
{
init();
}
然后在TTFLabelAtlasCache::load中创建完TTFLabelAtals之后,又主动调用了一次init函数:
std::shared_ptr<TTFLabelAtals> TTFLabelAtlasCache::load(const std::string &font, float fontSizeF, LabelLayoutInfo *info)
{
... ...
if (!atlas)
{
atlas = std::make_shared<TTFLabelAtals>(font, fontSize, info); // 构造函数调用init
if(!atlas->init()) // 主动调用init
{
return nullptr;
}
atlasWeak = atlas;
}
#else
...
#endif
return atlas;
}
如果init函数没做啥也就算了,问题是init里面会创建_ttfFont和_fontAtlas,而_ttfFont会调用loadFont,而loadFont会从文件加载上面说的14M的字体文件。可见init函数是一个很重的函数,每调用一次都是一次不小的性能开销。
上面重复调用init,会使_ttffont和_fontAtlas创建出来又被释放,然后又创建一次。代码里用的是share_ptr,把这个细节给隐藏掉了。
修正方法应该是构造函数中调用init去掉。
问题3:LabelRenderer重复调用genStringLayout
在LabelRenderer::render会调用一次genStringLayout:
void LabelRenderer::render()
{
std::string text = getString();
std::string fontPath = getFontPath();
if (!_effect || text.empty() || fontPath.empty()) return;
genStringLayout(); // 第1次调用,会创建_stringLayout
renderIfChange();
}
renderIfChange中又调用一次genStringLayout:
void LabelRenderer::renderIfChange()
{
if (!_stringLayout) return;
if (_cfg->updateFlags & UPDATE_FONT || _cfg->updateFlags & UPDATE_EFFECT)
{
// Label创建之后updateFlags应该是0xFFFFFFFF,所以这里第一次一定会调用进来。
_stringLayout.reset(); // 然后就释放_stringLayout
genStringLayout(); // 又调用一次
doRender();
}
... ...
_cfg->updateFlags = 0;
}
genStringLayout又是一个很重型的函数,里面又会去调用TTFLabelAtals::init函数,所以第1次创建一个带TTF的Char模式的Label时,你知道做了多少重复的事情吗?
问题3:CC_TTF_LABELATLAS_ENABLE_GC宏
这个宏会使TTFLabelAtlasCache用std::weak_ptr去缓存TTFLabelAtals,当我实例化Prefab又将其Destroy之后,TTFLabelAtals会在Label GC时自动从缓存中去掉。然后我再次实例化Prefab,它又会重新走加载字体文件,创建FreeType库等流程。
这就是为什么:每实例化几次,耗时就会回到600多毫秒,如此重复。
一般一个游戏用到的字体样式是不会很多的,完全不用担心缓存过多TTFLabelAtals的,所以是建议把这个宏去掉,或默认改为0。
问题4:TTFLabelAtals中_ttfFont的问题
从代码上看,一个TTFLabelAtals代表一个字体样式:字体名,字体大小,描边等。
TTFLabelAtals保存着一个_ttfFont成员(FontFreeType),它在创建时会初始FreeType库,再加载字体文件,然后创建一个Face出来。问题在于每种不同的样式,都要重复创建FreeType库和加载字体文件,如果字体文件很大,这也会是一个性能开销。
我认为这也是值得思考怎么优化的一个点。
问题5:LabelRenderer的getString和getFontPath
这涉及到更细致的性能支节了,但我认为既然要把代码搬到C++层,那应该对C++的内存管理有深入的认识。
getString和getFontPath都是从JS层取到字符串,然后从函数返回。这个过程会涉及到多少次内存的分配和释放?而在每次Render调用时都会调用好几次这些函数。
那么是不是应该优化一下,因为通过updateFlags就可以知道字体或内容的变化,那预先从JS层取出缓存起来,等到updateFlags提示有变化时再重新去取,这样就避免每次Render都从JS取值的这个过程。
再退一步,因为某些原因不能做缓存,那么从getFontPath取出来值之后,应该通过函数传参的方式传递,而不应该每个函数重新调用getFontPath等。具体的函数是:render, renderIfChange, genStringLayout。
虽然上面这些问题没法解释为何性能没超过None模式,但举一反三,仔细检查FontAtlas等类,一定能找出问题所在。
说好了要受喷,其实我还是很温柔的,希望写C++层的代码要多多斟酌,毕竟要驾驭好C++,写出真正性能高超的代码并不是那么容易的,特别是用了很多C++11的特性之后,把内存管理的细节隐藏得死死的,内存泄露是解决了,但性能问题也可能更严重。相对来说这一点,引擎的JS层代码要让人放心一些。
我把Demo工程上传了,希望引擎同学们好好检查一下,另外没有带TTF的Char模式和None模式的显示效果不一样,Char模式显示出来是糊的,这个也检查一下吧。下面是两张效果图:
没有带TTF的Char模式
没有带TTF的None模式



。我们在js层做再多骚操作,终究还是比不上c++层的修复

