公司引擎版本是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的对比了一下,发现已经被修正了。。。。。