公司引擎版本是2.1.4,这个问题在2.2.3上已经修复,其他版本不详
之前解决了tab的那个问题之后,又来一个问题,还是label相关的。
表现为,在完成任务之后,会弹出一个奖励提示框,里面会提示获得的奖励,但是有几个任务,一点完成,就会crash。怀疑是内存重复释放之类的问题,最后层层追踪之后,发现和其他的都无关,就是每次调用CCLabelTTF::create()之后就直接crash,悲催的开始跟进。
从 CCLabelTTF::create() -> CCLabelTTF::initWithString() -> CCLabelTTF::setString() -> CCLabelTTF::updateTexture() 跟到 CCTexture。到这里:
bool CCLabelTTF::updateTexture()
{
CCTexture2D *tex;
tex = new CCTexture2D();
if (!tex)
return false;
#if (CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID) || (CC_TARGET_PLATFORM == CC_PLATFORM_IOS)
ccFontDefinition texDef = _prepareTextDefinition(true);
CCLog("%s..........", m_string.c_str());
tex->initWithString( m_string.c_str(), &texDef );
#else
… … … …
#endif
… … … …
return true;
}
这个texDef是封装的一个环境描述类,主要就是写了字体,字号,label长宽高这些,因为如果label只设置宽度不设置高度,引擎会自动处理换行。所以需要这些。然后继续往下:
bool CCTexture2D::initWithString(const char *text, ccFontDefinition *textDefinition)
{
… … … …
CCImage* pImage = new CCImage();
do
{
CC_BREAK_IF(NULL == pImage);
bRet = pImage->initWithStringShadowStroke(text,
(int)textDefinition->m_dimensions.width,
(int)textDefinition->m_dimensions.height,
eAlign,
textDefinition->m_fontName.c_str(),
textDefinition->m_fontSize,
textDefinition->m_fontFillColor.r / 255,
textDefinition->m_fontFillColor.g / 255,
textDefinition->m_fontFillColor.b / 255,
shadowEnabled,
shadowDX,
shadowDY,
shadowOpacity,
shadowBlur,
strokeEnabled,
strokeColorR,
strokeColorG,
strokeColorB,
strokeSize);
CC_BREAK_IF(!bRet);
bRet = initWithImage(pImage);
} while (0);
CC_SAFE_RELEASE(pImage); // crashes here
return bRet;
… … … …
}
这里就诡异了,通过之前那个tab问题的经验,估计又是创建image出错了,但是这次还真不是。通过暴力log流,发现是crash在 release那里。之前的层层判断都能过,那么如果crash在这里,只能说明内存被破坏造成了野指针,然后开始跟initWithStringShadowStroke,发现这个函数就是从java去取一个image而已,调用的是
引擎目录/cocos2dx/platform/android/src
下面的 Cocos2dxBitmap.java 的 initWithStringShadowStroke函数,继续看这个函数:
public static void createTextBitmapShadowStroke() {
//重构字符
pString = Cocos2dxBitmap.refactorString(pString);
//创建画笔
final Paint paint = Cocos2dxBitmap.newPaint(pFontName, pFontSize,
horizontalAlignment);
//计算纹理大小和一些相关的东西
final TextProperty textProperty = Cocos2dxBitmap.computeTextProperty(
pString, pWidth, pHeight, paint);
//确定纹理高度
final int bitmapTotalHeight = (pHeight == 0 ? textProperty.mTotalHeight
: pHeight);
//设置阴影
if (shadow) {
。。。 。。。 。。。 。。。
}
//画字
final Bitmap bitmap = Bitmap.createBitmap(textProperty.mMaxWidth
+ (int) bitmapPaddingX, bitmapTotalHeight
+ (int) bitmapPaddingY, Bitmap.Config.ARGB_8888);
// 设置粗体
if (stroke) {
。。。 。。。 。。。 。。。
}
Cocos2dxBitmap.initNativeObject(bitmap);
}
这个函数太长,精简之后,就是上面这些关键点。先说这个函数的流程,把字符串,根据里面的 \n ,先把他断行,然后如果一行太长,超过label的长度,就把他分成2行显示,然后这些处理好了之后,就可以算出这个image需要的大小,这些都是在计算纹理大小这一步处理的。然后交给android创建bitmap,然后开始往上面写字,把这个图画出来。到这里之后,发现是在计算纹理大小那里奔溃的,继续跟:
private static TextProperty computeTextProperty(final String pString,
final int pWidth, final int pHeight, final Paint pPaint) {
… … … …
final String[] lines = Cocos2dxBitmap.splitString(pString, pWidth,
pHeight, pPaint);
… … … …
return new TextProperty(maxContentWidth, h, lines);
}
这里就会把这个字符串做拆行的具体处理了:
<span style="color:#333333;"> private static String[] splitString(final String pString,
final int pMaxWidth, final int pMaxHeight, final Paint pPaint) {
if (pMaxWidth != 0) {
final LinkedList<String> strList = new LinkedList<String>();
int iIndex=0;
for (final String line : lines) {
/*
* 如果一行的长度大于label宽度,就拆成2行
*/
final int lineWidth = (int) FloatMath.ceil(pPaint
.measureText(line));
if (lineWidth > pMaxWidth) {
strList.addAll(Cocos2dxBitmap.</span><span style="color:#ff0000;">divideStringWithMaxWidth</span><span style="color:#333333;">(
line, pMaxWidth, pPaint));
} else {
strList.add(line);
}
/* Should not exceed the max height. */
if (maxLines > 0 && strList.size() >= maxLines) {
break;
}
iIndex++;
}
/* Remove exceeding lines. */
if (maxLines > 0 && strList.size() > maxLines) {
while (strList.size() > maxLines) {
strList.removeLast();
}
}
ret = new String[strList.size()];
strList.toArray(ret);
} else if (pMaxHeight != 0 && lines.length > maxLines) {
… … … …
} else {
… … … …
}
return ret;
}</span>
上面这个函数,首先是按 \n 把文字拆航,如果label的宽度是0,那么就只按 \n 分行,走else。如果label的高度不为0,也就是说只指定了高度没指定宽度,拆行后的行数大于label指定高度能显示的内容,走 else if , 如果指定了宽度,就走if里面的。然后里面的处理,会判断按 \n 拆行过后的单行字符串,如果长度大于label宽度,就调用divideStringWithMaxWidth 把它分成多行:
private static LinkedList<String> divideStringWithMaxWidth(
final String pString, final int pMaxWidth, final Paint pPaint) {
final int charLength = pString.length();
int start = 0;
int tempWidth = 0;
final LinkedList<String> strList = new LinkedList<String>();
//遍历从start到i的字符串的显示长度
for (int i = 1; i <= charLength; ++i)
{
tempWidth = (int) FloatMath.ceil(pPaint.measureText(pString, start,
i));
//如果长度大于或等于label宽度,拆行
if (tempWidth >= pMaxWidth)
{
//找空格最后一次出现的位置
final int lastIndexOfSpace = pString.substring(0, i)
.lastIndexOf(" ");
//如果找到了空格(字符串有可能不包含空格),并且空格不是第一个字符,则截取start到空格之前的
if (lastIndexOfSpace != -1 && lastIndexOfSpace > start)
{
strList.add(pString.substring(start, lastIndexOfSpace));
i = lastIndexOfSpace + 1; // skip space
}
//没找到空格,直接按字符个数拆分
else
{
if (tempWidth > pMaxWidth)
{
strList.add(pString.substring(start, i - 1));
--i;
}
else
{
strList.add(pString.substring(start, i));
}
}
//**************如果拆行过后下一行的开头有空格,则去掉空格**************
if (pString.length() > 0 && i < pString.length()) {
while (pString.charAt(i) == ' ') {
++i;
}
}
start = i;
}
}
if (start < charLength) {
strList.add(pString.substring(start));
}
return strList;
}
请注意上面我怒写了很多星号的地方。
注意里面那个while,有没有写错啊,有木有啊。
++i 之后 i 有木有可能越界啊~~有木有啊~~!!
做如下修改后万事大吉:
while (i<charLength&&pString.charAt(i) == ' ') {
++i;
}
这个bug要出现,几率还是有限,条件还比较苛刻,要断行之后刚好多出一个空格才行。
PS:改完之后觉得自己发现一个牛B bug,准备去论坛提交,突然想起这个引擎版本很落后,找了2.2.3的对比了一下,发现已经被修正了。。。。。