内容简介:转发请注明来源:问题描述:在 App 中正常浏览一段时间后,某些文本行间距失效。问题一旦出现,新打开的页面也会有这个问题,必须通过杀掉进程才能恢复。
转发请注明来源: github.com/tuesda/blog…
问题描述:
在 App 中正常浏览一段时间后,某些文本行间距失效。问题一旦出现,新打开的页面也会有这个问题,必须通过杀掉进程才能恢复。
问题分析:
初步分析:
根据现象大胆猜测是触发了特定条件导致某个静态对象状态改变,否则不会杀掉进程才能恢复。
具体分析:
要找行间距失效的原因,我选择 TextView 的 draw() 方法作为切入点,因为这里是将字符展示在屏幕上的最后一步,肯定是有问题的。看代码可知 Layout.draw() 方法负责字符绘制,进入 Layout 类具体绘制方法是 drawText()
public void drawText() { ... for() { // 遍历每一行 ... int ltop = previousLineBottom; int lbottom = getLineTop(lineNum + 1); previousLineBottom = lbottom; int lbaseline = lbottom - getLineDescent(lineNum); ... } ... } 复制代码
由这块代码可以发现行间距是包含在 getLineDescent(lineNum) 中的,换句话说行间距属于上一行。这个结论也可以根据 TextView 的 getLineHeight() 方法得出:
public int getLineHeight() { return FastMath.round(mTextPaint.getFontMetricsInt(null) * mSpacingMult + mSpacingAdd); } 复制代码
所以行间距失效的 TextView 中,mLayout 的 getLineDescent() 方法肯定有问题。Layout 类是个抽象类,负责 TextView 的文字布局, 实现类有 StaticLayout 和 DynamicLayout,具体使用哪个取决于 text 是否为 Spannable,如果是 Spannable 使用 DynamicLayout 否则是 StaticLayout。通过添加调试代码发现出现问题的 TextView 都是用的 DynamicLayout 来布局,所以继续看看 DynamicLayout 的 getLineDescent() 是否正常。
DynamicLayout 中代码如下:
@Override public int getLineDescent(int line) { return mInts.getValue(line, DESCENT); } 复制代码
可以看出 lineDescent 是存在 mInts 这个 int 数组中,继续找计算的代码如下:
int desc = reflowed.getLineDescent(i); if (i == n - 1) desc += botpad; ints[DESCENT] = desc; 复制代码
上面代码最后一行将计算的 descent 数值存在 ints 这个 int 数组中,可以看到 DynamicLayout 的 lineDescent 计算是委托给 reflowed 这个对象。这个 reflowed 是 StaticLayout 对象,继续看它的 getLineDescent() 方法。
@Override public int getLineDescent(int line) { return mLines[mColumns * line + DESCENT]; } 复制代码
和 DynamicLayout 的 getLineDescent() 类似,lineDescent 值是存在一个 int 数组中,继续查找计算 lineDescent 的代码
... boolean lastLine = mEllipsized || (end == bufEnd); if (firstLine) { if (trackPad) { mTopPadding = top - above; } if (includePad) { above = top; } } int extra; if (lastLine) { if (trackPad) { mBottomPadding = bottom - below; } if (includePad) { below = bottom; } } if (needMultiply && !lastLine) { double ex = (below - above) * (spacingmult - 1) + spacingadd; if (ex >= 0) { extra = (int)(ex + EXTRA_ROUNDING); } else { extra = -(int)(-ex + EXTRA_ROUNDING); } } else { extra = 0; } lines[off + START] = start; lines[off + TOP] = v; lines[off + DESCENT] = below + extra; ... 复制代码
上面代码中的 extra 应该就是行间距的值,通过调试发现问题出在 lastLine 上,即使不是最后一行 lastLine 也为 true。lastLine 的赋值如下:
boolean lastLine = mEllipsized || (end == bufEnd); 复制代码
end == bufEnd 不总为 true, 而 mEllipsized 总是 true。通过在 StaticLayout.java 文件中查找 "mEllipsized =" 发现赋值代码只有一处在 calculateEllipsis() 方法中,这个方法负责 TextView 省略号的显示逻辑,当需要显示时 mEllipsized 被设置为 true,但是 StaticLayout 中却没有将 mEllipsized 设置为 false 的地方。这样的逻辑在单次布局计算时是没有问题的,但当 StaticLayout 对象被复用时就会出错,因为 mEllipsized 没有被恢复 false 的逻辑。再回到 DynamicLayout 类中可以看到用来计算 lineDescent 的 StaticLayout 对象正是复用的,代码如下:
// generate new layout for affected text StaticLayout reflowed; StaticLayout.Builder b; synchronized (sLock) { reflowed = sStaticLayout; b = sBuilder; sStaticLayout = null; sBuilder = null; } if (reflowed == null) { reflowed = new StaticLayout(null); b = StaticLayout.Builder.obtain(text, where, where + after, getPaint(), getWidth()); } 复制代码
综上所述,这个问题是 Android 官方代码 StaticLayout 的 bug。引入这个 bug 的版本是 sdk 26,在 sdk 27 的源码里增加了将 mEllipsized 重置为 false 的代码,这也正说明了 sdk 26 里的实现确实是个 bug。
找到问题后,规避这个 bug 很简单,只要将 DynamicLayout 中的静态变量 reflowed 设置为空,每次用到时候创建新的 StaticLayout 对象就可以了。代码如下:
@Nullable private Pair<Integer, Reflect> layoutReflect; @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); Layout layout = getLayout(); if (layout != null) { // Issue: 全局性行间距失效 // Reason: Sdk 26 DynamicLayout 的 sStaticLayout 成员变量中的 mEllipsized 一旦设为 true 无法恢复 if (SdkChecker.eq(26) && layout instanceof DynamicLayout) { nullRecycledStaticLayout((DynamicLayout) layout); } } } private void nullRecycledStaticLayout(DynamicLayout dl) { try { final int dlHash = dl.hashCode(); Reflect dlReflect; if (layoutReflect != null && layoutReflect.first != null && layoutReflect.second != null && layoutReflect.first == dlHash) { dlReflect = layoutReflect.second; } else { dlReflect = Reflect.on(dl); layoutReflect = new Pair<>(dlHash, dlReflect); } dlReflect.set("sStaticLayout", null); } catch (Exception e) { JLog.e(e); } } 复制代码
这里用到了反射来将 sStaticLayout 设置为空,这里用到的反射类库是 JOOR 。因为用到反射,混淆文件也要加上下面这行,防止被混淆。
-keep public class android.text.DynamicLayout { <fields>; } 复制代码
以上所述就是小编给大家介绍的《行间距失效问题》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- iOS UILabel添加行间距、字间距
- 前端实现设置缓存数据定时失效
- 5分钟探究Spring事务失效原因
- Andorid内Aspectj切面失效分析
- Xcode 代码提示失效以及引发的感想
- Firefox 已发布“插件失效”证书修复补丁
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
JSON 在线解析
在线 JSON 格式化工具
HEX CMYK 转换工具
HEX CMYK 互转工具