审查时间:2026-07-27
审查范围:xwidget/src/main/java/com/pichs/xwidget/全部 150 个源文件
所有问题均已通过阅读源码验证,非推测
文件: utils/XRoundBackgroundHelper.java:1701
// 当前代码(错误)
if (this.checkedCubeFrontHeight == left // 应该是 == front
&& checkedCubeBackHeight == back
&& checkedCubeRightHeight == right
&& checkedCubeLeftHeight == left) {
return;
}checkedCubeFrontHeight 与参数 left 做比较,应与 front 比较。
后果:当只有 front 发生变化时,如果初始 front == left,提前 return,背景不更新。
修复:
if (this.checkedCubeFrontHeight == front
&& checkedCubeBackHeight == back
&& checkedCubeRightHeight == right
&& checkedCubeLeftHeight == left) {
return;
}文件: utils/XAlphaHelper.java:208-210
public void setScaleOnDisabled(float scaleRate) {
this.mDisabledScale = scaleRate;
isChangeScaleOnDisable = mDisabledScale != scaleRate; // 刚赋值,永远 false
}mDisabledScale 刚被赋值为 scaleRate,两者必然相等,isChangeScaleOnDisable 始终为 false,disabled 缩放效果永远不生效。
修复:
isChangeScaleOnDisable = mNormalScale != scaleRate;文件: utils/XDisplayHelper.java:132-134
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
return getScreenWidth(context); // 错误:应该返回 getScreenHeight
}API < 23 时直接返回了屏幕宽度,方法名和注释都是"物理高"。
文件: edittext/XVerificationCodeEditText.java:501
for (int i = 0; i < maxLength; i++) {
// i 的范围是 [0, maxLength-1],i == maxLength 永不为 true
if (i == 0 || i == maxLength) {
continue;
}
...
}i == maxLength 在 i < maxLength 的循环中永远不成立,意图可能是 i == maxLength - 1(跳过最后一格的分割线)。
文件: utils/XRoundBackgroundHelper.java:410-430
@SuppressLint("DrawAllocation") // 用注解压制了警告,但问题依然存在
public void onDraw(Canvas canvas) {
float[] radii;
if (...) {
radii = new float[]{...}; // 每次 draw 都 new
} else {
radii = new float[]{...}; // 每次 draw 都 new
}
mClipPath.addRoundRect(0f, 0f, mWidth, mHeight, radii, Path.Direction.CW);
}float[8] 在每帧绘制时分配,对频繁重绘的 ImageView 会造成持续 GC 压力。
修复: 将 float[] mRadii = new float[8] 作为成员变量,只在圆角值变化时更新。
文件: edittext/XVerificationCodeEditText.java:353-365
case 1: // 每 50ms 触发一次
if (isLoading) {
borderPaint = new Paint(); // 每帧 new Paint
borderLoadingPaint = new Paint(); // 每帧 new Paint
...
}加载动画每 50ms 触发一次,每次 new Paint() × 2,1 秒内创建 40 个对象。
此外 onTextChanged() 中也每次 new Paint()(第 519 行)。
修复: 直接调用 paint.setAlpha(alpha) 和 paint.setColor(color) 更新已有对象,不要 new。
文件: utils/XWidgetCache.java:61,71
editor.commit(); // 阻塞调用setString 和 setInt 都用 commit(),字体设置路径(如 XTypefaceHelper.setGlobalTypefaceFromAssets)会在主线程触发磁盘 I/O。
修复: 改为 editor.apply(),如需立即写完才 commit()。
文件: utils/XDisplayHelper.java:180-210
每次调用都走多轮反射(getRawWidth/getRawHeight → getRealSize),且无缓存。
API 31+ 应改用 WindowMetrics(context.getSystemService(WindowManager.class).getCurrentWindowMetrics())。
文件: webview/XWebView.java:32-58
settings.setJavaScriptEnabled(true);
settings.setMixedContentMode(MIXED_CONTENT_ALWAYS_ALLOW);两个高危配置默认开启,且构造函数不提供关闭入口。
库使用者如果直接用 XWebView 加载内部页面,可能被 XSS 攻击或中间人劫持(HTTP 资源注入到 HTTPS 页)。
另外 SINGLE_COLUMN 布局算法在 API 23 已废弃。
建议:
- 提供
setJavaScriptEnabled(false)的可配置入口 MIXED_CONTENT_ALWAYS_ALLOW改为MIXED_CONTENT_NEVER_ALLOW作为默认值- 替换废弃的
SINGLE_COLUMN布局
文件: edittext/XVerificationCodeEditText.java:297-305, 536-545
// onAttachedToWindow
timer.scheduleAtFixedRate(timerTask, 0, cursorDuration);
// onDetachedFromWindow
timer.cancel(); // cancel 后 Timer 和 TimerTask 均不可复用Timer.cancel() 后该 Timer 实例报废,TimerTask 也不能重新提交。
View 从 RecyclerView 或 Fragment 中 detach 后再 attach,光标闪烁永久失效。
修复: 在 onDetachedFromWindow 中停止,在 onAttachedToWindow 中重新创建:
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
timerTask = new TimerTask() { ... };
timer = new Timer();
timer.scheduleAtFixedRate(timerTask, 0, cursorDuration);
}或直接用 postDelayed 替代 Timer(更简洁,天然在主线程执行)。
文件: edittext/XVerificationCodeEditText.java:569
canvas.drawText(String.valueOf(charSequence.charAt(i)), baseX, baseY, textPaint);String.valueOf(char) 每次 draw 都创建新 String 对象。
修复: 使用 char[] buffer:
private final char[] mCharBuffer = new char[1];
// drawText 时:
mCharBuffer[0] = charSequence.charAt(i);
canvas.drawText(mCharBuffer, 0, 1, baseX, baseY, textPaint);文件: utils/XTypefaceHelper.java
mTypefaceMap是WeakHashMap,非线程安全,但post()和observer()方法没有加锁,多线程访问时会抛ConcurrentModificationExceptionisOpenTypeface是public static字段,可被外部直接赋值绕过init()逻辑setGlobalTypefaceStyle()会覆盖之前设置的 typeface:mGlobalTypeface = Typeface.create(mGlobalTypeface, style),若mGlobalTypeface == null,相当于重置字体
每个 set*() 方法都独立重建所有状态的背景 Drawable,5 个状态 × N 个 setter = 数百行重复逻辑。
setRadius()、setBorderWidth()、setCubeSidesBorderColor() 等方法内都有 100-200 行几乎一样的代码块。
建议: 提取 rebuildAllBackgrounds() 方法,每个 setter 只更新对应字段,然后调用统一的重建方法。
文件: wheel/XWheelView.java:356
@NonNull
public abstract String getItem(int position);WheelView 只能显示字符串,不支持自定义数据类型绑定。
使用泛型 Adapter<T> 会更灵活,或至少加一个 getDisplayText(int position) 分离数据与显示逻辑。
文件: wheel/XWheelView.java:279
public void setCurrentItem(int position) {
mLayoutManager.scrollToPositionWithOffset(position, 0); // 直接跳转
}缺少 smoothScrollToPosition 的重载,用户无法控制初始化时是否需要动画。
文件: utils/XBackgroundHelper.java:28
public static final int DEFAULT_COLOR_TRANSPARENT = 0x0000000f;用一个 alpha=0、blue=15 的"几乎透明"颜色作为"未设置"的标志值。
若用户真的想把背景色设为 0x0000000f,会被误判为"未设置",背景显示异常。
更健壮的方案是用 Integer 可为 null,或独立的 boolean 标志位。
文件: utils/XColorUtils.java:25-32
int[] colors = new int[]{
tintColor - 0xAA000000, // 危险:整数溢出
tintColor - 0x99000000,
tintColor | 0xFF000000, // 强制 alpha = 0xFF,语义正确但不明显
...
};tintColor - 0xAA000000 的意图是"降低 alpha 值",但这是整数减法,当 tintColor 的 alpha 字节小于 0xAA 时(如传入半透明颜色),会发生下溢,导致 alpha、R 通道同时被破坏。
修复:
Color.argb(0x55, Color.red(tintColor), Color.green(tintColor), Color.blue(tintColor))文件: shinebutton/ShineView.java:79, 99
ValueAnimator.setFrameDelay(FRAME_REFRESH_DELAY); // 25ms,静态方法!ValueAnimator.setFrameDelay() 是静态方法,会影响整个 App 所有 ValueAnimator 的帧率(默认 ~16ms → 25ms)。
每次创建 ShineView 时都被调用,即使动画结束后也不会恢复,导致 App 内所有动画被永久降速。
修复: 删除此调用,或在动画结束后恢复默认值:
mShineAnimator.addListener(new AnimatorListenerAdapter() {
public void onAnimationEnd(Animator a) {
ValueAnimator.setFrameDelay(10); // 恢复默认
...
}
});文件: shinebutton/ShineView.java:214
private Paint getConfigPaint(Paint paint) {
if (mEnableFlashing) {
paint.setColor(mFlashingColors.get(mRandom.nextInt(mColorCount - 1)));
}
return paint;
}mColorCount 来自 mFlashingColors.size(),nextInt(mColorCount - 1) 当 mColorCount == 1 时调用 nextInt(0),抛出 IllegalArgumentException。
此外 mColorCount - 1 作为 bound 意味着永远不会取到最后一个颜色(off-by-one)。
修复: mRandom.nextInt(mColorCount) 且 mColorCount > 0 时才调用。
文件: shinebutton/ShineButton.java:111-113
public void fitFragment(Fragment fragment) {
initWindow(fragment.getActivity()); // getActivity() 可能为 null
}Fragment 未 attach 到 Activity 时,getActivity() 返回 null,initWindow(null) 中 activity.getWindow() 直接 NPE。
修复:
public void fitFragment(Fragment fragment) {
Activity activity = fragment.getActivity();
if (activity != null) {
initWindow(activity);
}
}文件: shinebutton/ShineButton.java:266-278
public void showAnim() {
ShineView shineView = new ShineView(getContext(), this, mShineParams);
ViewGroup rootView = (ViewGroup) getWindow().getDecorView();
rootView.addView(shineView, ...); // 没有防重入检查
...
}用户快速连击时,每次点击都会向 DecorView 添加一个新的 ShineView,多个动画同时运行且互相叠加,视觉混乱,同时也有内存泄漏风险(动画结束前 ShineView 持有对 ShineButton 的弱引用)。
文件: utils/XEditTextHelper.java:71-87, 126-142
Field editorField = TextView.class.getDeclaredField("mEditor"); // 私有字段
Field mInsertionControllerEnabledField = editorClass.getDeclaredField("mInsertionControllerEnabled");
// API 29+ 非 SDK 接口,反射被限制
@SuppressLint("BlockedPrivateApi")
Field eCursorDrawable = TextView.class.getDeclaredField("mCursorDrawable");mEditor, mInsertionControllerEnabled, mCursorDrawable, mDrawableForCursor 均为 Android 私有字段,在 API 29+ 被列为 @hide 非 SDK 接口,反射调用会被系统拦截。
代码用 catch (Exception e) { e.printStackTrace(); } 静默吞掉了所有异常,导致光标设置和禁用复制粘贴功能在 API 29+ 上悄无声息地失效,但开发者不会收到任何提示。
文件: nestedscroll/XNestedScrollLayout.java:357-363
public void scrollBy(int dy) {
if ((dy > 0 || mBottomView == null) && mTopAreaBehavior != null) {
mTopAreaBehavior.scroll(this, ((View) mTopView), dy);
} else if (dy != 0 && mBottomView != null) {
mBottomView.consumeScroll(dy); // dy < 0 时直接给 bottomView
}
}dy < 0(向上滚动)且 mBottomView != null 时,直接把滚动量全给 bottomView,不经过 topView 中间层。若 bottomView 已滚到顶部,剩余滚动量不会回传给 topView 继续消费,导致 topView 无法从代码触发向上滚动。
| 位置 | 废弃 API | 替代方案 |
|---|---|---|
XDisplayHelper.getRealScreenWidth/Height |
Display.getRealMetrics() (API 31 废弃) |
WindowMetrics.getBounds() |
XDisplayHelper.doGetRealScreenSize |
Display.getRealSize() (API 31 废弃) |
WindowMetrics.getBounds() |
XDisplayHelper.deviceHasNavigationBar |
WindowManagerService.hasNavigationBar() 私有反射 |
WindowInsets.isVisible(WindowInsets.Type.navigationBars()) |
XWebView |
LayoutAlgorithm.SINGLE_COLUMN (API 23 废弃) |
LayoutAlgorithm.TEXT_AUTOSIZING 或不设置 |
XViewHelper.setBackground |
View.setBackgroundDrawable() (API 16 废弃) |
View.setBackground() (已兼容到 API 16,可直接删除兼容分支) |
| 级别 | 编号 | 文件 | 问题简述 |
|---|---|---|---|
| 🔴 Bug | 1 | XRoundBackgroundHelper |
setCheckedCubeSidesHeight 参数比较用 left 代替了 front |
| 🔴 Bug | 2 | XAlphaHelper |
setScaleOnDisabled → isChangeScaleOnDisable 永远 false |
| 🔴 Bug | 3 | XDisplayHelper |
getPhysicalHeight 降级时返回宽度而非高度 |
| 🔴 Bug | 4 | XVerificationCodeEditText |
TYPE_HOLLOW 中 i == maxLength 死代码,分割线逻辑错误 |
| 🔴 Bug | 10 | XVerificationCodeEditText |
Timer 不可复用,View 重用后光标永久失效 |
| 🔴 Bug | 19 | ShineView |
nextInt(0) 在单色时崩溃 + off-by-one |
| 🔴 Bug | 20 | ShineButton |
fitFragment 未判断 getActivity() 为 null |
| 🟠 性能 | 5 | XRoundBackgroundHelper |
onDraw 每帧 new float[8],应缓存为成员变量 |
| 🟠 性能 | 6 | XVerificationCodeEditText |
动画帧/文字变化时重复 new Paint(),改用 setColor/setAlpha |
| 🟠 性能 | 7 | XWidgetCache |
editor.commit() 主线程同步写磁盘,改用 apply() |
| 🟠 性能 | 11 | XVerificationCodeEditText |
drawText 中每帧 String.valueOf(char) 创建对象 |
| 🟠 性能 | 18 | ShineView |
ValueAnimator.setFrameDelay() 是全局静态调用,拖慢全 App 动画 |
| 🟡 安全 | 9 | XWebView |
JS 默认开启 + MIXED_CONTENT_ALWAYS_ALLOW,无关闭入口 |
| 🟡 安全 | 22 | XEditTextHelper |
反射访问私有字段,API 29+ 静默失效,异常被吞掉 |
| 🟡 API设计 | 12 | XTypefaceHelper |
静态可变状态 + WeakHashMap 非线程安全 |
| 🟡 API设计 | 13 | XRoundBackgroundHelper |
Setter 间大量重复代码,需提取 rebuildAllBackgrounds() |
| 🟡 API设计 | 17 | XColorUtils |
颜色算术减法可能整数溢出,应改用 Color.argb() |
| 🟡 API设计 | 21 | ShineButton |
快速点击叠加多个 ShineView,无防重入 |
| 🟡 API设计 | 23 | XNestedScrollLayout |
scrollBy 向上滚动时不经过 topView,滚动链断裂 |
| 🔵 改进 | 8 | XDisplayHelper |
doGetRealScreenSize 多层反射无缓存,API 31+ 用 WindowMetrics |
| 🔵 改进 | 14 | XWheelView |
Adapter.getItem() 只返回 String,无法自定义数据类型 |
| 🔵 改进 | 15 | XWheelView |
setCurrentItem 仅支持瞬移,缺少平滑滚动重载 |
| 🔵 改进 | 16 | XBackgroundHelper |
DEFAULT_COLOR_TRANSPARENT=0x0000000f 魔数作哨兵值,易误判 |
| 🔵 改进 | — | 废弃 API | Display.getRealMetrics/getRealSize、SINGLE_COLUMN、私有 WM 反射 |